Skip to content

Commit

Permalink
device config: handle feature flag dps in entities and mapping lists
Browse files Browse the repository at this point in the history
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
  • Loading branch information
make-all committed Sep 28, 2024
1 parent 4793c15 commit 161bce7
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 17 deletions.
24 changes: 24 additions & 0 deletions custom_components/tuya_local/devices/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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).
Expand Down
53 changes: 38 additions & 15 deletions custom_components/tuya_local/helpers/device_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ def device_class(self):
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)

Expand Down Expand Up @@ -317,6 +317,13 @@ def find_dps(self, name):
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."""
Expand Down Expand Up @@ -479,6 +486,13 @@ async def async_set_value(self, device, value):
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():
Expand All @@ -490,29 +504,33 @@ def values(self, device):
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(
Expand Down Expand Up @@ -696,10 +714,12 @@ def _map_from_dps(self, val, device):
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")
Expand Down Expand Up @@ -768,15 +788,15 @@ def _find_map_for_value(self, value, device):

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", {}):
if "value" in c and str(c["value"]) == str(value):
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"),
Expand All @@ -785,7 +805,7 @@ def _find_map_for_value(self, value, device):
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
Expand Down Expand Up @@ -856,15 +876,17 @@ def get_values_to_set(self, device, value):
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))
cond_dpsval = cond.get("dps_val")
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,
Expand All @@ -883,7 +905,8 @@ def get_values_to_set(self, device, value):
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)
Expand Down
4 changes: 2 additions & 2 deletions custom_components/tuya_local/helpers/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def should_poll(self):

@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):
Expand Down Expand Up @@ -99,7 +99,7 @@ def extra_state_attributes(self):
@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()
Expand Down

0 comments on commit 161bce7

Please sign in to comment.