Explorar el Código

Add support for lock vista (pro) and lock pro wifi (#445)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
7eaves hace 3 días
padre
commit
7cf41e4265

+ 36 - 0
switchbot/adv_parser.py

@@ -755,6 +755,42 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
         "func": process_keypad_vision_pro,
         "manufacturer_id": 2409,
     },
+    b"\x00\x11\x69\x09": {
+        "modelName": SwitchbotModel.LOCK_VISION_PRO,
+        "modelFriendlyName": "Lock Vision Pro",
+        "func": process_lock2,
+        "manufacturer_id": 2409,
+    },
+    b"\x01\x11\x69\x09": {
+        "modelName": SwitchbotModel.LOCK_VISION_PRO,
+        "modelFriendlyName": "Lock Vision Pro",
+        "func": process_lock2,
+        "manufacturer_id": 2409,
+    },
+    b"\x00\x11\x69\x08": {
+        "modelName": SwitchbotModel.LOCK_VISION,
+        "modelFriendlyName": "Lock Vision",
+        "func": process_locklite,
+        "manufacturer_id": 2409,
+    },
+    b"\x01\x11\x69\x08": {
+        "modelName": SwitchbotModel.LOCK_VISION,
+        "modelFriendlyName": "Lock Vision",
+        "func": process_locklite,
+        "manufacturer_id": 2409,
+    },
+    b"\x00\x10\xff\x90": {
+        "modelName": SwitchbotModel.LOCK_PRO_WIFI,
+        "modelFriendlyName": "Lock Pro Wifi",
+        "func": process_wolock_pro,
+        "manufacturer_id": 2409,
+    },
+    b"\x01\x10\xff\x90": {
+        "modelName": SwitchbotModel.LOCK_PRO_WIFI,
+        "modelFriendlyName": "Lock Pro Wifi",
+        "func": process_wolock_pro,
+        "manufacturer_id": 2409,
+    },
 }
 
 _SWITCHBOT_MODEL_TO_CHAR: defaultdict[SwitchbotModel, list[str | bytes]] = defaultdict(

+ 3 - 0
switchbot/const/__init__.py

@@ -105,6 +105,9 @@ class SwitchbotModel(StrEnum):
     ART_FRAME = "Art Frame"
     KEYPAD_VISION = "Keypad Vision"
     KEYPAD_VISION_PRO = "Keypad Vision Pro"
+    LOCK_VISION_PRO = "Lock Vision Pro"
+    LOCK_VISION = "Lock Vision"
+    LOCK_PRO_WIFI = "Lock Pro Wifi"
 
 
 __all__ = [

+ 3 - 0
switchbot/devices/device.py

@@ -106,6 +106,9 @@ API_MODEL_TO_ENUM: dict[str, SwitchbotModel] = {
     "W1128000": SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
     "W1111000": SwitchbotModel.CLIMATE_PANEL,
     "W1130000": SwitchbotModel.ART_FRAME,
+    "W1141001": SwitchbotModel.LOCK_VISION_PRO,
+    "W1141000": SwitchbotModel.LOCK_VISION,
+    "W1114000": SwitchbotModel.LOCK_PRO_WIFI,
 }
 
 REQ_HEADER = "570f"

+ 20 - 2
switchbot/devices/lock.py

@@ -18,30 +18,45 @@ COMMAND_LOCK_INFO = {
     SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0f4f8101",
     SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4f8104",
     SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4f8107",
+    SwitchbotModel.LOCK_VISION_PRO: f"{COMMAND_HEADER}0f4f8102",
+    SwitchbotModel.LOCK_VISION: f"{COMMAND_HEADER}0f4f8102",
+    SwitchbotModel.LOCK_PRO_WIFI: f"{COMMAND_HEADER}0f4f810a",
 }
 COMMAND_UNLOCK = {
     SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e01011080",
     SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0f4e01011080",
     SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e0101000080",
     SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4e0101000080",
+    SwitchbotModel.LOCK_VISION_PRO: f"{COMMAND_HEADER}0f4e0101000080",
+    SwitchbotModel.LOCK_VISION: f"{COMMAND_HEADER}0f4e0101000080",
+    SwitchbotModel.LOCK_PRO_WIFI: f"{COMMAND_HEADER}0f4e0101000080",
 }
 COMMAND_UNLOCK_WITHOUT_UNLATCH = {
     SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e010110a0",
     SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0f4e010110a0",
     SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e01010000a0",
     SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4e01010000a0",
+    SwitchbotModel.LOCK_VISION_PRO: f"{COMMAND_HEADER}0f4e01010000a0",
+    SwitchbotModel.LOCK_VISION: f"{COMMAND_HEADER}0f4e01010000a0",
+    SwitchbotModel.LOCK_PRO_WIFI: f"{COMMAND_HEADER}0f4e01010000a0",
 }
 COMMAND_LOCK = {
     SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e01011000",
     SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0f4e01011000",
     SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e0101000000",
     SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4e0101000000",
+    SwitchbotModel.LOCK_VISION_PRO: f"{COMMAND_HEADER}0f4e0101000000",
+    SwitchbotModel.LOCK_VISION: f"{COMMAND_HEADER}0f4e0101000000",
+    SwitchbotModel.LOCK_PRO_WIFI: f"{COMMAND_HEADER}0f4e0101000000",
 }
 COMMAND_ENABLE_NOTIFICATIONS = {
     SwitchbotModel.LOCK: f"{COMMAND_HEADER}0e01001e00008101",
     SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0e01001e00008101",
     SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0e01001e00008104",
     SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0e01001e00008107",
+    SwitchbotModel.LOCK_VISION_PRO: f"{COMMAND_HEADER}0e01001e00008102",
+    SwitchbotModel.LOCK_VISION: f"{COMMAND_HEADER}0e01001e00008102",
+    SwitchbotModel.LOCK_PRO_WIFI: f"{COMMAND_HEADER}0e01001e0000810a",
 }
 COMMAND_DISABLE_NOTIFICATIONS = f"{COMMAND_HEADER}0e00"
 
@@ -74,6 +89,9 @@ class SwitchbotLock(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
             SwitchbotModel.LOCK_PRO,
             SwitchbotModel.LOCK_LITE,
             SwitchbotModel.LOCK_ULTRA,
+            SwitchbotModel.LOCK_VISION_PRO,
+            SwitchbotModel.LOCK_VISION,
+            SwitchbotModel.LOCK_PRO_WIFI,
         ):
             raise ValueError("initializing SwitchbotLock with a non-lock model")
         self._notifications_enabled: bool = False
@@ -236,7 +254,7 @@ class SwitchbotLock(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
 
     @staticmethod
     def _parse_lock_data(data: bytes, model: SwitchbotModel) -> dict[str, Any]:
-        if model == SwitchbotModel.LOCK:
+        if model in {SwitchbotModel.LOCK, SwitchbotModel.LOCK_VISION_PRO}:
             return {
                 "calibration": bool(data[0] & 0b10000000),
                 "status": LockStatus((data[0] & 0b01110000) >> 4),
@@ -244,7 +262,7 @@ class SwitchbotLock(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
                 "unclosed_alarm": bool(data[1] & 0b00100000),
                 "unlocked_alarm": bool(data[1] & 0b00010000),
             }
-        if model == SwitchbotModel.LOCK_LITE:
+        if model in {SwitchbotModel.LOCK_LITE, SwitchbotModel.LOCK_VISION}:
             return {
                 "calibration": bool(data[0] & 0b10000000),
                 "status": LockStatus((data[0] & 0b01110000) >> 4),

+ 160 - 0
tests/test_adv_parser.py

@@ -2733,6 +2733,74 @@ def test_hub3_with_empty_data() -> None:
             "Lock Ultra",
             SwitchbotModel.LOCK_ULTRA,
         ),
+        AdvTestCase(
+            b"\xb0\xe9\xfe\x6f\xc9\xa6\x0a\x00\x00\x2e\x00\x0c\x00\x00\x00\x00",
+            b"\x00\x80.\x00\x11i\x08",
+            {
+                "sequence_number": 10,
+                "battery": 46,
+                "calibration": False,
+                "status": LockStatus.LOCKED,
+                "update_from_secondary_lock": False,
+                "double_lock_mode": False,
+                "unlocked_alarm": False,
+                "night_latch": False,
+            },
+            b"\x00\x11\x69\x08",
+            "Lock Vision",
+            SwitchbotModel.LOCK_VISION,
+        ),
+        AdvTestCase(
+            b"\xb0\xe9\xfe\xe6\x1aq\x03\x00\x003\x00\x0c\x00\x00\x00\x00",
+            b"\x00\x803\x00\x11i\t",
+            {
+                "sequence_number": 3,
+                "battery": 51,
+                "calibration": False,
+                "status": LockStatus.LOCKED,
+                "update_from_secondary_lock": False,
+                "door_open": False,
+                "door_open_from_secondary_lock": False,
+                "double_lock_mode": False,
+                "is_secondary_lock": False,
+                "manual_unlock_linkage": False,
+                "unclosed_alarm": False,
+                "unlocked_alarm": False,
+                "auto_lock_paused": False,
+                "night_latch": False,
+                "power_alarm": False,
+                "battery_status": 4,
+            },
+            b"\x00\x11\x69\x09",
+            "Lock Vision Pro",
+            SwitchbotModel.LOCK_VISION_PRO,
+        ),
+        AdvTestCase(
+            b"\xb0\xe9\xfe\xb0^\xfe\x0e\x02\x00=\x00\x08",
+            b"\x00\x80=\x00\x10\xff\x90",
+            {
+                "sequence_number": 14,
+                "battery": 61,
+                "calibration": False,
+                "status": LockStatus.LOCKED,
+                "update_from_secondary_lock": False,
+                "door_open": False,
+                "door_open_from_secondary_lock": False,
+                "double_lock_mode": False,
+                "is_secondary_lock": False,
+                "left_battery_compartment_alarm": 0,
+                "right_battery_compartment_alarm": 0,
+                "low_temperature_alarm": False,
+                "manual_unlock_linkage": False,
+                "unclosed_alarm": False,
+                "unlocked_alarm": False,
+                "auto_lock_paused": False,
+                "night_latch": False,
+            },
+            b"\x00\x10\xff\x90",
+            "Lock Pro Wifi",
+            SwitchbotModel.LOCK_PRO_WIFI,
+        ),
     ],
 )
 def test_lock_active(test_case: AdvTestCase) -> None:
@@ -2851,6 +2919,74 @@ def test_lock_active(test_case: AdvTestCase) -> None:
             "Lock Ultra",
             SwitchbotModel.LOCK_ULTRA,
         ),
+        AdvTestCase(
+            b"\xb0\xe9\xfe\x6f\xc9\xa6\x04\x00\x00\x2e\x00\x0c\x00\x00\x00\x00",
+            b"\x00\x80.\x01\x11i\x08",
+            {
+                "sequence_number": 4,
+                "battery": None,
+                "calibration": False,
+                "status": LockStatus.LOCKED,
+                "update_from_secondary_lock": False,
+                "double_lock_mode": False,
+                "unlocked_alarm": False,
+                "night_latch": False,
+            },
+            b"\x00\x11\x69\x08",
+            "Lock Vision",
+            SwitchbotModel.LOCK_VISION,
+        ),
+        AdvTestCase(
+            b"\xb0\xe9\xfe\xe6\x1aq\x04\x00\x003\x00\x0c\x00\x00\x00\x00",
+            b"\x00\x803\x01\x11i\t",
+            {
+                "sequence_number": 4,
+                "battery": 51,
+                "calibration": False,
+                "status": LockStatus.LOCKED,
+                "update_from_secondary_lock": False,
+                "door_open": False,
+                "door_open_from_secondary_lock": False,
+                "double_lock_mode": False,
+                "is_secondary_lock": False,
+                "manual_unlock_linkage": False,
+                "unclosed_alarm": False,
+                "unlocked_alarm": False,
+                "auto_lock_paused": False,
+                "night_latch": False,
+                "power_alarm": False,
+                "battery_status": 4,
+            },
+            b"\x00\x11\x69\x09",
+            "Lock Vision Pro",
+            SwitchbotModel.LOCK_VISION_PRO,
+        ),
+        AdvTestCase(
+            b"\xb0\xe9\xfe\xb0^\xfe\x0e\x02\x00=\x00\x08",
+            b"\x00\x80=\x00\x10\xff\x90",
+            {
+                "sequence_number": 14,
+                "battery": 61,
+                "calibration": False,
+                "status": LockStatus.LOCKED,
+                "update_from_secondary_lock": False,
+                "door_open": False,
+                "door_open_from_secondary_lock": False,
+                "double_lock_mode": False,
+                "is_secondary_lock": False,
+                "left_battery_compartment_alarm": 0,
+                "right_battery_compartment_alarm": 0,
+                "low_temperature_alarm": False,
+                "manual_unlock_linkage": False,
+                "unclosed_alarm": False,
+                "unlocked_alarm": False,
+                "auto_lock_paused": False,
+                "night_latch": False,
+            },
+            b"\x00\x10\xff\x90",
+            "Lock Pro Wifi",
+            SwitchbotModel.LOCK_PRO_WIFI,
+        ),
     ],
 )
 def test_lock_passive(test_case: AdvTestCase) -> None:
@@ -2912,6 +3048,30 @@ def test_lock_passive(test_case: AdvTestCase) -> None:
             "Lock Ultra",
             SwitchbotModel.LOCK_ULTRA,
         ),
+        AdvTestCase(
+            None,
+            b"\x00\x80\x2e\x00\x11\x69\x08",
+            {},
+            b"\x00\x11\x69\x08",
+            "Lock Vision",
+            SwitchbotModel.LOCK_VISION,
+        ),
+        AdvTestCase(
+            None,
+            b"\x00\x803\x00\x11i\t",
+            {},
+            b"\x00\x11\x69\x09",
+            "Lock Vision Pro",
+            SwitchbotModel.LOCK_VISION_PRO,
+        ),
+        AdvTestCase(
+            None,
+            b"\x00\x80=\x00\x10\xff\x90",
+            {},
+            b"\x00\x10\xff\x90",
+            "Lock Pro Wifi",
+            SwitchbotModel.LOCK_PRO_WIFI,
+        ),
     ],
 )
 def test_lock_with_empty_data(test_case: AdvTestCase) -> None:

+ 126 - 2
tests/test_lock.py

@@ -24,6 +24,9 @@ def create_device_for_command_testing(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 def test_lock_init(model: str):
@@ -54,6 +57,9 @@ def test_lock_init_with_invalid_model(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 async def test_verify_encryption_key(model: str):
@@ -75,6 +81,9 @@ async def test_verify_encryption_key(model: str):
         (SwitchbotModel.LOCK_LITE, b"W\x0fN\x01\x01\x10\x81"),
         (SwitchbotModel.LOCK_PRO, b"W\x0fN\x01\x01\x10\x85"),
         (SwitchbotModel.LOCK_ULTRA, b"W\x0fN\x01\x01\x10\x86"),
+        (SwitchbotModel.LOCK_VISION, b"W\x0fN\x01\x01\x00\x80"),
+        (SwitchbotModel.LOCK_VISION_PRO, b"W\x0fN\x01\x01\x00\x80"),
+        (SwitchbotModel.LOCK_PRO_WIFI, b"W\x0fN\x01\x01\x10\x82"),
     ],
 )
 async def test_lock(model: str, command: bytes):
@@ -82,12 +91,15 @@ async def test_lock(model: str, command: bytes):
     device = create_device_for_command_testing(model)
     device._get_adv_value = Mock(return_value=LockStatus.UNLOCKED)
     with (
-        patch.object(device, "_send_command", return_value=b"\x01\x00"),
+        patch.object(
+            device, "_send_command", return_value=b"\x01\x00"
+        ) as mock_send_command,
         patch.object(device, "_enable_notifications", return_value=True),
         patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
     ):
         result = await device.lock()
         assert result is True
+        mock_send_command.assert_any_call(lock.COMMAND_LOCK[model])
 
 
 @pytest.mark.asyncio
@@ -98,6 +110,9 @@ async def test_lock(model: str, command: bytes):
         (SwitchbotModel.LOCK_LITE, b"W\x0fN\x01\x01\x10\x81"),
         (SwitchbotModel.LOCK_PRO, b"W\x0fN\x01\x01\x10\x84"),
         (SwitchbotModel.LOCK_ULTRA, b"W\x0fN\x01\x01\x10\x83"),
+        (SwitchbotModel.LOCK_VISION, b"W\x0fN\x01\x01\x00\x80"),
+        (SwitchbotModel.LOCK_VISION_PRO, b"W\x0fN\x01\x01\x00\x80"),
+        (SwitchbotModel.LOCK_PRO_WIFI, b"W\x0fN\x01\x01\x10\x81"),
     ],
 )
 async def test_unlock(model: str, command: bytes):
@@ -105,12 +120,15 @@ async def test_unlock(model: str, command: bytes):
     device = create_device_for_command_testing(model)
     device._get_adv_value = Mock(return_value=LockStatus.LOCKED)
     with (
-        patch.object(device, "_send_command", return_value=b"\x01\x00"),
+        patch.object(
+            device, "_send_command", return_value=b"\x01\x00"
+        ) as mock_send_command,
         patch.object(device, "_enable_notifications", return_value=True),
         patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
     ):
         result = await device.unlock()
         assert result is True
+        mock_send_command.assert_any_call(lock.COMMAND_UNLOCK[model])
 
 
 @pytest.mark.asyncio
@@ -121,6 +139,9 @@ async def test_unlock(model: str, command: bytes):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 async def test_unlock_without_unlatch(model: str):
@@ -144,6 +165,9 @@ async def test_unlock_without_unlatch(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 async def test_get_basic_info(model: str):
@@ -171,6 +195,9 @@ async def test_get_basic_info(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 async def test_get_basic_info_no_lock_data(model: str):
@@ -189,6 +216,9 @@ async def test_get_basic_info_no_lock_data(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 async def test_get_basic_info_no_basic_data(model: str):
@@ -219,6 +249,9 @@ def test_parse_basic_data():
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 def test_is_calibrated(model: str):
@@ -235,6 +268,9 @@ def test_is_calibrated(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 def test_get_lock_status(model: str):
@@ -250,6 +286,8 @@ def test_get_lock_status(model: str):
         SwitchbotModel.LOCK,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 def test_is_door_open(model: str):
@@ -265,6 +303,8 @@ def test_is_door_open(model: str):
         SwitchbotModel.LOCK,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 def test_is_unclosed_alarm_on(model: str):
@@ -281,6 +321,9 @@ def test_is_unclosed_alarm_on(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 def test_is_unlocked_alarm_on(model: str):
@@ -310,6 +353,9 @@ def test_is_auto_lock_paused(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 def test_is_night_latch_enabled(model: str):
@@ -327,6 +373,9 @@ def test_is_night_latch_enabled(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 async def test_get_lock_info(model: str):
@@ -346,6 +395,9 @@ async def test_get_lock_info(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 async def test_get_lock_info_failure(model: str):
@@ -364,6 +416,9 @@ async def test_get_lock_info_failure(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 async def test_enable_notifications(model: str):
@@ -382,6 +437,9 @@ async def test_enable_notifications(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 async def test_disable_notifications(model: str):
@@ -402,6 +460,9 @@ async def test_disable_notifications(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 async def test_disable_notifications_already_disabled(model: str):
@@ -421,6 +482,9 @@ async def test_disable_notifications_already_disabled(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 def test_notification_handler(model: str):
@@ -440,6 +504,9 @@ def test_notification_handler(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 def test_notification_handler_not_enabled(model: str):
@@ -465,6 +532,9 @@ def test_notification_handler_not_enabled(model: str):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 def test_notification_handler_during_disconnect(
@@ -493,6 +563,9 @@ def test_notification_handler_during_disconnect(
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 def test_update_lock_status(model: str):
@@ -553,6 +626,37 @@ def test_update_lock_status(model: str):
                 "unlocked_alarm": True,
             },
         ),
+        (
+            SwitchbotModel.LOCK_VISION,
+            b"\x80\x00\x00\x00\x00\x00",
+            {
+                "calibration": True,
+                "status": LockStatus.LOCKED,  # (0x80 & 0b01110000) >> 4 = 0 = LOCKED
+                "unlocked_alarm": False,
+            },
+        ),
+        (
+            SwitchbotModel.LOCK_VISION_PRO,
+            b"\x80\x00\x00\x00\x00\x00",
+            {
+                "calibration": True,
+                "status": LockStatus.LOCKED,  # (0x80 & 0b01111000) >> 3 = 0 = LOCKED
+                "door_open": False,
+                "unclosed_alarm": False,
+                "unlocked_alarm": False,
+            },
+        ),
+        (
+            SwitchbotModel.LOCK_PRO_WIFI,
+            b"\x80\x00\x00\x00\x00\x00",
+            {
+                "calibration": True,
+                "status": LockStatus.LOCKED,  # (0x80 & 0b01111000) >> 3 = 0 = LOCKED
+                "door_open": False,
+                "unclosed_alarm": False,
+                "unlocked_alarm": False,
+            },
+        ),
     ],
 )
 def test_parse_lock_data(model: str, data: bytes, expected: dict):
@@ -610,6 +714,17 @@ def test_parse_lock_data(model: str, data: bytes, expected: dict):
                 "unlocked_alarm": True,  # bit 6 of byte 5
             },
         ),
+        (
+            SwitchbotModel.LOCK_PRO_WIFI,
+            b"\x90\x10\x00\x00\x00\xc0",  # New format: status bits 3-6, door open bit 4 of byte 1
+            {
+                "calibration": True,
+                "status": LockStatus.LOCKING,  # (0x90 & 0b01111000) >> 3 = 0x10 >> 3 = 2 (LOCKING)
+                "door_open": True,  # bit 4 of byte 1 (0x10)
+                "unclosed_alarm": True,  # bit 7 of byte 5 (0xc0)
+                "unlocked_alarm": True,  # bit 6 of byte 5 (0xc0)
+            },
+        ),
     ],
 )
 def test_parse_lock_data_new_formats(model: str, data: bytes, expected: dict):
@@ -626,6 +741,9 @@ def test_parse_lock_data_new_formats(model: str, data: bytes, expected: dict):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 async def test_lock_with_update(model: str):
@@ -650,6 +768,9 @@ async def test_lock_with_update(model: str):
         (SwitchbotModel.LOCK_LITE, LockStatus.LOCKING),
         (SwitchbotModel.LOCK_PRO, LockStatus.LOCKED),
         (SwitchbotModel.LOCK_ULTRA, LockStatus.LOCKING),
+        (SwitchbotModel.LOCK_VISION, LockStatus.LOCKED),
+        (SwitchbotModel.LOCK_VISION_PRO, LockStatus.LOCKED),
+        (SwitchbotModel.LOCK_PRO_WIFI, LockStatus.LOCKED),
     ],
 )
 async def test_lock_already_locked(model: str, status: LockStatus):
@@ -670,6 +791,9 @@ async def test_lock_already_locked(model: str, status: LockStatus):
         SwitchbotModel.LOCK_LITE,
         SwitchbotModel.LOCK_PRO,
         SwitchbotModel.LOCK_ULTRA,
+        SwitchbotModel.LOCK_VISION,
+        SwitchbotModel.LOCK_VISION_PRO,
+        SwitchbotModel.LOCK_PRO_WIFI,
     ],
 )
 async def test_lock_with_invalid_basic_data(model: str):