Initial public release
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
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))
|
||||
Reference in New Issue
Block a user