Skip to content

Commit 6b61505

Browse files
authored
Merge pull request #485 from custom-components/sensor_entity_description
Add SensorEntityDescription to BLE monitor
2 parents 29b9dac + 54abe9f commit 6b61505

38 files changed

+1609
-1154
lines changed

custom_components/ble_monitor/__init__.py

+131-63
Large diffs are not rendered by default.

custom_components/ble_monitor/binary_sensor.py

+122-223
Large diffs are not rendered by default.

custom_components/ble_monitor/ble_parser/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def parse_data(self, data):
139139
adpayload_start += adstuct_size
140140

141141
# check for monitored device trackers
142-
if mac.lower() in self.tracker_whitelist:
142+
if mac in self.tracker_whitelist:
143143
tracker_data = {
144144
"is connected": True,
145145
"mac": ''.join('{:02X}'.format(x) for x in mac),

custom_components/ble_monitor/ble_parser/atc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def parse_atc(self, data, source_mac, rssi):
100100
return None
101101

102102
# check for MAC presence in sensor whitelist, if needed
103-
if self.discovery is False and atc_mac.lower() not in self.sensor_whitelist:
103+
if self.discovery is False and atc_mac not in self.sensor_whitelist:
104104
return None
105105

106106
try:

custom_components/ble_monitor/ble_parser/brifit.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def parse_brifit(self, data, source_mac, rssi):
4343
return None
4444

4545
# check for MAC presence in sensor whitelist, if needed
46-
if self.discovery is False and brifit_mac.lower() not in self.sensor_whitelist:
46+
if self.discovery is False and brifit_mac not in self.sensor_whitelist:
4747
_LOGGER.debug("Discovery is disabled. MAC: %s is not whitelisted!", to_mac(brifit_mac))
4848
return None
4949

custom_components/ble_monitor/ble_parser/govee.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def parse_govee(self, data, source_mac, rssi):
8787
return None
8888

8989
# check for MAC presence in sensor whitelist, if needed
90-
if self.discovery is False and govee_mac.lower() not in self.sensor_whitelist:
90+
if self.discovery is False and govee_mac not in self.sensor_whitelist:
9191
_LOGGER.debug("Discovery is disabled. MAC: %s is not whitelisted!", to_mac(govee_mac))
9292
return None
9393

custom_components/ble_monitor/ble_parser/inode.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def parse_inode(self, data, source_mac, rssi):
8787
self.lpacket_ids[inode_mac] = packet_id
8888

8989
# check for MAC presence in sensor whitelist, if needed
90-
if self.discovery is False and inode_mac.lower() not in self.sensor_whitelist:
90+
if self.discovery is False and inode_mac not in self.sensor_whitelist:
9191
_LOGGER.debug("Discovery is disabled. MAC: %s is not whitelisted!", to_mac(inode_mac))
9292
return None
9393

custom_components/ble_monitor/ble_parser/kegtron.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def parse_kegtron(self, data, source_mac, rssi):
7474
return None
7575

7676
# check for MAC presence in sensor whitelist, if needed
77-
if self.discovery is False and kegtron_mac.lower() not in self.sensor_whitelist:
77+
if self.discovery is False and kegtron_mac not in self.sensor_whitelist:
7878
_LOGGER.debug("Discovery is disabled. MAC: %s is not whitelisted!", to_mac(kegtron_mac))
7979
return None
8080

custom_components/ble_monitor/ble_parser/miscale.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def parse_miscale(self, data, source_mac, rssi):
9999
return None
100100

101101
# check for MAC presence in sensor whitelist, if needed
102-
if self.discovery is False and miscale_mac.lower() not in self.sensor_whitelist:
102+
if self.discovery is False and miscale_mac not in self.sensor_whitelist:
103103
_LOGGER.debug("Discovery is disabled. MAC: %s is not whitelisted!", to_mac(miscale_mac))
104104
return None
105105

custom_components/ble_monitor/ble_parser/qingping.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def parse_qingping(self, data, source_mac, rssi):
6464
return None
6565

6666
# check for MAC presence in sensor whitelist, if needed
67-
if self.discovery is False and qingping_mac.lower() not in self.sensor_whitelist:
67+
if self.discovery is False and qingping_mac not in self.sensor_whitelist:
6868
_LOGGER.debug("Discovery is disabled. MAC: %s is not whitelisted!", to_mac(qingping_mac))
6969
return None
7070

custom_components/ble_monitor/ble_parser/ruuvitag.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def parse_ruuvitag(self, data, source_mac, rssi):
192192
batt = 0
193193
result["battery"] = round(batt, 1)
194194
# check for MAC presence in sensor whitelist, if needed
195-
if self.discovery is False and ruuvitag_mac.lower() not in self.sensor_whitelist:
195+
if self.discovery is False and ruuvitag_mac not in self.sensor_whitelist:
196196
_LOGGER.debug("Discovery is disabled. MAC: %s is not whitelisted!", to_mac(ruuvitag_mac))
197197
return None
198198
if version < 5:

custom_components/ble_monitor/ble_parser/sensorpush.py

+11-10
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,23 @@
3232
"humidity"
3333
]
3434
}
35-
35+
36+
3637
def decode_values(mfg_data: bytes, device_type_id: int) -> dict:
3738
pack_params = SENSORPUSH_PACK_PARAMS.get(device_type_id, None)
3839
if pack_params is None:
3940
_LOGGER.error("SensorPush device type id %d unknown" % device_type_id)
4041
return {}
41-
42+
4243
values = {}
43-
44+
4445
packed_values = 0
45-
for i in range(1,len(mfg_data)):
46-
packed_values += mfg_data[i] << (8 * (i-1))
47-
46+
for i in range(1, len(mfg_data)):
47+
packed_values += mfg_data[i] << (8 * (i - 1))
48+
4849
mod = 1
4950
div = 1
50-
for i in range(0,len(pack_params)):
51+
for i in range(0, len(pack_params)):
5152
vp = pack_params[i]
5253
min_value = vp[0]
5354
max_value = vp[1]
@@ -60,15 +61,15 @@ def decode_values(mfg_data: bytes, device_type_id: int) -> dict:
6061
value = value / 100.0
6162
values[data_type] = value
6263
div *= int((max_value - min_value) / step + step / 2.0) + 1
63-
64+
6465
return values
6566

6667

6768
def parse_sensorpush(self, data, source_mac, rssi):
6869
result = {"firmware": "SensorPush"}
6970
sensorpush_mac = source_mac
7071
device_type = None
71-
72+
7273
# SensorPush puts encoded data in manufacturer data (0xFF)
7374
adpayload_start = 0
7475
adpayload_size = len(data)
@@ -97,7 +98,7 @@ def parse_sensorpush(self, data, source_mac, rssi):
9798
return None
9899

99100
# check for MAC presence in sensor whitelist, if needed
100-
if self.discovery is False and sensorpush_mac.lower() not in self.sensor_whitelist:
101+
if self.discovery is False and sensorpush_mac not in self.sensor_whitelist:
101102
_LOGGER.debug("Discovery is disabled. MAC: %s is not whitelisted!", to_mac(sensorpush_mac))
102103
return None
103104

custom_components/ble_monitor/ble_parser/teltonika.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ def parse_teltonika(self, data, source_mac, rssi):
2323
device_type = "Blue Puck T"
2424
elif dev_type == "PUCK_TH":
2525
device_type = "Blue Puck RHT"
26+
elif dev_type[0:3] == "C T":
27+
device_type = "Blue Coin T"
28+
elif dev_type[0:3] == "P T":
29+
device_type = "Blue Puck T"
2630
else:
2731
device_type = None
2832
elif adstuct_type == 0x16 and adstuct_size > 4:
@@ -41,15 +45,16 @@ def parse_teltonika(self, data, source_mac, rssi):
4145
if device_type is None:
4246
if self.report_unknown == "Teltonika":
4347
_LOGGER.info(
44-
"BLE ADV from UNKNOWN Teltonika DEVICE: RSSI: %s, MAC: %s, ADV: %s",
48+
"BLE ADV from UNKNOWN Teltonika DEVICE: RSSI: %s, MAC: %s, DEVICE TYPE: %s, ADV: %s",
4549
rssi,
4650
to_mac(source_mac),
51+
dev_type,
4752
data.hex()
4853
)
4954
return None
5055

5156
# check for MAC presence in sensor whitelist, if needed
52-
if self.discovery is False and teltonika_mac.lower() not in self.sensor_whitelist:
57+
if self.discovery is False and teltonika_mac not in self.sensor_whitelist:
5358
_LOGGER.debug("Discovery is disabled. MAC: %s is not whitelisted!", to_mac(teltonika_mac))
5459
return None
5560

custom_components/ble_monitor/ble_parser/thermoplus.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def parse_thermoplus(self, data, source_mac, rssi):
5858
return None
5959

6060
# check for MAC presence in sensor whitelist, if needed
61-
if self.discovery is False and thermoplus_mac.lower() not in self.sensor_whitelist:
61+
if self.discovery is False and thermoplus_mac not in self.sensor_whitelist:
6262
_LOGGER.debug("Discovery is disabled. MAC: %s is not whitelisted!", to_mac(thermoplus_mac))
6363
return None
6464

custom_components/ble_monitor/ble_parser/xiaomi.py

+153-2
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')
@@ -318,7 +451,23 @@ def obj1018(xobj):
318451

319452

320453
def obj1019(xobj):
321-
return {"opening": xobj[0]}
454+
open = xobj[0]
455+
if open == 0:
456+
opening = 1
457+
status = "opened"
458+
elif open == 1:
459+
opening = 0
460+
status = "closed"
461+
elif open == 2:
462+
opening = 1
463+
status = "closing timeout"
464+
elif open == 3:
465+
opening = 1
466+
status = "device reset"
467+
else:
468+
opening = 0
469+
status = None
470+
return {"opening": opening, "status": status}
322471

323472

324473
def obj100a(xobj):
@@ -354,7 +503,9 @@ def obj2000(xobj):
354503
# {dataObject_id: (converter}
355504
xiaomi_dataobject_dict = {
356505
0x0003: obj0003,
506+
0x0006: obj0006,
357507
0x0010: obj0010,
508+
0x000B: obj000b,
358509
0x000F: obj000f,
359510
0x1001: obj1001,
360511
0x1004: obj1004,
@@ -458,7 +609,7 @@ def parse_xiaomi(self, data, source_mac, rssi):
458609
sinfo += ', Standard certification'
459610

460611
# check for MAC presence in sensor whitelist, if needed
461-
if self.discovery is False and xiaomi_mac.lower() not in self.sensor_whitelist:
612+
if self.discovery is False and xiaomi_mac not in self.sensor_whitelist:
462613
_LOGGER.debug("Discovery is disabled. MAC: %s is not whitelisted!", to_mac(xiaomi_mac))
463614
return None
464615

0 commit comments

Comments
 (0)