Преглед на файлове

Snap numeric values to the nearest mapping.

Some configs have been made with numeric values with mappings, to mtch
specific devices.  If step was set and the mapped values evenly
spaced, this works for things like temperature where step is used by
HA.  But in the general case, it does not work.  Some of the existing
configs for light brightness with fixed steps assumed it works in the
general case, so were broken.

- Aspen ADV200: map brightnesses to 20%, 50&, 100% per the manual,
  instead of evenly space 33%, 66%, 100%.
- Aspen tests: as this light does not have an off setting, modify the
  tests to allow that, instead of relying on garbage in-garbage out,
  which no longer works with it snapping to nearest valid value.

- Heatstorm HS6000GC: invert the child lock per the device behaviour

Issue #510
Jason Rumney преди 2 години
родител
ревизия
50093c4a55

+ 2 - 2
custom_components/tuya_local/devices/aspen_asp200_fan.yaml

@@ -68,8 +68,8 @@ secondary_entities:
         name: brightness
         mapping:
           - dps_val: 1
-            value: 85
+            value: 51
           - dps_val: 2
-            value: 170
+            value: 128
           - dps_val: 3
             value: 255

+ 5 - 0
custom_components/tuya_local/devices/heatstorm_hs6000gc_heater.yaml

@@ -95,6 +95,11 @@ secondary_entities:
       - id: 7
         type: boolean
         name: lock
+        mapping:
+          - dps_val: true
+            value: false
+          - dps_val: false
+            value: true
   - entity: binary_sensor
     name: Fault
     class: problem

+ 22 - 7
custom_components/tuya_local/helpers/device_config.py

@@ -6,6 +6,7 @@ from base64 import b64decode, b64encode
 from collections.abc import Sequence
 from fnmatch import fnmatch
 import logging
+from numbers import Number
 from os import walk
 from os.path import join, dirname, splitext, exists
 
@@ -23,7 +24,7 @@ def _typematch(type, value):
         return False
 
     # Allow integers to pass as floats.
-    if type is float and isinstance(value, int):
+    if type is float and isinstance(value, Number):
         return True
 
     if isinstance(value, type):
@@ -619,13 +620,13 @@ class TuyaDpsConfig:
                 r_dps = self._entity.find_dps(mirror)
                 return r_dps.get_value(device)
 
-            if invert and isinstance(result, (int, float)):
+            if invert and isinstance(result, Number):
                 r = self._config.get("range")
                 if r and "min" in r and "max" in r:
                     result = -1 * result + r["min"] + r["max"]
                     replaced = True
 
-            if scale != 1 and isinstance(result, (int, float)):
+            if scale != 1 and isinstance(result, Number):
                 result = result / scale
                 replaced = True
 
@@ -642,11 +643,23 @@ class TuyaDpsConfig:
 
     def _find_map_for_value(self, value, device):
         default = None
+        nearest = None
+        distance = float("inf")
         for m in self._config.get("mapping", {}):
             if "dps_val" not in m:
                 default = m
             if "value" in m and str(m["value"]) == str(value):
                 return m
+            if (
+                "value" in m
+                and isinstance(m["value"], Number)
+                and isinstance(value, Number)
+            ):
+                d = abs(m["value"] - value)
+                if d < distance:
+                    distance = d
+                    nearest = m
+
             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):
@@ -668,6 +681,8 @@ class TuyaDpsConfig:
                     r_dps = self._entity.find_dps(c["value_mirror"])
                     if str(r_dps.get_value(device)) == str(value):
                         return m
+        if nearest:
+            return nearest
         return default
 
     def _active_condition(self, mapping, device, value=None):
@@ -711,7 +726,7 @@ class TuyaDpsConfig:
             invert = mapping.get("invert", False)
 
             step = mapping.get("step")
-            if not isinstance(step, (int, float)):
+            if not isinstance(step, Number):
                 step = None
             if "dps_val" in mapping:
                 result = mapping["dps_val"]
@@ -751,7 +766,7 @@ class TuyaDpsConfig:
                 r_dps = self._entity.find_dps(redirect)
                 return r_dps.get_values_to_set(device, value)
 
-            if scale != 1 and isinstance(result, (int, float)):
+            if scale != 1 and isinstance(result, Number):
                 _LOGGER.debug("Scaling %s by %s", result, scale)
                 result = result * scale
                 remap = self._find_map_for_value(result, device)
@@ -765,7 +780,7 @@ class TuyaDpsConfig:
                     result = -1 * result + r["min"] + r["max"]
                     replaced = True
 
-            if step and isinstance(result, (int, float)):
+            if step and isinstance(result, Number):
                 _LOGGER.debug("Stepping %s to %s", result, step)
                 result = step * round(float(result) / step)
                 remap = self._find_map_for_value(result, device)
@@ -783,7 +798,7 @@ class TuyaDpsConfig:
                 )
 
         r = self.range(device, scaled=False)
-        if r and isinstance(result, (int, float)):
+        if r and isinstance(result, Number):
             min = r["min"]
             max = r["max"]
             if result < min or result > max:

+ 4 - 3
tests/devices/test_aspen_adv200_fan.py

@@ -38,12 +38,13 @@ class TestAspenASP200Fan(
         self.setUpDimmableLight(
             LIGHT_DPS,
             self.entities.get("light_display"),
-            offval=0,
+            offval=1,
             tests=[
-                (1, 85),
-                (2, 170),
+                (1, 51),
+                (2, 128),
                 (3, 255),
             ],
+            no_off=True,
         )
         self.setUpSwitchable(SWITCH_DPS, self.subject)
         self.setUpTargetTemperature(

+ 11 - 2
tests/mixins/light.py

@@ -170,15 +170,24 @@ class MultiLightTests:
 
 
 class DimmableLightTests:
-    def setUpDimmableLight(self, dps, subject, offval=0, tests=[(100, 100)]):
+    def setUpDimmableLight(
+        self,
+        dps,
+        subject,
+        offval=0,
+        tests=[(100, 100)],
+        no_off=False,
+    ):
         self.dimmableLight = subject
         self.dimmableLightDps = dps
         self.dimmableLightOff = offval
         self.dimmableLightTest = tests
+        self.dimmableLightNoOff = no_off
 
     def test_dimmable_light_brightness(self):
         self.dps[self.dimmableLightDps] = self.dimmableLightOff
-        self.assertEqual(self.dimmableLight.brightness, 0)
+        if not self.dimmableLightNoOff:
+            self.assertEqual(self.dimmableLight.brightness, 0)
         for dps, val in self.dimmableLightTest:
             self.dps[self.dimmableLightDps] = dps
             self.assertEqual(self.dimmableLight.brightness, val)