Skip to content

Commit 0b62031

Browse files
committed
Add aqara locks
1 parent 5e400bf commit 0b62031

File tree

8 files changed

+374
-4
lines changed

8 files changed

+374
-4
lines changed

custom_components/ble_monitor/binary_sensor.py

+28-4
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,6 @@ async def async_added_to_hass(self):
256256
self._state = True
257257
elif old_state.state == STATE_OFF:
258258
self._state = False
259-
if "status" in old_state.attributes:
260-
self._extra_state_attributes["status"] = old_state.attributes[
261-
"status"
262-
]
263259
if "rssi" in old_state.attributes:
264260
self._extra_state_attributes["rssi"] = old_state.attributes["rssi"]
265261
if "firmware" in old_state.attributes:
@@ -272,6 +268,25 @@ async def async_added_to_hass(self):
272268
self._extra_state_attributes[ATTR_BATTERY_LEVEL] = old_state.attributes[
273269
ATTR_BATTERY_LEVEL
274270
]
271+
if "status" in old_state.attributes:
272+
self._extra_state_attributes["status"] = old_state.attributes["status"]
273+
if "motion timer" in old_state.attributes:
274+
self._extra_state_attributes["motion timer"] = old_state.attributes[
275+
"motion timer"
276+
]
277+
if "action" in old_state.attributes:
278+
self._extra_state_attributes["action"] = old_state.attributes["action"]
279+
if "method" in old_state.attributes:
280+
self._extra_state_attributes["method"] = old_state.attributes["method"]
281+
if "error" in old_state.attributes:
282+
self._extra_state_attributes["error"] = old_state.attributes["error"]
283+
if "key id" in old_state.attributes:
284+
self._extra_state_attributes["key id"] = old_state.attributes["key id"]
285+
if "timestamp" in old_state.attributes:
286+
self._extra_state_attributes["timestamp"] = old_state.attributes["timestamp"]
287+
if "result" in old_state.attributes:
288+
self._extra_state_attributes["result"] = old_state.attributes["result"]
289+
275290
self.ready_for_update = True
276291

277292
@property
@@ -358,6 +373,15 @@ def collect(self, data, batt_attr=None):
358373
self._extra_state_attributes["status"] = self._newstate
359374
if self.entity_description.key == "opening":
360375
self._extra_state_attributes["status"] = data["status"]
376+
if self.entity_description.key == "lock":
377+
self._extra_state_attributes["action"] = data["action"]
378+
self._extra_state_attributes["method"] = data["method"]
379+
self._extra_state_attributes["error"] = data["error"]
380+
self._extra_state_attributes["key id"] = data["key id"]
381+
self._extra_state_attributes["timestamp"] = data["timestamp"]
382+
if self.entity_description.key == "fingerprint":
383+
self._extra_state_attributes["result"] = data["result"]
384+
self._extra_state_attributes["key id"] = data["key id"]
361385

362386
async def async_update(self):
363387
"""Update sensor state and attribute."""

custom_components/ble_monitor/ble_parser/xiaomi.py

+135
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import struct
55
from Cryptodome.Cipher import AES
66

7+
from homeassistant.util import datetime
8+
79
_LOGGER = logging.getLogger(__name__)
810

911
# Device type dictionary
@@ -46,6 +48,8 @@
4648
0x04E6: "YLYK01YL-VENFAN",
4749
0x03BF: "YLYB01YL-BHFRC",
4850
0x03B6: "YLKG07YL/YLKG08YL",
51+
0x069E: "ZNMS16LM",
52+
0x069F: "ZNMS17LM",
4953
}
5054

5155
# Structured objects for data conversions
@@ -61,17 +65,146 @@
6165
P_STRUCT = struct.Struct("<H")
6266
BUTTON_STRUCT = struct.Struct("<BBB")
6367

68+
# Definition of lock messages
69+
BLE_LOCK_ERROR = {
70+
0xC0DE0000: "frequent unlocking with incorrect password",
71+
0xC0DE0001: "frequent unlocking with wrong fingerprints",
72+
0xC0DE0002: "operation timeout (password input timeout)",
73+
0xC0DE0003: "lock picking",
74+
0xC0DE0004: "reset button is pressed",
75+
0xC0DE0005: "the wrong key is frequently unlocked",
76+
0xC0DE0006: "foreign body in the keyhole",
77+
0xC0DE0007: "the key has not been taken out",
78+
0xC0DE0008: "error NFC frequently unlocks",
79+
0xC0DE0009: "timeout is not locked as required",
80+
0xC0DE000A: "failure to unlock frequently in multiple ways",
81+
0xC0DE000B: "unlocking the face frequently fails",
82+
0xC0DE000C: "failure to unlock the vein frequently",
83+
0xC0DE000D: "hijacking alarm",
84+
0xC0DE000E: "unlock inside the door after arming",
85+
0xC0DE000F: "palmprints frequently fail to unlock",
86+
0xC0DE0010: "the safe was moved",
87+
0xC0DE1000: "the battery level is less than 10%",
88+
0xC0DE1001: "the battery is less than 5%",
89+
0xC0DE1002: "the fingerprint sensor is abnormal",
90+
0xC0DE1003: "the accessory battery is low",
91+
0xC0DE1004: "mechanical failure",
92+
0xC0DE1005: "the lock sensor is faulty",
93+
}
94+
95+
BLE_LOCK_ACTION = {
96+
0b0000: [0, "unlock outside the door"],
97+
0b0001: [1, "lock"],
98+
0b0010: [1, "turn on anti-lock"],
99+
0b0011: [0, "turn off anti-lock"],
100+
0b0100: [0, "unlock inside the door"],
101+
0b0101: [1, "lock inside the door"],
102+
0b0110: [1, "turn on child lock"],
103+
0b0111: [0, "turn off child lock"],
104+
0b1000: [1, "lock outside the door"],
105+
0b1111: [0, "abnormal"],
106+
}
107+
108+
BLE_LOCK_METHOD = {
109+
0b0000: "bluetooth",
110+
0b0001: "password",
111+
0b0010: "biometrics",
112+
0b0011: "key",
113+
0b0100: "turntable",
114+
0b0101: "nfc",
115+
0b0110: "one-time password",
116+
0b0111: "two-step verification",
117+
0b1001: "Homekit",
118+
0b1000: "coercion",
119+
0b1010: "manual",
120+
0b1011: "automatic",
121+
0b1111: "abnormal"
122+
}
123+
64124

65125
# Advertisement conversion of measurement data
66126
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition
67127
def obj0003(xobj):
68128
return {"motion": xobj[0], "motion timer": xobj[0]}
69129

70130

131+
def obj0006(xobj):
132+
if len(xobj) == 5:
133+
key_id = xobj[0:4]
134+
match_byte = xobj[4]
135+
if key_id == b'\x00\x00\x00\x00':
136+
key_id = "administrator"
137+
elif key_id == b'\xff\xff\xff\xff':
138+
key_id = "unknown operator"
139+
else:
140+
key_id = int.from_bytes(key_id, 'little')
141+
if match_byte == 0x00:
142+
result = "match successful"
143+
elif match_byte == 0x01:
144+
result = "match failed"
145+
elif match_byte == 0x02:
146+
result = "timeout"
147+
elif match_byte == 0x033:
148+
result = "low quality (too light, fuzzy)"
149+
elif match_byte == 0x04:
150+
result = "insufficient area"
151+
elif match_byte == 0x05:
152+
result = "skin is too dry"
153+
elif match_byte == 0x06:
154+
result = "skin is too wet"
155+
else:
156+
result = None
157+
158+
fingerprint = 1 if match_byte == 0x00 else 0
159+
160+
return {
161+
"fingerprint": fingerprint,
162+
"result": result,
163+
"key id": key_id,
164+
}
165+
else:
166+
return {}
167+
168+
71169
def obj0010(xobj):
72170
return {"toothbrush mode": xobj[1]}
73171

74172

173+
def obj000b(xobj):
174+
if len(xobj) == 9:
175+
action = xobj[0] & 0x0F
176+
method = xobj[0] >> 4
177+
key_id = int.from_bytes(xobj[1:5], 'little')
178+
timestamp = int.from_bytes(xobj[5:], 'little')
179+
180+
timestamp = datetime.fromtimestamp(timestamp).isoformat()
181+
182+
# all keys except Bluetooth have only 65536 values
183+
error = BLE_LOCK_ERROR.get(key_id)
184+
if error is None and method > 0:
185+
key_id &= 0xFFFF
186+
elif error:
187+
key_id = hex(key_id)
188+
189+
if action not in BLE_LOCK_ACTION or method not in BLE_LOCK_METHOD:
190+
return {}
191+
192+
lock = BLE_LOCK_ACTION[action][0]
193+
action = BLE_LOCK_ACTION[action][1]
194+
method = BLE_LOCK_METHOD[method]
195+
196+
return {
197+
"lock": lock,
198+
"action": action,
199+
"method": method,
200+
"error": error,
201+
"key id": key_id,
202+
"timestamp": timestamp,
203+
}
204+
else:
205+
return {}
206+
207+
75208
def obj000f(xobj):
76209
if len(xobj) == 3:
77210
(value,) = LIGHT_STRUCT.unpack(xobj + b'\x00')
@@ -370,7 +503,9 @@ def obj2000(xobj):
370503
# {dataObject_id: (converter}
371504
xiaomi_dataobject_dict = {
372505
0x0003: obj0003,
506+
0x0006: obj0006,
373507
0x0010: obj0010,
508+
0x000B: obj000b,
374509
0x000F: obj000f,
375510
0x1001: obj1001,
376511
0x1004: obj1004,

custom_components/ble_monitor/const.py

+20
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
DEVICE_CLASS_MOTION,
1010
DEVICE_CLASS_OPENING,
1111
DEVICE_CLASS_SMOKE,
12+
DEVICE_CLASS_LOCK,
1213
BinarySensorEntityDescription,
1314
)
1415
from homeassistant.components.sensor import (
@@ -185,6 +186,21 @@ class BLEMonitorBinarySensorEntityDescription(
185186
unique_id="sd_",
186187
device_class=DEVICE_CLASS_SMOKE,
187188
),
189+
BLEMonitorBinarySensorEntityDescription(
190+
key="fingerprint",
191+
sensor_class="BaseBinarySensor",
192+
name="ble fingerprint",
193+
icon="mdi:fingerprint",
194+
unique_id="fp_",
195+
device_class=None,
196+
),
197+
BLEMonitorBinarySensorEntityDescription(
198+
key="lock",
199+
sensor_class="BaseBinarySensor",
200+
name="ble lock",
201+
unique_id="lock_",
202+
device_class=DEVICE_CLASS_LOCK,
203+
),
188204
)
189205

190206

@@ -534,6 +550,8 @@ class BLEMonitorBinarySensorEntityDescription(
534550
'RTCGQ02LM' : [["battery"], ["button"], ["light", "motion"]],
535551
'MMC-T201-1' : [["temperature", "battery"], [], []],
536552
'M1S-T500' : [["battery"], ["toothbrush mode"], []],
553+
'ZNMS16LM' : [["battery"], [], ["lock", "fingerprint"]],
554+
'ZNMS17LM' : [["battery"], [], ["lock", "fingerprint"]],
537555
'CGC1' : [["temperature", "humidity", "battery"], [], []],
538556
'CGD1' : [["temperature", "humidity", "battery"], [], []],
539557
'CGDK2' : [["temperature", "humidity", "battery"], [], []],
@@ -599,6 +617,8 @@ class BLEMonitorBinarySensorEntityDescription(
599617
'RTCGQ02LM' : 'Xiaomi',
600618
'MMC-T201-1' : 'Xiaomi',
601619
'M1S-T500' : 'Xiaomi Soocas',
620+
'ZNMS16LM' : 'Xiaomi Aqara',
621+
'ZNMS17LM' : 'Xiaomi Aqara',
602622
'CGC1' : 'Qingping',
603623
'CGD1' : 'Qingping',
604624
'CGDK2' : 'Qingping',

custom_components/ble_monitor/test/test_xiaomi_parser.py

+41
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,47 @@ def test_Xiaomi_M1S_T500(self):
324324
assert sensor_msg["toothbrush mode"] == 3
325325
assert sensor_msg["rssi"] == -36
326326

327+
def test_Xiaomi_ZNMS16LM_fingerprint(self):
328+
"""Test Xiaomi parser for ZNMS16LM."""
329+
data_string = "043e2a02010000918aeb441fd71e020106030295fe161695fe50449e0642918aeb441fd7060005ffffffff00a9"
330+
data = bytes(bytearray.fromhex(data_string))
331+
332+
# pylint: disable=unused-variable
333+
ble_parser = BleParser()
334+
sensor_msg, tracker_msg = ble_parser.parse_data(data)
335+
336+
assert sensor_msg["firmware"] == "Xiaomi (MiBeacon V4)"
337+
assert sensor_msg["type"] == "ZNMS16LM"
338+
assert sensor_msg["mac"] == "D71F44EB8A91"
339+
assert sensor_msg["packet"] == 66
340+
assert sensor_msg["data"]
341+
assert sensor_msg["fingerprint"] == 1
342+
assert sensor_msg["result"] == "match successful"
343+
assert sensor_msg["key id"] == "unknown operator"
344+
assert sensor_msg["rssi"] == -87
345+
346+
def test_Xiaomi_ZNMS16LM_lock(self):
347+
"""Test Xiaomi parser for ZNMS16LM."""
348+
data_string = "043e2e02010000918aeb441fd722020106030295fe1a1695fe50449e0643918aeb441fd70b000920020001807c442f61a9"
349+
data = bytes(bytearray.fromhex(data_string))
350+
351+
# pylint: disable=unused-variable
352+
ble_parser = BleParser()
353+
sensor_msg, tracker_msg = ble_parser.parse_data(data)
354+
355+
assert sensor_msg["firmware"] == "Xiaomi (MiBeacon V4)"
356+
assert sensor_msg["type"] == "ZNMS16LM"
357+
assert sensor_msg["mac"] == "D71F44EB8A91"
358+
assert sensor_msg["packet"] == 67
359+
assert sensor_msg["data"]
360+
assert sensor_msg["lock"] == 0
361+
assert sensor_msg["action"] == "unlock outside the door"
362+
assert sensor_msg["method"] == "biometrics"
363+
assert sensor_msg["error"] is None
364+
assert sensor_msg["key id"] == 2
365+
assert sensor_msg["timestamp"] == "2021-09-01T11:14:36"
366+
assert sensor_msg["rssi"] == -87
367+
327368
def test_Xiaomi_YLAI003(self):
328369
"""Test Xiaomi parser for YLAI003."""
329370

docs/_devices/ZNMS16LM.md

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
manufacturer: Xiaomi Aqara
3+
name: Lock N100
4+
model: ZNMS16LM
5+
image: ZNMS16LM.png
6+
physical_description:
7+
broadcasted_properties:
8+
- fingerprint
9+
- lock
10+
- battery
11+
- result
12+
- key id
13+
- action
14+
- method
15+
- error
16+
- timestamp
17+
broadcasted_property_notes:
18+
- property: fingerprint
19+
note: The fingerprint sensor is `On` if the fingerprint scan was succesful, otherwise it is `Off` The fingerprint entity has two extra attributes, `result` and `key id`.
20+
- property: result
21+
note: >
22+
`result` shows the result of the last fingerprint reading and can have the following values:
23+
* match successful
24+
* match failed
25+
* timeout
26+
* low quality (too light, fuzzy)
27+
* insufficient area
28+
* skin is too dry
29+
* skin is too wet
30+
- property: key id
31+
note: >
32+
`key id` is an id number. For the fingerprint sensor, it can also be `administrator` or `unknown operator`
33+
- property: lock
34+
note: The state of the lock depends on the last `action`. The lock entity has five extra attributes, `action`, `method`, `error` and `key id` and `timestamp`
35+
- property: action
36+
note: >
37+
`action` shows the last change in of the lock and can have the followng values:
38+
* unlock outside the door
39+
* lock
40+
* turn on anti-lock
41+
* turn off anti-lock
42+
* unlock inside the door
43+
* lock inside the door
44+
* turn on child lock
45+
* turn off child lock
46+
* lock outside the door
47+
* abnormal
48+
- property: method
49+
note: >
50+
`method` shows the last used locking mechanism and can have the following values:
51+
* unlock outside the door
52+
* lock
53+
* bluetooth
54+
* password
55+
* biometrics
56+
* key
57+
* turntable
58+
* nfc
59+
* one-time password
60+
* two-step verification
61+
* Homekit
62+
* coercion
63+
* manual
64+
* automatic
65+
* abnormal
66+
- property: error
67+
note: The error state of the lock
68+
- property: timestamp
69+
note: The timestamp of the latest lock change
70+
broadcast_rate:
71+
active_scan:
72+
encryption_key:
73+
custom_firmware:
74+
notes: Bluetooth version
75+
---

0 commit comments

Comments
 (0)