#!/usr/bin/env python3
"""
sigen-modbus-bridge.py
Reads Sigenergy data via local Modbus TCP and updates Shelly Virtual Components.
No cloud API, no authentication, no onboarding required.

pip install pymodbus requests

Configuration — set these environment variables before running:
  GATEWAY_IP   IP address of Sigenergy gateway on your LAN  e.g. 192.168.1.200
  SHELLY_IP    IP address of your Shelly device             e.g. 192.168.1.100

  Windows PowerShell:
    $env:GATEWAY_IP = "192.168.1.200"
    $env:SHELLY_IP  = "192.168.1.100"

  Linux / macOS:
    export GATEWAY_IP="192.168.1.200"
    export SHELLY_IP="192.168.1.100"

Run:
  python sigen-modbus-bridge.py

Auto-start on Windows:
  Task Scheduler -> Create Task -> Trigger: At startup -> Action: python <path>
"""

import struct, time, sys, os
import requests
from pymodbus.client import ModbusTcpClient

# == Configuration ============================================================
MODBUS_HOST = os.environ.get("GATEWAY_IP", "")
MODBUS_PORT = 502
UNIT_ID     = 247          # Sigenergy plant-level slave address
POLL_SEC    = 30           # read interval in seconds

SHELLY_IP   = os.environ.get("SHELLY_IP", "")

if not MODBUS_HOST or not SHELLY_IP:
    print("ERROR: set environment variables GATEWAY_IP and SHELLY_IP")
    sys.exit(1)

# Virtual Component IDs on the Shelly
VC = {
    "pvPower":      200,  # PV Power      (W)   >= 0
    "batterySoc":   201,  # Battery SOC   (%)   0-100.0
    "batteryPower": 202,  # Battery Power (W)   + = charging, - = discharging
    "gridPower":    203,  # Grid Power    (W)   + = export,   - = import
    "loadPower":    204,  # Load Power    (W)   >= 0
}

# Confirmed Modbus register map (FC04, PDU address = full register number)
REGISTERS = {
    "gridPower":    (30005, 2, "int32",  1),    # + import -> negate for VC sign convention
    "batterySoc":   (30014, 1, "uint16", 0.1),  # x0.1 -> %
    "pvPower":      (30035, 2, "int32",  1),
    "batteryPower": (30037, 2, "int32",  1),    # + charge, - discharge
    "loadPower":    (30284, 2, "int32",  1),
}
# =============================================================================


def read_registers(client):
    results = {}
    for name, (addr, count, rtype, scale) in REGISTERS.items():
        rr = client.read_input_registers(addr, count=count, device_id=UNIT_ID)
        if rr.isError():
            print(f"  Modbus error reading {name} @ addr {addr}: {rr}")
            return None
        if rtype == "int32":
            raw = struct.pack(">HH", rr.registers[0], rr.registers[1])
            val = struct.unpack(">i", raw)[0]
        else:  # uint16
            val = rr.registers[0]
        results[name] = round(val * scale, 1)
    return results


def update_shelly(data):
    # Grid sign: Modbus + = import, VC 203 convention + = export -> negate
    pv   = round(data["pvPower"])
    soc  = data["batterySoc"]
    bat  = round(data["batteryPower"])
    grid = -round(data["gridPower"])
    load = round(data["loadPower"])

    print(f"  PV={pv}W  SOC={soc}%  Bat={bat}W  Grid={grid}W  Load={load}W")

    for vc_id, value in [
        (VC["pvPower"],      pv),
        (VC["batterySoc"],   soc),
        (VC["batteryPower"], bat),
        (VC["gridPower"],    grid),
        (VC["loadPower"],    load),
    ]:
        try:
            requests.post(
                f"http://{SHELLY_IP}/rpc/Number.Set",
                json={"id": vc_id, "value": value},
                timeout=5
            )
        except Exception as e:
            print(f"  Shelly HTTP error VC {vc_id}: {e}")


def main():
    print(f"Sigen Modbus Bridge  |  {MODBUS_HOST}:{MODBUS_PORT} unit={UNIT_ID}  ->  Shelly {SHELLY_IP}")
    print(f"Poll interval: {POLL_SEC}s   Press Ctrl+C to stop\n")

    client = ModbusTcpClient(MODBUS_HOST, port=MODBUS_PORT)

    while True:
        try:
            if not client.connect():
                print("Modbus connect failed, retrying in 10s...")
                time.sleep(10)
                continue

            data = read_registers(client)
            if data:
                update_shelly(data)
            else:
                print("  Read failed, will retry next cycle")

        except Exception as e:
            print(f"Error: {e}")

        time.sleep(POLL_SEC)


if __name__ == "__main__":
    main()
