#!/usr/bin/env python3 import json import struct import time 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 HOLDING_BASE = 1000 DI_BASE = 0 POLL_INTERVAL_S = 0.1 PUBLISH_INTERVAL_S = 2.0 DEBOUNCE_S = 0.05 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.1.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_holdings(client): 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 read_discrete_inputs(client): rr = client.read_discrete_inputs( address=DI_BASE, count=2, device_id=WISE_UNIT, ) if rr.isError(): raise RuntimeError(f"Error leyendo discrete inputs: {rr}") di0 = bool(rr.bits[0]) di1 = bool(rr.bits[1]) return di0, di1 def decode_energy(regs): voltage_l1_v = f32_abcd(regs[0], regs[1]) current_l1_a = f32_abcd(regs[2], regs[3]) active_power_w = f32_abcd(regs[4], regs[5]) apparent_power_va = f32_abcd(regs[6], regs[7]) reactive_power_var = f32_abcd(regs[8], regs[9]) power_factor = f32_abcd(regs[10], regs[11]) frequency_hz = f32_abcd(regs[12], regs[13]) import_kwh = f32_abcd(regs[14], regs[15]) export_kwh = f32_abcd(regs[16], regs[17]) total_active_energy_kwh = f32_abcd(regs[18], regs[19]) return { "voltage_l1_v": round(voltage_l1_v, 3), "current_l1_a": round(current_l1_a, 6), "active_power_w": round(active_power_w, 3), "power_total_kw": round(active_power_w / 1000.0, 6), "apparent_power_va": round(apparent_power_va, 3), "reactive_power_var": round(reactive_power_var, 3), "power_factor": round(power_factor, 6), "frequency_hz": round(frequency_hz, 3), "import_kwh": round(import_kwh, 6), "export_kwh": round(export_kwh, 6), "total_active_energy_kwh": round(total_active_energy_kwh, 6), } def main(): client = ModbusTcpClient(WISE_IP, port=WISE_PORT, timeout=3) cycle_pulses = deque() cycle_pulse_count = 0 last_cycle_state = None last_auto_signal = None last_pulse_ts = 0.0 last_publish_ts = 0.0 print(f"Conectando con WISE {WISE_IP}:{WISE_PORT} unit={WISE_UNIT}") while True: now = time.time() try: if not client.connected: if not client.connect(): raise RuntimeError("No conecta con WISE") auto_signal, cycle_state = read_discrete_inputs(client) if last_cycle_state is None: last_cycle_state = cycle_state rising_edge = cycle_state and not last_cycle_state if rising_edge and (now - last_pulse_ts) >= DEBOUNCE_S: cycle_pulse_count += 1 cycle_pulses.append(now) last_pulse_ts = now last_cycle_state = cycle_state while cycle_pulses and (now - cycle_pulses[0]) > RATE_WINDOW_S: cycle_pulses.popleft() cycle_rate_ppm = len(cycle_pulses) * 60.0 / RATE_WINDOW_S last_cycle_age_s = None if not cycle_pulses else round(now - cycle_pulses[-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" event = { "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, } print("EVENT", json.dumps(event, ensure_ascii=False)) last_auto_signal = auto_signal if (now - last_publish_ts) >= PUBLISH_INTERVAL_S: regs = read_holdings(client) energy = decode_energy(regs) 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_state, "cycle_pulse_count": cycle_pulse_count, "cycle_rate_ppm": round(cycle_rate_ppm, 3), "last_cycle_age_s": last_cycle_age_s, "digital_inputs": { "DI0": auto_signal, "DI1": cycle_state, }, **energy, "comm_ok": True, "sample_valid": True, } print("TELEMETRY", json.dumps(payload, ensure_ascii=False)) last_publish_ts = now except KeyboardInterrupt: print("Parado 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(POLL_INTERVAL_S) if __name__ == "__main__": main()