Skip to content

Commit 0a0066b

Browse files
authored
Passive motion support (#158)
1 parent 0d7df5a commit 0a0066b

File tree

3 files changed

+282
-9
lines changed

3 files changed

+282
-9
lines changed

switchbot/adv_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class SwitchbotSupportedType(TypedDict):
5959
"modelName": SwitchbotModel.MOTION_SENSOR,
6060
"modelFriendlyName": "Motion Sensor",
6161
"func": process_wopresence,
62+
"manufacturer_data_length": 10,
6263
},
6364
"r": {
6465
"modelName": SwitchbotModel.LIGHT_STRIP,

switchbot/adv_parsers/motion.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,36 @@ def process_wopresence(
66
data: bytes | None, mfr_data: bytes | None
77
) -> dict[str, bool | int]:
88
"""Process WoPresence Sensor services data."""
9-
if data is None:
9+
if data is None and mfr_data is None:
1010
return {}
11+
tested = None
12+
battery = None
13+
led = None
14+
iot = None
15+
sense_distance = None
16+
light_intensity = None
17+
is_light = None
18+
19+
if data:
20+
tested = bool(data[1] & 0b10000000)
21+
motion_detected = bool(data[1] & 0b01000000)
22+
battery = data[2] & 0b01111111
23+
led = (data[5] & 0b00100000) >> 5
24+
iot = (data[5] & 0b00010000) >> 4
25+
sense_distance = (data[5] & 0b00001100) >> 2
26+
light_intensity = data[5] & 0b00000011
27+
is_light = bool(data[5] & 0b00000010)
28+
if mfr_data:
29+
motion_detected = bool(mfr_data[7] & 0b01000000)
30+
is_light = bool(mfr_data[7] & 0b00100000)
31+
1132
return {
12-
"tested": bool(data[1] & 0b10000000),
13-
"motion_detected": bool(data[1] & 0b01000000),
14-
"battery": data[2] & 0b01111111,
15-
"led": (data[5] & 0b00100000) >> 5,
16-
"iot": (data[5] & 0b00010000) >> 4,
17-
"sense_distance": (data[5] & 0b00001100) >> 2,
18-
"light_intensity": data[5] & 0b00000011,
19-
"is_light": bool(data[5] & 0b00000010),
33+
"tested": tested,
34+
"motion_detected": motion_detected,
35+
"battery": battery,
36+
"led": led,
37+
"iot": iot,
38+
"sense_distance": sense_distance,
39+
"light_intensity": light_intensity,
40+
"is_light": is_light,
2041
}

tests/test_adv_parser.py

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,3 +769,254 @@ def test_wosensor_passive_only():
769769
device=ble_device,
770770
rssi=-50,
771771
)
772+
773+
774+
def test_motion_sensor_clear():
775+
"""Test parsing motion sensor with clear data."""
776+
ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
777+
adv_data = generate_advertisement_data(
778+
manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIj\x1c\x00f"},
779+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s\x00\xe2\x00f\x01"},
780+
tx_power=-127,
781+
rssi=-87,
782+
)
783+
result = parse_advertisement_data(
784+
ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
785+
)
786+
assert result == SwitchBotAdvertisement(
787+
address="aa:bb:cc:dd:ee:ff",
788+
data={
789+
"data": {
790+
"battery": 98,
791+
"iot": 0,
792+
"is_light": False,
793+
"led": 0,
794+
"light_intensity": 1,
795+
"motion_detected": False,
796+
"sense_distance": 0,
797+
"tested": False,
798+
},
799+
"isEncrypted": False,
800+
"model": "s",
801+
"modelFriendlyName": "Motion Sensor",
802+
"modelName": SwitchbotModel.MOTION_SENSOR,
803+
"rawAdvData": b"s\x00\xe2\x00f\x01",
804+
},
805+
device=ble_device,
806+
rssi=-87,
807+
)
808+
809+
810+
def test_motion_sensor_clear_passive():
811+
"""Test parsing motion sensor with clear data."""
812+
ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
813+
adv_data = generate_advertisement_data(
814+
manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIj\x1c\x00f"},
815+
service_data={},
816+
tx_power=-127,
817+
rssi=-87,
818+
)
819+
result = parse_advertisement_data(
820+
ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
821+
)
822+
assert result == SwitchBotAdvertisement(
823+
address="aa:bb:cc:dd:ee:ff",
824+
data={
825+
"data": {
826+
"battery": None,
827+
"iot": None,
828+
"is_light": False,
829+
"led": None,
830+
"light_intensity": None,
831+
"motion_detected": False,
832+
"sense_distance": None,
833+
"tested": None,
834+
},
835+
"isEncrypted": False,
836+
"model": "s",
837+
"modelFriendlyName": "Motion Sensor",
838+
"modelName": SwitchbotModel.MOTION_SENSOR,
839+
"rawAdvData": None,
840+
},
841+
device=ble_device,
842+
rssi=-87,
843+
)
844+
845+
846+
def test_motion_sensor_motion():
847+
"""Test parsing motion sensor with motion data."""
848+
ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
849+
adv_data = generate_advertisement_data(
850+
manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIi\\\x008"},
851+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s@\xe2\x008\x01"},
852+
tx_power=-127,
853+
rssi=-87,
854+
)
855+
result = parse_advertisement_data(
856+
ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
857+
)
858+
assert result == SwitchBotAdvertisement(
859+
address="aa:bb:cc:dd:ee:ff",
860+
data={
861+
"data": {
862+
"battery": 98,
863+
"iot": 0,
864+
"is_light": False,
865+
"led": 0,
866+
"light_intensity": 1,
867+
"motion_detected": True,
868+
"sense_distance": 0,
869+
"tested": False,
870+
},
871+
"isEncrypted": False,
872+
"model": "s",
873+
"modelFriendlyName": "Motion Sensor",
874+
"modelName": SwitchbotModel.MOTION_SENSOR,
875+
"rawAdvData": b"s@\xe2\x008\x01",
876+
},
877+
device=ble_device,
878+
rssi=-87,
879+
)
880+
881+
882+
def test_motion_sensor_motion_passive():
883+
"""Test parsing motion sensor with motion data."""
884+
ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
885+
adv_data = generate_advertisement_data(
886+
manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIi\\\x008"},
887+
service_data={},
888+
tx_power=-127,
889+
rssi=-87,
890+
)
891+
result = parse_advertisement_data(
892+
ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
893+
)
894+
assert result == SwitchBotAdvertisement(
895+
address="aa:bb:cc:dd:ee:ff",
896+
data={
897+
"data": {
898+
"battery": None,
899+
"iot": None,
900+
"is_light": False,
901+
"led": None,
902+
"light_intensity": None,
903+
"motion_detected": True,
904+
"sense_distance": None,
905+
"tested": None,
906+
},
907+
"isEncrypted": False,
908+
"model": "s",
909+
"modelFriendlyName": "Motion Sensor",
910+
"modelName": SwitchbotModel.MOTION_SENSOR,
911+
"rawAdvData": None,
912+
},
913+
device=ble_device,
914+
rssi=-87,
915+
)
916+
917+
918+
def test_motion_sensor_is_light_passive():
919+
"""Test parsing motion sensor with motion data."""
920+
ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
921+
adv_data = generate_advertisement_data(
922+
manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIs,\x04g"},
923+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s\x00\xe2\x04g\x02"},
924+
tx_power=-127,
925+
rssi=-93,
926+
)
927+
result = parse_advertisement_data(
928+
ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
929+
)
930+
assert result == SwitchBotAdvertisement(
931+
address="aa:bb:cc:dd:ee:ff",
932+
data={
933+
"data": {
934+
"battery": 98,
935+
"iot": 0,
936+
"is_light": True,
937+
"led": 0,
938+
"light_intensity": 2,
939+
"motion_detected": False,
940+
"sense_distance": 0,
941+
"tested": False,
942+
},
943+
"isEncrypted": False,
944+
"model": "s",
945+
"modelFriendlyName": "Motion Sensor",
946+
"modelName": SwitchbotModel.MOTION_SENSOR,
947+
"rawAdvData": b"s\x00\xe2\x04g\x02",
948+
},
949+
device=ble_device,
950+
rssi=-93,
951+
)
952+
953+
954+
def test_motion_sensor_is_light_active():
955+
"""Test parsing motion sensor with motion data."""
956+
ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
957+
adv_data = generate_advertisement_data(
958+
manufacturer_data={},
959+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s\x00\xe2\x04g\x02"},
960+
tx_power=-127,
961+
rssi=-93,
962+
)
963+
result = parse_advertisement_data(
964+
ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
965+
)
966+
assert result == SwitchBotAdvertisement(
967+
address="aa:bb:cc:dd:ee:ff",
968+
data={
969+
"data": {
970+
"battery": 98,
971+
"iot": 0,
972+
"is_light": True,
973+
"led": 0,
974+
"light_intensity": 2,
975+
"motion_detected": False,
976+
"sense_distance": 0,
977+
"tested": False,
978+
},
979+
"isEncrypted": False,
980+
"model": "s",
981+
"modelFriendlyName": "Motion Sensor",
982+
"modelName": SwitchbotModel.MOTION_SENSOR,
983+
"rawAdvData": b"s\x00\xe2\x04g\x02",
984+
},
985+
device=ble_device,
986+
rssi=-93,
987+
)
988+
989+
990+
def test_motion_with_light_detected():
991+
ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
992+
adv_data = generate_advertisement_data(
993+
manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIvl\x00,"},
994+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s@\xe2\x00,\x02"},
995+
tx_power=-127,
996+
rssi=-84,
997+
)
998+
result = parse_advertisement_data(
999+
ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
1000+
)
1001+
assert result == SwitchBotAdvertisement(
1002+
address="aa:bb:cc:dd:ee:ff",
1003+
data={
1004+
"data": {
1005+
"battery": 98,
1006+
"iot": 0,
1007+
"is_light": True,
1008+
"led": 0,
1009+
"light_intensity": 2,
1010+
"motion_detected": True,
1011+
"sense_distance": 0,
1012+
"tested": False,
1013+
},
1014+
"isEncrypted": False,
1015+
"model": "s",
1016+
"modelFriendlyName": "Motion Sensor",
1017+
"modelName": SwitchbotModel.MOTION_SENSOR,
1018+
"rawAdvData": b"s@\xe2\x00,\x02",
1019+
},
1020+
device=ble_device,
1021+
rssi=-84,
1022+
)

0 commit comments

Comments
 (0)