Skip to content

Commit

Permalink
feat: support xiaomi occupancy(human presence) sensor (XMOSB01XS) (#93)
Browse files Browse the repository at this point in the history
feat: support 2024 new battery based xiaomi mijia occupancy(human presence) sensor (XMOSB01XS)

New test cases are added.
Also the change is verified by Home Assistant (with a small additional change 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
  • Loading branch information
ldfandian authored Aug 27, 2024
1 parent cf115f6 commit f10c72b
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/xiaomi_ble/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,9 @@ class ExtendedSensorDeviceClass(BaseDeviceClass):

# Toothbrush score
SCORE = "score"

# Has-Someone-Duration
DURATION_DETECTED = "duration_detected"

# No-One-Duration
DURATION_CLEARED = "duration_cleared"
4 changes: 4 additions & 0 deletions src/xiaomi_ble/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
44 changes: 44 additions & 0 deletions src/xiaomi_ble/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,47 @@ def obj4818(
return {}


def obj484e(
xobj: bytes, device: XiaomiBluetoothDeviceData, device_type: str
) -> dict[str, Any]:
"""From miot-spec: occupancy-status: uint8: 0 - No One, 1 - Has One"""
"""Translate to: occupancy: bool: 0 - Clear, 1 - Detected"""
device.update_predefined_binary_sensor(
BinarySensorDeviceClass.OCCUPANCY, xobj[0] > 0
)
return {}


def obj4851(
xobj: bytes, device: XiaomiBluetoothDeviceData, device_type: str
) -> dict[str, Any]:
"""From miot-spec: has-someone-duration: uint8: 2 - 2 minutes, 5 - 5 minutes"""
"""Translate to: duration_detected: uint8: 2 - 2 minutes, 5 - 5 minutes"""
device.update_sensor(
key=ExtendedSensorDeviceClass.DURATION_DETECTED,
name="Duration detected",
native_unit_of_measurement=Units.TIME_MINUTES,
device_class=ExtendedSensorDeviceClass.DURATION_DETECTED,
native_value=xobj[0],
)
return {}


def obj4852(
xobj: bytes, device: XiaomiBluetoothDeviceData, device_type: str
) -> dict[str, Any]:
"""From miot-spec: no-one-duration: uint8: 2/5/10/30 - 2/5/10/30 minutes"""
"""Translate to: duration_cleared: uint8: 2/5/10/30 - 2/5/10/30 minutes"""
device.update_sensor(
key=ExtendedSensorDeviceClass.DURATION_CLEARED,
name="Duration cleared",
native_unit_of_measurement=Units.TIME_MINUTES,
device_class=ExtendedSensorDeviceClass.DURATION_CLEARED,
native_value=xobj[0],
)
return {}


def obj4a01(
xobj: bytes, device: XiaomiBluetoothDeviceData, device_type: str
) -> dict[str, Any]:
Expand Down Expand Up @@ -1441,6 +1482,9 @@ def obj4e1c(
0x4805: obj4805,
0x4806: obj4806,
0x4818: obj4818,
0x484E: obj484e,
0x4851: obj4851,
0x4852: obj4852,
0x4A01: obj4a01,
0x4A08: obj4a08,
0x4A0C: obj4a0c,
Expand Down
97 changes: 97 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit f10c72b

Please sign in to comment.