#!/usr/bin/env python3 import argparse import json import struct import sys from datetime import datetime, timezone from pymodbus.client import ModbusSerialClient def decode_float_abcd(registers): if len(registers) != 2: raise ValueError(f"Expected 2 registers, got {len(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 main(): parser = argparse.ArgumentParser() parser.add_argument("--port", default="/dev/ttyRS485") parser.add_argument("--slave", type=int, default=1) parser.add_argument("--baud", type=int, default=9600) parser.add_argument("--parity", default="N") parser.add_argument("--stopbits", type=int, default=1) parser.add_argument("--bytesize", type=int, default=8) parser.add_argument("--timeout", type=float, default=1.5) args = parser.parse_args() client = ModbusSerialClient( port=args.port, baudrate=args.baud, parity=args.parity, stopbits=args.stopbits, bytesize=args.bytesize, timeout=args.timeout, ) if not client.connect(): print("[ERROR] Cannot open Modbus serial port", file=sys.stderr) return 2 try: voltage_v = read_float(client, 0, args.slave) current_a = read_float(client, 6, args.slave) active_power_w = read_float(client, 12, args.slave) frequency_hz = read_float(client, 70, args.slave) import_kwh = read_float(client, 72, args.slave) export_kwh = read_float(client, 74, args.slave) payload = { "ts": datetime.now(timezone.utc).isoformat(), "source": "revpi_sdm120m", "meter_model": "Eastron SDM120-M", "meter_id": f"modbus_{args.slave}", "port": args.port, "slave": args.slave, "baud": args.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, } print(json.dumps(payload, indent=2, ensure_ascii=False)) return 0 except Exception as exc: error_payload = { "ts": datetime.now(timezone.utc).isoformat(), "source": "revpi_sdm120m", "meter_model": "Eastron SDM120-M", "meter_id": f"modbus_{args.slave}", "port": args.port, "slave": args.slave, "baud": args.baud, "comm_ok": False, "error_text": f"{type(exc).__name__}: {exc}", } print(json.dumps(error_payload, indent=2, ensure_ascii=False)) return 1 finally: client.close() if __name__ == "__main__": sys.exit(main())