Ver código fonte

Add a "force" attribute to dps config.

This is to be set when we need to force certain dps to update with
updatedps(), instead of just calling status(), as is widespread for
energy monitoring smartplugs.

No configs are using this yet, as more testing is needed.

Issue #420
Jason Rumney 3 anos atrás
pai
commit
d8d9d44840

+ 16 - 4
custom_components/tuya_local/device.py

@@ -61,6 +61,7 @@ class TuyaLocalDevice(object):
         """
         self._name = name
         self._children = []
+        self._force_dps = []
         self._running = False
         self._shutdown_listener = None
         self._startup_listener = None
@@ -143,6 +144,7 @@ class TuyaLocalDevice(object):
             self._shutdown_listener()
             self._shutdown_listener = None
         self._children.clear()
+        self._force_dps.clear()
         if self._refresh_task:
             await self._refresh_task
         _LOGGER.debug(f"Monitor loop for {self.name} stopped")
@@ -154,6 +156,10 @@ class TuyaLocalDevice(object):
         should_poll = len(self._children) == 0
 
         self._children.append(entity)
+        for dp in entity._config.dps():
+            if dp.force and dp.id not in self._force_dps:
+                self._force_dps.append(dp.id)
+
         if not self._running and not self._startup_listener:
             self.start()
         if self.has_returned_state:
@@ -214,10 +220,16 @@ class TuyaLocalDevice(object):
                     self._api.set_socketPersistent(persist)
 
                 if now - last_cache > self._CACHE_TIMEOUT:
-                    poll = await self._retry_on_failed_connection(
-                        lambda: self._api.status(),
-                        f"Failed to refresh device state for {self.name}",
-                    )
+                    if self._force_dps:
+                        poll = await self._retry_on_failed_connection(
+                            lambda: self._api.updatedps(self._force_dps),
+                            f"Failed to refresh device state for {self.name}",
+                        )
+                    else:
+                        poll = await self._retry_on_failed_connection(
+                            lambda: self._api.status(),
+                            f"Failed to refresh device state for {self.name}",
+                        )
                 else:
                     await self._hass.async_add_executor_job(
                         self._api.heartbeat,

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

@@ -144,7 +144,10 @@ to use a secondary entity for that.
 
 A boolean setting to mark attributes as readonly. If not specified, the
 default is `false`.  If set to `true`, the attributes will be reported
-to Home Assistant, but no functionality for setting them will be exposed.
+to Home Assistant, but attempting to set them will result in an error.
+This is only needed in contexts where it would normally be possible to set
+the value.  If you are creating a sensor entity, or adding an attribute of an
+entity which is inherently read-only, then you do not need to specify this.
 
 ### `optional`
 
@@ -155,6 +158,16 @@ matched even if it is not sending the dp at the time when adding a new device.
 It can also be used to match a range of devices that have variations in the extra
 attributes that are sent.
 
+### `force`
+
+*Optional, default false.*
+
+A boolean setting to mark dps as requiring an explicit update request
+to fetch.  Many energy monitoring smartplugs require this, without a
+explicit request to update them, such plugs will only return monitoring data
+rarely or never.  Devices can misbehave if this is used on dps that do not
+require it.  Use this only where needed, and generally only on read-only dps.
+
 ### `mapping`
 
 *Optional.*

+ 1 - 0
custom_components/tuya_local/diagnostics.py

@@ -75,6 +75,7 @@ def _async_device_as_dict(
         "cached_state": device._cached_state,
         "pending_state": device._pending_updates,
         "connected": device._running,
+        "force_dps": device._force_dps,
     }
 
     device_registry = dr.async_get(hass)

+ 4 - 0
custom_components/tuya_local/helpers/device_config.py

@@ -299,6 +299,10 @@ class TuyaDpsConfig:
     def optional(self):
         return self._config.get("optional", False)
 
+    @property
+    def force(self):
+        return self._config.get("force", False)
+
     @property
     def format(self):
         fmt = self._config.get("format")

+ 8 - 1
tests/test_device.py

@@ -9,6 +9,8 @@ from homeassistant.const import (
 )
 
 from custom_components.tuya_local.device import TuyaLocalDevice
+from custom_components.tuya_local.helpers.device_config import TuyaEntityConfig
+from custom_components.tuya_local.switch import TuyaLocalSwitch
 
 from .const import (
     EUROM_600_HEATER_PAYLOAD,
@@ -474,7 +476,8 @@ class TestDevice(IsolatedAsyncioTestCase):
         self.subject._startup_listener = None
         self.subject.start = Mock()
         entity = AsyncMock()
-
+        entity._config = Mock()
+        entity._config.dps.return_value = []
         # Call the function under test
         self.subject.register_entity(entity)
 
@@ -488,6 +491,8 @@ class TestDevice(IsolatedAsyncioTestCase):
         # Set up preconditions
         first = AsyncMock()
         second = AsyncMock()
+        second._config = Mock()
+        second._config.dps.return_value = []
         self.subject._children = [first]
         self.subject._running = True
         self.subject._startup_listener = None
@@ -506,6 +511,8 @@ class TestDevice(IsolatedAsyncioTestCase):
         # Set up preconditions
         first = AsyncMock()
         second = AsyncMock()
+        second._config = Mock()
+        second._config.dps.return_value = []
         self.subject._children = [first]
         self.subject._running = False
         self.subject._startup_listener = Mock()