Jelajahi Sumber

Add basic support for lock pro (#241)

Co-authored-by: J. Nick Koston <nick@koston.org>
Leo Shen 9 bulan lalu
induk
melakukan
48398d079e
5 mengubah file dengan 112 tambahan dan 11 penghapusan
  1. 6 0
      switchbot/adv_parser.py
  2. 1 0
      switchbot/const.py
  3. 33 10
      switchbot/devices/lock.py
  4. 3 1
      switchbot/discovery.py
  5. 69 0
      tests/test_adv_parser.py

+ 6 - 0
switchbot/adv_parser.py

@@ -150,6 +150,12 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
         "func": process_wolock,
         "manufacturer_id": 2409,
     },
+    "$": {
+        "modelName": SwitchbotModel.LOCK_PRO,
+        "modelFriendlyName": "Lock Pro",
+        "func": process_wolock,
+        "manufacturer_id": 2409,
+    },
     "x": {
         "modelName": SwitchbotModel.BLIND_TILT,
         "modelFriendlyName": "Blind Tilt",

+ 1 - 0
switchbot/const.py

@@ -47,6 +47,7 @@ class SwitchbotModel(StrEnum):
     COLOR_BULB = "WoBulb"
     CEILING_LIGHT = "WoCeiling"
     LOCK = "WoLock"
+    LOCK_PRO = "WoLockPro"
     BLIND_TILT = "WoBlindTilt"
     HUB2 = "WoHub2"
 

+ 33 - 10
switchbot/devices/lock.py

@@ -16,15 +16,28 @@ from ..const import (
     SwitchbotAccountConnectionError,
     SwitchbotApiError,
     SwitchbotAuthenticationError,
+    SwitchbotModel,
 )
 from .device import SwitchbotDevice, SwitchbotOperationError
 
 COMMAND_HEADER = "57"
 COMMAND_GET_CK_IV = f"{COMMAND_HEADER}0f2103"
-COMMAND_LOCK_INFO = f"{COMMAND_HEADER}0f4f8101"
-COMMAND_UNLOCK = f"{COMMAND_HEADER}0f4e01011080"
-COMMAND_UNLOCK_WITHOUT_UNLATCH = f"{COMMAND_HEADER}0f4e010110a0"
-COMMAND_LOCK = f"{COMMAND_HEADER}0f4e01011000"
+COMMAND_LOCK_INFO = {
+    SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4f8101",
+    SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4f8102",
+}
+COMMAND_UNLOCK = {
+    SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e01011080",
+    SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e0101000080",
+}
+COMMAND_UNLOCK_WITHOUT_UNLATCH = {
+    SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e010110a0",
+    SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e01010000a0",
+}
+COMMAND_LOCK = {
+    SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e01011000",
+    SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e0101000000",
+}
 COMMAND_ENABLE_NOTIFICATIONS = f"{COMMAND_HEADER}0e01001e00008101"
 COMMAND_DISABLE_NOTIFICATIONS = f"{COMMAND_HEADER}0e00"
 
@@ -49,6 +62,7 @@ class SwitchbotLock(SwitchbotDevice):
         key_id: str,
         encryption_key: str,
         interface: int = 0,
+        model: SwitchbotModel = SwitchbotModel.LOCK,
         **kwargs: Any,
     ) -> None:
         if len(key_id) == 0:
@@ -59,20 +73,27 @@ class SwitchbotLock(SwitchbotDevice):
             raise ValueError("encryption_key is missing")
         elif len(encryption_key) != 32:
             raise ValueError("encryption_key is invalid")
+        if model not in (SwitchbotModel.LOCK, SwitchbotModel.LOCK_PRO):
+            raise ValueError("initializing SwitchbotLock with a non-lock model")
         self._iv = None
         self._cipher = None
         self._key_id = key_id
         self._encryption_key = bytearray.fromhex(encryption_key)
         self._notifications_enabled: bool = False
+        self._model: SwitchbotModel = model
         super().__init__(device, None, interface, **kwargs)
 
     @staticmethod
     async def verify_encryption_key(
-        device: BLEDevice, key_id: str, encryption_key: str
+        device: BLEDevice,
+        key_id: str,
+        encryption_key: str,
+        model: SwitchbotModel = SwitchbotModel.LOCK,
+        **kwargs: Any,
     ) -> bool:
         try:
             lock = SwitchbotLock(
-                device=device, key_id=key_id, encryption_key=encryption_key
+                device, key_id=key_id, encryption_key=encryption_key, model=model
             )
         except ValueError:
             return False
@@ -183,19 +204,19 @@ class SwitchbotLock(SwitchbotDevice):
     async def lock(self) -> bool:
         """Send lock command."""
         return await self._lock_unlock(
-            COMMAND_LOCK, {LockStatus.LOCKED, LockStatus.LOCKING}
+            COMMAND_LOCK[self._model], {LockStatus.LOCKED, LockStatus.LOCKING}
         )
 
     async def unlock(self) -> bool:
         """Send unlock command. If unlatch feature is enabled in EU firmware, also unlatches door"""
         return await self._lock_unlock(
-            COMMAND_UNLOCK, {LockStatus.UNLOCKED, LockStatus.UNLOCKING}
+            COMMAND_UNLOCK[self._model], {LockStatus.UNLOCKED, LockStatus.UNLOCKING}
         )
 
     async def unlock_without_unlatch(self) -> bool:
         """Send unlock command. This command will not unlatch the door."""
         return await self._lock_unlock(
-            COMMAND_UNLOCK_WITHOUT_UNLATCH,
+            COMMAND_UNLOCK_WITHOUT_UNLATCH[self._model],
             {LockStatus.UNLOCKED, LockStatus.UNLOCKING, LockStatus.NOT_FULLY_LOCKED},
         )
 
@@ -275,7 +296,9 @@ class SwitchbotLock(SwitchbotDevice):
 
     async def _get_lock_info(self) -> bytes | None:
         """Return lock info of device."""
-        _data = await self._send_command(key=COMMAND_LOCK_INFO, retry=self._retry_count)
+        _data = await self._send_command(
+            key=COMMAND_LOCK_INFO[self._model], retry=self._retry_count
+        )
 
         if not self._check_command_result(_data, 0, COMMAND_RESULT_EXPECTED_VALUES):
             _LOGGER.error("Unsuccessful, please try again")

+ 3 - 1
switchbot/discovery.py

@@ -120,7 +120,9 @@ class GetSwitchbotDevices:
 
     async def get_locks(self) -> dict[str, SwitchBotAdvertisement]:
         """Return all WoLock/Locks devices with services data."""
-        return await self._get_devices_by_model("o")
+        locks = await self._get_devices_by_model("o")
+        lock_pros = await self._get_devices_by_model("$")
+        return {**locks, **lock_pros}
 
     async def get_device_data(
         self, address: str

+ 69 - 0
tests/test_adv_parser.py

@@ -1390,6 +1390,75 @@ def test_parsing_lock_passive():
     )
 
 
+def test_parsing_lock_pro_active():
+    """Test parsing lock pro with active data."""
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: b"\xc8\xf5,\xd9-V\x07\x82\x00d\x00\x00"},
+        service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"$\x80d"},
+        rssi=-80,
+    )
+    result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LOCK_PRO)
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "data": {
+                "battery": 100,
+                "calibration": True,
+                "status": LockStatus.LOCKED,
+                "update_from_secondary_lock": False,
+                "door_open": False,
+                "double_lock_mode": False,
+                "unclosed_alarm": False,
+                "unlocked_alarm": False,
+                "auto_lock_paused": False,
+                "night_latch": False,
+            },
+            "model": "$",
+            "isEncrypted": False,
+            "modelFriendlyName": "Lock Pro",
+            "modelName": SwitchbotModel.LOCK_PRO,
+            "rawAdvData": b"$\x80d",
+        },
+        device=ble_device,
+        rssi=-80,
+        active=True,
+    )
+
+
+def test_parsing_lock_pro_passive():
+    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
+    adv_data = generate_advertisement_data(
+        manufacturer_data={2409: bytes.fromhex("aabbccddeeff208200640000")}, rssi=-67
+    )
+    result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LOCK_PRO)
+    assert result == SwitchBotAdvertisement(
+        address="aa:bb:cc:dd:ee:ff",
+        data={
+            "data": {
+                "battery": None,
+                "calibration": True,
+                "status": LockStatus.LOCKED,
+                "update_from_secondary_lock": False,
+                "door_open": False,
+                "double_lock_mode": False,
+                "unclosed_alarm": False,
+                "unlocked_alarm": False,
+                "auto_lock_paused": False,
+                "night_latch": False,
+            },
+            "model": "$",
+            "isEncrypted": False,
+            "modelFriendlyName": "Lock Pro",
+            "modelName": SwitchbotModel.LOCK_PRO,
+            "rawAdvData": None,
+        },
+        device=ble_device,
+        rssi=-67,
+        active=False,
+    )
+
+
 def test_parsing_lock_active_old_firmware():
     """Test parsing lock with active data. Old firmware."""
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")