Преглед изворни кода

feat(lock): allow unlock code that is set by the integration

Previously, unlocking via a code which the user had to find themselves
amongst the cloud logs was enabled. This has some issues, including
that AI cannot find the correct log to sniff this code from.

However, on most such devices it appears to be possible to set the
code at the same time that you use it. This defeats any security that
the code provides, but that is Tuya's issue, not ours.

Issue #5649 by @nihatds100

Unlike the proposed solution on that issue, we generate a random code
instead of asking the user for input, and limit the timeframe it can
be used for. A generous 5 minutes of validity is set, in case clocks
are not in sync.
Jason Rumney пре 16 часа
родитељ
комит
5a639fc5ec

+ 2 - 1
custom_components/tuya_local/devices/README.md

@@ -737,7 +737,8 @@ no information will be available about which specific credential was used to unl
 - **approve_unlock** (optional, boolean): a dp to unlock the lock in response to a request.
 - **approve_unlock** (optional, boolean): a dp to unlock the lock in response to a request.
 - **request_intercom** (optional, integer): a dp to signal that a request has been made via intercom to unlock, the value should indicate the time remaining for approval.
 - **request_intercom** (optional, integer): a dp to signal that a request has been made via intercom to unlock, the value should indicate the time remaining for approval.
 - **approve_intercom** (optional, boolean): a dp to unlock the lock in response to an intercom request.
 - **approve_intercom** (optional, boolean): a dp to unlock the lock in response to an intercom request.
-- **code_unlock** (optional, base64): a dp to unlock the lock by giving an 8 digit code. This corresponds in the Tuya info to `remote_no_dp_key` and has a specific format. The 8 digit key assigned to user 1 must be sent to unlock (and optionally lock) the lock.
+- **code_unlock** (optional, base64): a dp to unlock the lock by giving an 8 digit code. This corresponds in the Tuya info to `remote_no_dp_key` and has a specific format. If not accompanied by **set_unlock_code**(below), then the 8 digit key assigned to user 1 must be sent to unlock (and optionally lock) the lock. This can generally be found in the Tuya developer portal logs after opening the lock with the app on first phone that was paired.
+- **set_unlock_code** (optional, base64): a dp that allows setting the 8 digit code at the same time as it is used in code_unlock, so the user does not need to enter an 8 digit number. This corresponds in the Tuya info to `remote_no_pd_setkey` and has a specific format. If this is supplied, the integration will simultaneously set a random code in slot 7, and use it to unlock the lock, so the user does not need to provide any code.
 - **jammed** (optional, boolean): a dp to signal that the lock is jammed.
 - **jammed** (optional, boolean): a dp to signal that the lock is jammed.
 
 
 ### `number`
 ### `number`

+ 5 - 11
custom_components/tuya_local/devices/ailrinni_fingerprint_lock.yaml

@@ -49,6 +49,11 @@ entities:
         name: unlock_temp_pwd
         name: unlock_temp_pwd
         optional: true
         optional: true
         persist: false
         persist: false
+      - id: 60
+        type: base64
+        optional: true
+        sensitive: true
+        name: set_unlock_code
       - id: 61
       - id: 61
         type: base64
         type: base64
         optional: true
         optional: true
@@ -132,17 +137,6 @@ entities:
             step: 33
             step: 33
             value: 100
             value: 100
             icon: "mdi:volume-high"
             icon: "mdi:volume-high"
-  - entity: text
-    name: Remote unlock code
-    category: config
-    icon: "mdi:form-textbox-password"
-    hidden: true
-    dps:
-      - id: 60
-        type: base64
-        optional: true
-        sensitive: true
-        name: value
   - entity: text
   - entity: text
     name: New credential
     name: New credential
     category: config
     category: config

+ 5 - 11
custom_components/tuya_local/devices/ble_positivo_smart_fechadura.yaml

@@ -32,6 +32,11 @@ entities:
             value: true
             value: true
           - dps_val: true
           - dps_val: true
             value: false
             value: false
+      - id: 60
+        type: base64
+        optional: true
+        sensitive: true
+        name: set_unlock_code
       - id: 61
       - id: 61
         type: base64
         type: base64
         name: code_unlock
         name: code_unlock
@@ -186,17 +191,6 @@ entities:
             value: shock
             value: shock
           - dps_val: defense
           - dps_val: defense
             value: defense
             value: defense
-  - entity: text
-    name: Remote unlock code
-    category: config
-    icon: "mdi:form-textbox-password"
-    hidden: true
-    dps:
-      - id: 60
-        type: string
-        name: value
-        optional: true
-        sensitive: true
   - entity: text
   - entity: text
     name: New credential
     name: New credential
     category: config
     category: config

+ 5 - 11
custom_components/tuya_local/devices/gainsboroughliberty_entrance_lock.yaml

@@ -63,6 +63,11 @@ entities:
         name: unlock_temp_pwd
         name: unlock_temp_pwd
         optional: true
         optional: true
         persist: false
         persist: false
+      - id: 73
+        type: base64
+        optional: true
+        sensitive: true
+        name: set_unlock_code
       - id: 61
       - id: 61
         type: base64
         type: base64
         optional: true
         optional: true
@@ -395,14 +400,3 @@ entities:
         optional: true
         optional: true
         sensitive: true
         sensitive: true
         name: sensor
         name: sensor
-  - entity: text
-    name: Remote unlock code
-    category: config
-    icon: "mdi:form-textbox-password"
-    hidden: true
-    dps:
-      - id: 72
-        type: string
-        optional: true
-        sensitive: true
-        name: value

+ 5 - 14
custom_components/tuya_local/devices/hornbill_y4_smart_lock.yaml

@@ -108,6 +108,11 @@ entities:
       # door lock is connected to the Bluetooth gateway, can the voice control
       # door lock is connected to the Bluetooth gateway, can the voice control
       # door opening and closing function be realized with smart speaker
       # door opening and closing function be realized with smart speaker
       # products such as Alexa or Google Home.
       # products such as Alexa or Google Home.
+      - id: 73
+        type: base64
+        optional: true
+        sensitive: true
+        name: set_unlock_code
       - id: 61
       - id: 61
         type: base64
         type: base64
         optional: true
         optional: true
@@ -386,20 +391,6 @@ entities:
         range:
         range:
           min: 1
           min: 1
           max: 1800
           max: 1800
-  - entity: text
-    name: Remote unlock code
-    category: config
-    icon: "mdi:form-textbox-password"
-    hidden: true
-    dps:
-      # [Remote unlock] is used to configure the key required for remote
-      # unlocking. If the device needs to be unlocked remotely, this DP is
-      # required.
-      - id: 73
-        type: base64
-        name: value
-        optional: true
-        sensitive: true
 
 
   - entity: text
   - entity: text
     name: New credential
     name: New credential

+ 5 - 11
custom_components/tuya_local/devices/nice_digi_lock.yaml

@@ -32,6 +32,11 @@ entities:
             value: false
             value: false
           - dps_val: false
           - dps_val: false
             value: true
             value: true
+      - id: 73
+        type: base64
+        optional: true
+        sensitive: true
+        name: set_unlock_code
       - id: 61
       - id: 61
         type: base64
         type: base64
         name: code_unlock
         name: code_unlock
@@ -138,17 +143,6 @@ entities:
       - id: 78
       - id: 78
         type: boolean
         type: boolean
         name: switch
         name: switch
-  - entity: text
-    name: Remote unlock code
-    category: config
-    icon: "mdi:form-textbox-password"
-    hidden: true
-    dps:
-      - id: 73
-        type: base64
-        name: value
-        optional: true
-        sensitive: true
   - entity: text
   - entity: text
     name: New credential
     name: New credential
     category: config
     category: config

+ 5 - 11
custom_components/tuya_local/devices/orion_dl033ha_lock.yaml

@@ -58,6 +58,11 @@ entities:
         name: unlock_temp_pwd
         name: unlock_temp_pwd
         optional: true
         optional: true
         persist: false
         persist: false
+      - id: 73
+        type: base64
+        optional: true
+        sensitive: true
+        name: set_unlock_code
       - id: 61
       - id: 61
         type: base64
         type: base64
         optional: true
         optional: true
@@ -394,17 +399,6 @@ entities:
             icon: "mdi:broadcast"
             icon: "mdi:broadcast"
           - dps_val: false
           - dps_val: false
             icon: "mdi:broadcast-off"
             icon: "mdi:broadcast-off"
-  - entity: text
-    name: Remote unlock code
-    category: config
-    icon: "mdi:form-textbox-password"
-    hidden: true
-    dps:
-      - id: 73
-        type: base64
-        optional: true
-        sensitive: true
-        name: value
   - entity: text
   - entity: text
     name: New credential
     name: New credential
     category: config
     category: config

+ 5 - 11
custom_components/tuya_local/devices/otu_r1o1_lock.yaml

@@ -64,6 +64,11 @@ entities:
         optional: true
         optional: true
         persist: false
         persist: false
         name: unlock_temp_pwd
         name: unlock_temp_pwd
+      - id: 60
+        type: base64
+        optional: true
+        sensitive: true
+        name: set_unlock_code
       - id: 61
       - id: 61
         type: base64
         type: base64
         optional: true
         optional: true
@@ -164,17 +169,6 @@ entities:
         mapping:
         mapping:
           - dps_val: null
           - dps_val: null
             value: false
             value: false
-  - entity: text
-    name: Remote unlock code
-    category: config
-    icon: "mdi:form-textbox-password"
-    hidden: true
-    dps:
-      - id: 60
-        type: base64
-        optional: true
-        sensitive: true
-        name: value
   - entity: text
   - entity: text
     name: New credential
     name: New credential
     category: config
     category: config

+ 5 - 11
custom_components/tuya_local/devices/primebras_athenas_lock.yaml

@@ -79,6 +79,11 @@ entities:
         name: unlock_temp_pwd
         name: unlock_temp_pwd
         optional: true
         optional: true
         persist: false
         persist: false
+      - id: 73
+        type: base64
+        optional: true
+        sensitive: true
+        name: set_unlock_code
       - id: 61
       - id: 61
         type: base64
         type: base64
         optional: true
         optional: true
@@ -319,17 +324,6 @@ entities:
             value: Clockwise
             value: Clockwise
           - dps_val: anticlockwise
           - dps_val: anticlockwise
             value: Anti-clockwise
             value: Anti-clockwise
-  - entity: text
-    name: Remote unlock code
-    category: config
-    icon: "mdi:form-textbox-password"
-    hidden: true
-    dps:
-      - id: 73
-        type: base64
-        optional: true
-        sensitive: true
-        name: value
   - entity: text
   - entity: text
     name: New credential
     name: New credential
     category: config
     category: config

+ 5 - 11
custom_components/tuya_local/devices/raykube_a1promax_lock.yaml

@@ -31,6 +31,11 @@ entities:
         type: boolean
         type: boolean
         name: open
         name: open
         readonly: true
         readonly: true
+      - id: 73
+        type: base64
+        optional: true
+        sensitive: true
+        name: set_unlock_code
       - id: 61
       - id: 61
         type: base64
         type: base64
         optional: true
         optional: true
@@ -158,17 +163,6 @@ entities:
       - id: 78
       - id: 78
         type: boolean
         type: boolean
         name: switch
         name: switch
-  - entity: text
-    name: Remote unlock code
-    category: config
-    icon: "mdi:form-textbox-password"
-    hidden: true
-    dps:
-      - id: 73
-        type: string
-        optional: true
-        sensitive: true
-        name: value
   - entity: text
   - entity: text
     name: New credential
     name: New credential
     category: config
     category: config

+ 5 - 11
custom_components/tuya_local/devices/xcase_nx4964_lockbox.yaml

@@ -26,6 +26,11 @@ entities:
         optional: true
         optional: true
         persist: false
         persist: false
         name: unlock_temp_pwd
         name: unlock_temp_pwd
+      - id: 73
+        type: base64
+        optional: true
+        sensitive: true
+        name: set_unlock_code
       - id: 61
       - id: 61
         type: string
         type: string
         optional: true
         optional: true
@@ -114,17 +119,6 @@ entities:
             value: shock
             value: shock
           - dps_val: defense
           - dps_val: defense
             value: defense
             value: defense
-  - entity: text
-    name: Remote unlock code
-    category: config
-    icon: "mdi:form-textbox-password"
-    hidden: true
-    dps:
-      - id: 73
-        type: base64
-        optional: true
-        sensitive: true
-        name: value
   - entity: text
   - entity: text
     name: New credential
     name: New credential
     category: config
     category: config

+ 48 - 6
custom_components/tuya_local/lock.py

@@ -4,7 +4,8 @@ Setup for different kinds of Tuya lock devices
 
 
 import logging
 import logging
 from base64 import b64encode
 from base64 import b64encode
-
+from secrets import randbelow
+from time import time
 from homeassistant.components.lock import LockEntity, LockEntityFeature
 from homeassistant.components.lock import LockEntity, LockEntityFeature
 
 
 from .device import TuyaLocalDevice
 from .device import TuyaLocalDevice
@@ -88,6 +89,7 @@ class TuyaLocalLock(TuyaLocalEntity, LockEntity):
         self._req_unlock_dp = dps_map.pop("request_unlock", None)
         self._req_unlock_dp = dps_map.pop("request_unlock", None)
         self._approve_unlock_dp = dps_map.pop("approve_unlock", None)
         self._approve_unlock_dp = dps_map.pop("approve_unlock", None)
         self._code_unlock_dp = dps_map.pop("code_unlock", None)
         self._code_unlock_dp = dps_map.pop("code_unlock", None)
+        self._set_code_dp = dps_map.pop("set_unlock_code", None)
         self._req_intercom_dp = dps_map.pop("request_intercom", None)
         self._req_intercom_dp = dps_map.pop("request_intercom", None)
         self._approve_intercom_dp = dps_map.pop("approve_intercom", None)
         self._approve_intercom_dp = dps_map.pop("approve_intercom", None)
         self._jam_dp = dps_map.pop("jammed", None)
         self._jam_dp = dps_map.pop("jammed", None)
@@ -139,7 +141,7 @@ class TuyaLocalLock(TuyaLocalEntity, LockEntity):
     @property
     @property
     def code_format(self):
     def code_format(self):
         """Return the code format of the lock."""
         """Return the code format of the lock."""
-        if self._code_unlock_dp:
+        if self._code_unlock_dp and not self._set_code_dp:
             return r".{8}"
             return r".{8}"
         return None
         return None
 
 
@@ -183,6 +185,19 @@ class TuyaLocalLock(TuyaLocalEntity, LockEntity):
         if self._lock_dp and not self._lock_dp.readonly:
         if self._lock_dp and not self._lock_dp.readonly:
             _LOGGER.info("%s locking", self._config.config_id)
             _LOGGER.info("%s locking", self._config.config_id)
             await self._lock_dp.async_set_value(self._device, True)
             await self._lock_dp.async_set_value(self._device, True)
+        elif self._code_unlock_dp and self._set_code_dp:
+            code = sprintf("%08d", randbelow(100000000))
+            setting = self.build_code_set_msg(code)
+            msg = self.build_code_unlock_msg(
+                CODE_LOCK, member_id=7, code=code, source=CODE_SRC_UNKNOWN
+            )
+            _LOGGER.info("%s locking with random code", self._config.config_id)
+            await self._device.async_set_properties(
+                {
+                    self._set_code_dp.id: setting,
+                    self._code_unlock_dp.id: msg,
+                }
+            )
         elif self._code_unlock_dp:
         elif self._code_unlock_dp:
             code = kwargs.get("code")
             code = kwargs.get("code")
             if not code:
             if not code:
@@ -197,7 +212,23 @@ class TuyaLocalLock(TuyaLocalEntity, LockEntity):
 
 
     async def async_unlock(self, **kwargs):
     async def async_unlock(self, **kwargs):
         """Unlock the lock."""
         """Unlock the lock."""
-        if self._code_unlock_dp:
+        if self._lock_dp and not self._lock_dp.readonly:
+            _LOGGER.info("%s unlocking", self._config.config_id)
+            await self._lock_dp.async_set_value(self._device, False)
+        elif self._code_unlock_dp and self._set_code_dp:
+            code = sprintf("%08d", randbelow(100000000))
+            setting = self.build_code_set_msg(code)
+            msg = self.build_code_unlock_msg(
+                CODE_UNLOCK, member_id=7, code=code, source=CODE_SRC_UNKNOWN
+            )
+            _LOGGER.info("%s locking with random code", self._config.config_id)
+            await self._device.async_set_properties(
+                {
+                    self._set_code_dp.id: setting,
+                    self._code_unlock_dp.id: msg,
+                }
+            )
+        elif self._code_unlock_dp:
             code = kwargs.get("code")
             code = kwargs.get("code")
             if not code:
             if not code:
                 raise ValueError("Code required to unlock")
                 raise ValueError("Code required to unlock")
@@ -206,9 +237,6 @@ class TuyaLocalLock(TuyaLocalEntity, LockEntity):
             )
             )
             _LOGGER.info("%s unlocking with code", self._config.config_id)
             _LOGGER.info("%s unlocking with code", self._config.config_id)
             await self._code_unlock_dp.async_set_value(self._device, msg)
             await self._code_unlock_dp.async_set_value(self._device, msg)
-        elif self._lock_dp and not self._lock_dp.readonly:
-            _LOGGER.info("%s unlocking", self._config.config_id)
-            await self._lock_dp.async_set_value(self._device, False)
         elif self._approve_unlock_dp:
         elif self._approve_unlock_dp:
             if self._req_unlock_dp and not self._req_unlock_dp.get_value(self._device):
             if self._req_unlock_dp and not self._req_unlock_dp.get_value(self._device):
                 raise TimeoutError()
                 raise TimeoutError()
@@ -241,3 +269,17 @@ class TuyaLocalLock(TuyaLocalEntity, LockEntity):
         msg += source.to_bytes(2, "big")
         msg += source.to_bytes(2, "big")
         # msg += b"\x00"  # ordinary user (0x01 is admin)
         # msg += b"\x00"  # ordinary user (0x01 is admin)
         return b64encode(msg).decode("utf-8")
         return b64encode(msg).decode("utf-8")
+
+    def build_code_set_msg(self, code):
+        """Generate the set code message."""
+        if len(code) != 8 or not code.isascii():
+            raise ValueError("Code must be 8 ASCII characters")
+        validity = int(time())
+        msg = bytearray()
+        msg += (7).to_bytes(3, "big")  # valid + member ID
+        # start and end times. 5 minute allowance each way for clock drift
+        msg += (validity - 300).to_bytes(4, "big")
+        msg += (validity + 300).to_bytes(4, "big")
+        msg += (1).to_bytes(2, "big")  # usable times
+        msg += code.encode("ascii")
+        return b64encode(msg).decode("utf-8")