Parcourir la source

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 il y a 3 ans
Parent
commit
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`
 ### `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`
 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`
 ### `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.
 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.
 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 base64 import b64decode, b64encode
 
 
+from collections.abc import Sequence
 from fnmatch import fnmatch
 from fnmatch import fnmatch
 import logging
 import logging
 from os import walk
 from os import walk
@@ -61,7 +62,7 @@ _signed_fmts = {
 
 
 
 
 def _bytes_to_fmt(bytes, signed=False):
 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
     fmt = _signed_fmts if signed else _unsigned_fmts
 
 
     if bytes in fmt:
     if bytes in fmt:
@@ -70,6 +71,14 @@ def _bytes_to_fmt(bytes, signed=False):
         return f"{bytes}s"
         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:
 class TuyaDeviceConfig:
     """Representation of a device config for Tuya Local devices."""
     """Representation of a device config for Tuya Local devices."""
 
 
@@ -501,7 +510,7 @@ class TuyaDpsConfig:
             step = mapping.get("step", 1)
             step = mapping.get("step", 1)
             cond = self._active_condition(mapping, device)
             cond = self._active_condition(mapping, device)
             if cond:
             if cond:
-                constraint = mapping.get("constraint")
+                constraint = mapping.get("constraint", self.name)
                 _LOGGER.debug("Considering condition on %s", constraint)
                 _LOGGER.debug("Considering condition on %s", constraint)
                 step = cond.get("step", step)
                 step = cond.get("step", step)
         if step != 1 or scale != 1:
         if step != 1 or scale != 1:
@@ -640,11 +649,14 @@ class TuyaDpsConfig:
 
 
             for c in m.get("conditions", {}):
             for c in m.get("conditions", {}):
                 if "value" in c and str(c["value"]) == str(value):
                 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
                     # only consider the condition a match if we can change
                     # the dp to match, or it already matches
                     # 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
                         return m
                 if "value" not in c and "value_mirror" in c:
                 if "value" not in c and "value_mirror" in c:
@@ -654,14 +666,14 @@ class TuyaDpsConfig:
         return default
         return default
 
 
     def _active_condition(self, mapping, device, value=None):
     def _active_condition(self, mapping, device, value=None):
-        constraint = mapping.get("constraint")
+        constraint = mapping.get("constraint", self.name)
         conditions = mapping.get("conditions")
         conditions = mapping.get("conditions")
         c_match = None
         c_match = None
         if constraint and conditions:
         if constraint and conditions:
             c_dps = self._entity.find_dps(constraint)
             c_dps = self._entity.find_dps(constraint)
             c_val = None if c_dps is None else device.get_property(c_dps.id)
             c_val = None if c_dps is None else device.get_property(c_dps.id)
             for cond in conditions:
             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
                     c_match = cond
                 # Case where matching None, need extra checks to ensure we
                 # Case where matching None, need extra checks to ensure we
                 # are not just defaulting and it is really a match
                 # 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)
                         cval = self._entity.find_dps(r_dps).get_value(device)
 
 
                 if cval == value:
                 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
                 # Allow simple conditional mapping overrides
                 for m in cond.get("mapping", {}):
                 for m in cond.get("mapping", {}):