-
-
Notifications
You must be signed in to change notification settings - Fork 278
Add support for PTX-F1-Display and enhance debugging tools #1533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
caae392
c279925
c7462dc
d4de440
15847a5
ef4bb7e
9f9e498
8c2f12f
fb3815f
ed1efe8
59687de
dc98cfc
a4a0536
cf2d36b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,7 +86,8 @@ | |
| 0x3E17: "KS1BP", | ||
| 0x3BD5: "MJTZC01YM", | ||
| 0x50FB: "ES3", | ||
| 0x5DB1: "MBS17" | ||
| 0x5DB1: "MBS17", | ||
| 0x64C5: "PTX-F1-Display" | ||
| } | ||
|
|
||
| # Structured objects for data conversions | ||
|
|
@@ -1047,6 +1048,30 @@ def obj4e0c(xobj, device_type): | |
| "one btn switch": "toggle", | ||
| "button switch": "single press", | ||
| } | ||
| elif device_type == "PTX-F1-Display": | ||
| click = xobj[0] | ||
| if click == 1: | ||
| result = { | ||
| "four btn switch 1": "toggle", | ||
| "button switch": "single press", | ||
| } | ||
| elif click == 2: | ||
| result = { | ||
| "four btn switch 2": "toggle", | ||
| "button switch": "single press", | ||
| } | ||
| elif click == 3: | ||
| result = { | ||
| "four btn switch 3": "toggle", | ||
| "button switch": "single press", | ||
| } | ||
| elif click == 4: | ||
| result = { | ||
| "four btn switch 4": "toggle", | ||
| "button switch": "single press", | ||
| } | ||
| else: | ||
| result = None | ||
| else: | ||
| result = {} | ||
| return result | ||
|
|
@@ -1077,6 +1102,30 @@ def obj4e0d(xobj, device_type): | |
| "one btn switch": "toggle", | ||
| "button switch": "double press", | ||
| } | ||
| elif device_type == "PTX-F1-Display": | ||
| click = xobj[0] | ||
| if click == 1: | ||
| result = { | ||
| "four btn switch 1": "toggle", | ||
| "button switch": "double press", | ||
| } | ||
| elif click == 2: | ||
| result = { | ||
| "four btn switch 2": "toggle", | ||
| "button switch": "double press", | ||
| } | ||
| elif click == 3: | ||
| result = { | ||
| "four btn switch 3": "toggle", | ||
| "button switch": "double press", | ||
| } | ||
| elif click == 4: | ||
| result = { | ||
| "four btn switch 4": "toggle", | ||
| "button switch": "double press", | ||
| } | ||
| else: | ||
| result = None | ||
| else: | ||
| result = {} | ||
| return result | ||
|
|
@@ -1107,6 +1156,30 @@ def obj4e0e(xobj, device_type): | |
| "one btn switch": "toggle", | ||
| "button switch": "long press", | ||
| } | ||
| elif device_type == "PTX-F1-Display": | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same |
||
| click = xobj[0] | ||
| if click == 1: | ||
| result = { | ||
| "four btn switch 1": "toggle", | ||
| "button switch": "long press", | ||
| } | ||
| elif click == 2: | ||
| result = { | ||
| "four btn switch 2": "toggle", | ||
| "button switch": "long press", | ||
| } | ||
| elif click == 3: | ||
| result = { | ||
| "four btn switch 3": "toggle", | ||
| "button switch": "long press", | ||
| } | ||
| elif click == 4: | ||
| result = { | ||
| "four btn switch 4": "toggle", | ||
| "button switch": "long press", | ||
| } | ||
| else: | ||
| result = None | ||
| else: | ||
| result = {} | ||
| return result | ||
|
|
@@ -1282,6 +1355,20 @@ def obj5a16(xobj): | |
| return None | ||
|
|
||
|
|
||
| def obj6012(xobj): | ||
| """Humidity""" | ||
| return obj4802(xobj) | ||
|
|
||
|
|
||
| def obj605d(xobj): | ||
| """Temperature""" | ||
| if len(xobj) == 1: | ||
| temp = xobj[0] | ||
| return {"temperature": temp} | ||
| else: | ||
| return {} | ||
|
|
||
|
|
||
| def obj6e16(xobj): | ||
| """Body Composition Scale""" | ||
| (profile_id, data, _) = struct.unpack("<BII", xobj) | ||
|
|
@@ -1391,7 +1478,9 @@ def obj6e16(xobj): | |
| 0x560d: obj560d, | ||
| 0x560e: obj560e, | ||
| 0x5a16: obj5a16, | ||
| 0x6E16: obj6e16, | ||
| 0x6012: obj6012, | ||
| 0x605d: obj605d, | ||
| 0x6e16: obj6e16 | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -1495,6 +1584,8 @@ def parse_xiaomi(self, data: bytes, mac: bytes): | |
| # only process messages with same priority that have a unique packet id | ||
| if prev_packet == packet_id: | ||
| if self.filter_duplicates is True: | ||
| if _LOGGER.isEnabledFor(logging.DEBUG): | ||
| _LOGGER.debug("Duplicate packet received, not processing. Data: %s", data.hex()) | ||
| return None | ||
|
Comment on lines
1586
to
1589
|
||
| else: | ||
| pass | ||
|
|
@@ -1504,11 +1595,15 @@ def parse_xiaomi(self, data: bytes, mac: bytes): | |
| # do not process advertisements with lower priority (ATC advertisements will be used instead) | ||
| prev_adv_priority -= 1 | ||
| self.adv_priority[mac] = prev_adv_priority | ||
| if _LOGGER.isEnabledFor(logging.DEBUG): | ||
| _LOGGER.debug("Lower priority advertisement received, not processing. Data: %s", data.hex()) | ||
| return None | ||
| else: | ||
| if prev_packet == packet_id: | ||
| if self.filter_duplicates is True: | ||
| # only process messages with highest priority and messages with unique packet id | ||
| if _LOGGER.isEnabledFor(logging.DEBUG): | ||
| _LOGGER.debug("Duplicate packet received, not processing. Data: %s", data.hex()) | ||
| return None | ||
| self.lpacket_ids[mac] = packet_id | ||
|
|
||
|
|
@@ -1591,7 +1686,7 @@ def parse_xiaomi(self, data: bytes, mac: bytes): | |
| "0x4e0e", | ||
| "0x560c", | ||
| "0x560d", | ||
| "0x560e" | ||
| "0x560e", | ||
| ]: | ||
| result.update(resfunc(dobject, device_type)) | ||
| else: | ||
|
|
@@ -1620,6 +1715,8 @@ def decrypt_mibeacon_v4_v5(self, data, i, mac): | |
| if mac not in self.no_key_message: | ||
| _LOGGER.error("No encryption key found for device with MAC %s", to_mac(mac)) | ||
| self.no_key_message.append(mac) | ||
| if _LOGGER.isEnabledFor(logging.DEBUG): | ||
| _LOGGER.debug("Key error for device with MAC %s, cannot decrypt data. Data: %s", to_mac(mac), data.hex()) | ||
| return None | ||
|
|
||
| nonce = b"".join([mac[::-1], data[6:9], data[-7:-4]]) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1215,6 +1215,90 @@ def test_Xiaomi_PTX(self): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["button switch"] == "single press" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["rssi"] == -52 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_Xiaomi_PTX_F1_Display_single_press(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Test Xiaomi parser for PTX-F1-Display single press on switch 4.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.aeskeys = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data_string = "043E3E0201000066554433221132020106191695FE5859C5642D6655443322112D9475BB270100AB9CBFA914093039303631352E72656D6F74652E7831737764C6".replace(" ", "") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data = bytes(bytearray.fromhex(data_string)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aeskey = "00112233445566778899aabbccddeeff" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is_ext_packet = True if data[3] == 0x0D else False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mac = (data[8 if is_ext_packet else 7:14 if is_ext_packet else 13])[::-1] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mac_address = mac.hex() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p_mac = bytes.fromhex(mac_address.replace(":", "").lower()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p_key = bytes.fromhex(aeskey.lower()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.aeskeys[p_mac] = p_key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # pylint: disable=unused-variable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ble_parser = BleParser(aeskeys=self.aeskeys) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sensor_msg, tracker_msg = ble_parser.parse_raw_data(data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["firmware"] == "Xiaomi (MiBeacon V5 encrypted)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["type"] == "PTX-F1-Display" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["mac"] == "112233445566" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["packet"] == 45 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["data"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["four btn switch 4"] == "toggle" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["button switch"] == "single press" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["rssi"] == -58 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["local_name"] == "090615.remote.x1swd" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_Xiaomi_PTX_F1_Display_temperature(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Test Xiaomi parser for PTX-F1-Display temperature.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.aeskeys = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data_string = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "043E29020100006655443322111D020106191695FE5859C56433665544332211997B546B0C01009089C35AC4" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).replace(" ", "") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data = bytes(bytearray.fromhex(data_string)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aeskey = "00112233445566778899aabbccddeeff" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is_ext_packet = True if data[3] == 0x0D else False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mac = (data[8 if is_ext_packet else 7:14 if is_ext_packet else 13])[::-1] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mac_address = mac.hex() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p_mac = bytes.fromhex(mac_address.replace(":", "").lower()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p_key = bytes.fromhex(aeskey.lower()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.aeskeys[p_mac] = p_key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # pylint: disable=unused-variable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ble_parser = BleParser(aeskeys=self.aeskeys) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sensor_msg, tracker_msg = ble_parser.parse_raw_data(data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["firmware"] == "Xiaomi (MiBeacon V5 encrypted)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["type"] == "PTX-F1-Display" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["mac"] == "112233445566" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["packet"] == 51 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["data"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["temperature"] == 25 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["rssi"] == -60 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["local_name"] == "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_Xiaomi_PTX_F1_Display_humidity(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Test Xiaomi parser for PTX-F1-Display humidity.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.aeskeys = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data_string = "043E29020100006655443322111D020106191695FE5859C56433665544332211D67B54550C01001D8F98BBC4".replace(" ", "") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data = bytes(bytearray.fromhex(data_string)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aeskey = "00112233445566778899aabbccddeeff" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is_ext_packet = True if data[3] == 0x0D else False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mac = (data[8 if is_ext_packet else 7:14 if is_ext_packet else 13])[::-1] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mac_address = mac.hex() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p_mac = bytes.fromhex(mac_address.replace(":", "").lower()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p_key = bytes.fromhex(aeskey.lower()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.aeskeys[p_mac] = p_key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # pylint: disable=unused-variable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ble_parser = BleParser(aeskeys=self.aeskeys) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sensor_msg, tracker_msg = ble_parser.parse_raw_data(data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["firmware"] == "Xiaomi (MiBeacon V5 encrypted)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["type"] == "PTX-F1-Display" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["mac"] == "112233445566" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["packet"] == 51 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["data"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["humidity"] == 39 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["rssi"] == -60 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert sensor_msg["local_name"] == "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_Xiaomi_PTX_F1_Display_temperature(self): | |
| """Test Xiaomi parser for PTX-F1-Display temperature.""" | |
| self.aeskeys = {} | |
| data_string = "043E29020100006655443322111D020106191695FE5859C56433665544332211D67B54550C01001D8F98BBC4".replace(" ", "") | |
| data = bytes(bytearray.fromhex(data_string)) | |
| aeskey = "00112233445566778899aabbccddeeff" | |
| is_ext_packet = True if data[3] == 0x0D else False | |
| mac = (data[8 if is_ext_packet else 7:14 if is_ext_packet else 13])[::-1] | |
| mac_address = mac.hex() | |
| p_mac = bytes.fromhex(mac_address.replace(":", "").lower()) | |
| p_key = bytes.fromhex(aeskey.lower()) | |
| self.aeskeys[p_mac] = p_key | |
| # pylint: disable=unused-variable | |
| ble_parser = BleParser(aeskeys=self.aeskeys) | |
| sensor_msg, tracker_msg = ble_parser.parse_raw_data(data) | |
| assert sensor_msg["firmware"] == "Xiaomi (MiBeacon V5 encrypted)" | |
| assert sensor_msg["type"] == "PTX-F1-Display" | |
| assert sensor_msg["mac"] == "112233445566" | |
| assert sensor_msg["packet"] == 51 | |
| assert sensor_msg["data"] | |
| # Validate that temperature is decoded and numeric; exact value depends on obj605d payload. | |
| assert "temperature" in sensor_msg | |
| assert isinstance(sensor_msg["temperature"], (int, float)) | |
| assert sensor_msg["rssi"] == -60 | |
| assert sensor_msg["local_name"] == "" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| --- | ||
| manufacturer: Xiaomi | ||
| name: PTX F1 4-Button Wireless Switch (Display Version) | ||
| model: F1 | ||
| image: PTX_F1_Display.webp | ||
| physical_description: | ||
| broadcasted_properties: | ||
| - temperature | ||
| - humidity | ||
| - four btn switch 1 | ||
| - four btn switch 2 | ||
| - four btn switch 3 | ||
| - four btn switch 4 | ||
|
M1k0t0 marked this conversation as resolved.
|
||
| - button switch | ||
| - rssi | ||
| broadcasted_property_notes: | ||
| - property: four btn switch 1 | ||
| note: always "toggle"; actual press type ('short press', 'double press', 'long press') is reported via the 'button switch' property | ||
| - property: four btn switch 2 | ||
| note: always "toggle"; actual press type ('short press', 'double press', 'long press') is reported via the 'button switch' property | ||
| - property: four btn switch 3 | ||
| note: always "toggle"; actual press type ('short press', 'double press', 'long press') is reported via the 'button switch' property | ||
| - property: four btn switch 4 | ||
| note: always "toggle"; actual press type ('short press', 'double press', 'long press') is reported via the 'button switch' property | ||
| broadcast_rate: | ||
| active_scan: | ||
| encryption_key: true | ||
| custom_firmware: | ||
| notes: | ||
| - Unable to retrieve the battery percentage right now. (need help!) | ||
| - 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). | ||
| --- | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment at line 1065