test_keypad_vision.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. """Test keypad vision series device parsing and functionality."""
  2. from unittest.mock import AsyncMock, patch
  3. import pytest
  4. from bleak.backends.device import BLEDevice
  5. from switchbot import SwitchBotAdvertisement
  6. from switchbot.devices.device import SwitchbotEncryptedDevice
  7. from switchbot.devices.keypad_vision import (
  8. COMMAND_GET_PASSWORD_COUNT,
  9. SwitchbotKeypadVision,
  10. )
  11. from . import KEYPAD_VISION_INFO, KEYPAD_VISION_PRO_INFO
  12. from .test_adv_parser import AdvTestCase, generate_ble_device
  13. def create_device_for_command_testing(
  14. adv_info: AdvTestCase,
  15. init_data: dict | None = None,
  16. ):
  17. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  18. device = SwitchbotKeypadVision(
  19. ble_device, "ff", "ffffffffffffffffffffffffffffffff", model=adv_info.modelName
  20. )
  21. device._send_command = AsyncMock()
  22. device._send_command_sequence = AsyncMock()
  23. device.update = AsyncMock()
  24. device.update_from_advertisement(
  25. make_advertisement_data(ble_device, adv_info, init_data)
  26. )
  27. return device
  28. def make_advertisement_data(
  29. ble_device: BLEDevice, adv_info: AdvTestCase, init_data: dict | None = None
  30. ):
  31. """Set advertisement data with defaults."""
  32. if init_data is None:
  33. init_data = {}
  34. return SwitchBotAdvertisement(
  35. address="aa:bb:cc:dd:ee:ff",
  36. data={
  37. "rawAdvData": adv_info.service_data,
  38. "data": adv_info.data | init_data,
  39. "isEncrypted": False,
  40. "model": adv_info.model,
  41. "modelFriendlyName": adv_info.modelFriendlyName,
  42. "modelName": adv_info.modelName,
  43. }
  44. | init_data,
  45. device=ble_device,
  46. rssi=-80,
  47. active=True,
  48. )
  49. @pytest.mark.asyncio
  50. @pytest.mark.parametrize(
  51. ("adv_info"),
  52. [
  53. (KEYPAD_VISION_INFO),
  54. (KEYPAD_VISION_PRO_INFO),
  55. ],
  56. )
  57. async def test_get_basic_info_none(adv_info: AdvTestCase) -> None:
  58. """Test getting basic info returns None when no data."""
  59. device = create_device_for_command_testing(adv_info)
  60. device._get_basic_info = AsyncMock(return_value=None)
  61. info = await device.get_basic_info()
  62. assert info is None
  63. @pytest.mark.asyncio
  64. @pytest.mark.parametrize(
  65. ("adv_info", "basic_info", "result"),
  66. [
  67. (
  68. KEYPAD_VISION_INFO,
  69. b"\x01_\x18\x16\x01\x02\x00\n\x01\x02\x03\x05\x02\x00\x01\x00",
  70. [95, 2.4, 22, 1, True, True, True, 5, True, False],
  71. ),
  72. (
  73. KEYPAD_VISION_PRO_INFO,
  74. b"\x01_\x0b\x18\x01\x02\x00\n\x01\x02\x03\x05\x02\x00\x03\x00",
  75. [95, 1.1, 24, 1, True, True, True, 5, True, True],
  76. ),
  77. ],
  78. )
  79. async def test_get_basic_info(
  80. adv_info: AdvTestCase, basic_info: bytes, result: dict
  81. ) -> None:
  82. """Test getting basic info from Keypad Vision devices."""
  83. device = create_device_for_command_testing(adv_info)
  84. device._get_basic_info = AsyncMock(return_value=basic_info)
  85. info = await device.get_basic_info()
  86. assert info["battery"] == result[0]
  87. assert info["firmware"] == result[1]
  88. assert info["hardware"] == result[2]
  89. assert info["support_fingerprint"] == result[3]
  90. assert info["lock_button_enabled"] == result[4]
  91. assert info["tamper_alarm_enabled"] == result[5]
  92. assert info["backlight_enabled"] == result[6]
  93. assert info["backlight_level"] == result[7]
  94. assert info["prompt_tone_enabled"] == result[8]
  95. assert info["battery_charging"] == result[9]
  96. @pytest.mark.asyncio
  97. @pytest.mark.parametrize(
  98. "adv_info",
  99. [
  100. KEYPAD_VISION_INFO,
  101. KEYPAD_VISION_PRO_INFO,
  102. ],
  103. )
  104. async def test_add_invalid_password(adv_info: AdvTestCase) -> None:
  105. """Test adding an invalid password raises ValueError."""
  106. device = create_device_for_command_testing(adv_info)
  107. invalid_passwords = ["123", "abcdef", "1234567890123", "12 3456", "passw0rd!"]
  108. for password in invalid_passwords:
  109. with pytest.raises(ValueError, match=r"Password must be 6-12 digits."):
  110. await device.add_password(password)
  111. @pytest.mark.asyncio
  112. @pytest.mark.parametrize(
  113. "adv_info",
  114. [
  115. KEYPAD_VISION_INFO,
  116. KEYPAD_VISION_PRO_INFO,
  117. ],
  118. )
  119. @pytest.mark.parametrize(
  120. ("password", "expected_payload"),
  121. [
  122. (
  123. "123456",
  124. ["570F52020210FF0006010203040506"],
  125. ),
  126. (
  127. "123456789012",
  128. ["570F52020220FF000C0102030405060708", "570F5202022109000102"],
  129. ),
  130. ],
  131. )
  132. async def test_add_password(
  133. adv_info: AdvTestCase, password: str, expected_payload: list[str]
  134. ) -> None:
  135. """Test adding a valid password sends correct command."""
  136. device = create_device_for_command_testing(adv_info)
  137. await device.add_password(password)
  138. device._send_command_sequence.assert_awaited_once_with(expected_payload)
  139. @pytest.mark.asyncio
  140. @pytest.mark.parametrize(
  141. "adv_info",
  142. [
  143. KEYPAD_VISION_INFO,
  144. KEYPAD_VISION_PRO_INFO,
  145. ],
  146. )
  147. async def test_get_password_count_no_response(adv_info: AdvTestCase) -> None:
  148. """Test getting password count returns None when no response."""
  149. device = create_device_for_command_testing(adv_info)
  150. device._send_command.return_value = None
  151. result = await device.get_password_count()
  152. device._send_command.assert_awaited_once_with(COMMAND_GET_PASSWORD_COUNT)
  153. assert result is None
  154. @pytest.mark.asyncio
  155. async def test_get_password_count_for_keypad_vision_pro() -> None:
  156. """Test getting password count for Keypad Vision Pro."""
  157. device = create_device_for_command_testing(KEYPAD_VISION_PRO_INFO)
  158. device._send_command.return_value = bytes(
  159. [0x01, 0x05, 0x02, 0x03, 0x00, 0x02, 0x01, 0x00]
  160. )
  161. result = await device.get_password_count()
  162. device._send_command.assert_awaited_once_with(COMMAND_GET_PASSWORD_COUNT)
  163. assert result == {
  164. "pin": 5,
  165. "nfc": 2,
  166. "fingerprint": 3,
  167. "duress_pin": 0,
  168. "duress_fingerprint": 2,
  169. "face": 1,
  170. "palm_vein": 0,
  171. }
  172. @pytest.mark.asyncio
  173. async def test_get_password_count_for_keypad_vision() -> None:
  174. """Test getting password count for Keypad Vision."""
  175. device = create_device_for_command_testing(KEYPAD_VISION_INFO)
  176. device._send_command.return_value = bytes([0x01, 0x03, 0x02, 0x01, 0x01, 0x00])
  177. result = await device.get_password_count()
  178. device._send_command.assert_awaited_once_with(COMMAND_GET_PASSWORD_COUNT)
  179. assert result == {
  180. "pin": 3,
  181. "nfc": 2,
  182. "fingerprint": 1,
  183. "duress_pin": 1,
  184. "duress_fingerprint": 0,
  185. }
  186. @pytest.mark.asyncio
  187. @pytest.mark.parametrize(
  188. "adv_info",
  189. [
  190. KEYPAD_VISION_INFO,
  191. KEYPAD_VISION_PRO_INFO,
  192. ],
  193. )
  194. @patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
  195. async def test_verify_encryption_key(
  196. mock_parent_verify: AsyncMock, adv_info: AdvTestCase
  197. ):
  198. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  199. key_id = "ff"
  200. encryption_key = "ffffffffffffffffffffffffffffffff"
  201. mock_parent_verify.return_value = True
  202. result = await SwitchbotKeypadVision.verify_encryption_key(
  203. device=ble_device,
  204. key_id=key_id,
  205. encryption_key=encryption_key,
  206. model=adv_info.modelName,
  207. )
  208. mock_parent_verify.assert_awaited_once_with(
  209. ble_device,
  210. key_id,
  211. encryption_key,
  212. adv_info.modelName,
  213. )
  214. assert result is True