diff --git a/requirements.txt b/requirements.txt index 0aa2468..53eaa2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,12 +5,12 @@ phue fritzconnection # API -requests fastapi uvicorn[standard] -# Clients +# Bridges requests +paho-mqtt # Config file pyyaml diff --git a/src/mash/bridges/zigbee2mqtt.py b/src/mash/bridges/zigbee2mqtt.py index 2926a52..3de16d0 100644 --- a/src/mash/bridges/zigbee2mqtt.py +++ b/src/mash/bridges/zigbee2mqtt.py @@ -1,6 +1,76 @@ +from typing import Optional from mash.bridges.bridge import Bridge +import paho.mqtt.client as mqtt +import json class Z2mBridge(Bridge): - def __init__(self, *, id: str) -> None: + + def __init__( + self, + *, + id: str, + ip: str, + port: int = 1883, + keepalive: int = 60, + topic: str = "zigbee2mqtt", + ) -> None: super().__init__(id=id, type="zigbee2mqtt") + self._ip = ip + self._port = port + self._keepalive = keepalive + self._device_callbacks: dict[str, list] = {} + self._topic = topic.strip("/") + + self._client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + + self._client.on_connect = lambda client, userdata, flags, reason_code, properties: self.__on_connect__( + client, userdata, flags, reason_code, properties + ) + self._client.on_message = lambda client, userdata, msg: self.__on_message__( + client, userdata, msg + ) + + def __del__(self) -> None: + self.disconnect() + + def disconnect(self) -> None: + self._client.loop_stop() + self._client.disconnect() + + def connect(self) -> None: + self._client.connect(self._ip, self._port, self._keepalive) + self._client.loop_start() + + def __on_connect__(self, client, userdata, flags, reason_code, properties): + self._client.subscribe(f"{self._topic}/#") + + def __on_message__(self, client, userdata, msg: any): + device_name = msg.topic.split(self._topic + "/", 2)[-1].split("/", 2)[0] + + if device_name not in self._device_callbacks.keys(): + return + + for callback in self._device_callbacks[device_name]: + callback(device_name, json.loads(msg.payload)) + + def set_device(self, ieee_address: str, *, content: dict = {}) -> None: + self._client.publish(f"{self._topic}/{ieee_address}/set", json.dumps(content)) + + def get_device(self, ieee_address: str) -> None: + self._client.publish( + f"{self._topic}/{ieee_address}/get", json.dumps({"state": ""}) + ) + + def subscribe_device( + self, + callback, + *, + ieee_address: Optional[str] = None, + friendly_name: Optional[str] = None, + ) -> None: + for id in [ieee_address, friendly_name]: + if id not in self._device_callbacks.keys(): + self._device_callbacks[id] = [] + + self._device_callbacks[id].append(callback) diff --git a/src/mqtt_test.py b/src/mqtt_test.py new file mode 100644 index 0000000..9fda9cd --- /dev/null +++ b/src/mqtt_test.py @@ -0,0 +1,23 @@ +from time import sleep +from mash.bridges.zigbee2mqtt import Z2mBridge + + +z2m = Z2mBridge(id="z2m", ip="192.168.178.115") + + +def wardrobe_cb(device_name: str, payload: dict) -> None: + print(f"{device_name} - {'closed' if payload['contact'] else 'open'}") + + +z2m.subscribe_device(wardrobe_cb, friendly_name="max-wardrobe-door") +z2m.subscribe_device(wardrobe_cb, friendly_name="max-window-contact") + +z2m.connect() + +while True: + try: + sleep(3) + except KeyboardInterrupt: + break + +z2m.disconnect()