Browse Source

Synchronize accesses to sub-devices (#3929)

When multiple sub-devices are under the same parent device, we need to make sure to access only one sub-device at a time,
because the underlying tinytuya device for the parent cannot handle multiple acceses at the same time. This bug often
manifests as "No local key for device!" in the tinytuya logs.

This commit adds a lock that is associated with the parent device, and acquires the lock whenever we access a sub-device or
the parent device itself.

Issue #2289
Jim Chen 4 months ago
parent
commit
cdd6050495
1 changed files with 21 additions and 4 deletions
  1. 21 4
      custom_components/tuya_local/device.py

+ 21 - 4
custom_components/tuya_local/device.py

@@ -78,22 +78,33 @@ class TuyaLocalDevice(object):
             if dev_cid:
             if dev_cid:
                 if hass.data[DOMAIN].get(dev_id) and name != "Test":
                 if hass.data[DOMAIN].get(dev_id) and name != "Test":
                     parent = hass.data[DOMAIN][dev_id]["tuyadevice"]
                     parent = hass.data[DOMAIN][dev_id]["tuyadevice"]
+                    parent_lock = hass.data[DOMAIN][dev_id]["tuyadevicelock"]
                 else:
                 else:
                     parent = tinytuya.Device(dev_id, address, local_key)
                     parent = tinytuya.Device(dev_id, address, local_key)
+                    parent_lock = asyncio.Lock()
                     if name != "Test":
                     if name != "Test":
-                        hass.data[DOMAIN][dev_id] = {"tuyadevice": parent}
+                        hass.data[DOMAIN][dev_id] = {
+                            "tuyadevice": parent,
+                            "tuyadevicelock": parent_lock,
+                        }
                 self._api = tinytuya.Device(
                 self._api = tinytuya.Device(
                     dev_cid,
                     dev_cid,
                     cid=dev_cid,
                     cid=dev_cid,
                     parent=parent,
                     parent=parent,
                 )
                 )
+                self._api_lock = parent_lock
             else:
             else:
                 if hass.data[DOMAIN].get(dev_id) and name != "Test":
                 if hass.data[DOMAIN].get(dev_id) and name != "Test":
                     self._api = hass.data[DOMAIN][dev_id]["tuyadevice"]
                     self._api = hass.data[DOMAIN][dev_id]["tuyadevice"]
+                    self._api_lock = hass.data[DOMAIN][dev_id]["tuyadevicelock"]
                 else:
                 else:
                     self._api = tinytuya.Device(dev_id, address, local_key)
                     self._api = tinytuya.Device(dev_id, address, local_key)
+                    self._api_lock = asyncio.Lock()
                     if name != "Test":
                     if name != "Test":
-                        hass.data[DOMAIN][dev_id] = {"tuyadevice": self._api}
+                        hass.data[DOMAIN][dev_id] = {
+                            "tuyadevice": self._api,
+                            "tuyadevicelock": self._api_lock,
+                        }
         except Exception as e:
         except Exception as e:
             _LOGGER.error(
             _LOGGER.error(
                 "%s: %s while initialising device %s",
                 "%s: %s while initialising device %s",
@@ -288,6 +299,7 @@ class TuyaLocalDevice(object):
 
 
         while self._running:
         while self._running:
             error_count = self._api_working_protocol_failures
             error_count = self._api_working_protocol_failures
+            await self._api_lock.acquire()
             try:
             try:
                 last_cache = self._cached_state.get("updated_at", 0)
                 last_cache = self._cached_state.get("updated_at", 0)
                 now = time()
                 now = time()
@@ -332,6 +344,7 @@ class TuyaLocalDevice(object):
                         self._api.receive,
                         self._api.receive,
                     )
                     )
                 else:
                 else:
+                    self._api_lock.release()
                     await asyncio.sleep(5)
                     await asyncio.sleep(5)
                     poll = None
                     poll = None
 
 
@@ -360,8 +373,6 @@ class TuyaLocalDevice(object):
                         poll["full_poll"] = full_poll
                         poll["full_poll"] = full_poll
                         yield poll
                         yield poll
 
 
-                await asyncio.sleep(0.1 if self.has_returned_state else 5)
-
             except CancelledError:
             except CancelledError:
                 self._running = False
                 self._running = False
                 # Close the persistent connection when exiting the loop
                 # Close the persistent connection when exiting the loop
@@ -379,7 +390,13 @@ class TuyaLocalDevice(object):
                 self._api.set_socketPersistent(False)
                 self._api.set_socketPersistent(False)
                 if self._api.parent:
                 if self._api.parent:
                     self._api.parent.set_socketPersistent(False)
                     self._api.parent.set_socketPersistent(False)
+                self._api_lock.release()
                 await asyncio.sleep(5)
                 await asyncio.sleep(5)
+            finally:
+                if self._api_lock.locked():
+                    self._api_lock.release()
+
+            await asyncio.sleep(0.1 if self.has_returned_state else 5)
 
 
         # Close the persistent connection when exiting the loop
         # Close the persistent connection when exiting the loop
         self._api.set_socketPersistent(False)
         self._api.set_socketPersistent(False)