Skip to content

Commit 3d82e43

Browse files
zerzhangpre-commit-ci[bot]bdraco
authored
Add support for presence sensor (#410)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <[email protected]>
1 parent 8a98883 commit 3d82e43

File tree

6 files changed

+103
-0
lines changed

6 files changed

+103
-0
lines changed

switchbot/adv_parser.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from .adv_parsers.meter import process_wosensorth, process_wosensorth_c
3636
from .adv_parsers.motion import process_wopresence
3737
from .adv_parsers.plug import process_woplugmini
38+
from .adv_parsers.presence_sensor import process_presence_sensor
3839
from .adv_parsers.relay_switch import (
3940
process_garage_door_opener,
4041
process_relay_switch_1pm,
@@ -390,6 +391,12 @@ class SwitchbotSupportedType(TypedDict):
390391
"func": process_vacuum,
391392
"manufacturer_id": 2409,
392393
},
394+
b"\x00\x10\xcc\xc8": {
395+
"modelName": SwitchbotModel.PRESENCE_SENSOR,
396+
"modelFriendlyName": "Presence Sensor",
397+
"func": process_presence_sensor,
398+
"manufacturer_id": 2409,
399+
},
393400
}
394401

395402
_SWITCHBOT_MODEL_TO_CHAR = {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Advertisement data parser for presence sensor devices."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
7+
from ..const.presence_sensor import BATTERY_LEVEL_MAP
8+
9+
_LOGGER = logging.getLogger(__name__)
10+
11+
12+
def process_presence_sensor(
13+
data: bytes | None, mfr_data: bytes | None
14+
) -> dict[str, bool | int | str]:
15+
"""Process Presence Sensor data."""
16+
if mfr_data is None:
17+
return {}
18+
19+
seq_number = mfr_data[6]
20+
adaptive_state = bool(mfr_data[7] & 0x80)
21+
motion_detected = bool(mfr_data[7] & 0x40)
22+
battery_bits = (mfr_data[7] >> 2) & 0x03
23+
battery_range = BATTERY_LEVEL_MAP.get(battery_bits, "Unknown")
24+
trigger_flag = mfr_data[10]
25+
led_state = bool(mfr_data[11] & 0x80)
26+
light_level = mfr_data[11] & 0x0F
27+
28+
result = {
29+
"sequence_number": seq_number,
30+
"adaptive_state": adaptive_state,
31+
"motion_detected": motion_detected,
32+
"battery_range": battery_range,
33+
"trigger_flag": trigger_flag,
34+
"led_state": led_state,
35+
"lightLevel": light_level,
36+
}
37+
38+
if data:
39+
battery = data[2] & 0x7F
40+
result["battery"] = battery
41+
42+
_LOGGER.debug(
43+
"Processed presence sensor mfr data: %s, result: %s", mfr_data.hex(), result
44+
)
45+
return result

switchbot/const/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class SwitchbotModel(StrEnum):
101101
CLIMATE_PANEL = "Climate Panel"
102102
SMART_THERMOSTAT_RADIATOR = "Smart Thermostat Radiator"
103103
S20_VACUUM = "S20 Vacuum"
104+
PRESENCE_SENSOR = "Presence Sensor"
104105

105106

106107
__all__ = [

switchbot/const/presence_sensor.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Constants for presence sensor devices."""
2+
3+
BATTERY_LEVEL_MAP = {
4+
0: "<10%",
5+
1: "10-19%",
6+
2: "20-59%",
7+
3: ">=60%",
8+
}

switchbot/devices/device.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def _extract_region(userinfo: dict[str, Any]) -> str:
7474
"W1079000": SwitchbotModel.METER_PRO, # Meter Pro (another variant)
7575
"W1102001": SwitchbotModel.STRIP_LIGHT_3, # RGBWW Strip Light 3
7676
"W1106000": SwitchbotModel.S20_VACUUM,
77+
"W1101000": SwitchbotModel.PRESENCE_SENSOR,
7778
}
7879

7980
REQ_HEADER = "570f"

tests/test_adv_parser.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3501,6 +3501,23 @@ def test_humidifer_with_empty_data() -> None:
35013501
"S20 Vacuum",
35023502
SwitchbotModel.S20_VACUUM,
35033503
),
3504+
AdvTestCase(
3505+
b"\xb0\xe9\xfelf\xaa\x06\xcc\x04V\x00\x8c",
3506+
b"\x00 d\x00\x10\xcc\xc8",
3507+
{
3508+
"adaptive_state": True,
3509+
"battery": 100,
3510+
"battery_range": ">=60%",
3511+
"led_state": True,
3512+
"lightLevel": 12,
3513+
"motion_detected": True,
3514+
"sequence_number": 6,
3515+
"trigger_flag": 0,
3516+
},
3517+
b"\x00\x10\xcc\xc8",
3518+
"Presence Sensor",
3519+
SwitchbotModel.PRESENCE_SENSOR,
3520+
),
35043521
],
35053522
)
35063523
def test_adv_active(test_case: AdvTestCase) -> None:
@@ -3768,6 +3785,22 @@ def test_adv_active(test_case: AdvTestCase) -> None:
37683785
"S20 Vacuum",
37693786
SwitchbotModel.S20_VACUUM,
37703787
),
3788+
AdvTestCase(
3789+
b"\xb0\xe9\xfelf\xaa\x06\xcc\x04V\x00\x8c",
3790+
None,
3791+
{
3792+
"adaptive_state": True,
3793+
"battery_range": ">=60%",
3794+
"led_state": True,
3795+
"lightLevel": 12,
3796+
"motion_detected": True,
3797+
"sequence_number": 6,
3798+
"trigger_flag": 0,
3799+
},
3800+
b"\x00\x10\xcc\xc8",
3801+
"Presence Sensor",
3802+
SwitchbotModel.PRESENCE_SENSOR,
3803+
),
37713804
],
37723805
)
37733806
def test_adv_passive(test_case: AdvTestCase) -> None:
@@ -3957,6 +3990,14 @@ def test_adv_passive(test_case: AdvTestCase) -> None:
39573990
"S20 Vacuum",
39583991
SwitchbotModel.S20_VACUUM,
39593992
),
3993+
AdvTestCase(
3994+
None,
3995+
b"\x00 d\x00\x10\xcc\xc8",
3996+
{},
3997+
b"\x00\x10\xcc\xc8",
3998+
"Presence Sensor",
3999+
SwitchbotModel.PRESENCE_SENSOR,
4000+
),
39604001
],
39614002
)
39624003
def test_adv_with_empty_data(test_case: AdvTestCase) -> None:

0 commit comments

Comments
 (0)