|
|
@@ -0,0 +1,259 @@
|
|
|
+"""Test keypad vision series device parsing and functionality."""
|
|
|
+
|
|
|
+from unittest.mock import AsyncMock, patch
|
|
|
+
|
|
|
+import pytest
|
|
|
+from bleak.backends.device import BLEDevice
|
|
|
+
|
|
|
+from switchbot import SwitchBotAdvertisement
|
|
|
+from switchbot.devices.device import SwitchbotEncryptedDevice
|
|
|
+from switchbot.devices.keypad_vision import (
|
|
|
+ COMMAND_GET_PASSWORD_COUNT,
|
|
|
+ SwitchbotKeypadVision,
|
|
|
+)
|
|
|
+
|
|
|
+from . import KEYPAD_VISION_INFO, KEYPAD_VISION_PRO_INFO
|
|
|
+from .test_adv_parser import AdvTestCase, generate_ble_device
|
|
|
+
|
|
|
+
|
|
|
+def create_device_for_command_testing(
|
|
|
+ adv_info: AdvTestCase,
|
|
|
+ init_data: dict | None = None,
|
|
|
+):
|
|
|
+ ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
|
+ device = SwitchbotKeypadVision(
|
|
|
+ ble_device, "ff", "ffffffffffffffffffffffffffffffff", model=adv_info.modelName
|
|
|
+ )
|
|
|
+
|
|
|
+ device._send_command = AsyncMock()
|
|
|
+ device._send_command_sequence = AsyncMock()
|
|
|
+ device.update = AsyncMock()
|
|
|
+ device.update_from_advertisement(
|
|
|
+ make_advertisement_data(ble_device, adv_info, init_data)
|
|
|
+ )
|
|
|
+ return device
|
|
|
+
|
|
|
+
|
|
|
+def make_advertisement_data(
|
|
|
+ ble_device: BLEDevice, adv_info: AdvTestCase, init_data: dict | None = None
|
|
|
+):
|
|
|
+ """Set advertisement data with defaults."""
|
|
|
+ if init_data is None:
|
|
|
+ init_data = {}
|
|
|
+
|
|
|
+ return SwitchBotAdvertisement(
|
|
|
+ address="aa:bb:cc:dd:ee:ff",
|
|
|
+ data={
|
|
|
+ "rawAdvData": adv_info.service_data,
|
|
|
+ "data": adv_info.data | init_data,
|
|
|
+ "isEncrypted": False,
|
|
|
+ "model": adv_info.model,
|
|
|
+ "modelFriendlyName": adv_info.modelFriendlyName,
|
|
|
+ "modelName": adv_info.modelName,
|
|
|
+ }
|
|
|
+ | init_data,
|
|
|
+ device=ble_device,
|
|
|
+ rssi=-80,
|
|
|
+ active=True,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ ("adv_info"),
|
|
|
+ [
|
|
|
+ (KEYPAD_VISION_INFO),
|
|
|
+ (KEYPAD_VISION_PRO_INFO),
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_get_basic_info_none(adv_info: AdvTestCase) -> None:
|
|
|
+ """Test getting basic info returns None when no data."""
|
|
|
+ device = create_device_for_command_testing(adv_info)
|
|
|
+ device._get_basic_info = AsyncMock(return_value=None)
|
|
|
+
|
|
|
+ info = await device.get_basic_info()
|
|
|
+ assert info is None
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ ("adv_info", "basic_info", "result"),
|
|
|
+ [
|
|
|
+ (
|
|
|
+ KEYPAD_VISION_INFO,
|
|
|
+ b"\x01_\x18\x16\x01\x02\x00\n\x01\x02\x03\x05\x02\x00\x01\x00",
|
|
|
+ [95, 2.4, 22, 1, True, True, True, 5, True, False],
|
|
|
+ ),
|
|
|
+ (
|
|
|
+ KEYPAD_VISION_PRO_INFO,
|
|
|
+ b"\x01_\x0b\x18\x01\x02\x00\n\x01\x02\x03\x05\x02\x00\x03\x00",
|
|
|
+ [95, 1.1, 24, 1, True, True, True, 5, True, True],
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_get_basic_info(
|
|
|
+ adv_info: AdvTestCase, basic_info: bytes, result: dict
|
|
|
+) -> None:
|
|
|
+ """Test getting basic info from Keypad Vision devices."""
|
|
|
+ device = create_device_for_command_testing(adv_info)
|
|
|
+ device._get_basic_info = AsyncMock(return_value=basic_info)
|
|
|
+
|
|
|
+ info = await device.get_basic_info()
|
|
|
+ assert info["battery"] == result[0]
|
|
|
+ assert info["firmware"] == result[1]
|
|
|
+ assert info["hardware"] == result[2]
|
|
|
+ assert info["support_fingerprint"] == result[3]
|
|
|
+ assert info["lock_button_enabled"] == result[4]
|
|
|
+ assert info["tamper_alarm_enabled"] == result[5]
|
|
|
+ assert info["backlight_enabled"] == result[6]
|
|
|
+ assert info["backlight_level"] == result[7]
|
|
|
+ assert info["prompt_tone_enabled"] == result[8]
|
|
|
+ assert info["battery_charging"] == result[9]
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "adv_info",
|
|
|
+ [
|
|
|
+ KEYPAD_VISION_INFO,
|
|
|
+ KEYPAD_VISION_PRO_INFO,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_add_invalid_password(adv_info: AdvTestCase) -> None:
|
|
|
+ """Test adding an invalid password raises ValueError."""
|
|
|
+ device = create_device_for_command_testing(adv_info)
|
|
|
+
|
|
|
+ invalid_passwords = ["123", "abcdef", "1234567890123", "12 3456", "passw0rd!"]
|
|
|
+
|
|
|
+ for password in invalid_passwords:
|
|
|
+ with pytest.raises(ValueError, match=r"Password must be 6-12 digits."):
|
|
|
+ await device.add_password(password)
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "adv_info",
|
|
|
+ [
|
|
|
+ KEYPAD_VISION_INFO,
|
|
|
+ KEYPAD_VISION_PRO_INFO,
|
|
|
+ ],
|
|
|
+)
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ ("password", "expected_payload"),
|
|
|
+ [
|
|
|
+ (
|
|
|
+ "123456",
|
|
|
+ ["570F52020210FF0006010203040506"],
|
|
|
+ ),
|
|
|
+ (
|
|
|
+ "123456789012",
|
|
|
+ ["570F52020220FF000C0102030405060708", "570F5202022109000102"],
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_add_password(
|
|
|
+ adv_info: AdvTestCase, password: str, expected_payload: list[str]
|
|
|
+) -> None:
|
|
|
+ """Test adding a valid password sends correct command."""
|
|
|
+ device = create_device_for_command_testing(adv_info)
|
|
|
+
|
|
|
+ await device.add_password(password)
|
|
|
+
|
|
|
+ device._send_command_sequence.assert_awaited_once_with(expected_payload)
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "adv_info",
|
|
|
+ [
|
|
|
+ KEYPAD_VISION_INFO,
|
|
|
+ KEYPAD_VISION_PRO_INFO,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_get_password_count_no_response(adv_info: AdvTestCase) -> None:
|
|
|
+ """Test getting password count returns None when no response."""
|
|
|
+ device = create_device_for_command_testing(adv_info)
|
|
|
+
|
|
|
+ device._send_command.return_value = None
|
|
|
+
|
|
|
+ result = await device.get_password_count()
|
|
|
+ device._send_command.assert_awaited_once_with(COMMAND_GET_PASSWORD_COUNT)
|
|
|
+
|
|
|
+ assert result is None
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+async def test_get_password_count_for_keypad_vision_pro() -> None:
|
|
|
+ """Test getting password count for Keypad Vision Pro."""
|
|
|
+ device = create_device_for_command_testing(KEYPAD_VISION_PRO_INFO)
|
|
|
+
|
|
|
+ device._send_command.return_value = bytes(
|
|
|
+ [0x01, 0x05, 0x02, 0x03, 0x00, 0x02, 0x01, 0x00]
|
|
|
+ )
|
|
|
+
|
|
|
+ result = await device.get_password_count()
|
|
|
+ device._send_command.assert_awaited_once_with(COMMAND_GET_PASSWORD_COUNT)
|
|
|
+
|
|
|
+ assert result == {
|
|
|
+ "pin": 5,
|
|
|
+ "nfc": 2,
|
|
|
+ "fingerprint": 3,
|
|
|
+ "duress_pin": 0,
|
|
|
+ "duress_fingerprint": 2,
|
|
|
+ "face": 1,
|
|
|
+ "palm_vein": 0,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+async def test_get_password_count_for_keypad_vision() -> None:
|
|
|
+ """Test getting password count for Keypad Vision."""
|
|
|
+ device = create_device_for_command_testing(KEYPAD_VISION_INFO)
|
|
|
+
|
|
|
+ device._send_command.return_value = bytes([0x01, 0x03, 0x02, 0x01, 0x01, 0x00])
|
|
|
+
|
|
|
+ result = await device.get_password_count()
|
|
|
+ device._send_command.assert_awaited_once_with(COMMAND_GET_PASSWORD_COUNT)
|
|
|
+
|
|
|
+ assert result == {
|
|
|
+ "pin": 3,
|
|
|
+ "nfc": 2,
|
|
|
+ "fingerprint": 1,
|
|
|
+ "duress_pin": 1,
|
|
|
+ "duress_fingerprint": 0,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "adv_info",
|
|
|
+ [
|
|
|
+ KEYPAD_VISION_INFO,
|
|
|
+ KEYPAD_VISION_PRO_INFO,
|
|
|
+ ],
|
|
|
+)
|
|
|
+@patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
|
|
|
+async def test_verify_encryption_key(
|
|
|
+ mock_parent_verify: AsyncMock, adv_info: AdvTestCase
|
|
|
+):
|
|
|
+ ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
|
+ key_id = "ff"
|
|
|
+ encryption_key = "ffffffffffffffffffffffffffffffff"
|
|
|
+
|
|
|
+ mock_parent_verify.return_value = True
|
|
|
+
|
|
|
+ result = await SwitchbotKeypadVision.verify_encryption_key(
|
|
|
+ device=ble_device,
|
|
|
+ key_id=key_id,
|
|
|
+ encryption_key=encryption_key,
|
|
|
+ model=adv_info.modelName,
|
|
|
+ )
|
|
|
+
|
|
|
+ mock_parent_verify.assert_awaited_once_with(
|
|
|
+ ble_device,
|
|
|
+ key_id,
|
|
|
+ encryption_key,
|
|
|
+ adv_info.modelName,
|
|
|
+ )
|
|
|
+
|
|
|
+ assert result is True
|