Skip to content

Commit

Permalink
Merge branch 'devel' into main_battery
Browse files Browse the repository at this point in the history
  • Loading branch information
raetha authored Nov 28, 2020
2 parents f5f95fc + 0f710f1 commit 723d7fa
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 138 deletions.
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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
4 changes: 4 additions & 0 deletions Dockerfile.manual
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 6 additions & 7 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1
1.2
6 changes: 3 additions & 3 deletions wyzesense2mqtt/bridge_tool_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions wyzesense2mqtt/samples/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
110 changes: 63 additions & 47 deletions wyzesense2mqtt/wyzesense.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<None>"


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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 == 'alarm':
Expand All @@ -237,6 +242,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

Expand All @@ -251,7 +257,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 or event_type == 0xA1:
Expand All @@ -261,10 +267,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, ("alarm" if event_type == 0xA2 else "status"), (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)

Expand All @@ -277,7 +293,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))

Expand All @@ -286,12 +302,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,
}

Expand All @@ -313,7 +329,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]

Expand All @@ -335,9 +351,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)

Expand All @@ -346,10 +362,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)
Expand Down Expand Up @@ -377,7 +393,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
Expand All @@ -399,7 +415,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("<LLLL", *r))

resp = self._DoSimpleCommand(Packet.GetEnr(r_string))
Expand All @@ -414,14 +430,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())
Expand Down Expand Up @@ -513,16 +529,16 @@ def Scan(self, timeout=60):
log.debug("Start Scan...")

ctx = self.CmdContext(evt=threading.Event(), result=None)

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())


if ctx.evt.wait(timeout):
s_mac, s_type, s_ver = ctx.result
log.debug("Sensor found: mac=[%s], type=%d, version=%d", s_mac, s_type, s_ver)
Expand Down
Loading

0 comments on commit 723d7fa

Please sign in to comment.