diff --git a/config.yaml.example b/config.yaml.example index 0030998..95496c3 100755 --- a/config.yaml.example +++ b/config.yaml.example @@ -5,6 +5,8 @@ mqtt: password: password #ca_cert: /etc/ssl/certs/ca-certificates.crt # Uncomment to enable MQTT TLS, update path to appropriate location. #ca_verify: False # Verify TLS certificate chain and host, disable for testing with self-signed certificates, default to True + #client_cert: mosq_client.crt # If client_cert and client_key are specified, MQTT uses client certificate authentication instead of username + password + #client_key: mosq_client.key topic_prefix: hostname # All messages will have that prefix added, remove if you dont need this. client_id: bt-mqtt-gateway availability_topic: lwt_topic @@ -171,3 +173,10 @@ manager: # will be changed to `default_update_interval`. topic_subscription: blinds/+/+/+ update_interval: 300 + lightstring: + args: + devices: + led_outdoor: 00:11:22:33:44:55 + topic_prefix: lightstring/xmas + topic_subscription: lightstring/+/+/set + update_interval: 60 diff --git a/mqtt.py b/mqtt.py index be9caed..5e3a8eb 100644 --- a/mqtt.py +++ b/mqtt.py @@ -17,14 +17,19 @@ def __init__(self, config): userdata={"global_topic_prefix": self.topic_prefix}, ) - if self.username and self.password: + if self.username and self.password and not self.client_key: self.mqttc.username_pw_set(self.username, self.password) - if self.ca_cert: + if self.ca_cert and not self.client_key: cert_reqs = mqtt.ssl.CERT_REQUIRED if self.ca_verify else mqtt.ssl.CERT_NONE self.mqttc.tls_set(self.ca_cert, cert_reqs=cert_reqs) self.mqttc.tls_insecure_set(not self.ca_verify) + if self.ca_cert and self.client_key: + cert_reqs = mqtt.ssl.CERT_REQUIRED if self.ca_verify else mqtt.ssl.CERT_NONE + self.mqttc.tls_set(self.ca_cert, self.client_cert, self.client_key, cert_reqs=cert_reqs) + self.mqttc.tls_insecure_set(not self.ca_verify) + if self.availability_topic: topic = self._format_topic(self.availability_topic) _LOGGER.debug("Setting LWT to: %s" % topic) @@ -69,6 +74,14 @@ def password(self): def ca_cert(self): return self._config["ca_cert"] if "ca_cert" in self._config else None + @property + def client_cert(self): + return self._config["client_cert"] if "client_cert" in self._config else None + + @property + def client_key(self): + return self._config["client_key"] if "client_key" in self._config else None + @property def ca_verify(self): if "ca_verify" in self._config: diff --git a/workers/lightstring.py b/workers/lightstring.py new file mode 100644 index 0000000..e256b87 --- /dev/null +++ b/workers/lightstring.py @@ -0,0 +1,170 @@ +from builtins import staticmethod +import logging + +from mqtt import MqttMessage + +from workers.base import BaseWorker +import logger + +REQUIREMENTS = ["bluepy"] +_LOGGER = logger.get(__name__) + +STATE_ON = "ON" +STATE_OFF = "OFF" + +# reversed from com.scinan.novolink.lightstring apk +# https://play.google.com/store/apps/details?id=com.scinan.novolink.lightstring + +# write characteristics handle +HAND = 0x0025 + +# hex bytecodes for various operations +HEX_STATE_ON = "01010101" +HEX_STATE_OFF = "01010100" +HEX_CONF_PREFIX = "05010203" +HEX_ENUM_STATE = "000003" +HEX_ENUM_CONF = "02000000" + +class LightstringWorker(BaseWorker): + def _setup(self): + + _LOGGER.info("Adding %d %s devices", len(self.devices), repr(self)) + for name, mac in self.devices.items(): + _LOGGER.info("Adding %s device '%s' (%s)", repr(self), name, mac) + self.devices[name] = {"lightstring": None, "state": STATE_OFF, "conf": 0, "mac": mac} + + def format_state_topic(self, *args): + return "/".join([self.topic_prefix, *args, "state"]) + + def format_conf_topic(self, *args): + return "/".join([self.topic_prefix, *args, "conf"]) + + def status_update(self): + from bluepy import btle + import binascii + from bluepy.btle import Peripheral + + class MyDelegate(btle.DefaultDelegate): + def __init__(self): + self.state = '' + btle.DefaultDelegate.__init__(self) + def handleNotification(self, cHandle, data): + try: + if data[3] in (0, 3): + self.state = "OFF" + else: + self.state = "ON" + except: + self.state = -1 + + class ConfDelegate(btle.DefaultDelegate): + def __init__(self): + self.conf = 0 + btle.DefaultDelegate.__init__(self) + def handleNotification(self, cHandle, data): + try: + self.conf = int(data[17]) + except: + self.conf = -1 + + delegate = MyDelegate() + cdelegate = ConfDelegate() + ret = [] + _LOGGER.debug("Updating %d %s devices", len(self.devices), repr(self)) + for name, lightstring in self.devices.items(): + _LOGGER.debug("Updating %s device '%s' (%s)", repr(self), name, lightstring["mac"]) + try: + lightstring["lightstring"] = Peripheral(lightstring["mac"]) + lightstring["lightstring"].setDelegate(delegate) + lightstring["lightstring"].writeCharacteristic(HAND, binascii.a2b_hex(HEX_ENUM_STATE)) + lightstring["lightstring"].waitForNotifications(1.0) + lightstring["lightstring"].disconnect() + lightstring["lightstring"].connect(lightstring["mac"]) + lightstring["lightstring"].setDelegate(cdelegate) + lightstring["lightstring"].writeCharacteristic(HAND, binascii.a2b_hex(HEX_ENUM_CONF)) + lightstring["lightstring"].waitForNotifications(1.0) + lightstring["lightstring"].disconnect() + if delegate.state != -1: + lightstring["state"] = delegate.state + ret += self.update_device_state(name, lightstring["state"]) + if cdelegate.conf != -1: + lightstring["conf"] = cdelegate.conf + ret += self.update_device_conf(name, lightstring["conf"]) + except btle.BTLEException as e: + logger.log_exception( + _LOGGER, + "Error during update of %s device '%s' (%s): %s", + repr(self), + name, + lightstring["mac"], + type(e).__name__, + suppress=True, + ) + return ret + + def on_command(self, topic, value): + from bluepy import btle + import binascii + from bluepy.btle import Peripheral + + _, _, device_name, _ = topic.split("/") + + lightstring = self.devices[device_name] + + value = value.decode("utf-8") + + # It needs to be on separate if because first if can change method + + _LOGGER.debug( + "Setting %s on %s device '%s' (%s)", + value, + repr(self), + device_name, + lightstring["mac"], + ) + success = False + while not success: + try: + lightstring["lightstring"] = Peripheral(lightstring["mac"]) + if value == STATE_ON: + lightstring["lightstring"].writeCharacteristic(HAND, binascii.a2b_hex(HEX_STATE_ON)) + elif value == STATE_OFF: + lightstring["lightstring"].writeCharacteristic(HAND, binascii.a2b_hex(HEX_STATE_OFF)) + else: + lightstring["lightstring"].writeCharacteristic(HAND, binascii.a2b_hex(HEX_CONF_PREFIX)+bytes([int(value)])) + lightstring["lightstring"].disconnect() + success = True + except btle.BTLEException as e: + logger.log_exception( + _LOGGER, + "Error setting %s on %s device '%s' (%s): %s", + value, + repr(self), + device_name, + lightstring["mac"], + type(e).__name__, + ) + success = True + + try: + if value in (STATE_ON, STATE_OFF): + return self.update_device_state(device_name, value) + else: + return self.update_device_conf(device_name, value) + except btle.BTLEException as e: + logger.log_exception( + _LOGGER, + "Error during update of %s device '%s' (%s): %s", + repr(self), + device_name, + lightstring["mac"], + type(e).__name__, + suppress=True, + ) + return [] + + def update_device_state(self, name, value): + return [MqttMessage(topic=self.format_state_topic(name), payload=value)] + + def update_device_conf(self, name, value): + return [MqttMessage(topic=self.format_conf_topic(name), payload=value)]