|
|
@@ -2,6 +2,8 @@
|
|
|
Setup for different kinds of Tuya lock devices
|
|
|
"""
|
|
|
|
|
|
+from base64 import b64encode
|
|
|
+
|
|
|
from homeassistant.components.lock import LockEntity, LockEntityFeature
|
|
|
|
|
|
from .device import TuyaLocalDevice
|
|
|
@@ -9,6 +11,37 @@ from .entity import TuyaLocalEntity
|
|
|
from .helpers.config import async_tuya_setup_platform
|
|
|
from .helpers.device_config import TuyaEntityConfig
|
|
|
|
|
|
+# Remote Code unlocking protocol: 8 digit code set when paired by
|
|
|
+# remote_no_pd_setkey (typically dp 60), user can obtain by
|
|
|
+# eavesdropping cloud unlock messages.
|
|
|
+# Format in case this command can be supported outside of pairing process:
|
|
|
+# Request: Validity (1 byte 0 or 1), Member ID (2 bytes),
|
|
|
+# Start time (4 byte unixtime), End time (4 byte unixtime),
|
|
|
+# Usable times (2 bytes, 0=infinite), Key (8 bytes ASCII)
|
|
|
+
|
|
|
+# Same 8 digit ASCII code used along with binary member ID to generate
|
|
|
+# unlock command with remote_no_dp_key (typically dp 61)
|
|
|
+# Locking usually possible without code, but remote_no_dp_key seems
|
|
|
+# to also support locking with code.
|
|
|
+
|
|
|
+# Request: action (1 byte), Member (2 bytes 0-100), code (8 bytes ASCII),
|
|
|
+# source (2 bytes)
|
|
|
+CODE_LOCK = 0x00
|
|
|
+CODE_UNLOCK = 0x01
|
|
|
+
|
|
|
+CODE_SRC_UNKNOWN = 0x0000
|
|
|
+CODE_SRC_APP = 0x0001
|
|
|
+CODE_SRC_VOICE = 0x0002
|
|
|
+
|
|
|
+# Reply: status (1 byte), Member ID (2 bytes 1 - 100)
|
|
|
+CODE_REPLY_SUCCESS = 0x00
|
|
|
+CODE_REPLY_FAIL = 0x01
|
|
|
+CODE_REPLY_PWD_ERROR = 0x02
|
|
|
+CODE_REPLY_TIMEOUT = 0x03
|
|
|
+CODE_REPLY_OUTOFHOURS = 0x04
|
|
|
+CODE_REPLY_WRONGCODE = 0x05
|
|
|
+CODE_REPLY_DOUBLELOCKED = 0x06
|
|
|
+
|
|
|
|
|
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
|
|
config = {**config_entry.data, **config_entry.options}
|
|
|
@@ -51,6 +84,7 @@ class TuyaLocalLock(TuyaLocalEntity, LockEntity):
|
|
|
self._unlock_ibeacon_dp = dps_map.pop("unlock_ibeacon", None)
|
|
|
self._req_unlock_dp = dps_map.pop("request_unlock", None)
|
|
|
self._approve_unlock_dp = dps_map.pop("approve_unlock", None)
|
|
|
+ self._code_unlock_dp = dps_map.pop("code_unlock", None)
|
|
|
self._req_intercom_dp = dps_map.pop("request_intercom", None)
|
|
|
self._approve_intercom_dp = dps_map.pop("approve_intercom", None)
|
|
|
self._jam_dp = dps_map.pop("jammed", None)
|
|
|
@@ -99,6 +133,13 @@ class TuyaLocalLock(TuyaLocalEntity, LockEntity):
|
|
|
if self._jam_dp:
|
|
|
return self._jam_dp.get_value(self._device)
|
|
|
|
|
|
+ @property
|
|
|
+ def code_format(self):
|
|
|
+ """Return the code format of the lock."""
|
|
|
+ if self._code_unlock_dp:
|
|
|
+ return r".{8}"
|
|
|
+ return None
|
|
|
+
|
|
|
def unlocker_id(self, dp, type):
|
|
|
if dp:
|
|
|
unlock = dp.get_value(self._device)
|
|
|
@@ -136,14 +177,30 @@ class TuyaLocalLock(TuyaLocalEntity, LockEntity):
|
|
|
|
|
|
async def async_lock(self, **kwargs):
|
|
|
"""Lock the lock."""
|
|
|
- if self._lock_dp:
|
|
|
+ if self._lock_dp and not self._lock_dp.readonly:
|
|
|
await self._lock_dp.async_set_value(self._device, True)
|
|
|
+ elif self._code_unlock_dp:
|
|
|
+ code = kwargs.get("code")
|
|
|
+ if not code:
|
|
|
+ raise ValueError("Code required to lock")
|
|
|
+ msg = self.build_code_unlock_msg(
|
|
|
+ CODE_LOCK, member_id=1, code=code, source=CODE_SRC_UNKNOWN
|
|
|
+ )
|
|
|
+ await self._code_unlock_dp.async_set_value(self._device, msg)
|
|
|
else:
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
async def async_unlock(self, **kwargs):
|
|
|
"""Unlock the lock."""
|
|
|
- if self._lock_dp:
|
|
|
+ if self._code_unlock_dp:
|
|
|
+ code = kwargs.get("code")
|
|
|
+ if not code:
|
|
|
+ raise ValueError("Code required to unlock")
|
|
|
+ msg = self.build_code_unlock_msg(
|
|
|
+ CODE_UNLOCK, member_id=1, code=code, source=CODE_SRC_UNKNOWN
|
|
|
+ )
|
|
|
+ await self._code_unlock_dp.async_set_value(self._device, msg)
|
|
|
+ elif self._lock_dp and not self._lock_dp.readonly:
|
|
|
await self._lock_dp.async_set_value(self._device, False)
|
|
|
elif self._approve_unlock_dp:
|
|
|
if self._req_unlock_dp and not self._req_unlock_dp.get_value(self._device):
|
|
|
@@ -162,3 +219,15 @@ class TuyaLocalLock(TuyaLocalEntity, LockEntity):
|
|
|
"""Open the door latch."""
|
|
|
if self._open_dp:
|
|
|
await self._open_dp.async_set_value(self._device, True)
|
|
|
+
|
|
|
+ def build_code_unlock_msg(self, action, member_id, code, source=CODE_SRC_UNKNOWN):
|
|
|
+ """Generate the unlock code message."""
|
|
|
+ if len(code) != 8 or not code.isascii():
|
|
|
+ raise ValueError("Code must be 8 ASCII characters")
|
|
|
+ msg = bytearray()
|
|
|
+ msg.append(action)
|
|
|
+ msg += member_id.to_bytes(2, "big")
|
|
|
+ msg += code.encode("ascii")
|
|
|
+ msg += source.to_bytes(2, "big")
|
|
|
+ msg += b"\x00" # ordinary user (0x01 is admin)
|
|
|
+ return b64encode(msg).decode("utf-8")
|