Skip to content

Commit b9438e2

Browse files
committed
feat: support xiaomi occupancy(human presence) sensor (XMOSB01XS)
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
1 parent cf115f6 commit b9438e2

File tree

4 files changed

+151
-0
lines changed

4 files changed

+151
-0
lines changed

src/xiaomi_ble/const.py

+6
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,9 @@ class ExtendedSensorDeviceClass(BaseDeviceClass):
8080

8181
# Toothbrush score
8282
SCORE = "score"
83+
84+
# Has-Someone-Duration
85+
DURATION_DETECTED = "duration_detected"
86+
87+
# No-One-Duration
88+
DURATION_CLEARED = "duration_cleared"

src/xiaomi_ble/devices.py

+4
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ class DeviceEntry:
158158
name="Motion Sensor",
159159
model="XMPIRO2SXS",
160160
),
161+
0x4683: DeviceEntry(
162+
name="Occupancy Sensor",
163+
model="XMOSB01XS",
164+
),
161165
0x0863: DeviceEntry(
162166
name="Flood Detector",
163167
model="SJWS01LM",

src/xiaomi_ble/parser.py

+44
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,47 @@ def obj4818(
10741074
return {}
10751075

10761076

1077+
def obj484e(
1078+
xobj: bytes, device: XiaomiBluetoothDeviceData, device_type: str
1079+
) -> dict[str, Any]:
1080+
"""From miot-spec: occupancy-status: uint8: 0 - No One, 1 - Has One"""
1081+
"""Translate to: occupancy: bool: 0 - Clear, 1 - Detected"""
1082+
device.update_predefined_binary_sensor(
1083+
BinarySensorDeviceClass.OCCUPANCY, xobj[0] > 0
1084+
)
1085+
return {}
1086+
1087+
1088+
def obj4851(
1089+
xobj: bytes, device: XiaomiBluetoothDeviceData, device_type: str
1090+
) -> dict[str, Any]:
1091+
"""From miot-spec: has-someone-duration: uint8: 2 - 2 minutes, 5 - 5 minutes"""
1092+
"""Translate to: duration_detected: uint8: 2 - 2 minutes, 5 - 5 minutes"""
1093+
device.update_sensor(
1094+
key=ExtendedSensorDeviceClass.DURATION_DETECTED,
1095+
name="Duration detected",
1096+
native_unit_of_measurement=Units.TIME_MINUTES,
1097+
device_class=ExtendedSensorDeviceClass.DURATION_DETECTED,
1098+
native_value=xobj[0],
1099+
)
1100+
return {}
1101+
1102+
1103+
def obj4852(
1104+
xobj: bytes, device: XiaomiBluetoothDeviceData, device_type: str
1105+
) -> dict[str, Any]:
1106+
"""From miot-spec: no-one-duration: uint8: 2/5/10/30 - 2/5/10/30 minutes"""
1107+
"""Translate to: duration_cleared: uint8: 2/5/10/30 - 2/5/10/30 minutes"""
1108+
device.update_sensor(
1109+
key=ExtendedSensorDeviceClass.DURATION_CLEARED,
1110+
name="Duration cleared",
1111+
native_unit_of_measurement=Units.TIME_MINUTES,
1112+
device_class=ExtendedSensorDeviceClass.DURATION_CLEARED,
1113+
native_value=xobj[0],
1114+
)
1115+
return {}
1116+
1117+
10771118
def obj4a01(
10781119
xobj: bytes, device: XiaomiBluetoothDeviceData, device_type: str
10791120
) -> dict[str, Any]:
@@ -1441,6 +1482,9 @@ def obj4e1c(
14411482
0x4805: obj4805,
14421483
0x4806: obj4806,
14431484
0x4818: obj4818,
1485+
0x484E: obj484e,
1486+
0x4851: obj4851,
1487+
0x4852: obj4852,
14441488
0x4A01: obj4a01,
14451489
0x4A08: obj4a08,
14461490
0x4A0C: obj4a0c,

tests/test_parser.py

+97
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
KEY_BINARY_DOOR = DeviceKey(key="door", device_id=None)
3232
KEY_BINARY_FINGERPRINT = DeviceKey(key="fingerprint", device_id=None)
3333
KEY_BINARY_MOTION = DeviceKey(key="motion", device_id=None)
34+
KEY_BINARY_OCCUPANCY = DeviceKey(key="occupancy", device_id=None)
3435
KEY_BINARY_LIGHT = DeviceKey(key="light", device_id=None)
3536
KEY_BINARY_LOCK = DeviceKey(key="lock", device_id=None)
3637
KEY_BINARY_OPENING = DeviceKey(key="opening", device_id=None)
@@ -3009,6 +3010,102 @@ def test_Xiaomi_XMPIRO2SXS():
30093010
)
30103011

30113012

3013+
def test_Xiaomi_XMOSB01XS_ILLUMINANCE():
3014+
"""Test Xiaomi parser for Xiaomi Occupancy(Human Presence) Sensor XMOSB01XS."""
3015+
data_string = (
3016+
b"\x48\x59\x83\x46\x0D\xDC\x21\x3C\xE9\x81\xDA\x7A\xE2\x02\x00\x44\x41\xF8\x8C"
3017+
)
3018+
advertisement = bytes_to_service_info(data_string, address="0C:43:14:A1:41:1E")
3019+
bindkey = "0a4552cb19a639b72b8ed09bde6d5bfa"
3020+
3021+
device = XiaomiBluetoothDeviceData(bindkey=bytes.fromhex(bindkey))
3022+
assert device.supported(advertisement)
3023+
assert device.bindkey_verified
3024+
assert device.update(advertisement) == SensorUpdate(
3025+
title="Occupancy Sensor 411E (XMOSB01XS)",
3026+
devices={
3027+
None: SensorDeviceInfo(
3028+
name="Occupancy Sensor 411E",
3029+
manufacturer="Xiaomi",
3030+
model="XMOSB01XS",
3031+
hw_version=None,
3032+
sw_version="Xiaomi (MiBeacon V5 encrypted)",
3033+
)
3034+
},
3035+
entity_descriptions={
3036+
KEY_ILLUMINANCE: SensorDescription(
3037+
device_key=KEY_ILLUMINANCE,
3038+
device_class=DeviceClass.ILLUMINANCE,
3039+
native_unit_of_measurement=Units.LIGHT_LUX,
3040+
),
3041+
KEY_SIGNAL_STRENGTH: SensorDescription(
3042+
device_key=KEY_SIGNAL_STRENGTH,
3043+
device_class=DeviceClass.SIGNAL_STRENGTH,
3044+
native_unit_of_measurement="dBm",
3045+
),
3046+
},
3047+
entity_values={
3048+
KEY_ILLUMINANCE: SensorValue(
3049+
name="Illuminance", device_key=KEY_ILLUMINANCE, native_value=38
3050+
),
3051+
KEY_SIGNAL_STRENGTH: SensorValue(
3052+
name="Signal Strength", device_key=KEY_SIGNAL_STRENGTH, native_value=-60
3053+
),
3054+
},
3055+
binary_entity_descriptions={},
3056+
binary_entity_values={},
3057+
)
3058+
3059+
3060+
def test_Xiaomi_XMOSB01XS_OCCUPANCY():
3061+
"""Test Xiaomi parser for Xiaomi Occupancy(Human Presence) Sensor XMOSB01XS."""
3062+
data_string = (
3063+
b"\x58\x59\x83\x46\x1F\xBD\xB1\xC4\x67\x48\xD4"
3064+
b"\x9D\x1E\xFD\x8C\x04\x00\x00\xE5\x7E\x87\x3A"
3065+
)
3066+
advertisement = bytes_to_service_info(data_string, address="D4:48:67:C4:B1:BD")
3067+
bindkey = "920ce119b34410d38251ccea54c0f915"
3068+
3069+
device = XiaomiBluetoothDeviceData(bindkey=bytes.fromhex(bindkey))
3070+
assert device.supported(advertisement)
3071+
assert device.bindkey_verified
3072+
assert device.update(advertisement) == SensorUpdate(
3073+
title="Occupancy Sensor B1BD (XMOSB01XS)",
3074+
devices={
3075+
None: SensorDeviceInfo(
3076+
name="Occupancy Sensor B1BD",
3077+
manufacturer="Xiaomi",
3078+
model="XMOSB01XS",
3079+
hw_version=None,
3080+
sw_version="Xiaomi (MiBeacon V5 encrypted)",
3081+
)
3082+
},
3083+
entity_descriptions={
3084+
KEY_SIGNAL_STRENGTH: SensorDescription(
3085+
device_key=KEY_SIGNAL_STRENGTH,
3086+
device_class=DeviceClass.SIGNAL_STRENGTH,
3087+
native_unit_of_measurement="dBm",
3088+
),
3089+
},
3090+
entity_values={
3091+
KEY_SIGNAL_STRENGTH: SensorValue(
3092+
name="Signal Strength", device_key=KEY_SIGNAL_STRENGTH, native_value=-60
3093+
),
3094+
},
3095+
binary_entity_descriptions={
3096+
KEY_BINARY_OCCUPANCY: BinarySensorDescription(
3097+
device_key=KEY_BINARY_OCCUPANCY,
3098+
device_class=BinarySensorDeviceClass.OCCUPANCY,
3099+
),
3100+
},
3101+
binary_entity_values={
3102+
KEY_BINARY_OCCUPANCY: BinarySensorValue(
3103+
device_key=KEY_BINARY_OCCUPANCY, name="Occupancy", native_value=False
3104+
),
3105+
},
3106+
)
3107+
3108+
30123109
def test_Xiaomi_PTX_press():
30133110
"""Test Xiaomi parser for Xiaomi PTX YK1 QMIMB."""
30143111
bindkey = "a74510b40386d35ae6227a7451efc76e"

0 commit comments

Comments
 (0)