40-clients/ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_local_loop.py
2026-06-25 18:51:33 +02:00

223 lines
6.5 KiB
Python
Executable File

#!/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()