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

206 lines
5.6 KiB
Python
Executable File

#!/usr/bin/env python3
import json
import os
import struct
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
import paho.mqtt.client as mqtt
from pymodbus.client import ModbusSerialClient
BASE_DIR = Path(__file__).resolve().parent
ENV_FILE = BASE_DIR / ".env"
START_TIME = time.monotonic()
def load_env_file(path: Path):
if not path.exists():
return
for line in path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
def getenv(name, default=None, required=False):
value = os.getenv(name, default)
if required and not value:
print(f"[ERROR] Missing required environment variable: {name}", file=sys.stderr)
sys.exit(2)
return value
def decode_float_abcd(registers):
raw = registers[0].to_bytes(2, "big") + registers[1].to_bytes(2, "big")
return struct.unpack(">f", raw)[0]
def read_float(client, address, device_id):
rr = client.read_input_registers(
address=address,
count=2,
device_id=device_id,
)
if rr.isError():
raise RuntimeError(f"Modbus error at address {address}: {rr}")
return decode_float_abcd(rr.registers)
def read_sdm120m():
port = getenv("MODBUS_PORT", "/dev/ttyRS485")
slave = int(getenv("MODBUS_SLAVE", "1"))
baud = int(getenv("MODBUS_BAUD", "9600"))
parity = getenv("MODBUS_PARITY", "N")
stopbits = int(getenv("MODBUS_STOPBITS", "1"))
bytesize = int(getenv("MODBUS_BYTESIZE", "8"))
timeout = float(getenv("MODBUS_TIMEOUT", "1.5"))
client = ModbusSerialClient(
port=port,
baudrate=baud,
parity=parity,
stopbits=stopbits,
bytesize=bytesize,
timeout=timeout,
)
if not client.connect():
raise RuntimeError(f"Cannot open Modbus serial port {port}")
try:
voltage_v = read_float(client, 0, slave)
current_a = read_float(client, 6, slave)
active_power_w = read_float(client, 12, slave)
frequency_hz = read_float(client, 70, slave)
import_kwh = read_float(client, 72, slave)
export_kwh = read_float(client, 74, slave)
return {
"meter_model": "Eastron SDM120-M",
"meter_id": f"modbus_{slave}",
"modbus_port": port,
"modbus_slave": slave,
"modbus_baud": baud,
"voltage_l1_v": round(voltage_v, 3),
"current_l1_a": round(current_a, 3),
"power_total_kw": round(active_power_w / 1000.0, 6),
"frequency_hz": round(frequency_hz, 3),
"import_kwh": round(import_kwh, 3),
"export_kwh": round(export_kwh, 3),
"comm_ok": True,
"sample_valid": True,
"error_text": None,
}
finally:
client.close()
def build_payload():
tenant = getenv("TENANT", "ucepsa")
site = getenv("SITE", "demo_edge_oee")
vertical = getenv("VERTICAL", "edge_oee")
asset = getenv("ASSET", "revpi_oee_node_01")
base = {
"asset": asset,
"tenant": tenant,
"site": site,
"vertical": vertical,
"node_type": "revpi_sdm120m_oee_gateway",
"fw": "0.1.0",
"ts": datetime.now(timezone.utc).isoformat(),
"uptime_s": round(time.monotonic() - START_TIME, 1),
"machine_id": None,
"machine_running": None,
"cycle_pulse_count": None,
"cycle_rate_hz": None,
"digital_inputs": {},
}
try:
data = read_sdm120m()
base.update(data)
except Exception as exc:
base.update({
"comm_ok": False,
"sample_valid": False,
"meter_model": "Eastron SDM120-M",
"meter_id": f"modbus_{getenv('MODBUS_SLAVE', '1')}",
"voltage_l1_v": None,
"current_l1_a": None,
"power_total_kw": None,
"frequency_hz": None,
"import_kwh": None,
"export_kwh": None,
"error_text": f"{type(exc).__name__}: {exc}",
})
return base
def publish_once(payload):
mqtt_host = getenv("MQTT_HOST", required=True)
mqtt_port = int(getenv("MQTT_PORT", "1883"))
mqtt_user = getenv("MQTT_USER", required=True)
mqtt_password = getenv("MQTT_PASSWORD", required=True)
mqtt_client_id = getenv("MQTT_CLIENT_ID", required=True)
topic_telemetry = getenv("MQTT_TOPIC_TELEMETRY", required=True)
topic_status = getenv("MQTT_TOPIC_STATUS", required=True)
client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2,
client_id=mqtt_client_id,
clean_session=True,
)
client.username_pw_set(mqtt_user, mqtt_password)
client.will_set(topic_status, payload="offline", qos=0, retain=True)
client.connect(mqtt_host, mqtt_port, keepalive=30)
client.loop_start()
client.publish(topic_status, payload="online", qos=0, retain=True)
result = client.publish(
topic_telemetry,
payload=json.dumps(payload, ensure_ascii=False),
qos=0,
retain=False,
)
result.wait_for_publish(timeout=5)
client.loop_stop()
client.disconnect()
if result.rc != mqtt.MQTT_ERR_SUCCESS:
raise RuntimeError(f"MQTT publish failed rc={result.rc}")
def main():
load_env_file(ENV_FILE)
payload = build_payload()
print(json.dumps(payload, indent=2, ensure_ascii=False))
publish_once(payload)
print("[OK] MQTT telemetry published")
return 0 if payload.get("comm_ok") else 1
if __name__ == "__main__":
sys.exit(main())