Browse Source

Add support for passive curtain data (#160)

J. Nick Koston 1 year ago
parent
commit
89b931320a

+ 3 - 13
switchbot/adv_parser.py

@@ -38,6 +38,8 @@ class SwitchbotSupportedType(TypedDict):
     modelName: SwitchbotModel
     modelFriendlyName: str
     func: Callable[[bytes, bytes | None], dict[str, bool | int]]
+    manufacturer_id: int | None
+    manufacturer_data_length: int | None
 
 
 SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
@@ -73,12 +75,7 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
         "modelName": SwitchbotModel.CURTAIN,
         "modelFriendlyName": "Curtain",
         "func": process_wocurtain,
-        "service_uuids": {
-            "00001800-0000-1000-8000-00805f9b34fb",
-            "00001801-0000-1000-8000-00805f9b34fb",
-            "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
-        },
-        "manufacturer_id": 89,
+        "manufacturer_id": 2409,
     },
     "i": {
         "modelName": SwitchbotModel.METER,
@@ -166,7 +163,6 @@ def parse_advertisement_data(
 
     try:
         data = _parse_data(
-            "".join(sorted(advertisement_data.service_uuids)),
             _service_data,
             _mfr_data,
             _mfr_id,
@@ -186,7 +182,6 @@ def parse_advertisement_data(
 
 @lru_cache(maxsize=128)
 def _parse_data(
-    _service_uuids_str: str,
     _service_data: bytes | None,
     _mfr_data: bytes | None,
     _mfr_id: int | None = None,
@@ -199,15 +194,10 @@ def _parse_data(
         _model = _SWITCHBOT_MODEL_TO_CHAR[_switchbot_model]
 
     if not _model and _mfr_id and _mfr_id in MODELS_BY_MANUFACTURER_DATA:
-        _service_uuids = set(_service_uuids_str.split(","))
         for model_chr, model_data in MODELS_BY_MANUFACTURER_DATA[_mfr_id]:
             if model_data.get("manufacturer_data_length") == len(_mfr_data):
                 _model = model_chr
                 break
-            service_uuids = model_data.get("service_uuids", set())
-            if service_uuids and service_uuids.intersection(_service_uuids):
-                _model = model_chr
-                break
 
     if not _model:
         return None

+ 15 - 8
switchbot/adv_parsers/curtain.py

@@ -6,16 +6,23 @@ def process_wocurtain(
     data: bytes | None, mfr_data: bytes | None, reverse: bool = True
 ) -> dict[str, bool | int]:
     """Process woCurtain/Curtain services data."""
-    if data is None:
+    if mfr_data and len(mfr_data) >= 11:
+        device_data = mfr_data[8:11]
+    elif data:
+        device_data = data[3:6]
+    else:
         return {}
 
-    _position = max(min(data[3] & 0b01111111, 100), 0)
+    _position = max(min(device_data[0] & 0b01111111, 100), 0)
+    _in_motion = bool(device_data[0] & 0b10000000)
+    _light_level = (device_data[1] >> 4) & 0b00001111
+    _device_chain = device_data[1] & 0b00000111
 
     return {
-        "calibration": bool(data[1] & 0b01000000),
-        "battery": data[2] & 0b01111111,
-        "inMotion": bool(data[3] & 0b10000000),
+        "calibration": bool(data[1] & 0b01000000) if data else None,
+        "battery": data[2] & 0b01111111 if data else None,
+        "inMotion": _in_motion,
         "position": (100 - _position) if reverse else _position,
-        "lightLevel": (data[4] >> 4) & 0b00001111,
-        "deviceChain": data[4] & 0b00000111,
-    }
+        "lightLevel": _light_level,
+        "deviceChain": _device_chain,
+    }

+ 1 - 1
switchbot/adv_parsers/motion.py

@@ -25,7 +25,7 @@ def process_wopresence(
         sense_distance = (data[5] & 0b00001100) >> 2
         light_intensity = data[5] & 0b00000011
         is_light = bool(data[5] & 0b00000010)
-    if mfr_data:
+    if mfr_data and len(mfr_data) >= 8:
         motion_detected = bool(mfr_data[7] & 0b01000000)
         is_light = bool(mfr_data[7] & 0b00100000)
 

+ 30 - 0
tests/test_adv_parser.py

@@ -57,6 +57,36 @@ def test_parse_advertisement_data_curtain():
     )
 
 
+def test_parse_advertisement_data_curtain_passive():
+    """Test parse_advertisement_data for curtain passive."""
+    ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: b"\xe7\xabF\xac\x8f\x92|\x0f\x00\x11\x04"},
+        service_data={},
+        rssi=-80,
+    )
+    result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.CURTAIN)
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "rawAdvData": None,
+            "data": {
+                "calibration": None,
+                "battery": None,
+                "inMotion": False,
+                "position": 100,
+                "lightLevel": 1,
+                "deviceChain": 1,
+            },
+            "isEncrypted": False,
+            "model": "c",
+            "modelFriendlyName": "Curtain",
+            "modelName": SwitchbotModel.CURTAIN,
+        },
+        device=ble_device,
+        rssi=-80,
+    )
+
 def test_parse_advertisement_data_curtain_position_zero():
     """Test parse_advertisement_data for curtain position zero."""
     ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")