Skip to content

Commit 869ba6a

Browse files
authored
Merge pull request #1533 from M1k0t0/master
Add support for PTX-F1-Display and enhance debugging tools
2 parents e927064 + cf2d36b commit 869ba6a

6 files changed

Lines changed: 380 additions & 3 deletions

File tree

custom_components/ble_monitor/ble_parser/xiaomi.py

100755100644
Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@
8686
0x3E17: "KS1BP",
8787
0x3BD5: "MJTZC01YM",
8888
0x50FB: "ES3",
89-
0x5DB1: "MBS17"
89+
0x5DB1: "MBS17",
90+
0x64C5: "PTX-F1-Display"
9091
}
9192

9293
# Structured objects for data conversions
@@ -1047,6 +1048,30 @@ def obj4e0c(xobj, device_type):
10471048
"one btn switch": "toggle",
10481049
"button switch": "single press",
10491050
}
1051+
elif device_type == "PTX-F1-Display":
1052+
click = xobj[0]
1053+
if click == 1:
1054+
result = {
1055+
"four btn switch 1": "toggle",
1056+
"button switch": "single press",
1057+
}
1058+
elif click == 2:
1059+
result = {
1060+
"four btn switch 2": "toggle",
1061+
"button switch": "single press",
1062+
}
1063+
elif click == 3:
1064+
result = {
1065+
"four btn switch 3": "toggle",
1066+
"button switch": "single press",
1067+
}
1068+
elif click == 4:
1069+
result = {
1070+
"four btn switch 4": "toggle",
1071+
"button switch": "single press",
1072+
}
1073+
else:
1074+
result = None
10501075
else:
10511076
result = {}
10521077
return result
@@ -1077,6 +1102,30 @@ def obj4e0d(xobj, device_type):
10771102
"one btn switch": "toggle",
10781103
"button switch": "double press",
10791104
}
1105+
elif device_type == "PTX-F1-Display":
1106+
click = xobj[0]
1107+
if click == 1:
1108+
result = {
1109+
"four btn switch 1": "toggle",
1110+
"button switch": "double press",
1111+
}
1112+
elif click == 2:
1113+
result = {
1114+
"four btn switch 2": "toggle",
1115+
"button switch": "double press",
1116+
}
1117+
elif click == 3:
1118+
result = {
1119+
"four btn switch 3": "toggle",
1120+
"button switch": "double press",
1121+
}
1122+
elif click == 4:
1123+
result = {
1124+
"four btn switch 4": "toggle",
1125+
"button switch": "double press",
1126+
}
1127+
else:
1128+
result = None
10801129
else:
10811130
result = {}
10821131
return result
@@ -1107,6 +1156,30 @@ def obj4e0e(xobj, device_type):
11071156
"one btn switch": "toggle",
11081157
"button switch": "long press",
11091158
}
1159+
elif device_type == "PTX-F1-Display":
1160+
click = xobj[0]
1161+
if click == 1:
1162+
result = {
1163+
"four btn switch 1": "toggle",
1164+
"button switch": "long press",
1165+
}
1166+
elif click == 2:
1167+
result = {
1168+
"four btn switch 2": "toggle",
1169+
"button switch": "long press",
1170+
}
1171+
elif click == 3:
1172+
result = {
1173+
"four btn switch 3": "toggle",
1174+
"button switch": "long press",
1175+
}
1176+
elif click == 4:
1177+
result = {
1178+
"four btn switch 4": "toggle",
1179+
"button switch": "long press",
1180+
}
1181+
else:
1182+
result = None
11101183
else:
11111184
result = {}
11121185
return result
@@ -1282,6 +1355,20 @@ def obj5a16(xobj):
12821355
return None
12831356

12841357

1358+
def obj6012(xobj):
1359+
"""Humidity"""
1360+
return obj4802(xobj)
1361+
1362+
1363+
def obj605d(xobj):
1364+
"""Temperature"""
1365+
if len(xobj) == 1:
1366+
temp = xobj[0]
1367+
return {"temperature": temp}
1368+
else:
1369+
return {}
1370+
1371+
12851372
def obj6e16(xobj):
12861373
"""Body Composition Scale"""
12871374
(profile_id, data, _) = struct.unpack("<BII", xobj)
@@ -1391,7 +1478,9 @@ def obj6e16(xobj):
13911478
0x560d: obj560d,
13921479
0x560e: obj560e,
13931480
0x5a16: obj5a16,
1394-
0x6E16: obj6e16,
1481+
0x6012: obj6012,
1482+
0x605d: obj605d,
1483+
0x6e16: obj6e16
13951484
}
13961485

13971486

@@ -1495,6 +1584,8 @@ def parse_xiaomi(self, data: bytes, mac: bytes):
14951584
# only process messages with same priority that have a unique packet id
14961585
if prev_packet == packet_id:
14971586
if self.filter_duplicates is True:
1587+
if _LOGGER.isEnabledFor(logging.DEBUG):
1588+
_LOGGER.debug("Duplicate packet received, not processing. Data: %s", data.hex())
14981589
return None
14991590
else:
15001591
pass
@@ -1504,11 +1595,15 @@ def parse_xiaomi(self, data: bytes, mac: bytes):
15041595
# do not process advertisements with lower priority (ATC advertisements will be used instead)
15051596
prev_adv_priority -= 1
15061597
self.adv_priority[mac] = prev_adv_priority
1598+
if _LOGGER.isEnabledFor(logging.DEBUG):
1599+
_LOGGER.debug("Lower priority advertisement received, not processing. Data: %s", data.hex())
15071600
return None
15081601
else:
15091602
if prev_packet == packet_id:
15101603
if self.filter_duplicates is True:
15111604
# only process messages with highest priority and messages with unique packet id
1605+
if _LOGGER.isEnabledFor(logging.DEBUG):
1606+
_LOGGER.debug("Duplicate packet received, not processing. Data: %s", data.hex())
15121607
return None
15131608
self.lpacket_ids[mac] = packet_id
15141609

@@ -1591,7 +1686,7 @@ def parse_xiaomi(self, data: bytes, mac: bytes):
15911686
"0x4e0e",
15921687
"0x560c",
15931688
"0x560d",
1594-
"0x560e"
1689+
"0x560e",
15951690
]:
15961691
result.update(resfunc(dobject, device_type))
15971692
else:
@@ -1620,6 +1715,8 @@ def decrypt_mibeacon_v4_v5(self, data, i, mac):
16201715
if mac not in self.no_key_message:
16211716
_LOGGER.error("No encryption key found for device with MAC %s", to_mac(mac))
16221717
self.no_key_message.append(mac)
1718+
if _LOGGER.isEnabledFor(logging.DEBUG):
1719+
_LOGGER.debug("Key error for device with MAC %s, cannot decrypt data. Data: %s", to_mac(mac), data.hex())
16231720
return None
16241721

16251722
nonce = b"".join([mac[::-1], data[6:9], data[-7:-4]])

custom_components/ble_monitor/const.py

100755100644
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,6 +2120,7 @@ class BLEMonitorBinarySensorEntityDescription(
21202120
'XMWXKG01YL' : [["rssi"], ["two btn switch left", "two btn switch right"], []],
21212121
'XMWXKG01LM' : [["battery", "rssi"], ["one btn switch"], []],
21222122
'PTX' : [["battery", "rssi"], ["one btn switch"], []],
2123+
'PTX-F1-Display' : [["temperature", "humidity", "rssi"], ["four btn switch 1", "four btn switch 2", "four btn switch 3", "four btn switch 4"], []],
21232124
'YLAI003' : [["rssi", "battery"], ["button"], []],
21242125
'YLYK01YL' : [["rssi"], ["remote"], ["remote single press", "remote long press"]],
21252126
'YLYK01YL-FANCL' : [["rssi"], ["fan remote"], []],
@@ -2281,6 +2282,7 @@ class BLEMonitorBinarySensorEntityDescription(
22812282
'XMWXKG01YL' : 'Xiaomi',
22822283
'XMWXKG01LM' : 'Xiaomi',
22832284
'PTX' : 'Xiaomi',
2285+
'PTX-F1-Display' : 'Xiaomi',
22842286
'SV40' : 'Lockin',
22852287
'SU001-T' : 'Petoneer',
22862288
'ATC' : 'ATC',

custom_components/ble_monitor/test/test_xiaomi_parser.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,90 @@ def test_Xiaomi_PTX(self):
12151215
assert sensor_msg["button switch"] == "single press"
12161216
assert sensor_msg["rssi"] == -52
12171217

1218+
def test_Xiaomi_PTX_F1_Display_single_press(self):
1219+
"""Test Xiaomi parser for PTX-F1-Display single press on switch 4."""
1220+
self.aeskeys = {}
1221+
data_string = "043E3E0201000066554433221132020106191695FE5859C5642D6655443322112D9475BB270100AB9CBFA914093039303631352E72656D6F74652E7831737764C6".replace(" ", "")
1222+
data = bytes(bytearray.fromhex(data_string))
1223+
1224+
aeskey = "00112233445566778899aabbccddeeff"
1225+
1226+
is_ext_packet = True if data[3] == 0x0D else False
1227+
mac = (data[8 if is_ext_packet else 7:14 if is_ext_packet else 13])[::-1]
1228+
mac_address = mac.hex()
1229+
p_mac = bytes.fromhex(mac_address.replace(":", "").lower())
1230+
p_key = bytes.fromhex(aeskey.lower())
1231+
self.aeskeys[p_mac] = p_key
1232+
# pylint: disable=unused-variable
1233+
ble_parser = BleParser(aeskeys=self.aeskeys)
1234+
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)
1235+
1236+
assert sensor_msg["firmware"] == "Xiaomi (MiBeacon V5 encrypted)"
1237+
assert sensor_msg["type"] == "PTX-F1-Display"
1238+
assert sensor_msg["mac"] == "112233445566"
1239+
assert sensor_msg["packet"] == 45
1240+
assert sensor_msg["data"]
1241+
assert sensor_msg["four btn switch 4"] == "toggle"
1242+
assert sensor_msg["button switch"] == "single press"
1243+
assert sensor_msg["rssi"] == -58
1244+
assert sensor_msg["local_name"] == "090615.remote.x1swd"
1245+
1246+
def test_Xiaomi_PTX_F1_Display_temperature(self):
1247+
"""Test Xiaomi parser for PTX-F1-Display temperature."""
1248+
self.aeskeys = {}
1249+
data_string = (
1250+
"043E29020100006655443322111D020106191695FE5859C56433665544332211997B546B0C01009089C35AC4"
1251+
).replace(" ", "")
1252+
data = bytes(bytearray.fromhex(data_string))
1253+
1254+
aeskey = "00112233445566778899aabbccddeeff"
1255+
1256+
is_ext_packet = True if data[3] == 0x0D else False
1257+
mac = (data[8 if is_ext_packet else 7:14 if is_ext_packet else 13])[::-1]
1258+
mac_address = mac.hex()
1259+
p_mac = bytes.fromhex(mac_address.replace(":", "").lower())
1260+
p_key = bytes.fromhex(aeskey.lower())
1261+
self.aeskeys[p_mac] = p_key
1262+
# pylint: disable=unused-variable
1263+
ble_parser = BleParser(aeskeys=self.aeskeys)
1264+
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)
1265+
1266+
assert sensor_msg["firmware"] == "Xiaomi (MiBeacon V5 encrypted)"
1267+
assert sensor_msg["type"] == "PTX-F1-Display"
1268+
assert sensor_msg["mac"] == "112233445566"
1269+
assert sensor_msg["packet"] == 51
1270+
assert sensor_msg["data"]
1271+
assert sensor_msg["temperature"] == 25
1272+
assert sensor_msg["rssi"] == -60
1273+
assert sensor_msg["local_name"] == ""
1274+
1275+
def test_Xiaomi_PTX_F1_Display_humidity(self):
1276+
"""Test Xiaomi parser for PTX-F1-Display humidity."""
1277+
self.aeskeys = {}
1278+
data_string = "043E29020100006655443322111D020106191695FE5859C56433665544332211D67B54550C01001D8F98BBC4".replace(" ", "")
1279+
data = bytes(bytearray.fromhex(data_string))
1280+
1281+
aeskey = "00112233445566778899aabbccddeeff"
1282+
1283+
is_ext_packet = True if data[3] == 0x0D else False
1284+
mac = (data[8 if is_ext_packet else 7:14 if is_ext_packet else 13])[::-1]
1285+
mac_address = mac.hex()
1286+
p_mac = bytes.fromhex(mac_address.replace(":", "").lower())
1287+
p_key = bytes.fromhex(aeskey.lower())
1288+
self.aeskeys[p_mac] = p_key
1289+
# pylint: disable=unused-variable
1290+
ble_parser = BleParser(aeskeys=self.aeskeys)
1291+
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)
1292+
1293+
assert sensor_msg["firmware"] == "Xiaomi (MiBeacon V5 encrypted)"
1294+
assert sensor_msg["type"] == "PTX-F1-Display"
1295+
assert sensor_msg["mac"] == "112233445566"
1296+
assert sensor_msg["packet"] == 51
1297+
assert sensor_msg["data"]
1298+
assert sensor_msg["humidity"] == 39
1299+
assert sensor_msg["rssi"] == -60
1300+
assert sensor_msg["local_name"] == ""
1301+
12181302
def test_Xiaomi_XMPIRO2SXS(self):
12191303
"""Test Xiaomi parser for XMPIRO2SXS."""
12201304
self.aeskeys = {}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
manufacturer: Xiaomi
3+
name: PTX F1 4-Button Wireless Switch (Display Version)
4+
model: F1
5+
image: PTX_F1_Display.webp
6+
physical_description:
7+
broadcasted_properties:
8+
- temperature
9+
- humidity
10+
- four btn switch 1
11+
- four btn switch 2
12+
- four btn switch 3
13+
- four btn switch 4
14+
- button switch
15+
- rssi
16+
broadcasted_property_notes:
17+
- property: four btn switch 1
18+
note: always "toggle"; actual press type ('short press', 'double press', 'long press') is reported via the 'button switch' property
19+
- property: four btn switch 2
20+
note: always "toggle"; actual press type ('short press', 'double press', 'long press') is reported via the 'button switch' property
21+
- property: four btn switch 3
22+
note: always "toggle"; actual press type ('short press', 'double press', 'long press') is reported via the 'button switch' property
23+
- property: four btn switch 4
24+
note: always "toggle"; actual press type ('short press', 'double press', 'long press') is reported via the 'button switch' property
25+
broadcast_rate:
26+
active_scan:
27+
encryption_key: true
28+
custom_firmware:
29+
notes:
30+
- Unable to retrieve the battery percentage right now. (need help!)
31+
- The switch sensor state will return to `no press` after the time set with the [reset_timer](configuration_params#reset_timer) option. It is advised to change the reset time to 1 second (default = 35 seconds).
32+
---
4.4 KB
Loading

0 commit comments

Comments
 (0)