From fab8edd03fa5d36194aff6cc4011ed333c96f344 Mon Sep 17 00:00:00 2001 From: Dale Higgs Date: Sat, 18 Jul 2020 03:15:09 -0500 Subject: [PATCH 01/24] Update bug report template on devel branch --- .github/ISSUE_TEMPLATE/bug_report.md | 40 ++++++++++++++++++---------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..ef2fd3f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,32 +7,44 @@ assignees: '' --- -**Describe the bug** +## Describe the Bug + A clear and concise description of what the bug is. -**To Reproduce** +### Steps To Reproduce + Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error -**Expected behavior** +### Expected Behavior + A clear and concise description of what you expected to happen. -**Screenshots** +### Screenshots + If applicable, add screenshots to help explain your problem. -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] +### Desktop + +Please complete the following information: + +- OS: [e.g. iOS] +- Browser: [e.g. Chrome, Safari] +- Version: [e.g. 22] + +### Smartphone + +Please complete the following information: + +- Device: [e.g. iPhone 6] +- OS: [e.g. iOS 8.1] +- Browser: [e.g. Stock Browser, Safari] +- Version [e.g. 22] -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] +## Additional Context -**Additional context** Add any other context about the problem here. From da6ed4f8bdb43333e2d453e7676472abd591af5d Mon Sep 17 00:00:00 2001 From: Dale Higgs Date: Sat, 18 Jul 2020 03:26:25 -0500 Subject: [PATCH 02/24] Add support for Neos Leak Sensors --- wyzesense2mqtt/wyzesense.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/wyzesense2mqtt/wyzesense.py b/wyzesense2mqtt/wyzesense.py index 5d29aa0..52f3bcc 100755 --- a/wyzesense2mqtt/wyzesense.py +++ b/wyzesense2mqtt/wyzesense.py @@ -82,14 +82,14 @@ def Length(self): @property def Cmd(self): return self._cmd - + @property def Payload(self): return self._payload def Send(self, fd): pkt = bytes() - + pkt += struct.pack(">HB", 0xAA55, self._cmd >> 8) if self._cmd == self.ASYNC_ACK: pkt += struct.pack("BB", (self._payload & 0xFF), self._cmd & 0xFF) @@ -143,11 +143,11 @@ def Parse(cls, s): @classmethod def GetVersion(cls): return cls(cls.CMD_GET_DONGLE_VERSION) - + @classmethod def Inquiry(cls): return cls(cls.CMD_INQUIRY) - + @classmethod def GetEnr(cls, r): assert isinstance(r, bytes) @@ -157,7 +157,7 @@ def GetEnr(cls, r): @classmethod def GetMAC(cls): return cls(cls.CMD_GET_MAC) - + @classmethod def GetKey(cls): return cls(cls.CMD_GET_KEY) @@ -188,7 +188,7 @@ def DelSensor(cls, mac): assert isinstance(mac, str) assert len(mac) == 8 return cls(cls.CMD_DEL_SENSOR, mac.encode('ascii')) - + @classmethod def GetSensorR1(cls, mac, r): assert isinstance(r, bytes) @@ -226,7 +226,7 @@ def __init__(self, mac, timestamp, event_type, event_data): self.Timestamp = timestamp self.Type = event_type self.Data = event_data - + def __str__(self): s = "[%s][%s]" % (self.Timestamp.strftime("%Y-%m-%d %H:%M:%S"), self.MAC) if self.Type == 'state': @@ -259,10 +259,20 @@ def _OnSensorAlarm(self, pkt): elif alarm_data[0] == 0x02: sensor_type = "motion" sensor_state = "active" if alarm_data[5] == 1 else "inactive" + elif alarm_data[0] == 0x03: + sensor_type = "leak" + sensor_state = "wet" if alarm_data[5] == 1 else "dry" else: - sensor_type = "uknown" + sensor_type = "unknown" sensor_state = "unknown" e = SensorEvent(sensor_mac, timestamp, "state", (sensor_type, sensor_state, alarm_data[2], alarm_data[8])) + elif event_type == 0xE8: + if alarm_data[0] == 0x03: + # alarm_data[7] might be humidity in some form, but as an integer + # is reporting way to high to actually be humidity. + sensor_type = "leak:temperature" + sensor_state = "%d.%d" % (alarm_data[5], alarm_data[6]) + e = SensorEvent(sensor_mac, timestamp, "state", (sensor_type, sensor_state, alarm_data[2], alarm_data[8])) else: e = SensorEvent(sensor_mac, timestamp, "raw_%02X" % event_type, alarm_data) @@ -333,7 +343,7 @@ def _HandlePacket(self, pkt): log.debug("<=== Received: %s", str(pkt)) with self.__lock: handler = self.__handlers.get(pkt.Cmd, self._DefaultHandler) - + if (pkt.Cmd >> 8) == TYPE_ASYNC and pkt.Cmd != Packet.ASYNC_ACK: #log.info("Sending ACK packet for cmd %04X", pkt.Cmd) self._SendPacket(Packet.AsyncAck(pkt.Cmd)) @@ -344,7 +354,7 @@ def _Worker(self): while True: if self.__exit_event.isSet(): break - + s += self._ReadRawHID() #if s: # log.info("Incoming buffer: %s", bytes_to_hex(s)) @@ -412,14 +422,14 @@ def _GetMac(self): mac = resp.Payload.decode('ascii') log.debug("GetMAC returns %s", mac) return mac - + def _GetKey(self): log.debug("Start GetKey...") resp = self._DoSimpleCommand(Packet.GetKey()) assert len(resp.Payload) == 16 log.debug("GetKey returns %s", resp.Payload) return resp.Payload - + def _GetVersion(self): log.debug("Start GetVersion...") resp = self._DoSimpleCommand(Packet.GetVersion()) @@ -515,7 +525,7 @@ def scan_handler(pkt): assert len(pkt.Payload) == 11 ctx.result = (pkt.Payload[1:9].decode('ascii'), pkt.Payload[9], pkt.Payload[10]) ctx.evt.set() - + old_handler = self._SetHandler(Packet.NOTIFY_SENSOR_SCAN, scan_handler) try: self._DoSimpleCommand(Packet.EnableScan()) From 265d824285bcb9ab0c92799c8424523887b785f4 Mon Sep 17 00:00:00 2001 From: Dale Higgs Date: Mon, 20 Jul 2020 01:20:09 -0500 Subject: [PATCH 03/24] Fix most flake8 issues --- wyzesense2mqtt/bridge_tool_cli.py | 6 +- wyzesense2mqtt/wyzesense.py | 98 ++++++++++++++++--------------- wyzesense2mqtt/wyzesense2mqtt.py | 84 +++++++++++++------------- 3 files changed, 96 insertions(+), 92 deletions(-) diff --git a/wyzesense2mqtt/bridge_tool_cli.py b/wyzesense2mqtt/bridge_tool_cli.py index 35e33ce..1f50e58 100644 --- a/wyzesense2mqtt/bridge_tool_cli.py +++ b/wyzesense2mqtt/bridge_tool_cli.py @@ -95,9 +95,9 @@ def Unpair(mac_list): def Fix(unused_args): invalid_mac_list = [ - "00000000", - "\0\0\0\0\0\0\0\0", - "\x00\x00\x00\x00\x00\x00\x00\x00" + "00000000", + "\0\0\0\0\0\0\0\0", + "\x00\x00\x00\x00\x00\x00\x00\x00" ] print("Un-pairing bad sensors") logging.debug("Un-pairing bad sensors") diff --git a/wyzesense2mqtt/wyzesense.py b/wyzesense2mqtt/wyzesense.py index 5d29aa0..cb4b397 100755 --- a/wyzesense2mqtt/wyzesense.py +++ b/wyzesense2mqtt/wyzesense.py @@ -6,59 +6,63 @@ import struct import threading import datetime -import argparse import binascii import logging log = logging.getLogger(__name__) + def bytes_to_hex(s): if s: return binascii.hexlify(s) else: return "" + def checksum_from_bytes(s): return sum(bytes(s)) & 0xFFFF -TYPE_SYNC = 0x43 -TYPE_ASYNC = 0x53 + +TYPE_SYNC = 0x43 +TYPE_ASYNC = 0x53 + def MAKE_CMD(type, cmd): return (type << 8) | cmd + class Packet(object): _CMD_TIMEOUT = 5 # Sync packets: # Commands initiated from host side - CMD_GET_ENR = MAKE_CMD(TYPE_SYNC, 0x02) - CMD_GET_MAC = MAKE_CMD(TYPE_SYNC, 0x04) - CMD_GET_KEY = MAKE_CMD(TYPE_SYNC, 0x06) - CMD_INQUIRY = MAKE_CMD(TYPE_SYNC, 0x27) - CMD_UPDATE_CC1310 = MAKE_CMD(TYPE_SYNC, 0x12) - CMD_SET_CH554_UPGRADE = MAKE_CMD(TYPE_SYNC, 0x0E) + CMD_GET_ENR = MAKE_CMD(TYPE_SYNC, 0x02) + CMD_GET_MAC = MAKE_CMD(TYPE_SYNC, 0x04) + CMD_GET_KEY = MAKE_CMD(TYPE_SYNC, 0x06) + CMD_INQUIRY = MAKE_CMD(TYPE_SYNC, 0x27) + CMD_UPDATE_CC1310 = MAKE_CMD(TYPE_SYNC, 0x12) + CMD_SET_CH554_UPGRADE = MAKE_CMD(TYPE_SYNC, 0x0E) # Async packets: - ASYNC_ACK = MAKE_CMD(TYPE_ASYNC, 0xFF) + ASYNC_ACK = MAKE_CMD(TYPE_ASYNC, 0xFF) # Commands initiated from dongle side - CMD_FINISH_AUTH = MAKE_CMD(TYPE_ASYNC, 0x14) - CMD_GET_DONGLE_VERSION = MAKE_CMD(TYPE_ASYNC, 0x16) - CMD_START_STOP_SCAN = MAKE_CMD(TYPE_ASYNC, 0x1C) - CMD_GET_SENSOR_R1 = MAKE_CMD(TYPE_ASYNC, 0x21) - CMD_VERIFY_SENSOR = MAKE_CMD(TYPE_ASYNC, 0x23) - CMD_DEL_SENSOR = MAKE_CMD(TYPE_ASYNC, 0x25) - CMD_GET_SENSOR_COUNT = MAKE_CMD(TYPE_ASYNC, 0x2E) - CMD_GET_SENSOR_LIST = MAKE_CMD(TYPE_ASYNC, 0x30) + CMD_FINISH_AUTH = MAKE_CMD(TYPE_ASYNC, 0x14) + CMD_GET_DONGLE_VERSION = MAKE_CMD(TYPE_ASYNC, 0x16) + CMD_START_STOP_SCAN = MAKE_CMD(TYPE_ASYNC, 0x1C) + CMD_GET_SENSOR_R1 = MAKE_CMD(TYPE_ASYNC, 0x21) + CMD_VERIFY_SENSOR = MAKE_CMD(TYPE_ASYNC, 0x23) + CMD_DEL_SENSOR = MAKE_CMD(TYPE_ASYNC, 0x25) + CMD_GET_SENSOR_COUNT = MAKE_CMD(TYPE_ASYNC, 0x2E) + CMD_GET_SENSOR_LIST = MAKE_CMD(TYPE_ASYNC, 0x30) # Notifications initiated from dongle side - NOTIFY_SENSOR_ALARM = MAKE_CMD(TYPE_ASYNC, 0x19) - NOTIFY_SENSOR_SCAN = MAKE_CMD(TYPE_ASYNC, 0x20) - NOITFY_SYNC_TIME = MAKE_CMD(TYPE_ASYNC, 0x32) - NOTIFY_EVENT_LOG = MAKE_CMD(TYPE_ASYNC, 0x35) + NOTIFY_SENSOR_ALARM = MAKE_CMD(TYPE_ASYNC, 0x19) + NOTIFY_SENSOR_SCAN = MAKE_CMD(TYPE_ASYNC, 0x20) + NOITFY_SYNC_TIME = MAKE_CMD(TYPE_ASYNC, 0x32) + NOTIFY_EVENT_LOG = MAKE_CMD(TYPE_ASYNC, 0x35) - def __init__(self, cmd, payload = bytes()): + def __init__(self, cmd, payload=bytes()): self._cmd = cmd if self._cmd == self.ASYNC_ACK: assert isinstance(payload, int) @@ -82,14 +86,14 @@ def Length(self): @property def Cmd(self): return self._cmd - + @property def Payload(self): return self._payload def Send(self, fd): pkt = bytes() - + pkt += struct.pack(">HB", 0xAA55, self._cmd >> 8) if self._cmd == self.ASYNC_ACK: pkt += struct.pack("BB", (self._payload & 0xFF), self._cmd & 0xFF) @@ -143,11 +147,11 @@ def Parse(cls, s): @classmethod def GetVersion(cls): return cls(cls.CMD_GET_DONGLE_VERSION) - + @classmethod def Inquiry(cls): return cls(cls.CMD_INQUIRY) - + @classmethod def GetEnr(cls, r): assert isinstance(r, bytes) @@ -157,7 +161,7 @@ def GetEnr(cls, r): @classmethod def GetMAC(cls): return cls(cls.CMD_GET_MAC) - + @classmethod def GetKey(cls): return cls(cls.CMD_GET_KEY) @@ -188,7 +192,7 @@ def DelSensor(cls, mac): assert isinstance(mac, str) assert len(mac) == 8 return cls(cls.CMD_DEL_SENSOR, mac.encode('ascii')) - + @classmethod def GetSensorR1(cls, mac, r): assert isinstance(r, bytes) @@ -220,13 +224,14 @@ def AsyncAck(cls, cmd): assert (cmd >> 0x8) == TYPE_ASYNC return cls(cls.ASYNC_ACK, cmd) + class SensorEvent(object): def __init__(self, mac, timestamp, event_type, event_data): self.MAC = mac self.Timestamp = timestamp self.Type = event_type self.Data = event_data - + def __str__(self): s = "[%s][%s]" % (self.Timestamp.strftime("%Y-%m-%d %H:%M:%S"), self.MAC) if self.Type == 'state': @@ -235,6 +240,7 @@ def __str__(self): s += "RawEvent: type=%s, data=%s" % (self.Type, bytes_to_hex(self.Data)) return s + class Dongle(object): _CMD_TIMEOUT = 2 @@ -249,7 +255,7 @@ def _OnSensorAlarm(self, pkt): return timestamp, event_type, sensor_mac = struct.unpack_from(">QB8s", pkt.Payload) - timestamp = datetime.datetime.fromtimestamp(timestamp/1000.0) + timestamp = datetime.datetime.fromtimestamp(timestamp / 1000.0) sensor_mac = sensor_mac.decode('ascii') alarm_data = pkt.Payload[17:] if event_type == 0xA2: @@ -275,7 +281,7 @@ def _OnEventLog(self, pkt): assert len(pkt.Payload) >= 9 ts, msg_len = struct.unpack_from(">QB", pkt.Payload) # assert msg_len + 8 == len(pkt.Payload) - tm = datetime.datetime.fromtimestamp(ts/1000.0) + tm = datetime.datetime.fromtimestamp(ts / 1000.0) msg = pkt.Payload[9:] log.info("LOG: time=%s, data=%s", tm.isoformat(), bytes_to_hex(msg)) @@ -284,12 +290,12 @@ def __init__(self, device, event_handler): self.__fd = os.open(device, os.O_RDWR | os.O_NONBLOCK) self.__sensors = {} self.__exit_event = threading.Event() - self.__thread = threading.Thread(target = self._Worker) + self.__thread = threading.Thread(target=self._Worker) self.__on_event = event_handler self.__handlers = { Packet.NOITFY_SYNC_TIME: self._OnSyncTime, - Packet.NOTIFY_SENSOR_ALARM: self._OnSensorAlarm, + Packet.NOTIFY_SENSOR_ALARM: self._OnSensorAlarm, Packet.NOTIFY_EVENT_LOG: self._OnEventLog, } @@ -311,7 +317,7 @@ def _ReadRawHID(self): if length > 0x3F: length = 0x3F - #log.debug("Raw HID packet: %s", bytes_to_hex(s)) + # log.debug("Raw HID packet: %s", bytes_to_hex(s)) assert len(s) >= length + 1 return s[1: 1 + length] @@ -333,9 +339,9 @@ def _HandlePacket(self, pkt): log.debug("<=== Received: %s", str(pkt)) with self.__lock: handler = self.__handlers.get(pkt.Cmd, self._DefaultHandler) - + if (pkt.Cmd >> 8) == TYPE_ASYNC and pkt.Cmd != Packet.ASYNC_ACK: - #log.info("Sending ACK packet for cmd %04X", pkt.Cmd) + # log.info("Sending ACK packet for cmd %04X", pkt.Cmd) self._SendPacket(Packet.AsyncAck(pkt.Cmd)) handler(pkt) @@ -344,10 +350,10 @@ def _Worker(self): while True: if self.__exit_event.isSet(): break - + s += self._ReadRawHID() - #if s: - # log.info("Incoming buffer: %s", bytes_to_hex(s)) + # if s: + # log.info("Incoming buffer: %s", bytes_to_hex(s)) start = s.find(b"\x55\xAA") if start == -1: time.sleep(0.1) @@ -375,7 +381,7 @@ def _DoCommand(self, pkt, handler, timeout=_CMD_TIMEOUT): raise TimeoutError("_DoCommand") def _DoSimpleCommand(self, pkt, timeout=_CMD_TIMEOUT): - ctx = self.CmdContext(result = None) + ctx = self.CmdContext(result=None) def cmd_handler(pkt, e): ctx.result = pkt @@ -397,7 +403,7 @@ def _Inquiry(self): def _GetEnr(self, r): log.debug("Start GetEnr...") assert len(r) == 4 - assert all(isinstance(x, int) for x in r) + assert all(isinstance(x, int) for x in r) r_string = bytes(struct.pack(" Date: Thu, 23 Jul 2020 02:20:18 -0500 Subject: [PATCH 04/24] Refactor boolean state handling logic --- wyzesense2mqtt/wyzesense2mqtt.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/wyzesense2mqtt/wyzesense2mqtt.py b/wyzesense2mqtt/wyzesense2mqtt.py index 4f22cc4..553d9a1 100644 --- a/wyzesense2mqtt/wyzesense2mqtt.py +++ b/wyzesense2mqtt/wyzesense2mqtt.py @@ -377,6 +377,17 @@ def on_message_reload(MQTT_CLIENT, userdata, msg): # Process event def on_event(WYZESENSE_DONGLE, event): global SENSORS + + # Simplify mapping of device classes. + DEVICE_CLASSES = { + 'leak': 'moisture', + 'motion': 'motion', + 'switch': 'opening', + } + + # List of states that correlate to ON. + STATES_ON = ['active', 'open', 'wet'] + if (valid_sensor_mac(event.MAC)): if (event.Type == "state"): LOGGER.info(f"State event data: {event}") @@ -391,8 +402,7 @@ def on_event(WYZESENSE_DONGLE, event): event_payload = { 'available': True, 'mac': event.MAC, - 'device_class': ("motion" if (sensor_type == "motion") - else "opening"), + 'device_class': DEVICE_CLASSES.get(sensor_type), 'last_seen': event.Timestamp.timestamp(), 'last_seen_iso': event.Timestamp.isoformat(), 'signal_strength': sensor_signal * -1, @@ -402,14 +412,14 @@ def on_event(WYZESENSE_DONGLE, event): if (CONFIG.get('publish_sensor_name')): event_payload['name'] = SENSORS[event.MAC]['name'] - if (SENSORS[event.MAC].get('invert_state')): - event_payload['state'] = (0 if (sensor_state == "open") or - (sensor_state == "active") - else 1) - else: - event_payload['state'] = (1 if (sensor_state == "open") or - (sensor_state == "active") - else 0) + # Set state depending on state string and `invert_state` setting. + # State ON ^ NOT Inverted = True + # State OFF ^ NOT Inverted = False + # State ON ^ Inverted = False + # State OFF ^ Inverted = True + event_payload['state'] = int( + (sensor_state in STATES_ON) + ^ (SENSORS[event.MAC].get('invert_state'))) LOGGER.debug(event_payload) From 8e4055e9927f8419cbfd1b05eee76f217b69e3f7 Mon Sep 17 00:00:00 2001 From: Raetha Date: Fri, 24 Jul 2020 20:16:41 -0400 Subject: [PATCH 05/24] Minor readme updates --- readme.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index ba3427c..7b80d22 100644 --- a/readme.md +++ b/readme.md @@ -6,15 +6,14 @@ [![GitHub PRs](https://img.shields.io/github/issues-pr/raetha/wyzesense2mqtt)](https://github.com/raetha/wyzesense2mqtt/pulls) [![GitHub Release](https://img.shields.io/github/v/release/raetha/wyzesense2mqtt)](https://github.com/raetha/wyzesense2mqtt/releases) [![Python Validation](https://github.com/raetha/wyzesense2mqtt/workflows/Python%20Validation/badge.svg)](https://github.com/raetha/wyzesense2mqtt/actions?query=workflow%3A%22Python+Validation%22) -[![GitHub Downloads](https://img.shields.io/github/downloads/raetha/wyzesense2mqtt/total)]() [![dockeri.co](https://dockeri.co/image/raetha/wyzesense2mqtt)](https://hub.docker.com/r/raetha/wyzesense2mqtt) Configurable WyzeSense to MQTT Gateway intended for use with Home Assistant or other platforms that use MQTT discovery mechanisms. The gateway allows direct local access to [Wyze Sense](https://wyze.com/wyze-sense.html) products without the need for a Wyze Cam or cloud services. This project and its dependencies have no relation to Wyze Labs Inc. ## Special Thanks -* [HcLX](https://hclxing.wordpress.com) for [WyzeSensePy](https://github.com/HclX/WyzeSensePy), the core library this component uses. -* [Kevin Vincent](http://kevinvincent.me) for [HA-WyzeSense](https://github.com/kevinvincent/ha-wyzesense), the refernce code I used to get things working right with the calls to WyzeSensePy. +* [HcLX](https://hclxing.wordpress.com) for [WyzeSensePy](https://github.com/HclX/WyzeSensePy), the core library this project uses. +* [Kevin Vincent](http://kevinvincent.me) for [HA-WyzeSense](https://github.com/kevinvincent/ha-wyzesense), the reference code I used to get things working right with the calls to WyzeSensePy. * [ozczecho](https://github.com/ozczecho) for [wyze-mqtt](https://github.com/ozczecho/wyze-mqtt), the inspiration for this project. * [rmoriz](https://roland.io/) for [multiarch-test](https://github.com/rmoriz/multiarch-test), this allowed the Docker Hub Autobuilder to work for multiple architectures including ARM32v7 (Raspberry Pi) and AMD64 (Linux). @@ -75,7 +74,7 @@ mkdir /docker/wyzesense2mqtt/logs ```bash docker-compose up -d ``` -8. Pair sensors following instructions below. You do NOT need to re-pair sensors that were already paired, they should be found automatically on start and added to the config file with default values, but the sensor version will be unknown. +8. Pair sensors following [instructions below](#pairing-a-sensor). You do NOT need to re-pair sensors that were already paired, they should be found automatically on start and added to the config file with default values, but the sensor version will be unknown. ### Linux Systemd @@ -119,7 +118,7 @@ sudo systemctl daemon-reload sudo systemctl start wyzesense2mqtt sudo systemctl status wyzesense2mqtt ``` -9. Pair sensors following instructions below. You do NOT need to re-pair sensors that were already paired, they should be found automatically on start and added to the config file with default values, but the sensor version will be unknown. +9. Pair sensors following [instructions below](#pairing-a-sensor). You do NOT need to re-pair sensors that were already paired, they should be found automatically on start and added to the config file with default values, but the sensor version will be unknown. ## Config Files @@ -232,5 +231,5 @@ Home Assistant simply needs to be configured with the MQTT broker that the gatew ## Tested On -* Alpine Linux (Docker image) -* Raspbian Buster +* Debian Buster (Docker) +* Raspbian Buster (RPi 4) From 393dd259dde958b6c38d0102fa233970b2173bed Mon Sep 17 00:00:00 2001 From: Raetha Date: Fri, 24 Jul 2020 20:17:19 -0400 Subject: [PATCH 06/24] Bridge connection retries should now continue when an IOError is triggered (#17) --- wyzesense2mqtt/wyzesense2mqtt.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wyzesense2mqtt/wyzesense2mqtt.py b/wyzesense2mqtt/wyzesense2mqtt.py index 4f22cc4..0441ea9 100644 --- a/wyzesense2mqtt/wyzesense2mqtt.py +++ b/wyzesense2mqtt/wyzesense2mqtt.py @@ -110,8 +110,13 @@ def init_mqtt_client(): ) +# Retry forever on IO Error +def retry_if_io_error(exception): + return isinstance(exception, IOError) + + # Initialize USB dongle -@retry(wait_exponential_multiplier=1000, wait_exponential_max=10000) +@retry(wait_exponential_multiplier=1000, wait_exponential_max=30000, retry_on_exception=retry_if_io_error) def init_wyzesense_dongle(): global WYZESENSE_DONGLE, CONFIG if (CONFIG['usb_dongle'].lower() == "auto"): From 35b9e838183436f1b84affb2554efd25ceb0f58c Mon Sep 17 00:00:00 2001 From: Raetha Date: Fri, 24 Jul 2020 21:48:14 -0400 Subject: [PATCH 07/24] Minor respacing for readability --- wyzesense2mqtt/wyzesense2mqtt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wyzesense2mqtt/wyzesense2mqtt.py b/wyzesense2mqtt/wyzesense2mqtt.py index c84cead..7df097e 100644 --- a/wyzesense2mqtt/wyzesense2mqtt.py +++ b/wyzesense2mqtt/wyzesense2mqtt.py @@ -305,8 +305,8 @@ def on_connect(MQTT_CLIENT, userdata, flags, rc): if rc == 0: MQTT_CLIENT.subscribe( [(SCAN_TOPIC, CONFIG['mqtt_qos']), - (REMOVE_TOPIC, CONFIG['mqtt_qos']), - (RELOAD_TOPIC, CONFIG['mqtt_qos'])] + (REMOVE_TOPIC, CONFIG['mqtt_qos']), + (RELOAD_TOPIC, CONFIG['mqtt_qos'])] ) MQTT_CLIENT.message_callback_add(SCAN_TOPIC, on_message_scan) MQTT_CLIENT.message_callback_add(REMOVE_TOPIC, on_message_remove) From 12fccdd6b2d828db5efa2f127209264e9097fd7a Mon Sep 17 00:00:00 2001 From: Raetha Date: Fri, 24 Jul 2020 21:49:41 -0400 Subject: [PATCH 08/24] Version bump to 1.2 --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 9459d4b..5625e59 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.1 +1.2 From e3f36b4917b200ad8e70adfdd8deab546a425334 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Tue, 11 Aug 2020 19:47:56 -0400 Subject: [PATCH 09/24] Add vim to docker image --- Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dockerfile b/Dockerfile index a609306..22ea2ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,11 @@ RUN pip3 install --no-cache-dir --upgrade pip \ && pip3 install --no-cache-dir -r /wyzesense2mqtt/requirements.txt \ && chmod u+x /wyzesense2mqtt/service.sh +RUN apt-get update \ + && apt-get install -y --no-install-recommends + vim \ + && rm -rf /var/lib/apt/lists/* + VOLUME /wyzesense2mqtt/config /wyzesense2mqtt/logs ENTRYPOINT /wyzesense2mqtt/service.sh From 0eddd332a5ad43a95e8b32258a6fa4c8e731827d Mon Sep 17 00:00:00 2001 From: Raetha Date: Sun, 18 Oct 2020 22:00:58 -0400 Subject: [PATCH 10/24] Format tweak to allow easy merge of PR #32 --- wyzesense2mqtt/wyzesense2mqtt.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/wyzesense2mqtt/wyzesense2mqtt.py b/wyzesense2mqtt/wyzesense2mqtt.py index 7df097e..7b5337b 100644 --- a/wyzesense2mqtt/wyzesense2mqtt.py +++ b/wyzesense2mqtt/wyzesense2mqtt.py @@ -408,11 +408,13 @@ def on_event(WYZESENSE_DONGLE, event): event_payload['name'] = SENSORS[event.MAC]['name'] if (SENSORS[event.MAC].get('invert_state')): - event_payload['state'] = (0 if (sensor_state == "open") - or (sensor_state == "active") else 1) + event_payload['state'] = (0 if (sensor_state == "open") or + (sensor_state == "active") + else 1) else: - event_payload['state'] = (1 if (sensor_state == "open") - or (sensor_state == "active") else 0) + event_payload['state'] = (1 if (sensor_state == "open") or + (sensor_state == "active") + else 0) LOGGER.debug(event_payload) From a3b3e67204c8f0c25152f310e2205c9c51846f69 Mon Sep 17 00:00:00 2001 From: Raetha Date: Sun, 18 Oct 2020 22:05:34 -0400 Subject: [PATCH 11/24] Add Docker install of vim to manual Docker file --- Dockerfile | 3 +-- Dockerfile.manual | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 22ea2ec..8f54bcb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,8 +16,7 @@ RUN pip3 install --no-cache-dir --upgrade pip \ && chmod u+x /wyzesense2mqtt/service.sh RUN apt-get update \ - && apt-get install -y --no-install-recommends - vim \ + && apt-get install -y --no-install-recommends vim \ && rm -rf /var/lib/apt/lists/* VOLUME /wyzesense2mqtt/config /wyzesense2mqtt/logs diff --git a/Dockerfile.manual b/Dockerfile.manual index a35b801..c192d2a 100644 --- a/Dockerfile.manual +++ b/Dockerfile.manual @@ -8,6 +8,10 @@ RUN pip3 install --no-cache-dir --upgrade pip \ && pip3 install --no-cache-dir -r /wyzesense2mqtt/requirements.txt \ && chmod u+x /wyzesense2mqtt/service.sh +RUN apt-get update \ + && apt-get install -y --no-install-recommends vim \ + && rm -rf /var/lib/apt/lists/* + VOLUME /wyzesense2mqtt/config /wyzesense2mqtt/logs ENTRYPOINT /wyzesense2mqtt/service.sh From b7dbba9b43591297c939d8f5d8964158824b009a Mon Sep 17 00:00:00 2001 From: Gregory Schmidt Date: Sun, 22 Nov 2020 12:25:05 -0900 Subject: [PATCH 12/24] Added support for status messages --- wyzesense2mqtt/wyzesense.py | 4 ++-- wyzesense2mqtt/wyzesense2mqtt.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/wyzesense2mqtt/wyzesense.py b/wyzesense2mqtt/wyzesense.py index 5d29aa0..2b203e7 100755 --- a/wyzesense2mqtt/wyzesense.py +++ b/wyzesense2mqtt/wyzesense.py @@ -252,7 +252,7 @@ def _OnSensorAlarm(self, pkt): timestamp = datetime.datetime.fromtimestamp(timestamp/1000.0) sensor_mac = sensor_mac.decode('ascii') alarm_data = pkt.Payload[17:] - if event_type == 0xA2: + if event_type == 0xA2 or event_type == 0xA1: if alarm_data[0] == 0x01: sensor_type = "switch" sensor_state = "open" if alarm_data[5] == 1 else "close" @@ -262,7 +262,7 @@ def _OnSensorAlarm(self, pkt): else: sensor_type = "uknown" sensor_state = "unknown" - e = SensorEvent(sensor_mac, timestamp, "state", (sensor_type, sensor_state, alarm_data[2], alarm_data[8])) + e = SensorEvent(sensor_mac, timestamp, ("alarm" if event_type == 0xA2 else "status"), (sensor_type, sensor_state, alarm_data[2], alarm_data[8])) else: e = SensorEvent(sensor_mac, timestamp, "raw_%02X" % event_type, alarm_data) diff --git a/wyzesense2mqtt/wyzesense2mqtt.py b/wyzesense2mqtt/wyzesense2mqtt.py index 4f22cc4..04da4aa 100644 --- a/wyzesense2mqtt/wyzesense2mqtt.py +++ b/wyzesense2mqtt/wyzesense2mqtt.py @@ -378,7 +378,7 @@ def on_message_reload(MQTT_CLIENT, userdata, msg): def on_event(WYZESENSE_DONGLE, event): global SENSORS if (valid_sensor_mac(event.MAC)): - if (event.Type == "state"): + if (event.Type == "alarm") or (event.Type == "status"): LOGGER.info(f"State event data: {event}") (sensor_type, sensor_state, sensor_battery, sensor_signal) = event.Data @@ -389,6 +389,7 @@ def on_event(WYZESENSE_DONGLE, event): # Build event payload event_payload = { + 'event' : event.Type, 'available': True, 'mac': event.MAC, 'device_class': ("motion" if (sensor_type == "motion") From a5a1b9cec8d8e107ab55e6880c30cdd05ab09802 Mon Sep 17 00:00:00 2001 From: Gregory Schmidt Date: Sun, 22 Nov 2020 12:44:23 -0900 Subject: [PATCH 13/24] Correct SensorEvent string method --- wyzesense2mqtt/wyzesense.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wyzesense2mqtt/wyzesense.py b/wyzesense2mqtt/wyzesense.py index 2b203e7..c19d966 100755 --- a/wyzesense2mqtt/wyzesense.py +++ b/wyzesense2mqtt/wyzesense.py @@ -229,7 +229,9 @@ def __init__(self, mac, timestamp, event_type, event_data): def __str__(self): s = "[%s][%s]" % (self.Timestamp.strftime("%Y-%m-%d %H:%M:%S"), self.MAC) - if self.Type == 'state': + if self.Type == 'alarm': + s += "StateEvent: sensor_type=%s, state=%s, battery=%d, signal=%d" % self.Data + elif self.Type == 'status': s += "StateEvent: sensor_type=%s, state=%s, battery=%d, signal=%d" % self.Data else: s += "RawEvent: type=%s, data=%s" % (self.Type, bytes_to_hex(self.Data)) From f5f95fc1a46328d601612f91e67c8ad46110e9e5 Mon Sep 17 00:00:00 2001 From: Gregory Schmidt Date: Sun, 22 Nov 2020 18:36:08 -0900 Subject: [PATCH 14/24] Further correction of SensorEvent string method --- wyzesense2mqtt/wyzesense.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wyzesense2mqtt/wyzesense.py b/wyzesense2mqtt/wyzesense.py index c19d966..b7901dd 100755 --- a/wyzesense2mqtt/wyzesense.py +++ b/wyzesense2mqtt/wyzesense.py @@ -230,9 +230,9 @@ def __init__(self, mac, timestamp, event_type, event_data): def __str__(self): s = "[%s][%s]" % (self.Timestamp.strftime("%Y-%m-%d %H:%M:%S"), self.MAC) if self.Type == 'alarm': - s += "StateEvent: sensor_type=%s, state=%s, battery=%d, signal=%d" % self.Data + s += "AlarmEvent: sensor_type=%s, state=%s, battery=%d, signal=%d" % self.Data elif self.Type == 'status': - s += "StateEvent: sensor_type=%s, state=%s, battery=%d, signal=%d" % self.Data + s += "StatusEvent: sensor_type=%s, state=%s, battery=%d, signal=%d" % self.Data else: s += "RawEvent: type=%s, data=%s" % (self.Type, bytes_to_hex(self.Data)) return s From 0f710f195420db0bd63070b6739b1755d205d3df Mon Sep 17 00:00:00 2001 From: Raetha Date: Fri, 27 Nov 2020 22:55:45 -0500 Subject: [PATCH 15/24] Handle not having a sensors.yaml file on startup (Fixes #44, Reference PR #45) Logging enhancements (Reference PR #45) Add support to disable HASS discovery topics (Reference PR #45) Refactored configuration initialization to be self-updating Removing a sensor now removes it from config file (Reference PR #45) --- wyzesense2mqtt/samples/config.yaml | 1 + wyzesense2mqtt/wyzesense2mqtt.py | 122 +++++++++++++++++++---------- 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/wyzesense2mqtt/samples/config.yaml b/wyzesense2mqtt/samples/config.yaml index 112f22a..b1c34a4 100644 --- a/wyzesense2mqtt/samples/config.yaml +++ b/wyzesense2mqtt/samples/config.yaml @@ -9,5 +9,6 @@ mqtt_qos: 0 mqtt_retain: true self_topic_root: wyzesense2mqtt hass_topic_root: homeassistant +hass_discovery: true publish_sensor_name: true usb_dongle: auto diff --git a/wyzesense2mqtt/wyzesense2mqtt.py b/wyzesense2mqtt/wyzesense2mqtt.py index 8452566..53ea7a1 100644 --- a/wyzesense2mqtt/wyzesense2mqtt.py +++ b/wyzesense2mqtt/wyzesense2mqtt.py @@ -73,19 +73,31 @@ def init_logging(): # Initialize configuration def init_config(): global CONFIG - LOGGER.debug("Reading configuration...") - if (not os.path.isfile(CONFIG_PATH + MAIN_CONFIG_FILE)): - LOGGER.info("Copying default config file...") - try: - shutil.copy2(SAMPLES_PATH + MAIN_CONFIG_FILE, CONFIG_PATH) - except IOError as error: - LOGGER.error(f"Unable to copy default config file. {str(error)}") - CONFIG = read_yaml_file(CONFIG_PATH + MAIN_CONFIG_FILE) + LOGGER.debug("Initializing configuration...") + + # load base config - allows for auto addition of new settings + if (os.path.isfile(SAMPLES_PATH + MAIN_CONFIG_FILE)): + CONFIG = read_yaml_file(SAMPLES_PATH + MAIN_CONFIG_FILE) + + # load user config over base + if (os.path.isfile(CONFIG_PATH + MAIN_CONFIG_FILE)): + user_config = read_yaml_file(CONFIG_PATH + MAIN_CONFIG_FILE) + CONFIG.update(user_config) + + # fail on no config + if (CONFIG is None): + LOGGER.error(f"Failed to load configuration, please configure.") + exit(1) + + # write updated config file if needed + if (CONFIG != user_config): + LOGGER.info("Writing updated config file") + write_yaml_file(CONFIG_PATH + MAIN_CONFIG_FILE, CONFIG) # Initialize MQTT client connection def init_mqtt_client(): - global MQTT_CLIENT, CONFIG + global MQTT_CLIENT, CONFIG, LOGGER # Configure MQTT Client MQTT_CLIENT = mqtt.Client( @@ -100,6 +112,7 @@ def init_mqtt_client(): MQTT_CLIENT.on_connect = on_connect MQTT_CLIENT.on_disconnect = on_disconnect MQTT_CLIENT.on_message = on_message + MQTT_CLIENT.enable_logger(LOGGER) # Connect to MQTT LOGGER.info(f"Connecting to MQTT host {CONFIG['mqtt_host']}") @@ -136,18 +149,24 @@ def init_wyzesense_dongle(): LOGGER.debug(f" MAC: {WYZESENSE_DONGLE.MAC}," f" VER: {WYZESENSE_DONGLE.Version}," f" ENR: {WYZESENSE_DONGLE.ENR}") - except IOError: - LOGGER.warning(f"No device found on path {CONFIG['usb_dongle']}") + except IOError as error: + LOGGER.warning(f"No device found on path {CONFIG['usb_dongle']}: {str(error)}") # Initialize sensor configuration def init_sensors(): + # Initialize sensor dictionary global SENSORS + SENSORS = dict() + + # Load config file LOGGER.debug("Reading sensors configuration...") if (os.path.isfile(CONFIG_PATH + SENSORS_CONFIG_FILE)): SENSORS = read_yaml_file(CONFIG_PATH + SENSORS_CONFIG_FILE) + sensors_config_file_found = True else: LOGGER.info("No sensors config file found.") + sensors_config_file_found = False # Add invert_state value if missing for sensor_mac in SENSORS: @@ -168,10 +187,16 @@ def init_sensors(): except TimeoutError: pass + # Save sensors file if didn't exist + if (not sensors_config_file_found): + LOGGER.info("Writing Sensors Config File") + write_yaml_file(CONFIG_PATH + SENSORS_CONFIG_FILE, SENSORS) + # Send discovery topics - for sensor_mac in SENSORS: - if (valid_sensor_mac(sensor_mac)): - send_discovery_topics(sensor_mac) + if(CONFIG['hass_discovery']): + for sensor_mac in SENSORS: + if (valid_sensor_mac(sensor_mac)): + send_discovery_topics(sensor_mac) # Validate sensor MAC @@ -211,9 +236,22 @@ def add_sensor_to_config(sensor_mac, sensor_type, sensor_version): write_yaml_file(CONFIG_PATH + SENSORS_CONFIG_FILE, SENSORS) +# Delete sensor from config +def delete_sensor_from_config(sensor_mac): + global SENSORS + try: + del SENSORS[sensor_mac] + except KeyError: + LOGGER.debug(f"{sensor_mac} not found in SENSORS") + + LOGGER.info("Writing Sensors Config File") + write_yaml_file(CONFIG_PATH + SENSORS_CONFIG_FILE, SENSORS) + + # Send discovery topics def send_discovery_topics(sensor_mac): global SENSORS, CONFIG + LOGGER.info(f"Publishing discovery topics for {sensor_mac}") sensor_name = SENSORS[sensor_mac]['name'] @@ -257,13 +295,11 @@ def send_discovery_topics(sensor_mac): for entity, entity_payload in entity_payloads.items(): entity_payload['val_tpl'] = f"{{{{ value_json.{entity} }}}}" entity_payload['uniq_id'] = f"wyzesense_{sensor_mac}_{entity}" - entity_payload['stat_t'] = \ - f"{CONFIG['self_topic_root']}/{sensor_mac}" + entity_payload['stat_t'] = f"{CONFIG['self_topic_root']}/{sensor_mac}" entity_payload['dev'] = device_payload sensor_type = ("binary_sensor" if (entity == "state") else "sensor") - entity_topic = f"{CONFIG['hass_topic_root']}/{sensor_type}/" \ - f"wyzesense_{sensor_mac}/{entity}/config" + entity_topic = f"{CONFIG['hass_topic_root']}/{sensor_type}/wyzesense_{sensor_mac}/{entity}/config" MQTT_CLIENT.publish( entity_topic, payload=json.dumps(entity_payload), @@ -286,23 +322,26 @@ def clear_topics(sensor_mac): retain=CONFIG['mqtt_retain'] ) - entity_types = ['state', 'signal_strength', 'battery'] - for entity_type in entity_types: - sensor_type = ("binary_sensor" if (entity_type == "state") - else "sensor") - entity_topic = f"{CONFIG['hass_topic_root']}/{sensor_type}/" \ - f"wyzesense_{sensor_mac}/{entity_type}/config" - MQTT_CLIENT.publish( - entity_topic, - payload=None, - qos=CONFIG['mqtt_qos'], - retain=CONFIG['mqtt_retain'] - ) + # clear discovery topics if configured + if(CONFIG['hass_discovery']): + entity_types = ['state', 'signal_strength', 'battery'] + for entity_type in entity_types: + sensor_type = ( + "binary_sensor" if (entity_type == "state") + else "sensor" + ) + entity_topic = f"{CONFIG['hass_topic_root']}/{sensor_type}/wyzesense_{sensor_mac}/{entity_type}/config" + MQTT_CLIENT.publish( + entity_topic, + payload=None, + qos=CONFIG['mqtt_qos'], + retain=CONFIG['mqtt_retain'] + ) def on_connect(MQTT_CLIENT, userdata, flags, rc): global CONFIG - if rc == 0: + if rc == mqtt.MQTT_ERR_SUCCESS: MQTT_CLIENT.subscribe( [(SCAN_TOPIC, CONFIG['mqtt_qos']), (REMOVE_TOPIC, CONFIG['mqtt_qos']), @@ -311,16 +350,14 @@ def on_connect(MQTT_CLIENT, userdata, flags, rc): MQTT_CLIENT.message_callback_add(SCAN_TOPIC, on_message_scan) MQTT_CLIENT.message_callback_add(REMOVE_TOPIC, on_message_remove) MQTT_CLIENT.message_callback_add(RELOAD_TOPIC, on_message_reload) - LOGGER.info(f"Connected to MQTT: return code {str(rc)}") - elif rc == 3: - LOGGER.warning(f"Connect to MQTT failed: server unavailable {str(rc)}") + LOGGER.info(f"Connected to MQTT: {mqtt.error_string(rc)}") else: - LOGGER.warning(f"Connect to MQTT failed: return code {str(rc)}") + LOGGER.warning(f"Connect to MQTT failed: {mqtt.error_string(rc)}") exit(1) def on_disconnect(MQTT_CLIENT, userdata, rc): - LOGGER.info(f"Disconnected from MQTT: return code {str(rc)}") + LOGGER.info(f"Disconnected from MQTT: {mqtt.error_string(rc)}") MQTT_CLIENT.message_callback_remove(SCAN_TOPIC) MQTT_CLIENT.message_callback_remove(REMOVE_TOPIC) MQTT_CLIENT.message_callback_remove(RELOAD_TOPIC) @@ -349,7 +386,8 @@ def on_message_scan(MQTT_CLIENT, userdata, msg): sensor_type, sensor_version ) - send_discovery_topics(sensor_mac) + if(CONFIG['hass_discovery']): + send_discovery_topics(sensor_mac) else: LOGGER.debug(f"Invalid sensor found: {sensor_mac}") else: @@ -367,6 +405,7 @@ def on_message_remove(MQTT_CLIENT, userdata, msg): try: WYZESENSE_DONGLE.Delete(sensor_mac) clear_topics(sensor_mac) + delete_sensor_from_config(sensor_mac) except TimeoutError: pass else: @@ -401,7 +440,8 @@ def on_event(WYZESENSE_DONGLE, event): # Add sensor if it doesn't already exist if (event.MAC not in SENSORS): add_sensor_to_config(event.MAC, sensor_type, None) - send_discovery_topics(event.MAC) + if(CONFIG['hass_discovery']): + send_discovery_topics(event.MAC) # Build event payload event_payload = { @@ -414,7 +454,7 @@ def on_event(WYZESENSE_DONGLE, event): 'battery': sensor_battery } - if (CONFIG.get('publish_sensor_name')): + if (CONFIG['publish_sensor_name']): event_payload['name'] = SENSORS[event.MAC]['name'] # Set state depending on state string and `invert_state` setting. @@ -429,12 +469,14 @@ def on_event(WYZESENSE_DONGLE, event): LOGGER.debug(event_payload) state_topic = f"{CONFIG['self_topic_root']}/{event.MAC}" - MQTT_CLIENT.publish( + mqtt_message_info = MQTT_CLIENT.publish( state_topic, payload=json.dumps(event_payload), qos=CONFIG['mqtt_qos'], retain=CONFIG['mqtt_retain'] ) + if (mqtt_message_info.rc != mqtt.MQTT_ERR_SUCCESS): + LOGGER.warning(f"on_event publish error: {mqtt.error_string(mqtt_message_info.rc)}") else: LOGGER.debug(f"Non-state event data: {event}") From 556b524712744a131b9a7b3cd9694f74f4750c70 Mon Sep 17 00:00:00 2001 From: Raetha Date: Fri, 27 Nov 2020 23:48:22 -0500 Subject: [PATCH 16/24] Set Python and shell scripts as executable (Fixes #48) Added documentation to create config and logs folders for Systemd install (Fixes #48) --- readme.md | 4 +++- wyzesense2mqtt/bridge_tool_cli.py | 0 wyzesense2mqtt/service.sh | 0 wyzesense2mqtt/wyzesense2mqtt.py | 0 4 files changed, 3 insertions(+), 1 deletion(-) mode change 100644 => 100755 wyzesense2mqtt/bridge_tool_cli.py mode change 100644 => 100755 wyzesense2mqtt/service.sh mode change 100644 => 100755 wyzesense2mqtt/wyzesense2mqtt.py diff --git a/readme.md b/readme.md index 7b80d22..0560212 100644 --- a/readme.md +++ b/readme.md @@ -85,11 +85,13 @@ The gateway can also be run as a systemd service for those not wanting to use Do cd /tmp git clone https://github.com/raetha/wyzesense2mqtt.git ``` -3. Create local application folder (Select a location that works for you, example uses /wyzesense2mqtt) +3. Create local application folders (Select a location that works for you, example uses /wyzesense2mqtt) ```bash mv /tmp/wyzesense2mqtt/wyzesense2mqtt /wyzesense2mqtt rm -rf /tmp/wyzesense2mqtt cd /wyzesense2mqtt +mkdir config +mkdir logs ``` 4. Prepare config.yaml file. You must set MQTT host parameters! Username and password can be blank if unused. (see sample below) ```bash diff --git a/wyzesense2mqtt/bridge_tool_cli.py b/wyzesense2mqtt/bridge_tool_cli.py old mode 100644 new mode 100755 diff --git a/wyzesense2mqtt/service.sh b/wyzesense2mqtt/service.sh old mode 100644 new mode 100755 diff --git a/wyzesense2mqtt/wyzesense2mqtt.py b/wyzesense2mqtt/wyzesense2mqtt.py old mode 100644 new mode 100755 From a1e353125e2b538f06d3ceea56640fc9643905f6 Mon Sep 17 00:00:00 2001 From: Raetha Date: Sat, 28 Nov 2020 00:13:26 -0500 Subject: [PATCH 17/24] Added command for how to test the devel branch --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 0560212..a65a678 100644 --- a/readme.md +++ b/readme.md @@ -84,6 +84,8 @@ The gateway can also be run as a systemd service for those not wanting to use Do ```bash cd /tmp git clone https://github.com/raetha/wyzesense2mqtt.git +# Use the below command instead if you want to help test the devel branch +# git clone -branch devel https://github.com/raetha/wyzesense2mqtt.git ``` 3. Create local application folders (Select a location that works for you, example uses /wyzesense2mqtt) ```bash From 1a1509d7c9c2b6c6349a0395a9d0232de67649c7 Mon Sep 17 00:00:00 2001 From: Raetha Date: Sat, 28 Nov 2020 00:21:24 -0500 Subject: [PATCH 18/24] Additional fixes for Systemd install instructions --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index a65a678..9499d01 100644 --- a/readme.md +++ b/readme.md @@ -78,7 +78,7 @@ docker-compose up -d ### Linux Systemd -The gateway can also be run as a systemd service for those not wanting to use Docker. Requires Python 3.6 or newer. +The gateway can also be run as a systemd service for those not wanting to use Docker. Requires Python 3.6 or newer. You may need to do all commands as root, depending on filesystem permissions. 1. Plug Wyze Sense Bridge into USB port on Linux host. 2. Pull down a copy of the repository ```bash @@ -112,7 +112,7 @@ vim config/sensors.yaml ``` 7. Install dependencies ```bash -pip3 install -r requirements.txt +sudo pip3 install -r requirements.txt ``` 8. Start the service. ```bash From 02ac6e04398e6f477fff1ba4924c597f704f2fe2 Mon Sep 17 00:00:00 2001 From: Raetha Date: Sat, 28 Nov 2020 00:56:42 -0500 Subject: [PATCH 19/24] One more systemd fix --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 9499d01..53e59b2 100644 --- a/readme.md +++ b/readme.md @@ -114,13 +114,14 @@ vim config/sensors.yaml ```bash sudo pip3 install -r requirements.txt ``` -8. Start the service. +8. Configure the service ```bash vim wyzesense2mqtt.service # Only modify if not using default application path sudo cp wyzesense2mqtt.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl start wyzesense2mqtt sudo systemctl status wyzesense2mqtt +sudo systemctl enable wyzesense2mqtt # Enable start on reboot ``` 9. Pair sensors following [instructions below](#pairing-a-sensor). You do NOT need to re-pair sensors that were already paired, they should be found automatically on start and added to the config file with default values, but the sensor version will be unknown. From b0dd0d42f588e12709d75dd9275d5e6de65d2a73 Mon Sep 17 00:00:00 2001 From: Raetha Date: Sat, 28 Nov 2020 01:03:16 -0500 Subject: [PATCH 20/24] Fix for git clone command --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 53e59b2..8f0cfb7 100644 --- a/readme.md +++ b/readme.md @@ -85,7 +85,7 @@ The gateway can also be run as a systemd service for those not wanting to use Do cd /tmp git clone https://github.com/raetha/wyzesense2mqtt.git # Use the below command instead if you want to help test the devel branch -# git clone -branch devel https://github.com/raetha/wyzesense2mqtt.git +# git clone -b devel https://github.com/raetha/wyzesense2mqtt.git ``` 3. Create local application folders (Select a location that works for you, example uses /wyzesense2mqtt) ```bash From 78793d70bcde3e8671b21828a14658d2778bce46 Mon Sep 17 00:00:00 2001 From: Raetha Date: Sat, 28 Nov 2020 01:06:02 -0500 Subject: [PATCH 21/24] Updated example config file with new hass_discovery option --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 8f0cfb7..845d3ff 100644 --- a/readme.md +++ b/readme.md @@ -143,6 +143,7 @@ mqtt_qos: 2 mqtt_retain: true self_topic_root: wyzesense2mqtt hass_topic_root: homeassistant +hass_discovery: true publish_sensor_name: true usb_dongle: auto ``` From 8236094a4fa1a11c500408f4f3da2b686bae3c3e Mon Sep 17 00:00:00 2001 From: Elias Hunt Date: Sat, 28 Nov 2020 01:17:41 -0500 Subject: [PATCH 22/24] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..70c1b24 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,67 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master, devel ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master, devel ] + schedule: + - cron: '40 14 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 9981434698d1c95a4fdb24f48bc02e96fdf2f542 Mon Sep 17 00:00:00 2001 From: Raetha Date: Sun, 29 Nov 2020 21:14:59 -0500 Subject: [PATCH 23/24] Removed exit in on_connect function that might have caused MQTT connection issues (#46) Prepared for alternate MQTT connection if problems stay (currently commented out) (#46) Moved MQTT publish to separate function with logging added (#46) Tweaked main script to properly close MQTT and USB Dongle connections on SIGINT --- wyzesense2mqtt/wyzesense2mqtt.py | 138 ++++++++++++++++--------------- 1 file changed, 73 insertions(+), 65 deletions(-) diff --git a/wyzesense2mqtt/wyzesense2mqtt.py b/wyzesense2mqtt/wyzesense2mqtt.py index d1f5241..0b3232a 100755 --- a/wyzesense2mqtt/wyzesense2mqtt.py +++ b/wyzesense2mqtt/wyzesense2mqtt.py @@ -10,6 +10,10 @@ import subprocess import yaml +# Used for alternate MQTT connection method +# import signal +# import time + import paho.mqtt.client as mqtt import wyzesense from retrying import retry @@ -98,16 +102,12 @@ def init_config(): # Initialize MQTT client connection def init_mqtt_client(): global MQTT_CLIENT, CONFIG, LOGGER + # Used for alternate MQTT connection method + # mqtt.Client.connected_flag = False # Configure MQTT Client - MQTT_CLIENT = mqtt.Client( - client_id=CONFIG['mqtt_client_id'], - clean_session=CONFIG['mqtt_clean_session'] - ) - MQTT_CLIENT.username_pw_set( - username=CONFIG['mqtt_username'], - password=CONFIG['mqtt_password'] - ) + MQTT_CLIENT = mqtt.Client(client_id=CONFIG['mqtt_client_id'], clean_session=CONFIG['mqtt_clean_session']) + MQTT_CLIENT.username_pw_set(username=CONFIG['mqtt_username'], password=CONFIG['mqtt_password']) MQTT_CLIENT.reconnect_delay_set(min_delay=1, max_delay=120) MQTT_CLIENT.on_connect = on_connect MQTT_CLIENT.on_disconnect = on_disconnect @@ -116,11 +116,12 @@ def init_mqtt_client(): # Connect to MQTT LOGGER.info(f"Connecting to MQTT host {CONFIG['mqtt_host']}") - MQTT_CLIENT.connect( - CONFIG['mqtt_host'], - port=CONFIG['mqtt_port'], - keepalive=CONFIG['mqtt_keepalive'] - ) + MQTT_CLIENT.connect_async(CONFIG['mqtt_host'], port=CONFIG['mqtt_port'], keepalive=CONFIG['mqtt_keepalive']) + + # Used for alternate MQTT connection method + # MQTT_CLIENT.loop_start() + # while (not MQTT_CLIENT.connected_flag): + # time.sleep(1) # Retry forever on IO Error @@ -146,9 +147,10 @@ def init_wyzesense_dongle(): LOGGER.info(f"Connecting to dongle {CONFIG['usb_dongle']}") try: WYZESENSE_DONGLE = wyzesense.Open(CONFIG['usb_dongle'], on_event) - LOGGER.debug(f" MAC: {WYZESENSE_DONGLE.MAC}," - f" VER: {WYZESENSE_DONGLE.Version}," - f" ENR: {WYZESENSE_DONGLE.ENR}") + LOGGER.debug(f"Dongle {CONFIG['usb_dongle']}: [" + f" MAC: {WYZESENSE_DONGLE.MAC}," + f" VER: {WYZESENSE_DONGLE.Version}," + f" ENR: {WYZESENSE_DONGLE.ENR}]") except IOError as error: LOGGER.warning(f"No device found on path {CONFIG['usb_dongle']}: {str(error)}") @@ -248,6 +250,19 @@ def delete_sensor_from_config(sensor_mac): write_yaml_file(CONFIG_PATH + SENSORS_CONFIG_FILE, SENSORS) +# Publish MQTT topic +def mqtt_publish(mqtt_topic, mqtt_payload): + global MQTT_CLIENT, CONFIG + mqtt_message_info = MQTT_CLIENT.publish( + mqtt_topic, + payload=json.dumps(mqtt_payload), + qos=CONFIG['mqtt_qos'], + retain=CONFIG['mqtt_retain'] + ) + if (mqtt_message_info.rc != mqtt.MQTT_ERR_SUCCESS): + LOGGER.warning(f"MQTT publish error: {mqtt.error_string(mqtt_message_info.rc)}") + + # Send discovery topics def send_discovery_topics(sensor_mac): global SENSORS, CONFIG @@ -300,12 +315,7 @@ def send_discovery_topics(sensor_mac): sensor_type = ("binary_sensor" if (entity == "state") else "sensor") entity_topic = f"{CONFIG['hass_topic_root']}/{sensor_type}/wyzesense_{sensor_mac}/{entity}/config" - MQTT_CLIENT.publish( - entity_topic, - payload=json.dumps(entity_payload), - qos=CONFIG['mqtt_qos'], - retain=CONFIG['mqtt_retain'] - ) + mqtt_publish(entity_topic, entity_payload) LOGGER.debug(f" {entity_topic}") LOGGER.debug(f" {json.dumps(entity_payload)}") @@ -315,12 +325,7 @@ def clear_topics(sensor_mac): global CONFIG LOGGER.info("Clearing sensor topics") state_topic = f"{CONFIG['self_topic_root']}/{sensor_mac}" - MQTT_CLIENT.publish( - state_topic, - payload=None, - qos=CONFIG['mqtt_qos'], - retain=CONFIG['mqtt_retain'] - ) + mqtt_publish(state_topic, None) # clear discovery topics if configured if(CONFIG['hass_discovery']): @@ -331,12 +336,7 @@ def clear_topics(sensor_mac): else "sensor" ) entity_topic = f"{CONFIG['hass_topic_root']}/{sensor_type}/wyzesense_{sensor_mac}/{entity_type}/config" - MQTT_CLIENT.publish( - entity_topic, - payload=None, - qos=CONFIG['mqtt_qos'], - retain=CONFIG['mqtt_retain'] - ) + mqtt_publish(entity_topic, None) def on_connect(MQTT_CLIENT, userdata, flags, rc): @@ -350,17 +350,20 @@ def on_connect(MQTT_CLIENT, userdata, flags, rc): MQTT_CLIENT.message_callback_add(SCAN_TOPIC, on_message_scan) MQTT_CLIENT.message_callback_add(REMOVE_TOPIC, on_message_remove) MQTT_CLIENT.message_callback_add(RELOAD_TOPIC, on_message_reload) + # Used for alternate MQTT connection method + # MQTT_CLIENT.connected_flag = True LOGGER.info(f"Connected to MQTT: {mqtt.error_string(rc)}") else: - LOGGER.warning(f"Connect to MQTT failed: {mqtt.error_string(rc)}") - exit(1) + LOGGER.warning(f"Connection to MQTT failed: {mqtt.error_string(rc)}") def on_disconnect(MQTT_CLIENT, userdata, rc): - LOGGER.info(f"Disconnected from MQTT: {mqtt.error_string(rc)}") MQTT_CLIENT.message_callback_remove(SCAN_TOPIC) MQTT_CLIENT.message_callback_remove(REMOVE_TOPIC) MQTT_CLIENT.message_callback_remove(RELOAD_TOPIC) + # Used for alternate MQTT connection method + # MQTT_CLIENT.connected_flag = False + LOGGER.info(f"Disconnected from MQTT: {mqtt.error_string(rc)}") # Process messages @@ -445,7 +448,7 @@ def on_event(WYZESENSE_DONGLE, event): # Build event payload event_payload = { - 'event' : event.Type, + 'event': event.Type, 'available': True, 'mac': event.MAC, 'device_class': DEVICE_CLASSES.get(sensor_type), @@ -463,21 +466,12 @@ def on_event(WYZESENSE_DONGLE, event): # State OFF ^ NOT Inverted = False # State ON ^ Inverted = False # State OFF ^ Inverted = True - event_payload['state'] = int( - (sensor_state in STATES_ON) - ^ (SENSORS[event.MAC].get('invert_state'))) + event_payload['state'] = int((sensor_state in STATES_ON) ^ (SENSORS[event.MAC].get('invert_state'))) LOGGER.debug(event_payload) state_topic = f"{CONFIG['self_topic_root']}/{event.MAC}" - mqtt_message_info = MQTT_CLIENT.publish( - state_topic, - payload=json.dumps(event_payload), - qos=CONFIG['mqtt_qos'], - retain=CONFIG['mqtt_retain'] - ) - if (mqtt_message_info.rc != mqtt.MQTT_ERR_SUCCESS): - LOGGER.warning(f"on_event publish error: {mqtt.error_string(mqtt_message_info.rc)}") + mqtt_publish(state_topic, event_payload) else: LOGGER.debug(f"Non-state event data: {event}") @@ -486,25 +480,39 @@ def on_event(WYZESENSE_DONGLE, event): LOGGER.warning(f"Event data: {event}") -# Initialize logging -init_logging() +if __name__ == "__main__": + # Initialize logging + init_logging() -# Initialize configuration -init_config() + # Initialize configuration + init_config() -# Set MQTT Topics -SCAN_TOPIC = f"{CONFIG['self_topic_root']}/scan" -REMOVE_TOPIC = f"{CONFIG['self_topic_root']}/remove" -RELOAD_TOPIC = f"{CONFIG['self_topic_root']}/reload" + # Set MQTT Topics + SCAN_TOPIC = f"{CONFIG['self_topic_root']}/scan" + REMOVE_TOPIC = f"{CONFIG['self_topic_root']}/remove" + RELOAD_TOPIC = f"{CONFIG['self_topic_root']}/reload" -# Initialize MQTT client connection -init_mqtt_client() + # Initialize MQTT client connection + init_mqtt_client() -# Initialize USB dongle -init_wyzesense_dongle() + # Initialize USB dongle + init_wyzesense_dongle() -# Initialize sensor configuration -init_sensors() + # Initialize sensor configuration + init_sensors() + + # Loop forever until keyboard interrupt or SIGINT + try: + while True: + MQTT_CLIENT.loop_forever(retry_first_connection=False) + + # Used for alternate MQTT connection method + # signal.pause() + except KeyboardInterrupt: + pass + finally: + # Used with alternate MQTT connection method + # MQTT_CLIENT.loop_stop() -# MQTT client loop forever -MQTT_CLIENT.loop_forever(retry_first_connection=True) + MQTT_CLIENT.disconnect() + WYZESENSE_DONGLE.Stop() From 185cc2cb5f90be79b6c8a987c5fff1ca085f860e Mon Sep 17 00:00:00 2001 From: Raetha Date: Sun, 29 Nov 2020 21:53:27 -0500 Subject: [PATCH 24/24] Minor logging tweaks around adding/removing sensors --- wyzesense2mqtt/wyzesense2mqtt.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/wyzesense2mqtt/wyzesense2mqtt.py b/wyzesense2mqtt/wyzesense2mqtt.py index 0b3232a..f402017 100755 --- a/wyzesense2mqtt/wyzesense2mqtt.py +++ b/wyzesense2mqtt/wyzesense2mqtt.py @@ -203,7 +203,7 @@ def init_sensors(): # Validate sensor MAC def valid_sensor_mac(sensor_mac): - LOGGER.debug(f"sensor_mac: {sensor_mac}") + #LOGGER.debug(f"Validating MAC: {sensor_mac}") invalid_mac_list = [ "00000000", "\0\0\0\0\0\0\0\0", @@ -224,6 +224,7 @@ def valid_sensor_mac(sensor_mac): # Add sensor to config def add_sensor_to_config(sensor_mac, sensor_type, sensor_version): global SENSORS + LOGGER.info(f"Adding sensor to config: {sensor_mac}") SENSORS[sensor_mac] = dict() SENSORS[sensor_mac]['name'] = f"Wyze Sense {sensor_mac}" SENSORS[sensor_mac]['class'] = ( @@ -234,21 +235,19 @@ def add_sensor_to_config(sensor_mac, sensor_type, sensor_version): if (sensor_version is not None): SENSORS[sensor_mac]['sw_version'] = sensor_version - LOGGER.info("Writing Sensors Config File") write_yaml_file(CONFIG_PATH + SENSORS_CONFIG_FILE, SENSORS) # Delete sensor from config def delete_sensor_from_config(sensor_mac): global SENSORS + LOGGER.info(f"Deleting sensor from config: {sensor_mac}") try: del SENSORS[sensor_mac] + write_yaml_file(CONFIG_PATH + SENSORS_CONFIG_FILE, SENSORS) except KeyError: LOGGER.debug(f"{sensor_mac} not found in SENSORS") - LOGGER.info("Writing Sensors Config File") - write_yaml_file(CONFIG_PATH + SENSORS_CONFIG_FILE, SENSORS) - # Publish MQTT topic def mqtt_publish(mqtt_topic, mqtt_payload):