فهرست منبع

device config: handle feature flag dps in entities and mapping lists

Allow feature flag dps to be used to hide mappings and disable
entities.

For entities, this also affects their default hidden state, but this
may depend on startup timing, as HA likely checks this early during
startup, when the device communication may not be available yet. At
least though they will show as Unavailable and the user can hide them,
rather than having a false value that misleads as to that feature working.

Issue #2337
Jason Rumney 1 سال پیش
والد
کامیت
161bce713d

+ 24 - 0
custom_components/tuya_local/devices/README.md

@@ -332,6 +332,18 @@ example, some devices have a "Manual" mode, which is automatically selected
 when adjustments are made to other settings, but should not be available as
 an explicit mode for the user to select.
 
+### `available`
+
+*Optional.*
+
+This works the similarly to `hidden` above, but instead of a boolean
+value, this should be set to the name of an attribute, which returns a
+boolean value, so that the value can be dynamically hidden or shown. A
+typical use is where variants of a device use the same config, and
+have a flag attribute that indicates whether certain features are
+available or not. The mapping will be hidden from the values list when
+the referenced attribute is showing `false`, and shown when it is `true`.
+
 ### `scale`
 
 *Optional, default=1.*
@@ -532,6 +544,18 @@ Note that each condition must specify a `dps_val` to match againt. If you want t
 ```
 
 
+## Generic dps
+
+The following dps may be defined for any entity type. The names should be
+avoided for any extra attribute that is not for the listed purpose.
+
+- **available** (optional, string) a dp name that returns a boolean indicating
+whether the entity should show as available or not (even when it appears to be
+returning valid state). This may be used to disable entities that the device
+indicates it does not support, through a feature flag dp. This should only be
+used when the device is permanently indicating a missing feature, as HA may
+hide the entity if it is marked as unavailable early enough during startup.
+
 ## Entity types
 
 Entities have specific mappings of dp names to functions. Any unrecognized dp name is added to the entity as a read-only extra attribute, so can be observed and queried from HA, but if you need to be able to change it, you should split it into its own entity of an appropriate type (number, select, switch for example).

+ 38 - 15
custom_components/tuya_local/helpers/device_config.py

@@ -289,7 +289,7 @@ class TuyaEntityConfig:
         return self._config.get("class")
 
     def icon(self, device):
-        """Return the icon for this device, with state as given."""
+        """Return the icon for this entity, with state as given."""
         icon = self._config.get("icon", None)
         priority = self._config.get("icon_priority", 100)
 
@@ -317,6 +317,13 @@ class TuyaEntityConfig:
                 return d
         return None
 
+    def available(self, device):
+        """Return whether this entity should be available, with state as given."""
+        avail_dp = self.find_dps("available")
+        if avail_dp and device.has_returned_state:
+            return avail_dp.get_value(device)
+        return True
+
 
 class TuyaDpsConfig:
     """Representation of a dps config."""
@@ -479,6 +486,13 @@ class TuyaDpsConfig:
         settings = self.get_values_to_set(device, value)
         await device.async_set_properties(settings)
 
+    def should_show_mapping(self, mapping, device):
+        """Determine if this mapping should be shown in the list of values."""
+        if "value" not in mapping or mapping.get("hidden", False):
+            return False
+        avail_dp = self._entity.find_dps(mapping.get("available"))
+        return avail_dp.get_value(device) if avail_dp else True
+
     def values(self, device):
         """Return the possible values a dps can take."""
         if "mapping" not in self._config.keys():
@@ -490,29 +504,33 @@ class TuyaDpsConfig:
             return []
         val = []
         for m in self._config["mapping"]:
-            if "value" in m and not m.get("hidden", False):
+            if self.should_show_mapping(m, device):
                 val.append(m["value"])
             # If there is mirroring without override, include mirrored values
             elif "value_mirror" in m:
                 r_dps = self._entity.find_dps(m["value_mirror"])
-                val = val + r_dps.values(device)
+                if r_dps:
+                    val = val + r_dps.values(device)
             for c in m.get("conditions", {}):
-                if "value" in c and not c.get("hidden", False):
+                if self.should_show_mapping(c, device):
                     val.append(c["value"])
                 elif "value_mirror" in c:
                     r_dps = self._entity.find_dps(c["value_mirror"])
-                    val = val + r_dps.values(device)
+                    if r_dps:
+                        val = val + r_dps.values(device)
 
             cond = self._active_condition(m, device)
             if cond and "mapping" in cond:
                 _LOGGER.debug("Considering conditional mappings")
                 c_val = []
                 for m2 in cond["mapping"]:
-                    if "value" in m2 and not m2.get("hidden", False):
+                    if self.should_show_mapping(m2, device):
                         c_val.append(m2["value"])
+
                     elif "value_mirror" in m:
                         r_dps = self._entity.find_dps(m["value_mirror"])
-                        c_val = c_val + r_dps.values(device)
+                        if r_dps:
+                            c_val = c_val + r_dps.values(device)
                 # if given, the conditional mapping is an override
                 if c_val:
                     _LOGGER.debug(
@@ -696,10 +714,12 @@ class TuyaDpsConfig:
             if redirect:
                 _LOGGER.debug("Redirecting %s to %s", self.name, redirect)
                 r_dps = self._entity.find_dps(redirect)
-                return r_dps.get_value(device)
+                if r_dps:
+                    return r_dps.get_value(device)
             if mirror:
                 r_dps = self._entity.find_dps(mirror)
-                return r_dps.get_value(device)
+                if r_dps:
+                    return r_dps.get_value(device)
 
             if invert and isinstance(result, Number):
                 r = self._config.get("range")
@@ -768,7 +788,7 @@ class TuyaDpsConfig:
 
             if "value" not in m and "value_mirror" in m:
                 r_dps = self._entity.find_dps(m["value_mirror"])
-                if str(r_dps.get_value(device)) == str(value):
+                if r_dps and str(r_dps.get_value(device)) == str(value):
                     return m
 
             for c in m.get("conditions", {}):
@@ -776,7 +796,7 @@ class TuyaDpsConfig:
                     c_dp = self._entity.find_dps(m.get("constraint", self.name))
                     # only consider the condition a match if we can change
                     # the dp to match, or it already matches
-                    if (c_dp.id != self.id and not c_dp.readonly) or (
+                    if (c_dp and c_dp.id != self.id and not c_dp.readonly) or (
                         _equal_or_in(
                             device.get_property(c_dp.id),
                             c.get("dps_val"),
@@ -785,7 +805,7 @@ class TuyaDpsConfig:
                         return m
                 if "value" not in c and "value_mirror" in c:
                     r_dps = self._entity.find_dps(c["value_mirror"])
-                    if str(r_dps.get_value(device)) == str(value):
+                    if r_dps and str(r_dps.get_value(device)) == str(value):
                         return m
         if nearest:
             return nearest
@@ -856,7 +876,9 @@ class TuyaDpsConfig:
                 if cval is None:
                     r_dps = cond.get("value_mirror")
                     if r_dps:
-                        cval = self._entity.find_dps(r_dps).get_value(device)
+                        mirror = self._entity.find_dps(r_dps)
+                        if mirror:
+                            cval = mirror.get_value(device)
 
                 if cval == value:
                     c_dps = self._entity.find_dps(mapping.get("constraint", self.name))
@@ -864,7 +886,7 @@ class TuyaDpsConfig:
                     single_match = isinstance(cond_dpsval, str) or (
                         not isinstance(cond_dpsval, Sequence)
                     )
-                    if c_dps.id != self.id and single_match:
+                    if c_dps and c_dps.id != self.id and single_match:
                         c_val = c_dps._map_from_dps(
                             cond.get("dps_val", device.get_property(c_dps.id)),
                             device,
@@ -883,7 +905,8 @@ class TuyaDpsConfig:
             if redirect:
                 _LOGGER.debug("Redirecting %s to %s", self.name, redirect)
                 r_dps = self._entity.find_dps(redirect)
-                return r_dps.get_values_to_set(device, value)
+                if r_dps:
+                    return r_dps.get_values_to_set(device, value)
 
             if scale != 1 and isinstance(result, Number):
                 _LOGGER.debug("Scaling %s by %s", result, scale)

+ 2 - 2
custom_components/tuya_local/helpers/mixin.py

@@ -37,7 +37,7 @@ class TuyaLocalEntity:
 
     @property
     def available(self):
-        return self._device.has_returned_state
+        return self._device.has_returned_state and self._config.available(self._device)
 
     @property
     def has_entity_name(self):
@@ -99,7 +99,7 @@ class TuyaLocalEntity:
     @property
     def entity_registry_enabled_default(self):
         """Disable deprecated entities on new installations"""
-        return not self._config.deprecated
+        return not self._config.deprecated and self._config.available(self._device)
 
     async def async_update(self):
         await self._device.async_refresh()