#!/usr/bin/env python3
"""
Hoymiles DTU BLE → MQTT bridge
Runs on blebridge (RPi Zero W 2) via bleak
DTU MAC: 24:19:72:61:9D:62
GATT handles (confirmed via discovery):
  Write char:   UUID 53564c0e-92a6-d5bb-d44b-230001003053  val_h=18
  Notify char1: UUID 53564c0e-92a6-d5bb-d44b-230004003053  val_h=20
  Notify char2: UUID 53564c0e-92a6-d5bb-d44b-230005003053  val_h=23
"""

import asyncio
import struct
import time
import json
import logging
import sys
from bleak import BleakClient, BleakScanner

try:
    import paho.mqtt.client as mqtt
except ImportError:
    mqtt = None

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("blebridge")

DTU_MAC      = "24:19:72:61:9D:62"
MQTT_BROKER  = "192.168.1.175"   # Home Assistant MQTT broker
MQTT_PORT    = 1883
MQTT_TOPIC   = "hoymiles/dtu/raw"
MQTT_STATUS  = "hoymiles/dtu/status"
POLL_INTERVAL = 30  # seconds between data requests

# UUIDs
UUID_WRITE   = "53300001-0023-4bd4-bbd5-a6920e4c5653"
UUID_NOTIFY1 = "53300004-0023-4bd4-bbd5-a6920e4c5653"
UUID_NOTIFY2 = "53300005-0023-4bd4-bbd5-a6920e4c5653"

SN = b"30000251257777051"


def crc16(data: bytes) -> int:
    crc = 0xFFFF
    for b in data:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
    return crc & 0xFFFF


def build_frame(cmd: bytes, seq: int, payload: bytes = b"") -> bytes:
    crc = crc16(payload) if payload else 0
    length = len(payload) + 10
    return b"HM" + cmd + struct.pack(">HHH", seq, crc, length) + payload


def build_real_data_req(seq: int) -> bytes:
    payload = b"\x0a" + bytes([len(SN)]) + SN
    return build_frame(b"\xa2\x03", seq, payload)


def build_hb_req(seq: int) -> bytes:
    return build_frame(b"\xa2\x02", seq, b"")


class DTUBridge:
    def __init__(self):
        self.seq = 1
        self.notify_buf = bytearray()
        self.notify_event = asyncio.Event()
        self.mqttc = None
        self._setup_mqtt()

    def _setup_mqtt(self):
        if mqtt is None:
            log.warning("paho-mqtt not available, MQTT disabled")
            return
        self.mqttc = mqtt.Client(client_id="blebridge-dtu")
        self.mqttc.will_set(MQTT_STATUS, "offline", retain=True)
        try:
            self.mqttc.connect(MQTT_BROKER, MQTT_PORT, keepalive=60)
            self.mqttc.loop_start()
            self.mqttc.publish(MQTT_STATUS, "online", retain=True)
            log.info(f"MQTT connected to {MQTT_BROKER}")
        except Exception as e:
            log.error(f"MQTT connect failed: {e}")
            self.mqttc = None

    def publish(self, topic: str, payload):
        if self.mqttc:
            if isinstance(payload, (dict, list)):
                payload = json.dumps(payload)
            self.mqttc.publish(topic, payload, retain=False)

    def on_notify(self, sender, data: bytearray):
        log.info(f"NOTIFY {len(data)}b: {bytes(data).hex()}")
        self.notify_buf.extend(data)
        self.notify_event.set()
        # Publish raw hex to MQTT
        self.publish(MQTT_TOPIC, bytes(data).hex())

    async def run(self):
        log.info(f"Scanning for DTU {DTU_MAC}...")
        while True:
            try:
                await self._connect_and_poll()
            except Exception as e:
                log.error(f"Bridge error: {e}, retrying in 15s")
                await asyncio.sleep(15)

    async def _connect_and_poll(self):
        async with BleakClient(DTU_MAC, timeout=15.0) as client:
            log.info(f"Connected to DTU, MTU={client.mtu_size}")
            self.publish(MQTT_STATUS, "connected")

            # Subscribe to both notify characteristics
            await client.start_notify(UUID_NOTIFY1, self.on_notify)
            await client.start_notify(UUID_NOTIFY2, self.on_notify)
            log.info("Notifications enabled")

            while client.is_connected:
                self.notify_buf.clear()
                self.notify_event.clear()

                # Send real data request
                frame = build_real_data_req(self.seq)
                self.seq = (self.seq + 1) & 0xFFFF
                log.info(f"TX {len(frame)}b: {frame.hex()}")

                await client.write_gatt_char(UUID_WRITE, frame, response=True)
                log.info("Request sent, waiting for response...")

                # Wait up to 10s for notify
                try:
                    await asyncio.wait_for(self.notify_event.wait(), timeout=10.0)
                    log.info(f"Got response: {self.notify_buf.hex()}")
                except asyncio.TimeoutError:
                    log.warning("No response from DTU, sending heartbeat")
                    hb = build_hb_req(self.seq)
                    self.seq = (self.seq + 1) & 0xFFFF
                    await client.write_gatt_char(UUID_WRITE, hb, response=True)

                await asyncio.sleep(POLL_INTERVAL)

        log.warning("Disconnected from DTU")
        self.publish(MQTT_STATUS, "disconnected")


if __name__ == "__main__":
    bridge = DTUBridge()
    asyncio.run(bridge.run())
