Skip to content

Commit fb376c4

Browse files
authored
Add support for retrieving sensor data from WoHub2 device (#240)
1 parent 3a41a6d commit fb376c4

File tree

5 files changed

+86
-40
lines changed

5 files changed

+86
-40
lines changed

switchbot/adv_parser.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Library to handle connection with Switchbot."""
2+
23
from __future__ import annotations
34

45
import logging
@@ -15,6 +16,7 @@
1516
from .adv_parsers.ceiling_light import process_woceiling
1617
from .adv_parsers.contact import process_wocontact
1718
from .adv_parsers.curtain import process_wocurtain
19+
from .adv_parsers.hub2 import process_wohub2
1820
from .adv_parsers.humidifier import process_wohumidifier
1921
from .adv_parsers.light_strip import process_wostrip
2022
from .adv_parsers.lock import process_wolock
@@ -54,7 +56,6 @@ class SwitchbotSupportedType(TypedDict):
5456
"modelName": SwitchbotModel.BOT,
5557
"modelFriendlyName": "Bot",
5658
"func": process_wohand,
57-
"service_uuids": {"cba20d00-224d-11e6-9fb8-0002a5d5c51b"},
5859
"manufacturer_id": 89,
5960
},
6061
"s": {
@@ -100,6 +101,12 @@ class SwitchbotSupportedType(TypedDict):
100101
"func": process_wosensorth,
101102
"manufacturer_id": 2409,
102103
},
104+
"v": {
105+
"modelName": SwitchbotModel.HUB2,
106+
"modelFriendlyName": "Hub 2",
107+
"func": process_wohub2,
108+
"manufacturer_id": 2409,
109+
},
103110
"g": {
104111
"modelName": SwitchbotModel.PLUG_MINI,
105112
"modelFriendlyName": "Plug Mini",

switchbot/adv_parsers/hub2.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Hub2 parser."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
8+
def process_wohub2(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]:
9+
"""Process woHub2 sensor manufacturer data."""
10+
temp_data = None
11+
12+
if mfr_data:
13+
status = mfr_data[12]
14+
temp_data = mfr_data[13:16]
15+
16+
if not temp_data:
17+
return {}
18+
19+
_temp_sign = 1 if temp_data[1] & 0b10000000 else -1
20+
_temp_c = _temp_sign * (
21+
(temp_data[1] & 0b01111111) + ((temp_data[0] & 0b00001111) / 10)
22+
)
23+
_temp_f = (_temp_c * 9 / 5) + 32
24+
_temp_f = (_temp_f * 10) / 10
25+
humidity = temp_data[2] & 0b01111111
26+
light_level = status & 0b11111
27+
28+
if _temp_c == 0 and humidity == 0:
29+
return {}
30+
31+
_wohub2_data = {
32+
# Data should be flat, but we keep the original structure for now
33+
"temp": {"c": _temp_c, "f": _temp_f},
34+
"temperature": _temp_c,
35+
"fahrenheit": bool(temp_data[2] & 0b10000000),
36+
"humidity": humidity,
37+
"lightLevel": light_level,
38+
}
39+
40+
return _wohub2_data

switchbot/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class SwitchbotModel(StrEnum):
4848
CEILING_LIGHT = "WoCeiling"
4949
LOCK = "WoLock"
5050
BLIND_TILT = "WoBlindTilt"
51+
HUB2 = "WoHub2"
5152

5253

5354
class LockStatus(Enum):

switchbot/discovery.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ async def discover(
4242

4343
devices = None
4444
devices = bleak.BleakScanner(
45+
detection_callback=self.detection_callback,
4546
# TODO: Find new UUIDs to filter on. For example, see
4647
# https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/blob/4ad138bb09f0fbbfa41b152ca327a78c1d0b6ba9/devicetypes/meter.md
4748
adapter=self._interface,
4849
)
49-
devices.register_detection_callback(self.detection_callback)
5050

5151
async with CONNECT_LOCK:
5252
await devices.start()
@@ -111,7 +111,8 @@ async def get_tempsensors(self) -> dict[str, SwitchBotAdvertisement]:
111111
base_meters = await self._get_devices_by_model("T")
112112
plus_meters = await self._get_devices_by_model("i")
113113
io_meters = await self._get_devices_by_model("w")
114-
return {**base_meters, **plus_meters, **io_meters}
114+
hub2_meters = await self._get_devices_by_model("v")
115+
return {**base_meters, **plus_meters, **io_meters, **hub2_meters}
115116

116117
async def get_contactsensors(self) -> dict[str, SwitchBotAdvertisement]:
117118
"""Return all WoContact/Contact sensor devices with services data."""

tests/test_adv_parser.py

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,40 @@ def test_wosensor_active_zero_data():
964964
)
965965

966966

967+
def test_wohub2_passive_and_active():
968+
"""Test parsing wosensor as passive with active data as well."""
969+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
970+
adv_data = generate_advertisement_data(
971+
manufacturer_data={
972+
2409: b"\xaa\xbb\xcc\xdd\xee\xff\x00\xfffT\x1a\xf1\x82\x07\x9a2\x00"
973+
},
974+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"v\x00"},
975+
tx_power=-127,
976+
rssi=-50,
977+
)
978+
result = parse_advertisement_data(ble_device, adv_data)
979+
assert result == SwitchBotAdvertisement(
980+
address="aa:bb:cc:dd:ee:ff",
981+
data={
982+
"data": {
983+
"fahrenheit": False,
984+
"humidity": 50,
985+
"lightLevel": 2,
986+
"temp": {"c": 26.7, "f": 80.06},
987+
"temperature": 26.7,
988+
},
989+
"isEncrypted": False,
990+
"model": "v",
991+
"modelFriendlyName": "Hub 2",
992+
"modelName": SwitchbotModel.HUB2,
993+
"rawAdvData": b"v\x00",
994+
},
995+
device=ble_device,
996+
rssi=-50,
997+
active=True,
998+
)
999+
1000+
9671001
def test_woiosensor_passive_and_active():
9681002
"""Test parsing woiosensor as passive with active data as well."""
9691003
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
@@ -1286,43 +1320,6 @@ def test_motion_with_light_detected():
12861320
)
12871321

12881322

1289-
def test_motion_sensor_motion_passive():
1290-
"""Test parsing motion sensor with motion data."""
1291-
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
1292-
adv_data = generate_advertisement_data(
1293-
manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIi\\\x008"},
1294-
service_data={},
1295-
tx_power=-127,
1296-
rssi=-87,
1297-
)
1298-
result = parse_advertisement_data(
1299-
ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
1300-
)
1301-
assert result == SwitchBotAdvertisement(
1302-
address="aa:bb:cc:dd:ee:ff",
1303-
data={
1304-
"data": {
1305-
"battery": None,
1306-
"iot": None,
1307-
"is_light": False,
1308-
"led": None,
1309-
"light_intensity": None,
1310-
"motion_detected": True,
1311-
"sense_distance": None,
1312-
"tested": None,
1313-
},
1314-
"isEncrypted": False,
1315-
"model": "s",
1316-
"modelFriendlyName": "Motion Sensor",
1317-
"modelName": SwitchbotModel.MOTION_SENSOR,
1318-
"rawAdvData": None,
1319-
},
1320-
device=ble_device,
1321-
rssi=-87,
1322-
active=False,
1323-
)
1324-
1325-
13261323
def test_parsing_lock_active():
13271324
"""Test parsing lock with active data."""
13281325
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")

0 commit comments

Comments
 (0)