from __future__ import annotations import json from ha_deconz_bridge.ha import HomeAssistantBridge from ha_deconz_bridge.lights import ChannelSpec, ControllerModeSpec, ControllerSpec, VirtualLightSpec from ha_deconz_bridge.light import VirtualLightState from ha_deconz_bridge.color import Color class FakeMQTTClient: def __init__(self) -> None: self.connected = False self.on_connect = None self.on_message = None self.published: list[tuple[str, str, bool]] = [] self.subscriptions: list[str] = [] def username_pw_set(self, username, password) -> None: self.username = username self.password = password def connect(self, host: str, port: int, keepalive: int) -> None: self.host = host self.port = port self.keepalive = keepalive def loop_start(self) -> None: self.connected = True if self.on_connect is not None: self.on_connect(self, None, None, 0) def loop_stop(self) -> None: self.connected = False def disconnect(self) -> None: self.connected = False def publish(self, topic: str, payload: str, retain: bool = False): if not self.connected: raise RuntimeError("publish before MQTT connect") self.published.append((topic, payload, retain)) def subscribe(self, topic: str) -> None: if not self.connected: raise RuntimeError("subscribe before MQTT connect") self.subscriptions.append(topic) def test_bridge_waits_for_connect_before_discovery(settings, rgb_light_spec) -> None: mqtt = FakeMQTTClient() bridge = HomeAssistantBridge(settings, mqtt_client=mqtt) bridge.start(on_command=lambda event: None) bridge.publish_discovery(rgb_light_spec) bridge.publish_state( rgb_light_spec, VirtualLightState( on=True, color=rgb_light_spec.controllers["1"].modes["rgb"].channels["r"].color, ha_mode="rgb", brightness=128, exact_chroma_match=True, ), ) assert mqtt.published[0][0] == "homeassistant/light/rgb_light/config" assert json.loads(mqtt.published[0][1])["schema"] == "json" assert json.loads(mqtt.published[0][1])["name"] == "rgb_light" assert mqtt.subscriptions == ["ha-deconz-bridge/light/rgb_light/set"] def test_bridge_sanitizes_mqtt_object_id_for_discovery_topics(settings) -> None: mqtt = FakeMQTTClient() bridge = HomeAssistantBridge(settings, mqtt_client=mqtt) bridge.start(on_command=lambda event: None) spec = VirtualLightSpec( name="Example Light North", supported_color_modes=("brightness",), controllers={ "example_controller": ControllerSpec( zigbee_id="example_controller", name="Example controller", modes={ "brightness": ControllerModeSpec( name="brightness", channels={ "w": ChannelSpec( "w", Color.from_color_temp(182, brightness=1.0), semantic_label="color_temp", ) }, ) }, ) }, ) bridge.publish_discovery(spec) assert mqtt.published[0][0] == "homeassistant/light/Example_Light_North/config" payload = json.loads(mqtt.published[0][1]) assert payload["name"] == "Example Light North" assert payload["unique_id"] == "Example_Light_North" assert payload["device"]["identifiers"] == ["ha-deconz-bridge:Example_Light_North"] assert payload["device"]["name"] == "HA deCONZ Bridge: Example Light North" assert payload["command_topic"] == "ha-deconz-bridge/light/Example_Light_North/set" assert payload["state_topic"] == "ha-deconz-bridge/light/Example_Light_North/state" assert mqtt.subscriptions == ["ha-deconz-bridge/light/Example_Light_North/set"] def test_bridge_resubscribes_to_command_topics_on_reconnect(settings, rgb_light_spec) -> None: mqtt = FakeMQTTClient() bridge = HomeAssistantBridge(settings, mqtt_client=mqtt) bridge.start(on_command=lambda event: None) bridge.publish_discovery(rgb_light_spec) mqtt.loop_stop() mqtt.loop_start() assert mqtt.subscriptions == [ "ha-deconz-bridge/light/rgb_light/set", "ha-deconz-bridge/light/rgb_light/set", ]