Browse Source

Add basic support for lock pro (#241)

Co-authored-by: J. Nick Koston <nick@koston.org>
Leo Shen 11 months ago
parent
commit
48398d079e
5 changed files with 112 additions and 11 deletions
  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,
         "func": process_wolock,
         "manufacturer_id": 2409,
         "manufacturer_id": 2409,
     },
     },
+    "$": {
+        "modelName": SwitchbotModel.LOCK_PRO,
+        "modelFriendlyName": "Lock Pro",
+        "func": process_wolock,
+        "manufacturer_id": 2409,
+    },
     "x": {
     "x": {
         "modelName": SwitchbotModel.BLIND_TILT,
         "modelName": SwitchbotModel.BLIND_TILT,
         "modelFriendlyName": "Blind Tilt",
         "modelFriendlyName": "Blind Tilt",

+ 1 - 0
switchbot/const.py

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

+ 33 - 10
switchbot/devices/lock.py

@@ -16,15 +16,28 @@ from ..const import (
     SwitchbotAccountConnectionError,
     SwitchbotAccountConnectionError,
     SwitchbotApiError,
     SwitchbotApiError,
     SwitchbotAuthenticationError,
     SwitchbotAuthenticationError,
+    SwitchbotModel,
 )
 )
 from .device import SwitchbotDevice, SwitchbotOperationError
 from .device import SwitchbotDevice, SwitchbotOperationError
 
 
 COMMAND_HEADER = "57"
 COMMAND_HEADER = "57"
 COMMAND_GET_CK_IV = f"{COMMAND_HEADER}0f2103"
 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_ENABLE_NOTIFICATIONS = f"{COMMAND_HEADER}0e01001e00008101"
 COMMAND_DISABLE_NOTIFICATIONS = f"{COMMAND_HEADER}0e00"
 COMMAND_DISABLE_NOTIFICATIONS = f"{COMMAND_HEADER}0e00"
 
 
@@ -49,6 +62,7 @@ class SwitchbotLock(SwitchbotDevice):
         key_id: str,
         key_id: str,
         encryption_key: str,
         encryption_key: str,
         interface: int = 0,
         interface: int = 0,
+        model: SwitchbotModel = SwitchbotModel.LOCK,
         **kwargs: Any,
         **kwargs: Any,
     ) -> None:
     ) -> None:
         if len(key_id) == 0:
         if len(key_id) == 0:
@@ -59,20 +73,27 @@ class SwitchbotLock(SwitchbotDevice):
             raise ValueError("encryption_key is missing")
             raise ValueError("encryption_key is missing")
         elif len(encryption_key) != 32:
         elif len(encryption_key) != 32:
             raise ValueError("encryption_key is invalid")
             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._iv = None
         self._cipher = None
         self._cipher = None
         self._key_id = key_id
         self._key_id = key_id
         self._encryption_key = bytearray.fromhex(encryption_key)
         self._encryption_key = bytearray.fromhex(encryption_key)
         self._notifications_enabled: bool = False
         self._notifications_enabled: bool = False
+        self._model: SwitchbotModel = model
         super().__init__(device, None, interface, **kwargs)
         super().__init__(device, None, interface, **kwargs)
 
 
     @staticmethod
     @staticmethod
     async def verify_encryption_key(
     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:
     ) -> bool:
         try:
         try:
             lock = SwitchbotLock(
             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:
         except ValueError:
             return False
             return False
@@ -183,19 +204,19 @@ class SwitchbotLock(SwitchbotDevice):
     async def lock(self) -> bool:
     async def lock(self) -> bool:
         """Send lock command."""
         """Send lock command."""
         return await self._lock_unlock(
         return await self._lock_unlock(
-            COMMAND_LOCK, {LockStatus.LOCKED, LockStatus.LOCKING}
+            COMMAND_LOCK[self._model], {LockStatus.LOCKED, LockStatus.LOCKING}
         )
         )
 
 
     async def unlock(self) -> bool:
     async def unlock(self) -> bool:
         """Send unlock command. If unlatch feature is enabled in EU firmware, also unlatches door"""
         """Send unlock command. If unlatch feature is enabled in EU firmware, also unlatches door"""
         return await self._lock_unlock(
         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:
     async def unlock_without_unlatch(self) -> bool:
         """Send unlock command. This command will not unlatch the door."""
         """Send unlock command. This command will not unlatch the door."""
         return await self._lock_unlock(
         return await self._lock_unlock(
-            COMMAND_UNLOCK_WITHOUT_UNLATCH,
+            COMMAND_UNLOCK_WITHOUT_UNLATCH[self._model],
             {LockStatus.UNLOCKED, LockStatus.UNLOCKING, LockStatus.NOT_FULLY_LOCKED},
             {LockStatus.UNLOCKED, LockStatus.UNLOCKING, LockStatus.NOT_FULLY_LOCKED},
         )
         )
 
 
@@ -275,7 +296,9 @@ class SwitchbotLock(SwitchbotDevice):
 
 
     async def _get_lock_info(self) -> bytes | None:
     async def _get_lock_info(self) -> bytes | None:
         """Return lock info of device."""
         """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):
         if not self._check_command_result(_data, 0, COMMAND_RESULT_EXPECTED_VALUES):
             _LOGGER.error("Unsuccessful, please try again")
             _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]:
     async def get_locks(self) -> dict[str, SwitchBotAdvertisement]:
         """Return all WoLock/Locks devices with services data."""
         """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(
     async def get_device_data(
         self, address: str
         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():
 def test_parsing_lock_active_old_firmware():
     """Test parsing lock with active data. Old firmware."""
     """Test parsing lock with active data. Old firmware."""
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
     ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")