Files
ha-deconz-bridge/src/ha_deconz_bridge/ha.py
T
2026-05-30 15:44:40 +00:00

115 lines
4.9 KiB
Python

from __future__ import annotations
import json
import logging
import re
import threading
from ha_deconz_bridge.events import HaCommandEvent
from ha_deconz_bridge.lights import LightEntitySpec, VirtualLightSpec
from ha_deconz_bridge.settings import Settings
from ha_deconz_bridge.state import VirtualLightState
from ha_deconz_bridge.translate import ha_payload_from_state
class HomeAssistantBridge:
def __init__(self, settings: Settings, *, mqtt_client=None) -> None:
if mqtt_client is None:
import paho.mqtt.client as mqtt
mqtt_client = mqtt.Client()
self.settings = settings
self._mqtt = mqtt_client
if settings.mqtt_user is not None:
self._mqtt.username_pw_set(settings.mqtt_user, settings.mqtt_password)
self._on_command = None
self._connected = threading.Event()
self._connect_result: int | None = None
self._light_name_by_object_id: dict[str, str] = {}
self._command_topics: set[str] = set()
def start(self, *, on_command) -> None:
self._on_command = on_command
self._connected.clear()
self._connect_result = None
self._mqtt.on_connect = self._handle_connect
self._mqtt.on_message = self._handle_message
self._mqtt.connect(self.settings.mqtt_host, self.settings.mqtt_port, 60)
self._mqtt.loop_start()
if not self._connected.wait(timeout=5.0):
raise RuntimeError("MQTT connection did not become ready.")
if self._connect_result not in {0, None}:
raise RuntimeError(f"MQTT connection failed with result {self._connect_result}.")
def stop(self) -> None:
try:
self._mqtt.loop_stop()
finally:
self._mqtt.disconnect()
def publish_discovery(self, spec: VirtualLightSpec | LightEntitySpec) -> None:
object_id = self.object_id(spec.name)
self._light_name_by_object_id[object_id] = spec.name
command_topic = self.command_topic(spec.name)
self._command_topics.add(command_topic)
payload: dict[str, object] = {
"name": spec.name,
"unique_id": object_id,
"device": {
"identifiers": [f"ha-deconz-bridge:{object_id}"],
"name": f"HA deCONZ Bridge: {spec.name}",
"manufacturer": "ha-deconz-bridge",
"model": "Virtual Light",
},
"schema": "json",
"command_topic": command_topic,
"state_topic": self.state_topic(spec.name),
"supported_color_modes": list(spec.supported_color_modes),
}
if spec.exposed_mireds is not None:
payload["min_mireds"] = spec.exposed_mireds[0]
payload["max_mireds"] = spec.exposed_mireds[1]
topic = f"{self.settings.mqtt_discovery_prefix}/light/{object_id}/config"
self._mqtt.publish(topic, json.dumps(payload), retain=True)
self._mqtt.subscribe(command_topic)
logging.info("Published discovery for %s on %s", spec.name, topic)
def publish_state(self, spec: VirtualLightSpec | LightEntitySpec, state: VirtualLightState) -> None:
payload = ha_payload_from_state(state)
self._mqtt.publish(self.state_topic(spec.name), json.dumps(payload), retain=True)
logging.info("Published state for %s: %s", spec.name, payload)
def command_topic(self, light_name: str) -> str:
return f"{self.settings.mqtt_topic_prefix}/light/{self.object_id(light_name)}/set"
def state_topic(self, light_name: str) -> str:
return f"{self.settings.mqtt_topic_prefix}/light/{self.object_id(light_name)}/state"
@staticmethod
def object_id(light_name: str) -> str:
sanitized = re.sub(r"[^a-zA-Z0-9_-]+", "_", light_name.strip())
sanitized = re.sub(r"_+", "_", sanitized).strip("_")
if not sanitized:
raise ValueError("Light name produces an empty MQTT object_id.")
return sanitized
def _handle_connect(self, client, userdata, flags, reason_code, properties=None) -> None:
self._connect_result = int(reason_code)
if self._connect_result == 0:
logging.info("Connected to MQTT broker at %s:%s", self.settings.mqtt_host, self.settings.mqtt_port)
for topic in sorted(self._command_topics):
self._mqtt.subscribe(topic)
logging.info("Subscribed to %s", topic)
else:
logging.error("MQTT connect failed with result %s", self._connect_result)
self._connected.set()
def _handle_message(self, client, userdata, message) -> None:
if self._on_command is None:
return
object_id = message.topic.split("/")[-2]
light_name = self._light_name_by_object_id.get(object_id, object_id)
payload = json.loads(message.payload.decode())
logging.info("Received MQTT command for %s on %s: %s", light_name, message.topic, payload)
self._on_command(HaCommandEvent.from_payload(light_name, payload))