#!/usr/bin/env python
import asyncio
import json
import random
import struct

import websockets  # pip install websockets
from pymodbus.client import AsyncModbusTcpClient  # pip install pymodbus


clients = set()

# -------------------------------------------------------------
# Modbus-Konfiguration
# -------------------------------------------------------------
# Hier trägst du deine Geräte / Register ein.
# Jedes Dict in MODBUS_VALUES beschreibt EINEN Messwert.
#
# WICHTIG:
# - Jeder Wert benutzt 2 Register => 32 Bit.
# - "type": "int"  -> 32-Bit-Integer
#          "float" -> 32-Bit-Float (IEEE754)
# - byte_swap: True  -> innerhalb jedes 16-Bit-Registers die Bytes tauschen
#              False -> wie geliefert
# - word_swap: True  -> Reihenfolge der 16-Bit-Register vertauschen (High/Low)
#              False -> wie geliefert
# - scale: z.B. 0.001, wenn der Wert in W kommt und du kW willst.

MODBUS_VALUES = [
    {
        "key": "netz_kw",        # Name im JSON
        "host": "192.168.178.2",
        "port": 502,
        "unit_id": 1,
        "address": 32790,          # Start-Register (2 Register werden gelesen)
        "type": "float",         # oder "int"
        "byte_swap": False,      # Gerät OHNE "Little endian Byte swap"
        "word_swap": False,
        "scale": 1,          # z.B. W -> kW
    },
    {
        "key": "wallbox_kw",        # Name im JSON
        "host": "192.168.178.3",
        "port": 502,
        "unit_id": 1,
        "address": 32790,          # Start-Register (2 Register werden gelesen)
        "type": "float",         # oder "int"
        "byte_swap": False,      # Gerät OHNE "Little endian Byte swap"
        "word_swap": True,
        "scale": 1,          # z.B. W -> kW
    },
    {
        "key": "pv_kw",        # Name im JSON
        "host": "192.168.178.4",
        "port": 502,
        "unit_id": 1,
        "address": 32800,          # Start-Register (2 Register werden gelesen)
        "type": "float",         # oder "int"
        "byte_swap": False,      # Gerät OHNE "Little endian Byte swap"
        "word_swap": True,
        "scale": 1,          # z.B. W -> kW
    },
    {
        "key": "last_kw",        # Name im JSON
        "host": "192.168.178.4",
        "port": 502,
        "unit_id": 1,
        "address": 32802,          # Start-Register (2 Register werden gelesen)
        "type": "float",         # oder "int"
        "byte_swap": False,      # Gerät OHNE "Little endian Byte swap"
        "word_swap": True,
        "scale": 1,          # z.B. W -> kW
    },
]


# -------------------------------------------------------------
# Hilfsfunktionen für 32-Bit-Werte ohne BinaryPayloadDecoder
# -------------------------------------------------------------

def swap_bytes_16(value: int) -> int:
    """Byte-Swap innerhalb eines 16-Bit-Wertes: 0x1234 -> 0x3412."""
    return ((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8)


def build_32bit_value(regs, byte_swap=False, word_swap=False) -> int:
    """
    regs: Liste/Tuple mit ZWEI 16-Bit-Registern [reg0, reg1]
    byte_swap:
      - True:  in jedem 16-Bit-Register Bytes tauschen (Little-Endian im Wort)
      - False: wie geliefert
    word_swap:
      - True:  Reihenfolge der 16-Bit-Register tauschen (Low/High-Word Tausch)
      - False: wie geliefert

    Rückgabe: 32-Bit-Integer (0..0xFFFFFFFF), OHNE Vorzeichen.
    """
    if len(regs) != 2:
        raise ValueError("Es werden genau 2 Register benötigt")

    r0, r1 = regs[0], regs[1]

    # Optional Byte-Swap in JEDEM 16-Bit-Register
    if byte_swap:
        r0 = swap_bytes_16(r0)
        r1 = swap_bytes_16(r1)

    # Optional Word-Swap der Register
    if word_swap:
        r0, r1 = r1, r0

    # 32-Bit-Wert zusammensetzen: r0 = High-Word, r1 = Low-Word
    value_32 = (r0 << 16) | r1
    return value_32


def int32_from_uint32(unsigned_val: int) -> int:
    """
    Interpretiert einen 32-Bit-Unsigned-Wert als SIGNED 32-Bit-Integer.
    """
    if unsigned_val & 0x80000000:
        return unsigned_val - 0x100000000
    else:
        return unsigned_val


def float32_from_uint32(unsigned_val: int) -> float:
    """
    Interpretiert einen 32-Bit-Unsigned-Wert als IEEE754-Float.
    Verwendet struct (kein BinaryPayloadDecoder nötig).
    """
    # In 4 Bytes big-endian packen:
    b = unsigned_val.to_bytes(4, byteorder="big", signed=False)
    # Als big-endian float auspacken:
    return struct.unpack(">f", b)[0]


# -------------------------------------------------------------
# Modbus-Lese-Funktion
# -------------------------------------------------------------

async def read_32bit_modbus_value(vdef) -> tuple[float | int | None, bool]:
    """
    Liest einen 32-Bit-Wert aus 2 Registern und wendet Byte-/Word-Swap an.

    Rückgabe:
      (wert, True)  bei Erfolg
      (None, False) bei Fehler
    """
    host = vdef["host"]
    port = vdef["port"]
    unit_id = vdef["unit_id"]
    address = vdef["address"]

    client = AsyncModbusTcpClient(host, port=port)
    try:
        await client.connect()
        if not client.connected:
            print(f"[Modbus] Keine Verbindung zu {host}:{port}")
            return None, False

        rr = await client.read_input_registers(
            address=address,
            count=2,
            device_id=unit_id,
        )
        if rr.isError():
            print(f"[Modbus] Fehler {host}:{port}, Reg {address}: {rr}")
            return None, False

        regs = rr.registers  # Liste mit 2 int-Werten
        if len(regs) != 2:
            print(f"[Modbus] Unerwartete Registeranzahl von {host}:{port}, Reg {address}: {regs}")
            return None, False

        raw32 = build_32bit_value(
            regs,
            byte_swap=vdef.get("byte_swap", False),
            word_swap=vdef.get("word_swap", False),
        )

        if vdef["type"] == "int":
            val = int32_from_uint32(raw32)
        elif vdef["type"] == "float":
            val = float32_from_uint32(raw32)
        else:
            print(f"[Modbus] Unbekannter Typ {vdef['type']} für {vdef['key']}")
            return None, False

        scale = vdef.get("scale", 1.0)
        val_scaled = val * scale
        #print(f"[Modbus OK] {vdef['key']} = {val_scaled}")
        return val_scaled, True

    except Exception as exc:
        print(f"[Modbus] Fehler {host}:{port} -> {exc}")
        return None, False

    finally:
        client.close()


async def read_all_values():
    """
    Gibt ein Dict mit Werten und Status zurück, z.B.:
    {
      "wallbox_kw": 3.7,
      "_status_wallbox_kw": True
    }
    """
    result = {}
    for vdef in MODBUS_VALUES:
        key = vdef["key"]
        val, ok = await read_32bit_modbus_value(vdef)
        if val is None:
            val = 0.0
        result[key] = val
        result[f"_status_{key}"] = ok
    return result



# -------------------------------------------------------------
# WebSocket-Producer
# -------------------------------------------------------------

async def producer():
    while True:
        values = await read_all_values()

        netz_kw = values.get("netz_kw", 0.0)
        pv_kw = values.get("pv_kw", 0.0)
        last_kw = values.get("last_kw", 0.0)
        wallbox_kw = values.get("wallbox_kw", 0.0)
        #wallbox_ok = values.get("_status_wallbox_kw", False)
        batt_kw = 0

        msg = {
            "netz_kw": netz_kw,
            "pv_kw": pv_kw,
            "last_kw": last_kw,
            "wallbox_kw": wallbox_kw
        }
        print(msg)
        data = json.dumps(msg)

        for ws in list(clients):
            try:
                await ws.send(data)
            except Exception:
                clients.discard(ws)

        await asyncio.sleep(0.2)


# -------------------------------------------------------------
# WebSocket-Handler
# -------------------------------------------------------------

async def handler(websocket):
    clients.add(websocket)
    try:
        async for message in websocket:
            print("Nachricht vom Client:", message)

            try:
                data = json.loads(message)
            except Exception:
                data = None

            if isinstance(data, dict):
                msg_type = data.get("type")
                action = data.get("action")

                if msg_type == "button" and action == "toggle_load":
                    print("Toggle Last wurde angefordert")

    finally:
        clients.discard(websocket)


# -------------------------------------------------------------
# main()
# -------------------------------------------------------------

async def main():
    server = await websockets.serve(handler, "0.0.0.0", 8765)
    print("WebSocket-Server läuft auf ws://0.0.0.0:8765")
    asyncio.create_task(producer())
    await server.wait_closed()


if __name__ == "__main__":
    asyncio.run(main())