1
0
Эх сурвалжийг харах

feat: add support for Hub4 (#605)

Onero-testdev 1 өдөр өмнө
parent
commit
9cc03244c7

+ 13 - 0
switchbot/adv_parser.py

@@ -27,6 +27,7 @@ from .adv_parsers.fan import (
 )
 from .adv_parsers.hub2 import process_wohub2
 from .adv_parsers.hub3 import process_hub3
+from .adv_parsers.hub4 import process_hub4
 from .adv_parsers.hubmini_matter import process_hubmini_matter
 from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohumidifier
 from .adv_parsers.keypad import process_wokeypad
@@ -574,6 +575,18 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
         "func": process_hub3,
         "manufacturer_id": 2409,
     },
+    b"\x00\x12\x82\x48": {
+        "modelName": SwitchbotModel.HUB4,
+        "modelFriendlyName": "Hub4",
+        "func": process_hub4,
+        "manufacturer_id": 2409,
+    },
+    b"\x01\x12\x82\x48": {
+        "modelName": SwitchbotModel.HUB4,
+        "modelFriendlyName": "Hub4",
+        "func": process_hub4,
+        "manufacturer_id": 2409,
+    },
     "-": {
         "modelName": SwitchbotModel.LOCK_LITE,
         "modelFriendlyName": "Lock Lite",

+ 18 - 0
switchbot/adv_parsers/hub4.py

@@ -0,0 +1,18 @@
+"""Hub4 advertisement parser."""
+
+from __future__ import annotations
+
+from typing import Any
+
+from .hub3 import process_hub3
+
+
+def process_hub4(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]:
+    """Parse Hub4 sensor data and its 20-level light reading."""
+    parsed = process_hub3(data, mfr_data)
+    if not parsed:
+        return {}
+    parsed["lightLevel"] = mfr_data[12] & 0x1F
+    # Hub4 has no documented lux mapping for its 20 light levels.
+    parsed.pop("illuminance")
+    return parsed

+ 1 - 0
switchbot/const/__init__.py

@@ -111,6 +111,7 @@ class SwitchbotModel(StrEnum):
     AIR_PURIFIER_TABLE_US = "Air Purifier Table US"
     AIR_PURIFIER_TABLE_JP = "Air Purifier Table JP"
     HUB3 = "Hub3"
+    HUB4 = "Hub4"
     LOCK_ULTRA = "Lock Ultra"
     LOCK_LITE = "Lock Lite"
     GARAGE_DOOR_OPENER = "Garage Door Opener"

+ 72 - 0
tests/test_hub4.py

@@ -0,0 +1,72 @@
+import pytest
+
+from switchbot import SwitchbotModel, parse_advertisement_data
+
+from .test_adv_parser import generate_advertisement_data, generate_ble_device
+
+
+@pytest.mark.parametrize("pairing", [False, True])
+@pytest.mark.parametrize("light_level", [1, 15, 16, 20])
+@pytest.mark.parametrize("sensor_inserted", [False, True])
+@pytest.mark.parametrize("motion", [False, True])
+def test_hub4_advertisement(pairing, light_level, sensor_inserted, motion):
+    service = bytes([0, 0, 100, int(pairing), 0x12, 0x82, 0x48])
+    manufacturer = bytearray.fromhex("01020304050601ff000000008003993480")
+    manufacturer[12] = 0x80 | (0 if sensor_inserted else 0x20) | light_level
+    manufacturer[16] = 0x80 if motion else 0
+    result = parse_advertisement_data(
+        generate_ble_device("01:02:03:04:05:06", "Hub4"),
+        generate_advertisement_data(
+            manufacturer_data={2409: bytes(manufacturer)},
+            service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": service},
+        ),
+    )
+    assert result is not None
+    assert result.data["modelName"] is SwitchbotModel.HUB4
+    assert result.data["modelFriendlyName"] == "Hub4"
+    assert result.data["model"] == service[3:]
+    assert result.data["data"] == {
+        "sequence_number": 1,
+        "network_state": 2,
+        "sensor_inserted": sensor_inserted,
+        "lightLevel": light_level,
+        "temperature_alarm": False,
+        "humidity_alarm": False,
+        "temp": {"c": 25.3, "f": 77.5},
+        "temperature": 25.3,
+        "humidity": 52,
+        "motion_detected": motion,
+    }
+
+
+@pytest.mark.parametrize("manufacturer", [None, b"", bytes(16)])
+def test_hub4_short_manufacturer_data(manufacturer):
+    result = parse_advertisement_data(
+        generate_ble_device("01:02:03:04:05:06", "Hub4"),
+        generate_advertisement_data(
+            manufacturer_data={2409: manufacturer},
+            service_data={
+                "0000fd3d-0000-1000-8000-00805f9b34fb": bytes.fromhex("00006400128248")
+            },
+        ),
+    )
+    assert result.data["data"] == {}
+
+
+def test_hub4_passive_advertisement():
+    result = parse_advertisement_data(
+        generate_ble_device("01:02:03:04:05:06", "Hub4"),
+        generate_advertisement_data(
+            manufacturer_data={
+                2409: bytes.fromhex("01020304050601ff000000009493193480")
+            },
+        ),
+        SwitchbotModel.HUB4,
+    )
+    assert result.data["modelName"] is SwitchbotModel.HUB4
+    assert result.data["data"]["lightLevel"] == 20
+    assert result.data["data"]["temperature"] == -25.3
+    assert result.data["data"]["temperature_alarm"] is True
+    assert result.data["data"]["humidity_alarm"] is True
+    assert "illuminance" not in result.data["data"]
+    assert result.active is False