115 lines
4.9 KiB
Python
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))
|