223 lines
6.5 KiB
Python
Executable File
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()
|