lock.py 9.4 KB

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