4
0

lock.py 9.4 KB

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