keypad_vision.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. """Keypad Vision (Pro) device handling."""
  2. import logging
  3. import re
  4. from typing import Any
  5. from bleak.backends.device import BLEDevice
  6. from ..const import SwitchbotModel
  7. from .device import SwitchbotEncryptedDevice, SwitchbotSequenceDevice
  8. PASSWORD_RE = re.compile(r"^\d{6,12}$")
  9. COMMAND_GET_PASSWORD_COUNT = "570F530100"
  10. _LOGGER = logging.getLogger(__name__)
  11. class SwitchbotKeypadVision(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
  12. """Representation of a Switchbot Keypad Vision (Pro) device."""
  13. def __init__(
  14. self,
  15. device: BLEDevice,
  16. key_id: str,
  17. encryption_key: str,
  18. model: SwitchbotModel,
  19. **kwargs: Any,
  20. ) -> None:
  21. """Initialize Keypad Vision (Pro) device."""
  22. super().__init__(device, key_id, encryption_key, model, **kwargs)
  23. @classmethod
  24. async def verify_encryption_key(
  25. cls,
  26. device: BLEDevice,
  27. key_id: str,
  28. encryption_key: str,
  29. model: SwitchbotModel,
  30. **kwargs: Any,
  31. ) -> bool:
  32. return await super().verify_encryption_key(
  33. device, key_id, encryption_key, model, **kwargs
  34. )
  35. async def get_basic_info(self) -> dict[str, Any] | None:
  36. """Get device basic settings."""
  37. if not (_data := await self._get_basic_info()):
  38. return None
  39. _LOGGER.debug("Raw model %s basic info data: %s", self._model, _data.hex())
  40. battery = _data[1] & 0x7F
  41. firmware = _data[2] / 10.0
  42. hardware = _data[3]
  43. support_fingerprint = _data[4]
  44. lock_button_enabled = bool(_data[5] != 1)
  45. tamper_alarm_enabled = bool(_data[9])
  46. backlight_enabled = bool(_data[10] != 1)
  47. backlight_level = _data[11]
  48. prompt_tone_enabled = bool(_data[12] != 1)
  49. if self._model == SwitchbotModel.KEYPAD_VISION:
  50. battery_charging = bool((_data[14] & 0x06) >> 1)
  51. else:
  52. battery_charging = bool((_data[14] & 0x0E) >> 1)
  53. result = {
  54. "battery": battery,
  55. "firmware": firmware,
  56. "hardware": hardware,
  57. "support_fingerprint": support_fingerprint,
  58. "lock_button_enabled": lock_button_enabled,
  59. "tamper_alarm_enabled": tamper_alarm_enabled,
  60. "backlight_enabled": backlight_enabled,
  61. "backlight_level": backlight_level,
  62. "prompt_tone_enabled": prompt_tone_enabled,
  63. "battery_charging": battery_charging,
  64. }
  65. _LOGGER.debug("%s basic info: %s", self._model, result)
  66. return result
  67. def _check_password_rules(self, password: str) -> None:
  68. """Check if the password compliant with the rules."""
  69. if not PASSWORD_RE.fullmatch(password):
  70. raise ValueError("Password must be 6-12 digits.")
  71. def _build_password_payload(self, password: str) -> bytes:
  72. """Build password payload."""
  73. pwd_bytes = bytes(int(ch) for ch in password)
  74. pwd_length = len(pwd_bytes)
  75. payload = bytearray()
  76. payload.append(0xFF)
  77. payload.append(0x00)
  78. payload.append(pwd_length)
  79. payload.extend(pwd_bytes)
  80. return bytes(payload)
  81. def _build_add_password_cmd(self, password: str) -> list[str]:
  82. """Build command to add a password."""
  83. cmd_header = bytes.fromhex("570F520202")
  84. payload = self._build_password_payload(password)
  85. max_payload = 11
  86. chunks = [
  87. payload[i : i + max_payload] for i in range(0, len(payload), max_payload)
  88. ]
  89. total = len(chunks)
  90. cmds: list[str] = []
  91. for idx, chunk in enumerate(chunks):
  92. packet_info = ((total & 0x0F) << 4) | (idx & 0x0F)
  93. cmd = bytearray()
  94. cmd.extend(cmd_header)
  95. cmd.append(packet_info)
  96. cmd.extend(chunk)
  97. cmds.append(cmd.hex().upper())
  98. _LOGGER.debug(
  99. "device: %s add password commands: %s", self._device.address, cmds
  100. )
  101. return cmds
  102. async def add_password(self, password: str) -> bool:
  103. """Add a password to the Keypad Vision (Pro)."""
  104. self._check_password_rules(password)
  105. cmds = self._build_add_password_cmd(password)
  106. return await self._send_command_sequence(cmds)
  107. async def get_password_count(self) -> dict[str, int] | None:
  108. """Get the number of passwords stored in the Keypad Vision (Pro)."""
  109. if not (_data := await self._send_command(COMMAND_GET_PASSWORD_COUNT)):
  110. return None
  111. _LOGGER.debug("Raw model %s password count data: %s", self._model, _data.hex())
  112. pin = _data[1]
  113. nfc = _data[2]
  114. fingerprint = _data[3]
  115. duress_pin = _data[4]
  116. duress_fingerprint = _data[5]
  117. result = {
  118. "pin": pin,
  119. "nfc": nfc,
  120. "fingerprint": fingerprint,
  121. "duress_pin": duress_pin,
  122. "duress_fingerprint": duress_fingerprint,
  123. }
  124. if self._model == SwitchbotModel.KEYPAD_VISION_PRO:
  125. face = _data[6]
  126. palm_vein = _data[7]
  127. result.update(
  128. {
  129. "face": face,
  130. "palm_vein": palm_vein,
  131. }
  132. )
  133. _LOGGER.debug("%s password count: %s", self._model, result)
  134. return result