diff --git a/switchbot/adv_parser.py b/switchbot/adv_parser.py index 6cd6dd3..bba2a69 100644 --- a/switchbot/adv_parser.py +++ b/switchbot/adv_parser.py @@ -35,6 +35,7 @@ from .adv_parsers.meter import process_wosensorth, process_wosensorth_c from .adv_parsers.motion import process_wopresence from .adv_parsers.plug import process_woplugmini +from .adv_parsers.presence_sensor import process_presence_sensor from .adv_parsers.relay_switch import ( process_garage_door_opener, process_relay_switch_1pm, @@ -384,6 +385,12 @@ class SwitchbotSupportedType(TypedDict): "func": process_smart_thermostat_radiator, "manufacturer_id": 2409, }, + b"\x00\x10\xcc\xc8": { + "modelName": SwitchbotModel.PRESENCE_SENSOR, + "modelFriendlyName": "Presence Sensor", + "func": process_presence_sensor, + "manufacturer_id": 2409, + }, } _SWITCHBOT_MODEL_TO_CHAR = { diff --git a/switchbot/adv_parsers/presence_sensor.py b/switchbot/adv_parsers/presence_sensor.py new file mode 100644 index 0000000..ef83849 --- /dev/null +++ b/switchbot/adv_parsers/presence_sensor.py @@ -0,0 +1,43 @@ +"""Advertisement data parser for presence sensor devices.""" + +import logging + +from ..const.presence_sensor import BATTERY_LEVEL_MAP + +_LOGGER = logging.getLogger(__name__) + + +def process_presence_sensor( + data: bytes | None, mfr_data: bytes | None +) -> dict[str, bool | int | str]: + """Process Presence Sensor data.""" + if mfr_data is None: + return {} + + seq_number = mfr_data[6] + adaptive_state = bool(mfr_data[7] & 0x80) + motion_detected = bool(mfr_data[7] & 0x40) + battery_bits = (mfr_data[7] >> 2) & 0x03 + battery_range = BATTERY_LEVEL_MAP.get(battery_bits, "Unknown") + trigger_flag = mfr_data[10] + led_state = bool(mfr_data[11] & 0x80) + light_level = mfr_data[11] & 0x0F + + result = { + "sequence_number": seq_number, + "adaptive_state": adaptive_state, + "motion_detected": motion_detected, + "battery_range": battery_range, + "trigger_flag": trigger_flag, + "led_state": led_state, + "lightLevel": light_level, + } + + if data: + battery = data[2] & 0x7F + result["battery"] = battery + + _LOGGER.debug( + "Processed presence sensor mfr data: %s, result: %s", mfr_data.hex(), result + ) + return result diff --git a/switchbot/const/__init__.py b/switchbot/const/__init__.py index 0b8ecaf..0ecd9c7 100644 --- a/switchbot/const/__init__.py +++ b/switchbot/const/__init__.py @@ -100,6 +100,7 @@ class SwitchbotModel(StrEnum): K11_VACUUM = "K11+ Vacuum" CLIMATE_PANEL = "Climate Panel" SMART_THERMOSTAT_RADIATOR = "Smart Thermostat Radiator" + PRESENCE_SENSOR = "Presence Sensor" __all__ = [ diff --git a/switchbot/const/presence_sensor.py b/switchbot/const/presence_sensor.py new file mode 100644 index 0000000..e0b6537 --- /dev/null +++ b/switchbot/const/presence_sensor.py @@ -0,0 +1,8 @@ +"""Constants for presence sensor devices.""" + +BATTERY_LEVEL_MAP = { + 0: "<10%", + 1: "10-19%", + 2: "20-59%", + 3: ">=60%", +} diff --git a/switchbot/devices/device.py b/switchbot/devices/device.py index cda6c73..0ba9e27 100644 --- a/switchbot/devices/device.py +++ b/switchbot/devices/device.py @@ -73,6 +73,7 @@ def _extract_region(userinfo: dict[str, Any]) -> str: "W1083002": SwitchbotModel.RELAY_SWITCH_1, # Relay Switch 1 "W1079000": SwitchbotModel.METER_PRO, # Meter Pro (another variant) "W1102001": SwitchbotModel.STRIP_LIGHT_3, # RGBWW Strip Light 3 + "W1101000": SwitchbotModel.PRESENCE_SENSOR, } REQ_HEADER = "570f" diff --git a/tests/test_adv_parser.py b/tests/test_adv_parser.py index f60d21c..40528fd 100644 --- a/tests/test_adv_parser.py +++ b/tests/test_adv_parser.py @@ -3468,6 +3468,23 @@ def test_humidifer_with_empty_data() -> None: "Smart Thermostat Radiator", SwitchbotModel.SMART_THERMOSTAT_RADIATOR, ), + AdvTestCase( + b"\xb0\xe9\xfelf\xaa\x06\xcc\x04V\x00\x8c", + b"\x00 d\x00\x10\xcc\xc8", + { + "adaptive_state": True, + "battery": 100, + "battery_range": ">=60%", + "led_state": True, + "lightLevel": 12, + "motion_detected": True, + "sequence_number": 6, + "trigger_flag": 0, + }, + b"\x00\x10\xcc\xc8", + "Presence Sensor", + SwitchbotModel.PRESENCE_SENSOR, + ), ], ) def test_adv_active(test_case: AdvTestCase) -> None: @@ -3720,6 +3737,22 @@ def test_adv_active(test_case: AdvTestCase) -> None: "Smart Thermostat Radiator", SwitchbotModel.SMART_THERMOSTAT_RADIATOR, ), + AdvTestCase( + b"\xb0\xe9\xfelf\xaa\x06\xcc\x04V\x00\x8c", + None, + { + "adaptive_state": True, + "battery_range": ">=60%", + "led_state": True, + "lightLevel": 12, + "motion_detected": True, + "sequence_number": 6, + "trigger_flag": 0, + }, + b"\x00\x10\xcc\xc8", + "Presence Sensor", + SwitchbotModel.PRESENCE_SENSOR, + ), ], ) def test_adv_passive(test_case: AdvTestCase) -> None: @@ -3901,6 +3934,14 @@ def test_adv_passive(test_case: AdvTestCase) -> None: "Smart Thermostat Radiator", SwitchbotModel.SMART_THERMOSTAT_RADIATOR, ), + AdvTestCase( + None, + b"\x00 d\x00\x10\xcc\xc8", + {}, + b"\x00\x10\xcc\xc8", + "Presence Sensor", + SwitchbotModel.PRESENCE_SENSOR, + ), ], ) def test_adv_with_empty_data(test_case: AdvTestCase) -> None: