lock.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. """
  2. Setup for different kinds of Tuya lock devices
  3. """
  4. from base64 import b64encode
  5. from homeassistant.components.lock import LockEntity, LockEntityFeature
  6. from .device import TuyaLocalDevice
  7. from .entity import TuyaLocalEntity
  8. from .helpers.config import async_tuya_setup_platform
  9. from .helpers.device_config import TuyaEntityConfig
  10. # Remote Code unlocking protocol: 8 digit code set when paired by
  11. # remote_no_pd_setkey (typically dp 60), user can obtain by
  12. # eavesdropping cloud unlock messages.
  13. # Format in case this command can be supported outside of pairing process:
  14. # Request: Validity (1 byte 0 or 1), Member ID (2 bytes),
  15. # Start time (4 byte unixtime), End time (4 byte unixtime),
  16. # Usable times (2 bytes, 0=infinite), Key (8 bytes ASCII)
  17. # Same 8 digit ASCII code used along with binary member ID to generate
  18. # unlock command with remote_no_dp_key (typically dp 61)
  19. # Locking usually possible without code, but remote_no_dp_key seems
  20. # to also support locking with code.
  21. # Request: action (1 byte), Member (2 bytes 0-100), code (8 bytes ASCII),
  22. # source (2 bytes)
  23. CODE_LOCK = 0x00
  24. CODE_UNLOCK = 0x01
  25. CODE_SRC_UNKNOWN = 0x0000
  26. CODE_SRC_APP = 0x0001
  27. CODE_SRC_VOICE = 0x0002
  28. # Reply: status (1 byte), Member ID (2 bytes 1 - 100)
  29. CODE_REPLY_SUCCESS = 0x00
  30. CODE_REPLY_FAIL = 0x01
  31. CODE_REPLY_PWD_ERROR = 0x02
  32. CODE_REPLY_TIMEOUT = 0x03
  33. CODE_REPLY_OUTOFHOURS = 0x04
  34. CODE_REPLY_WRONGCODE = 0x05
  35. CODE_REPLY_DOUBLELOCKED = 0x06
  36. async def async_setup_entry(hass, config_entry, async_add_entities):
  37. config = {**config_entry.data, **config_entry.options}
  38. await async_tuya_setup_platform(
  39. hass,
  40. async_add_entities,
  41. config,
  42. "lock",
  43. TuyaLocalLock,
  44. )
  45. class TuyaLocalLock(TuyaLocalEntity, LockEntity):
  46. """Representation of a Tuya Wi-Fi connected lock."""
  47. def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
  48. """
  49. Initialise the lock.
  50. Args:
  51. device (TuyaLocalDevice): The device API instance.
  52. config (TuyaEntityConfig): The configuration for this entity.
  53. """
  54. super().__init__()
  55. dps_map = self._init_begin(device, config)
  56. self._lock_dp = dps_map.pop("lock", None)
  57. self._lock_state_dp = dps_map.pop("lock_state", None)
  58. self._open_dp = dps_map.pop("open", None)
  59. self._unlock_fp_dp = dps_map.pop("unlock_fingerprint", None)
  60. self._unlock_pw_dp = dps_map.pop("unlock_password", None)
  61. self._unlock_tmppw_dp = dps_map.pop("unlock_temp_pwd", None)
  62. self._unlock_dynpw_dp = dps_map.pop("unlock_dynamic_pwd", None)
  63. self._unlock_offlinepw_dp = dps_map.pop("unlock_offline_pwd", None)
  64. self._unlock_card_dp = dps_map.pop("unlock_card", None)
  65. self._unlock_app_dp = dps_map.pop("unlock_app", None)
  66. self._unlock_key_dp = dps_map.pop("unlock_key", None)
  67. self._unlock_ble_dp = dps_map.pop("unlock_ble", None)
  68. self._unlock_voice_dp = dps_map.pop("unlock_voice", None)
  69. self._unlock_face_dp = dps_map.pop("unlock_face", None)
  70. self._unlock_multi_dp = dps_map.pop("unlock_multi", None)
  71. self._unlock_ibeacon_dp = dps_map.pop("unlock_ibeacon", None)
  72. self._req_unlock_dp = dps_map.pop("request_unlock", None)
  73. self._approve_unlock_dp = dps_map.pop("approve_unlock", None)
  74. self._code_unlock_dp = dps_map.pop("code_unlock", None)
  75. self._req_intercom_dp = dps_map.pop("request_intercom", None)
  76. self._approve_intercom_dp = dps_map.pop("approve_intercom", None)
  77. self._jam_dp = dps_map.pop("jammed", None)
  78. self._init_end(dps_map)
  79. if self._open_dp and not self._open_dp.readonly:
  80. self._attr_supported_features = LockEntityFeature.OPEN
  81. @property
  82. def is_locked(self):
  83. """Return the a boolean representing whether the lock is locked."""
  84. lock = None
  85. if self._lock_state_dp:
  86. lock = self._lock_state_dp.get_value(self._device)
  87. if lock is None and self._lock_dp:
  88. lock = self._lock_dp.get_value(self._device)
  89. if lock is None:
  90. for d in (
  91. self._unlock_card_dp,
  92. self._unlock_dynpw_dp,
  93. self._unlock_fp_dp,
  94. self._unlock_offlinepw_dp,
  95. self._unlock_pw_dp,
  96. self._unlock_tmppw_dp,
  97. self._unlock_app_dp,
  98. self._unlock_key_dp,
  99. self._unlock_ble_dp,
  100. self._unlock_voice_dp,
  101. self._unlock_face_dp,
  102. self._unlock_multi_dp,
  103. self._unlock_ibeacon_dp,
  104. ):
  105. if d:
  106. if d.get_value(self._device):
  107. lock = False
  108. elif lock is None:
  109. lock = True
  110. return lock
  111. @property
  112. def is_open(self):
  113. if self._open_dp:
  114. return self._open_dp.get_value(self._device)
  115. @property
  116. def is_jammed(self):
  117. if self._jam_dp:
  118. return self._jam_dp.get_value(self._device)
  119. @property
  120. def code_format(self):
  121. """Return the code format of the lock."""
  122. if self._code_unlock_dp:
  123. return r".{8}"
  124. return None
  125. def unlocker_id(self, dp, type):
  126. if dp:
  127. unlock = dp.get_value(self._device)
  128. if unlock:
  129. if unlock is True:
  130. return f"{type}"
  131. else:
  132. return f"{type} #{unlock}"
  133. @property
  134. def changed_by(self):
  135. for dp, desc in {
  136. self._unlock_app_dp: "App",
  137. self._unlock_ble_dp: "Bluetooth",
  138. self._unlock_card_dp: "Card",
  139. self._unlock_dynpw_dp: "Dynamic Password",
  140. self._unlock_fp_dp: "Finger",
  141. self._unlock_key_dp: "Key",
  142. self._unlock_offlinepw_dp: "Offline Password",
  143. self._unlock_pw_dp: "Password",
  144. self._unlock_tmppw_dp: "Temporary Password",
  145. self._unlock_voice_dp: "Voice",
  146. self._unlock_face_dp: "Face",
  147. self._unlock_multi_dp: "Multifactor",
  148. self._unlock_ibeacon_dp: "iBeacon",
  149. }.items():
  150. by = self.unlocker_id(dp, desc)
  151. if by:
  152. # clear non-persistent dps immediately on reporting, instead
  153. # of waiting for the next poll, to make the lock more responsive
  154. # to multiple attempts
  155. if not dp.persist:
  156. self._device._cached_state.pop(dp.id, None)
  157. return by
  158. async def async_lock(self, **kwargs):
  159. """Lock the lock."""
  160. if self._lock_dp and not self._lock_dp.readonly:
  161. await self._lock_dp.async_set_value(self._device, True)
  162. elif self._code_unlock_dp:
  163. code = kwargs.get("code")
  164. if not code:
  165. raise ValueError("Code required to lock")
  166. msg = self.build_code_unlock_msg(
  167. CODE_LOCK, member_id=1, code=code, source=CODE_SRC_UNKNOWN
  168. )
  169. await self._code_unlock_dp.async_set_value(self._device, msg)
  170. else:
  171. raise NotImplementedError()
  172. async def async_unlock(self, **kwargs):
  173. """Unlock the lock."""
  174. if self._code_unlock_dp:
  175. code = kwargs.get("code")
  176. if not code:
  177. raise ValueError("Code required to unlock")
  178. msg = self.build_code_unlock_msg(
  179. CODE_UNLOCK, member_id=1, code=code, source=CODE_SRC_UNKNOWN
  180. )
  181. await self._code_unlock_dp.async_set_value(self._device, msg)
  182. elif self._lock_dp and not self._lock_dp.readonly:
  183. await self._lock_dp.async_set_value(self._device, False)
  184. elif self._approve_unlock_dp:
  185. if self._req_unlock_dp and not self._req_unlock_dp.get_value(self._device):
  186. raise TimeoutError()
  187. await self._approve_unlock_dp.async_set_value(self._device, True)
  188. elif self._approve_intercom_dp:
  189. if self._req_intercom_dp and not self._req_intercom_dp.get_value(
  190. self._device
  191. ):
  192. raise TimeoutError()
  193. await self._approve_intercom_dp.async_set_value(self._device, True)
  194. else:
  195. raise NotImplementedError()
  196. async def async_open(self, **kwargs):
  197. """Open the door latch."""
  198. if self._open_dp:
  199. await self._open_dp.async_set_value(self._device, True)
  200. def build_code_unlock_msg(self, action, member_id, code, source=CODE_SRC_UNKNOWN):
  201. """Generate the unlock code message."""
  202. if len(code) != 8 or not code.isascii():
  203. raise ValueError("Code must be 8 ASCII characters")
  204. msg = bytearray()
  205. msg.append(action)
  206. msg += member_id.to_bytes(2, "big")
  207. msg += code.encode("ascii")
  208. msg += source.to_bytes(2, "big")
  209. # msg += b"\x00" # ordinary user (0x01 is admin)
  210. return b64encode(msg).decode("utf-8")