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

304 lines
9.3 KiB
Python
Executable File

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