#!/usr/bin/env python3
# ============================================================
#  SIGEN -> SHELLY VC BRIDGE  (Recipe B: local Modbus TCP)
#  Runs on any always-on host on the same LAN as the inverter
#  (a small OpenWrt router is a good fit). Pure Python stdlib,
#  no external libraries.
#
#  Reads live data from the Sigenergy inverter over Modbus TCP
#  and pushes it to Shelly Virtual Components 200-204 + boolean 200.
#
#  Install:   copy to the host (e.g. /root/sigen-bridge.py)
#  Test:      python3 sigen-bridge.py
#  Autostart: run as a service (procd on OpenWrt, systemd on Linux,
#             Scheduled Task on Windows).
#
#  Configure the two IPs below, or via the SIGEN_GATEWAY_IP and
#  SHELLY_IP environment variables. This recipe writes the SAME
#  Virtual Components as the RS485 add-on recipe - run only one.
# ============================================================

import os
import socket
import struct
import urllib.request
import time
import sys

# -- Configuration -------------------------------------------------
MODBUS_IP   = os.environ.get("SIGEN_GATEWAY_IP", "192.168.1.100")  # Sigenergy inverter (reached via the gateway on the LAN)
MODBUS_PORT = 502
UNIT_ID     = 247               # plant-level (whole-system) slave address

SHELLY_IP   = os.environ.get("SHELLY_IP", "192.168.1.101")         # Shelly device holding the Virtual Components
POLL_SEC    = 1                 # poll interval in seconds

# Virtual Component IDs on the Shelly
VC_PV      = 200  # PV Power (W)
VC_SOC     = 201  # Battery SOC (%)
VC_BAT     = 202  # Battery Power (W)  + = charging
VC_GRID    = 203  # Grid Power (W)     + = export
VC_LOAD    = 204  # Load Power (W)
VC_ONGRID  = 200  # Boolean (id 200):  True = on-grid
# ------------------------------------------------------------------

def read_input_registers(ip, port, unit_id, address, count, timeout=5):
    """Read input registers via Modbus TCP (FC04). Returns a list of uint16."""
    tid = 1
    req = struct.pack('>HHHBBHH', tid, 0, 6, unit_id, 4, address, count)
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(timeout)
    s.connect((ip, port))
    s.sendall(req)
    resp = b''
    while len(resp) < 9 + count * 2:
        chunk = s.recv(256)
        if not chunk:
            break
        resp += chunk
    s.close()
    if len(resp) < 9 + count * 2:
        raise ValueError("Short Modbus response: %d bytes" % len(resp))
    data = resp[9:]
    return [struct.unpack('>H', data[i*2:(i+1)*2])[0] for i in range(count)]

def to_int32(hi, lo):
    u = hi * 65536 + lo
    if u > 2147483647:
        u -= 4294967296
    return u

def set_vc(shelly_ip, vc_id, value):
    """Write a Shelly Virtual Component (number type)."""
    url = "http://%s/rpc/Number.Set" % shelly_ip
    body = ('{"id":%d,"value":%s}' % (vc_id, value)).encode()
    req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req, timeout=5)

def set_bool_vc(shelly_ip, vc_id, value):
    """Write a Shelly Virtual Component (boolean type)."""
    url = "http://%s/rpc/Boolean.Set" % shelly_ip
    body = ('{"id":%d,"value":%s}' % (vc_id, "true" if value else "false")).encode()
    req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req, timeout=5)

def poll():
    try:
        v = read_input_registers(MODBUS_IP, MODBUS_PORT, UNIT_ID, 30035, 2)   # PV power (W)
        pv_w = to_int32(v[0], v[1])
        v = read_input_registers(MODBUS_IP, MODBUS_PORT, UNIT_ID, 30014, 1)   # Battery SOC
        soc_pc = round(v[0] * 0.1, 1)
        v = read_input_registers(MODBUS_IP, MODBUS_PORT, UNIT_ID, 30037, 2)   # Battery power (+ charging)
        bat_w = to_int32(v[0], v[1])
        v = read_input_registers(MODBUS_IP, MODBUS_PORT, UNIT_ID, 30005, 2)   # Grid power (Modbus + = import)
        grid_import = to_int32(v[0], v[1])
        v = read_input_registers(MODBUS_IP, MODBUS_PORT, UNIT_ID, 30284, 2)   # Load power (W)
        load_w = to_int32(v[0], v[1])
        v = read_input_registers(MODBUS_IP, MODBUS_PORT, UNIT_ID, 30009, 1)   # On/Off-grid status
        on_grid = (v[0] == 0)                                                 # 0 = on-grid, 1/2 = off-grid

        set_vc(SHELLY_IP, VC_PV,   pv_w)
        set_vc(SHELLY_IP, VC_SOC,  soc_pc)
        set_vc(SHELLY_IP, VC_BAT,  bat_w)
        set_vc(SHELLY_IP, VC_GRID, -grid_import)   # VC + = export
        set_vc(SHELLY_IP, VC_LOAD, load_w)
        set_bool_vc(SHELLY_IP, VC_ONGRID, on_grid)

        print("PV=%dW SOC=%s%% Bat=%dW Grid=%dW Load=%dW [%s]" %
              (pv_w, soc_pc, bat_w, -grid_import, load_w, "ON-GRID" if on_grid else "OFF-GRID"))
    except Exception as e:
        print("ERROR: %s" % e, file=sys.stderr)

def main():
    print("Sigen bridge started - polling every %ds" % POLL_SEC)
    print("  Modbus: %s:%d uid=%d" % (MODBUS_IP, MODBUS_PORT, UNIT_ID))
    print("  Shelly: %s" % SHELLY_IP)
    while True:
        poll()
        time.sleep(POLL_SEC)

if __name__ == "__main__":
    main()
