Kaynağa Gözat

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 yıl önce
ebeveyn
işleme
d8d9d44840

+ 16 - 4
custom_components/tuya_local/device.py

@@ -61,6 +61,7 @@ class TuyaLocalDevice(object):
         """
         """
         self._name = name
         self._name = name
         self._children = []
         self._children = []
+        self._force_dps = []
         self._running = False
         self._running = False
         self._shutdown_listener = None
         self._shutdown_listener = None
         self._startup_listener = None
         self._startup_listener = None
@@ -143,6 +144,7 @@ class TuyaLocalDevice(object):
             self._shutdown_listener()
             self._shutdown_listener()
             self._shutdown_listener = None
             self._shutdown_listener = None
         self._children.clear()
         self._children.clear()
+        self._force_dps.clear()
         if self._refresh_task:
         if self._refresh_task:
             await self._refresh_task
             await self._refresh_task
         _LOGGER.debug(f"Monitor loop for {self.name} stopped")
         _LOGGER.debug(f"Monitor loop for {self.name} stopped")
@@ -154,6 +156,10 @@ class TuyaLocalDevice(object):
         should_poll = len(self._children) == 0
         should_poll = len(self._children) == 0
 
 
         self._children.append(entity)
         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:
         if not self._running and not self._startup_listener:
             self.start()
             self.start()
         if self.has_returned_state:
         if self.has_returned_state:
@@ -214,10 +220,16 @@ class TuyaLocalDevice(object):
                     self._api.set_socketPersistent(persist)
                     self._api.set_socketPersistent(persist)
 
 
                 if now - last_cache > self._CACHE_TIMEOUT:
                 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:
                 else:
                     await self._hass.async_add_executor_job(
                     await self._hass.async_add_executor_job(
                         self._api.heartbeat,
                         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
 A boolean setting to mark attributes as readonly. If not specified, the
 default is `false`.  If set to `true`, the attributes will be reported
 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`
 ### `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
 It can also be used to match a range of devices that have variations in the extra
 attributes that are sent.
 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`
 ### `mapping`
 
 
 *Optional.*
 *Optional.*

+ 1 - 0
custom_components/tuya_local/diagnostics.py

@@ -75,6 +75,7 @@ def _async_device_as_dict(
         "cached_state": device._cached_state,
         "cached_state": device._cached_state,
         "pending_state": device._pending_updates,
         "pending_state": device._pending_updates,
         "connected": device._running,
         "connected": device._running,
+        "force_dps": device._force_dps,
     }
     }
 
 
     device_registry = dr.async_get(hass)
     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):
     def optional(self):
         return self._config.get("optional", False)
         return self._config.get("optional", False)
 
 
+    @property
+    def force(self):
+        return self._config.get("force", False)
+
     @property
     @property
     def format(self):
     def format(self):
         fmt = self._config.get("format")
         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.device import TuyaLocalDevice
+from custom_components.tuya_local.helpers.device_config import TuyaEntityConfig
+from custom_components.tuya_local.switch import TuyaLocalSwitch
 
 
 from .const import (
 from .const import (
     EUROM_600_HEATER_PAYLOAD,
     EUROM_600_HEATER_PAYLOAD,
@@ -474,7 +476,8 @@ class TestDevice(IsolatedAsyncioTestCase):
         self.subject._startup_listener = None
         self.subject._startup_listener = None
         self.subject.start = Mock()
         self.subject.start = Mock()
         entity = AsyncMock()
         entity = AsyncMock()
-
+        entity._config = Mock()
+        entity._config.dps.return_value = []
         # Call the function under test
         # Call the function under test
         self.subject.register_entity(entity)
         self.subject.register_entity(entity)
 
 
@@ -488,6 +491,8 @@ class TestDevice(IsolatedAsyncioTestCase):
         # Set up preconditions
         # Set up preconditions
         first = AsyncMock()
         first = AsyncMock()
         second = AsyncMock()
         second = AsyncMock()
+        second._config = Mock()
+        second._config.dps.return_value = []
         self.subject._children = [first]
         self.subject._children = [first]
         self.subject._running = True
         self.subject._running = True
         self.subject._startup_listener = None
         self.subject._startup_listener = None
@@ -506,6 +511,8 @@ class TestDevice(IsolatedAsyncioTestCase):
         # Set up preconditions
         # Set up preconditions
         first = AsyncMock()
         first = AsyncMock()
         second = AsyncMock()
         second = AsyncMock()
+        second._config = Mock()
+        second._config.dps.return_value = []
         self.subject._children = [first]
         self.subject._children = [first]
         self.subject._running = False
         self.subject._running = False
         self.subject._startup_listener = Mock()
         self.subject._startup_listener = Mock()