#!/usr/bin/env python3 import json import os import struct import time import urllib.request from collections import deque from datetime import datetime, timezone from pymodbus.client import ModbusTcpClient WISE_IP = "10.43.54.249" WISE_PORT = 502 WISE_UNIT = 1 WISE_DI_URL = f"http://{WISE_IP}/di_value/slot_0" WISE_SESSION_ID = os.getenv("WISE_SESSION_ID", "").strip() HOLDING_BASE = 1000 DI_POLL_INTERVAL_S = 0.2 ENERGY_INTERVAL_S = 2.0 PRINT_INTERVAL_S = 2.0 RATE_WINDOW_S = 60.0 TENANT = "ucepsa" SITE = "demo_edge_oee" VERTICAL = "edge_oee" ASSET = "revpi_oee_node_01" MACHINE_ID = "CORT-01" SOURCE_NODE = "wise-banco-01" NODE_TYPE = "revpi_wise_oee_gateway" FW = "0.2.0" def utc_now(): return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") def f32_abcd(reg_hi, reg_lo): raw = int(reg_hi).to_bytes(2, "big") + int(reg_lo).to_bytes(2, "big") return struct.unpack(">f", raw)[0] def read_wise_di_api(): from urllib.error import HTTPError if not WISE_SESSION_ID: raise RuntimeError( "Falta WISE_SESSION_ID. Ejecuta primero: export WISE_SESSION_ID" ) req = urllib.request.Request( WISE_DI_URL, headers={ "Accept": "text/plain, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", "Referer": f"http://{WISE_IP}/config/index.html", "User-Agent": "Mozilla/5.0", "Cookie": f"adamsessionid={WISE_SESSION_ID}", }, ) try: with urllib.request.urlopen(req, timeout=2) as response: text = response.read().decode("utf-8", errors="replace") except HTTPError as e: body = e.read().decode("utf-8", errors="replace") raise RuntimeError( f"WISE HTTP error {e.code}: {e.reason}. Body={body[:300]!r}" ) data = json.loads(text) if isinstance(data, dict) and data.get("Err"): raise RuntimeError( f"WISE API error Err={data.get('Err')} Msg={data.get('Msg')!r}" ) channels = {int(item["Ch"]): item for item in data.get("DIVal", [])} ch0 = channels.get(0, {}) ch1 = channels.get(1, {}) return { "di0_stat": bool(ch0.get("Stat", 0)), "di1_stat": bool(ch1.get("Stat", 0)), "di1_counter_raw": int(ch1.get("CtFq", 0)), "di1_counting": bool(ch1.get("Cnting", 0)), "di1_mode": int(ch1.get("Md", -1)), "raw": data, } def read_holdings(client): if not client.connected: if not client.connect(): raise RuntimeError("No conecta con WISE por Modbus/TCP") rr = client.read_holding_registers( address=HOLDING_BASE, count=20, device_id=WISE_UNIT, ) if rr.isError(): raise RuntimeError(f"Error leyendo holding registers: {rr}") return rr.registers def decode_energy(regs): active_power_w = f32_abcd(regs[4], regs[5]) return { "voltage_l1_v": round(f32_abcd(regs[0], regs[1]), 3), "current_l1_a": round(f32_abcd(regs[2], regs[3]), 6), "active_power_w": round(active_power_w, 3), "power_total_kw": round(active_power_w / 1000.0, 6), "apparent_power_va": round(f32_abcd(regs[6], regs[7]), 3), "reactive_power_var": round(f32_abcd(regs[8], regs[9]), 3), "power_factor": round(f32_abcd(regs[10], regs[11]), 6), "frequency_hz": round(f32_abcd(regs[12], regs[13]), 3), "import_kwh": round(f32_abcd(regs[14], regs[15]), 6), "export_kwh": round(f32_abcd(regs[16], regs[17]), 6), "total_active_energy_kwh": round(f32_abcd(regs[18], regs[19]), 6), } def counter_delta(previous, current): if previous is None: return 0, False if current >= previous: return current - previous, False # Reset manual, reinicio del WISE o pérdida del contador. return 0, True def main(): client = ModbusTcpClient(WISE_IP, port=WISE_PORT, timeout=3) previous_wise_counter = None cycle_pulse_count = 0 cycle_events = deque() last_auto_signal = None last_energy = {} last_energy_ts = 0.0 last_print_ts = 0.0 last_delta = 0 counter_reset_detected = False print(f"Leyendo WISE {WISE_IP}") print(f"DI API: {WISE_DI_URL}") print(f"Modbus/TCP: {WISE_IP}:{WISE_PORT}, holding base {HOLDING_BASE}") print("DI0 = marcha / automático") print("DI1 = contador de ciclos\n") while True: now = time.time() try: di = read_wise_di_api() auto_signal = di["di0_stat"] cycle_input_state = di["di1_stat"] wise_counter = di["di1_counter_raw"] counter_valid = di["di1_mode"] == 1 and di["di1_counting"] delta, reset = counter_delta(previous_wise_counter, wise_counter) counter_reset_detected = reset if delta > 0: cycle_pulse_count += delta for _ in range(min(delta, 1000)): cycle_events.append(now) print( "CYCLES", json.dumps( { "ts": utc_now(), "delta": delta, "wise_counter": wise_counter, "cycle_pulse_count": cycle_pulse_count, }, ensure_ascii=False, ), ) previous_wise_counter = wise_counter last_delta = delta while cycle_events and (now - cycle_events[0]) > RATE_WINDOW_S: cycle_events.popleft() cycle_rate_ppm = len(cycle_events) * 60.0 / RATE_WINDOW_S last_cycle_age_s = None if not cycle_events else round(now - cycle_events[-1], 3) if last_auto_signal is not None and auto_signal != last_auto_signal: event_name = "machine_started" if auto_signal else "machine_stopped" print( "EVENT", json.dumps( { "ts": utc_now(), "tenant": TENANT, "site": SITE, "vertical": VERTICAL, "asset": ASSET, "machine_id": MACHINE_ID, "source_node": SOURCE_NODE, "event_type": "machine_state_changed", "event_name": event_name, "previous_auto_signal": last_auto_signal, "new_auto_signal": auto_signal, "machine_running": auto_signal, "reason_pending": not auto_signal, }, ensure_ascii=False, ), ) last_auto_signal = auto_signal if now - last_energy_ts >= ENERGY_INTERVAL_S: regs = read_holdings(client) last_energy = decode_energy(regs) last_energy_ts = now if now - last_print_ts >= PRINT_INTERVAL_S: payload = { "ts": utc_now(), "asset": ASSET, "tenant": TENANT, "site": SITE, "vertical": VERTICAL, "node_type": NODE_TYPE, "fw": FW, "machine_id": MACHINE_ID, "source_node": SOURCE_NODE, "wise_host": WISE_IP, "machine_auto_signal": auto_signal, "machine_running": auto_signal, "cycle_input_state": cycle_input_state, "wise_cycle_counter_raw": wise_counter, "cycle_delta_last": last_delta, "cycle_pulse_count": cycle_pulse_count, "cycle_rate_ppm": round(cycle_rate_ppm, 3), "last_cycle_age_s": last_cycle_age_s, "counter_valid": counter_valid, "counter_reset_detected": counter_reset_detected, "digital_inputs": { "DI0": auto_signal, "DI1": cycle_input_state, }, **last_energy, "comm_ok": True, "sample_valid": True, } print("TELEMETRY", json.dumps(payload, ensure_ascii=False)) last_print_ts = now except KeyboardInterrupt: print("\nParado por usuario") break except Exception as exc: print( "ERROR", json.dumps( { "ts": utc_now(), "asset": ASSET, "machine_id": MACHINE_ID, "source_node": SOURCE_NODE, "wise_host": WISE_IP, "comm_ok": False, "sample_valid": False, "error": str(exc), }, ensure_ascii=False, ), ) try: client.close() except Exception: pass time.sleep(2) time.sleep(DI_POLL_INTERVAL_S) if __name__ == "__main__": main()