From 4007de3865599fbf798cdc2fd5dd409636570e41 Mon Sep 17 00:00:00 2001 From: Dian FAN Date: Tue, 27 Aug 2024 19:38:38 +0800 Subject: [PATCH] support Xiaomi Occupancy(Human Presence) Sensor XMOSB01XS Support 2024 New battery based Xiaomi Mijia Occupancy (Human Presence) Sensor (XMOSB01XS) New test cases are added. Also verified by adding it to Home Assistant (with a small additional changes in xiaomi-ble component for additional new properties) Hardware Info: https://www.aliexpress.com/i/1005007104780534.html https://www.mi.com/shop/buy/detail?product_id=19994 --- src/xiaomi_ble/const.py | 6 +++ src/xiaomi_ble/devices.py | 4 ++ src/xiaomi_ble/parser.py | 41 +++++++++++++++++ tests/test_parser.py | 97 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+) diff --git a/src/xiaomi_ble/const.py b/src/xiaomi_ble/const.py index 644e843..803e5db 100644 --- a/src/xiaomi_ble/const.py +++ b/src/xiaomi_ble/const.py @@ -80,3 +80,9 @@ class ExtendedSensorDeviceClass(BaseDeviceClass): # Toothbrush score SCORE = "score" + + # Has-Someone-Duration + HAS_SOMEONE_DURATION = "has_someone_duration" + + # No-One-Duration + NO_ONE_DURATION = "no_one_duration" diff --git a/src/xiaomi_ble/devices.py b/src/xiaomi_ble/devices.py index 9f0bfae..5c2f578 100644 --- a/src/xiaomi_ble/devices.py +++ b/src/xiaomi_ble/devices.py @@ -158,6 +158,10 @@ class DeviceEntry: name="Motion Sensor", model="XMPIRO2SXS", ), + 0x4683: DeviceEntry( + name="Occupancy Sensor", + model="XMOSB01XS", + ), 0x0863: DeviceEntry( name="Flood Detector", model="SJWS01LM", diff --git a/src/xiaomi_ble/parser.py b/src/xiaomi_ble/parser.py index df08e52..be2cccb 100644 --- a/src/xiaomi_ble/parser.py +++ b/src/xiaomi_ble/parser.py @@ -1074,6 +1074,44 @@ def obj4818( return {} +def obj484e( + xobj: bytes, device: XiaomiBluetoothDeviceData, device_type: str +) -> dict[str, Any]: + """Occupancy-status: uint8: 0 - No One, 1 - Has One""" + device.update_predefined_binary_sensor( + BinarySensorDeviceClass.OCCUPANCY, xobj[0] > 0 + ) + return {} + + +def obj4851( + xobj: bytes, device: XiaomiBluetoothDeviceData, device_type: str +) -> dict[str, Any]: + """has-someone-duration: uint8: 2 - 2 minutes, 5 - 5 minutes""" + device.update_sensor( + key=ExtendedSensorDeviceClass.HAS_SOMEONE_DURATION, + name="Has someone duration", + native_unit_of_measurement=Units.TIME_MINUTES, + device_class=ExtendedSensorDeviceClass.HAS_SOMEONE_DURATION, + native_value=xobj[0], + ) + return {} + + +def obj4852( + xobj: bytes, device: XiaomiBluetoothDeviceData, device_type: str +) -> dict[str, Any]: + """no-one-duration: uint8: 2/5/10/30 - 2/5/10/30 minutes""" + device.update_sensor( + key=ExtendedSensorDeviceClass.NO_ONE_DURATION, + name="No one duration", + native_unit_of_measurement=Units.TIME_MINUTES, + device_class=ExtendedSensorDeviceClass.NO_ONE_DURATION, + native_value=xobj[0], + ) + return {} + + def obj4a01( xobj: bytes, device: XiaomiBluetoothDeviceData, device_type: str ) -> dict[str, Any]: @@ -1441,6 +1479,9 @@ def obj4e1c( 0x4805: obj4805, 0x4806: obj4806, 0x4818: obj4818, + 0x484E: obj484e, + 0x4851: obj4851, + 0x4852: obj4852, 0x4A01: obj4a01, 0x4A08: obj4a08, 0x4A0C: obj4a0c, diff --git a/tests/test_parser.py b/tests/test_parser.py index 4bf8110..3e1b1c5 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -31,6 +31,7 @@ KEY_BINARY_DOOR = DeviceKey(key="door", device_id=None) KEY_BINARY_FINGERPRINT = DeviceKey(key="fingerprint", device_id=None) KEY_BINARY_MOTION = DeviceKey(key="motion", device_id=None) +KEY_BINARY_OCCUPANCY = DeviceKey(key="occupancy", device_id=None) KEY_BINARY_LIGHT = DeviceKey(key="light", device_id=None) KEY_BINARY_LOCK = DeviceKey(key="lock", device_id=None) KEY_BINARY_OPENING = DeviceKey(key="opening", device_id=None) @@ -3009,6 +3010,102 @@ def test_Xiaomi_XMPIRO2SXS(): ) +def test_Xiaomi_XMOSB01XS_ILLUMINANCE(): + """Test Xiaomi parser for Xiaomi Occupancy(Human Presence) Sensor XMOSB01XS.""" + data_string = ( + b"\x48\x59\x83\x46\x0D\xDC\x21\x3C\xE9\x81\xDA\x7A\xE2\x02\x00\x44\x41\xF8\x8C" + ) + advertisement = bytes_to_service_info(data_string, address="0C:43:14:A1:41:1E") + bindkey = "0a4552cb19a639b72b8ed09bde6d5bfa" + + device = XiaomiBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) + assert device.supported(advertisement) + assert device.bindkey_verified + assert device.update(advertisement) == SensorUpdate( + title="Occupancy Sensor 411E (XMOSB01XS)", + devices={ + None: SensorDeviceInfo( + name="Occupancy Sensor 411E", + manufacturer="Xiaomi", + model="XMOSB01XS", + hw_version=None, + sw_version="Xiaomi (MiBeacon V5 encrypted)", + ) + }, + entity_descriptions={ + KEY_ILLUMINANCE: SensorDescription( + device_key=KEY_ILLUMINANCE, + device_class=DeviceClass.ILLUMINANCE, + native_unit_of_measurement=Units.LIGHT_LUX, + ), + KEY_SIGNAL_STRENGTH: SensorDescription( + device_key=KEY_SIGNAL_STRENGTH, + device_class=DeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement="dBm", + ), + }, + entity_values={ + KEY_ILLUMINANCE: SensorValue( + name="Illuminance", device_key=KEY_ILLUMINANCE, native_value=38 + ), + KEY_SIGNAL_STRENGTH: SensorValue( + name="Signal Strength", device_key=KEY_SIGNAL_STRENGTH, native_value=-60 + ), + }, + binary_entity_descriptions={}, + binary_entity_values={}, + ) + + +def test_Xiaomi_XMOSB01XS_OCCUPANCY(): + """Test Xiaomi parser for Xiaomi Occupancy(Human Presence) Sensor XMOSB01XS.""" + data_string = ( + b"\x58\x59\x83\x46\x1F\xBD\xB1\xC4\x67\x48\xD4" + b"\x9D\x1E\xFD\x8C\x04\x00\x00\xE5\x7E\x87\x3A" + ) + advertisement = bytes_to_service_info(data_string, address="D4:48:67:C4:B1:BD") + bindkey = "920ce119b34410d38251ccea54c0f915" + + device = XiaomiBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) + assert device.supported(advertisement) + assert device.bindkey_verified + assert device.update(advertisement) == SensorUpdate( + title="Occupancy Sensor B1BD (XMOSB01XS)", + devices={ + None: SensorDeviceInfo( + name="Occupancy Sensor B1BD", + manufacturer="Xiaomi", + model="XMOSB01XS", + hw_version=None, + sw_version="Xiaomi (MiBeacon V5 encrypted)", + ) + }, + entity_descriptions={ + KEY_SIGNAL_STRENGTH: SensorDescription( + device_key=KEY_SIGNAL_STRENGTH, + device_class=DeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement="dBm", + ), + }, + entity_values={ + KEY_SIGNAL_STRENGTH: SensorValue( + name="Signal Strength", device_key=KEY_SIGNAL_STRENGTH, native_value=-60 + ), + }, + binary_entity_descriptions={ + KEY_BINARY_OCCUPANCY: BinarySensorDescription( + device_key=KEY_BINARY_OCCUPANCY, + device_class=BinarySensorDeviceClass.OCCUPANCY, + ), + }, + binary_entity_values={ + KEY_BINARY_OCCUPANCY: BinarySensorValue( + device_key=KEY_BINARY_OCCUPANCY, name="Occupancy", native_value=False + ), + }, + ) + + def test_Xiaomi_PTX_press(): """Test Xiaomi parser for Xiaomi PTX YK1 QMIMB.""" bindkey = "a74510b40386d35ae6227a7451efc76e"