Skip to content

Commit 451dc9c

Browse files
luc-assbdracopre-commit-ci[bot]
authored
add Leak Detector (#262)
Co-authored-by: J. Nick Koston <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 9f939ce commit 451dc9c

File tree

4 files changed

+185
-0
lines changed

4 files changed

+185
-0
lines changed

switchbot/adv_parser.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .adv_parsers.hub2 import process_wohub2
2020
from .adv_parsers.humidifier import process_wohumidifier
2121
from .adv_parsers.keypad import process_wokeypad
22+
from .adv_parsers.leak import process_leak
2223
from .adv_parsers.light_strip import process_wostrip
2324
from .adv_parsers.lock import process_wolock, process_wolock_pro
2425
from .adv_parsers.meter import process_wosensorth, process_wosensorth_c
@@ -178,6 +179,12 @@ class SwitchbotSupportedType(TypedDict):
178179
"func": process_woblindtilt,
179180
"manufacturer_id": 2409,
180181
},
182+
"3": {
183+
"modelName": SwitchbotModel.LEAK,
184+
"modelFriendlyName": "Leak Detector",
185+
"func": process_leak,
186+
"manufacturer_id": 2409,
187+
},
181188
"y": {
182189
"modelName": SwitchbotModel.KEYPAD,
183190
"modelFriendlyName": "Keypad",

switchbot/adv_parsers/leak.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Leak detector adv parser."""
2+
3+
4+
def process_leak(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool | int]:
5+
"""Process SwitchBot Water Leak Detector advertisement data."""
6+
if data is None or len(data) < 3 or mfr_data is None or len(mfr_data) < 2:
7+
return {}
8+
9+
water_leak_detected = None
10+
device_tampered = None
11+
battery_level = None
12+
low_battery = None
13+
14+
# Byte 1: Event Flags
15+
event_flags = mfr_data[8]
16+
water_leak_detected = bool(event_flags & 0b00000001) # Bit 0
17+
device_tampered = bool(event_flags & 0b00000010) # Bit 1
18+
19+
# Byte 2: Battery Info
20+
battery_info = mfr_data[7]
21+
battery_level = battery_info & 0b01111111 # Bits 0-6
22+
low_battery = bool(battery_info & 0b10000000) # Bit 7
23+
24+
return {
25+
"leak": water_leak_detected,
26+
"tampered": device_tampered,
27+
"battery": battery_level,
28+
"low_battery": low_battery,
29+
}

switchbot/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class SwitchbotModel(StrEnum):
5353
LOCK_PRO = "WoLockPro"
5454
BLIND_TILT = "WoBlindTilt"
5555
HUB2 = "WoHub2"
56+
LEAK = "Leak Detector"
5657
KEYPAD = "WoKeypad"
5758
RELAY_SWITCH_1PM = "Relay Switch 1PM"
5859
RELAY_SWITCH_1 = "Relay Switch 1"

tests/test_adv_parser.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1737,3 +1737,151 @@ def test_parse_advertisement_data_relay_switch_1():
17371737
rssi=-67,
17381738
active=True,
17391739
)
1740+
1741+
1742+
def test_leak_active():
1743+
"""Test parse_advertisement_data for the leak detector."""
1744+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
1745+
adv_data = generate_advertisement_data(
1746+
manufacturer_data={2409: b"\xc4407Lz\x18N\x98g^\x94Q<\x05\x00\x00\x00\x00"},
1747+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"&\x00N"},
1748+
rssi=-72,
1749+
)
1750+
result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LEAK)
1751+
assert result == SwitchBotAdvertisement(
1752+
address="aa:bb:cc:dd:ee:ff",
1753+
data={
1754+
"data": {
1755+
"leak": False,
1756+
"tampered": False,
1757+
"battery": 78,
1758+
"low_battery": False,
1759+
},
1760+
"isEncrypted": False,
1761+
"model": "3",
1762+
"modelFriendlyName": "Leak Detector",
1763+
"modelName": SwitchbotModel.LEAK,
1764+
"rawAdvData": b"&\x00N",
1765+
},
1766+
device=ble_device,
1767+
rssi=-72,
1768+
active=True,
1769+
)
1770+
1771+
1772+
def test_leak_passive():
1773+
"""Test parse_advertisement_data for the leak detector."""
1774+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
1775+
adv_data = generate_advertisement_data(
1776+
manufacturer_data={2409: b"\xc4407Lz\x18N\x98g^\x94Q<\x05\x00\x00\x00\x00"},
1777+
rssi=-72,
1778+
)
1779+
result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LEAK)
1780+
assert result == SwitchBotAdvertisement(
1781+
address="aa:bb:cc:dd:ee:ff",
1782+
data={
1783+
"data": {},
1784+
"isEncrypted": False,
1785+
"model": "3",
1786+
"rawAdvData": None,
1787+
},
1788+
device=ble_device,
1789+
rssi=-72,
1790+
active=False,
1791+
)
1792+
1793+
1794+
def test_leak_no_leak_detected():
1795+
"""Test parse_advertisement_data for the leak detector."""
1796+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "Any")
1797+
adv_data = generate_advertisement_data(
1798+
manufacturer_data={
1799+
2409: b"\xc4407LzJd\x98ga\xc4\n<\x05\x00\x00\x00\x00"
1800+
}, # no leak, batt
1801+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"&\x00d"},
1802+
rssi=-73,
1803+
)
1804+
result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LEAK)
1805+
assert result == SwitchBotAdvertisement(
1806+
address="aa:bb:cc:dd:ee:ff",
1807+
data={
1808+
"data": {
1809+
"leak": False,
1810+
"tampered": False,
1811+
"battery": 100,
1812+
"low_battery": False,
1813+
},
1814+
"isEncrypted": False,
1815+
"model": "3",
1816+
"modelFriendlyName": "Leak Detector",
1817+
"modelName": SwitchbotModel.LEAK,
1818+
"rawAdvData": b"&\x00d",
1819+
},
1820+
device=ble_device,
1821+
rssi=-73,
1822+
active=True,
1823+
)
1824+
1825+
1826+
def test_leak_leak_detected():
1827+
"""Test parse_advertisement_data for the leak detector."""
1828+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "Any")
1829+
adv_data = generate_advertisement_data(
1830+
manufacturer_data={
1831+
2409: b"\xc4407LzGd\xf9ga\xc4\x08<\x05\x00\x00\x00\x00"
1832+
}, # leak, batt
1833+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"&\x00d"},
1834+
rssi=-73,
1835+
)
1836+
result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LEAK)
1837+
assert result == SwitchBotAdvertisement(
1838+
address="aa:bb:cc:dd:ee:ff",
1839+
data={
1840+
"data": {
1841+
"leak": True,
1842+
"tampered": False,
1843+
"battery": 100,
1844+
"low_battery": False,
1845+
},
1846+
"isEncrypted": False,
1847+
"model": "3",
1848+
"modelFriendlyName": "Leak Detector",
1849+
"modelName": SwitchbotModel.LEAK,
1850+
"rawAdvData": b"&\x00d",
1851+
},
1852+
device=ble_device,
1853+
rssi=-73,
1854+
active=True,
1855+
)
1856+
1857+
1858+
def test_leak_low_battery():
1859+
"""Test parse_advertisement_data for the leak detector."""
1860+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "Any")
1861+
adv_data = generate_advertisement_data(
1862+
manufacturer_data={
1863+
2409: b"\xc4407Lz\x02\t\x98\x00\x00\x00\x00<\x05\x00\x00\x00\x00"
1864+
}, # no leak, low battery
1865+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"&\x00d"},
1866+
rssi=-73,
1867+
)
1868+
result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LEAK)
1869+
assert result == SwitchBotAdvertisement(
1870+
address="aa:bb:cc:dd:ee:ff",
1871+
data={
1872+
"data": {
1873+
"leak": False,
1874+
"tampered": False,
1875+
"battery": 9,
1876+
"low_battery": False,
1877+
},
1878+
"isEncrypted": False,
1879+
"model": "3",
1880+
"modelFriendlyName": "Leak Detector",
1881+
"modelName": SwitchbotModel.LEAK,
1882+
"rawAdvData": b"&\x00d",
1883+
},
1884+
device=ble_device,
1885+
rssi=-73,
1886+
active=True,
1887+
)

0 commit comments

Comments
 (0)