From cfe5c8e5fdcdb78dae330a0380986bc53d552ab7 Mon Sep 17 00:00:00 2001 From: zerzhang <916250497@qq.com> Date: Thu, 6 Nov 2025 15:35:33 +0800 Subject: [PATCH 1/4] add support for presence sensor --- switchbot/adv_parser.py | 7 ++++ switchbot/adv_parsers/presence_sensor.py | 42 ++++++++++++++++++++++++ switchbot/const/__init__.py | 1 + switchbot/const/presence_sensor.py | 9 +++++ switchbot/devices/device.py | 1 + tests/test_adv_parser.py | 41 +++++++++++++++++++++++ 6 files changed, 101 insertions(+) create mode 100644 switchbot/adv_parsers/presence_sensor.py create mode 100644 switchbot/const/presence_sensor.py diff --git a/switchbot/adv_parser.py b/switchbot/adv_parser.py index 378b4c20..738cb731 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, @@ -390,6 +391,12 @@ class SwitchbotSupportedType(TypedDict): "func": process_vacuum, "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 00000000..967ea695 --- /dev/null +++ b/switchbot/adv_parsers/presence_sensor.py @@ -0,0 +1,42 @@ +"""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 0b15c81a..56c75924 100644 --- a/switchbot/const/__init__.py +++ b/switchbot/const/__init__.py @@ -101,6 +101,7 @@ class SwitchbotModel(StrEnum): CLIMATE_PANEL = "Climate Panel" SMART_THERMOSTAT_RADIATOR = "Smart Thermostat Radiator" S20_VACUUM = "S20 Vacuum" + PRESENCE_SENSOR = "Presence Sensor" __all__ = [ diff --git a/switchbot/const/presence_sensor.py b/switchbot/const/presence_sensor.py new file mode 100644 index 00000000..4c558636 --- /dev/null +++ b/switchbot/const/presence_sensor.py @@ -0,0 +1,9 @@ +"""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 d2d52323..14abf95a 100644 --- a/switchbot/devices/device.py +++ b/switchbot/devices/device.py @@ -74,6 +74,7 @@ def _extract_region(userinfo: dict[str, Any]) -> str: "W1079000": SwitchbotModel.METER_PRO, # Meter Pro (another variant) "W1102001": SwitchbotModel.STRIP_LIGHT_3, # RGBWW Strip Light 3 "W1106000": SwitchbotModel.S20_VACUUM, + "W1101000": SwitchbotModel.PRESENCE_SENSOR, } REQ_HEADER = "570f" diff --git a/tests/test_adv_parser.py b/tests/test_adv_parser.py index 3ff48056..406ef80d 100644 --- a/tests/test_adv_parser.py +++ b/tests/test_adv_parser.py @@ -3501,6 +3501,23 @@ def test_humidifer_with_empty_data() -> None: "S20 Vacuum", SwitchbotModel.S20_VACUUM, ), + 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: @@ -3768,6 +3785,22 @@ def test_adv_active(test_case: AdvTestCase) -> None: "S20 Vacuum", SwitchbotModel.S20_VACUUM, ), + 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: @@ -3957,6 +3990,14 @@ def test_adv_passive(test_case: AdvTestCase) -> None: "S20 Vacuum", SwitchbotModel.S20_VACUUM, ), + 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: From a5b669e0b0464e3aafe2707d8a730bb97ae7b0d0 Mon Sep 17 00:00:00 2001 From: zerzhang <916250497@qq.com> Date: Thu, 6 Nov 2025 16:17:18 +0800 Subject: [PATCH 2/4] fix --- switchbot/adv_parsers/presence_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/switchbot/adv_parsers/presence_sensor.py b/switchbot/adv_parsers/presence_sensor.py index 967ea695..ef83849d 100644 --- a/switchbot/adv_parsers/presence_sensor.py +++ b/switchbot/adv_parsers/presence_sensor.py @@ -1,6 +1,7 @@ """Advertisement data parser for presence sensor devices.""" import logging + from ..const.presence_sensor import BATTERY_LEVEL_MAP _LOGGER = logging.getLogger(__name__) From 864692504be6cfdb5100fd55508e4c3e1177f9f3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 08:20:26 +0000 Subject: [PATCH 3/4] chore(pre-commit.ci): auto fixes --- switchbot/const/presence_sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/switchbot/const/presence_sensor.py b/switchbot/const/presence_sensor.py index 4c558636..e0b65371 100644 --- a/switchbot/const/presence_sensor.py +++ b/switchbot/const/presence_sensor.py @@ -1,6 +1,5 @@ """Constants for presence sensor devices.""" - BATTERY_LEVEL_MAP = { 0: "<10%", 1: "10-19%", From 1dd000e274d3508cd4dafa7f1bd1435ba766bdf6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 08:05:10 -0600 Subject: [PATCH 4/4] Update switchbot/adv_parsers/presence_sensor.py --- switchbot/adv_parsers/presence_sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/switchbot/adv_parsers/presence_sensor.py b/switchbot/adv_parsers/presence_sensor.py index ef83849d..e638ff54 100644 --- a/switchbot/adv_parsers/presence_sensor.py +++ b/switchbot/adv_parsers/presence_sensor.py @@ -1,5 +1,7 @@ """Advertisement data parser for presence sensor devices.""" +from __future__ import annotations + import logging from ..const.presence_sensor import BATTERY_LEVEL_MAP