Просмотр исходного кода

conditions: default constraint to current dp, allow list dps_val

- when constraint is unspecified, match against the current attribute.
  This is mainly only useful to provide distinct mappings depending on
  value (eg to handle multiple devices in a single config)
- allow dps_val in a condition to match multiple values by specifying
  a list.  This makes for some more concise conditional mappings.

The purpose of these changes to to make it easier to make a single
config cover multiple devices following a common layout.  Part of what is
forcing separate configs currently is that although they follow a
common dps layout, they use different ways of representing enum mappings.
Jason Rumney 3 лет назад
Родитель
Сommit
bcb91eec30

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

@@ -383,29 +383,20 @@ pick a defaulttone to use to turn on the siren.
 
 ### `constraint`
 
-*Optional, always paired with `conditions`.*
+*Optional, always paired with `conditions`.  Default if unspecified is the current attribute*
 
 If a rule depends on an attribute other than the current one, then `constraint`
-can be used to specify the element that `conditions` applies to.
+can be used to specify the element that `conditions` applies to.  `constraint` can also refer back to the same attribute - this can be useful for specifying conditional mappings, for example to support two different variants of a device in a single config file, where the only difference is the way they represent enum attributes.
 
 ### `conditions`
 
-*Optional, always paired with `constraint.`*
+*Optional, usually paired with `constraint.`*
 
-Conditions defines a list of rules that are applied based on the `constraint`
-attribute. The contents are the same as Mapping Rules, but `dps_val` applies
-to the attribute specified by `constraint`. All others act on the current
-attribute as they would in the mapping.  Although conditions are specified
-within a mapping, they can also contain a `mapping` of their own to override
-that mapping.  These nested mappings are limited to simple `dps_val` to `value`
-substitutions, as more complex rules would quickly become too complex to
-manage.
+Conditions defines a list of rules that are applied based on the `constraint` attribute. The contents are the same as Mapping Rules, but `dps_val` applies to the attribute specified by `constraint`, and also can be a list of values to match as well rather than a single value.  All others act on the current attribute as they would in the mapping.  Although conditions are specified within a mapping, they can also contain a `mapping` of their own to override that mapping.  These nested mappings are limited to simple `dps_val` to `value` substitutions, as more complex rules would quickly become too complex to manage.
 
 When setting a dp which has conditions attached, the behaviour is slightly different depending on whether the constraint dp is readonly or not.
 
-For non-readonly constraints, the constraint dp will be set along with the
-target dp so that the first condition with a value matching the target value
-is met.
+For non-readonly constraints that specify a single dps_val, the constraint dp will be set along with the target dp so that the first condition with a value matching the target value is met.
 
 For readonly constraints, the condition must match the constraint dp's current value for anything to be set.
 

+ 29 - 12
custom_components/tuya_local/helpers/device_config.py

@@ -3,6 +3,7 @@ Config parser for Tuya Local devices.
 """
 from base64 import b64decode, b64encode
 
+from collections.abc import Sequence
 from fnmatch import fnmatch
 import logging
 from os import walk
@@ -61,7 +62,7 @@ _signed_fmts = {
 
 
 def _bytes_to_fmt(bytes, signed=False):
-    "Convert a byte count to an unpack format."
+    """Convert a byte count to an unpack format."""
     fmt = _signed_fmts if signed else _unsigned_fmts
 
     if bytes in fmt:
@@ -70,6 +71,14 @@ def _bytes_to_fmt(bytes, signed=False):
         return f"{bytes}s"
 
 
+def _equal_or_in(value1, values2):
+    """Return true if value1 is the same as values2, or appears in values2."""
+    if type(values2) is not str and isinstance(values2, Sequence):
+        return value1 in values2
+    else:
+        return value1 == values2
+
+
 class TuyaDeviceConfig:
     """Representation of a device config for Tuya Local devices."""
 
@@ -501,7 +510,7 @@ class TuyaDpsConfig:
             step = mapping.get("step", 1)
             cond = self._active_condition(mapping, device)
             if cond:
-                constraint = mapping.get("constraint")
+                constraint = mapping.get("constraint", self.name)
                 _LOGGER.debug("Considering condition on %s", constraint)
                 step = cond.get("step", step)
         if step != 1 or scale != 1:
@@ -640,11 +649,14 @@ class TuyaDpsConfig:
 
             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"))
+                    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 not c_dp.readonly or (
-                        device.get_property(c_dp.id) == c.get("dps_val")
+                    if (c_dp.id != self.id and not c_dp.readonly) or (
+                        _equal_or_in(
+                            device.get_property(c_dp.id),
+                            c.get("dps_val"),
+                        )
                     ):
                         return m
                 if "value" not in c and "value_mirror" in c:
@@ -654,14 +666,14 @@ class TuyaDpsConfig:
         return default
 
     def _active_condition(self, mapping, device, value=None):
-        constraint = mapping.get("constraint")
+        constraint = mapping.get("constraint", self.name)
         conditions = mapping.get("conditions")
         c_match = None
         if constraint and conditions:
             c_dps = self._entity.find_dps(constraint)
             c_val = None if c_dps is None else device.get_property(c_dps.id)
             for cond in conditions:
-                if c_val is not None and c_val == cond.get("dps_val"):
+                if c_val is not None and (_equal_or_in(c_val, cond.get("dps_val"))):
                     c_match = cond
                 # Case where matching None, need extra checks to ensure we
                 # are not just defaulting and it is really a match
@@ -709,12 +721,17 @@ class TuyaDpsConfig:
                         cval = self._entity.find_dps(r_dps).get_value(device)
 
                 if cval == value:
-                    c_dps = self._entity.find_dps(mapping["constraint"])
-                    c_val = c_dps._map_from_dps(
-                        cond.get("dps_val", device.get_property(c_dps.id)),
-                        device,
+                    c_dps = self._entity.find_dps(mapping.get("constraint", self.name))
+                    cond_dpsval = cond.get("dps_val")
+                    single_match = type(cond_dpsval) == str or (
+                        not isinstance(cond_dpsval, Sequence)
                     )
-                    dps_map.update(c_dps.get_values_to_set(device, c_val))
+                    if 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,
+                        )
+                        dps_map.update(c_dps.get_values_to_set(device, c_val))
 
                 # Allow simple conditional mapping overrides
                 for m in cond.get("mapping", {}):