Sfoglia il codice sorgente

Improve handling of scaled ranges.

Add handling of min and max temperatures for devices with separate low and high controls.  May improve behaviour of Inkbird ITC306A as reported on Issue #19.
Jason Rumney 4 anni fa
parent
commit
a2b7d51bf3

+ 20 - 0
custom_components/tuya_local/devices/inkbird_itc306a_thermostat.yaml

@@ -37,6 +37,16 @@ primary_entity:
       name: target_temp_low
       mapping:
         - scale: 10
+          constraint: temperature_unit
+          conditions:
+            - dps_val: "C"
+              range:
+                min: 200
+                max: 350
+            - dps_val: "F"
+              range:
+                min: 680
+                max: 950
     - id: 108
       type: integer
       name: heat_time_alarm_threshold_hours
@@ -76,6 +86,16 @@ primary_entity:
       name: target_temp_high
       mapping:
         - scale: 10
+          constraint: temperature_unit
+          conditions:
+            - dps_val: "C"
+              range:
+                min: 200
+                max: 350
+            - dps_val: "F"
+              range:
+                min: 680
+                max: 950
     - id: 115
       type: boolean
       name: switch_state

+ 10 - 4
custom_components/tuya_local/generic/climate.py

@@ -196,16 +196,22 @@ class TuyaLocalClimate(ClimateEntity):
     def min_temp(self):
         """Return the minimum supported target temperature."""
         if self._temperature_dps is None:
-            return None
-        r = self._temperature_dps.range(self._device)
+            if self._temp_low_dps is None:
+                return None
+            r = self._temp_low_dps.range(self._device)
+        else:
+            r = self._temperature_dps.range(self._device)
         return DEFAULT_MIN_TEMP if r is None else r["min"]
 
     @property
     def max_temp(self):
         """Return the maximum supported target temperature."""
         if self._temperature_dps is None:
-            return None
-        r = self._temperature_dps.range(self._device)
+            if self._temp_high_dps is None:
+                return None
+            r = self._temp_high_dps.range(self._device)
+        else:
+            r = self._temperature_dps.range(self._device)
         return DEFAULT_MAX_TEMP if r is None else r["max"]
 
     async def async_set_temperature(self, **kwargs):

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

@@ -32,6 +32,13 @@ def _typematch(type, value):
     return False
 
 
+def _scale_range(r, s):
+    "Scale range r by factor s"
+    if s == 1:
+        return r
+    return {"min": r["min"] / s, "max": r["max"] / s}
+
+
 class TuyaDeviceConfig:
     """Representation of a device config for Tuya Local devices."""
 
@@ -279,26 +286,31 @@ class TuyaDpsConfig:
         _LOGGER.debug(f"{self.name} values: {val}")
         return list(set(val)) if val else None
 
-    def range(self, device):
+    def range(self, device, scaled=True):
         """Return the range for this dps if configured."""
         mapping = self._find_map_for_dps(device.get_property(self.id))
+        scale = 1
         if mapping:
             _LOGGER.debug(f"Considering mapping for range of {self.name}")
+            if scaled:
+                scale = mapping.get("scale", scale)
             cond = self._active_condition(mapping, device)
             if cond:
                 constraint = mapping.get("constraint")
+                if scaled:
+                    scale = mapping.get("scale", scale)
                 _LOGGER.debug(f"Considering condition on {constraint}")
             r = None if cond is None else cond.get("range")
             if r and "min" in r and "max" in r:
                 _LOGGER.info(f"Conditional range returned for {self.name}")
-                return r
+                return _scale_range(r, scale)
             r = mapping.get("range")
             if r and "min" in r and "max" in r:
                 _LOGGER.info(f"Mapped range returned for {self.name}")
-                return r
+                return _scale_range(r, scale)
         r = self._config.get("range")
         if r and "min" in r and "max" in r:
-            return r
+            return _scale_range(r, scale)
         else:
             return None
 
@@ -498,13 +510,17 @@ class TuyaDpsConfig:
                     value,
                 )
 
-        r = self.range(device)
+        r = self.range(device, scaled=False)
         if r:
             minimum = r["min"]
             maximum = r["max"]
             if result < minimum or result > maximum:
+                # Output scaled values in the error message
+                r = self.range(device, scaled=True)
+                minimum = r["min"]
+                maximum = r["max"]
                 raise ValueError(
-                    f"{self.name} ({result}) must be between {minimum} and {maximum}"
+                    f"{self.name} ({value}) must be between {minimum} and {maximum}"
                 )
 
         if self.type is int:

+ 7 - 7
tests/devices/test_hellnar_heatpump.py

@@ -63,15 +63,15 @@ class TestHellnarHeatpump(TuyaDeviceTestCase):
 
     def test_minimum_target_temperature(self):
         self.dps[HVACMODE_DPS] = "cold"
-        self.assertEqual(self.subject.min_temp, 170)
+        self.assertEqual(self.subject.min_temp, 17.0)
         self.dps[HVACMODE_DPS] = "hot"
-        self.assertEqual(self.subject.min_temp, 0)
+        self.assertEqual(self.subject.min_temp, 0.0)
 
     def test_maximum_target_temperature(self):
         self.dps[HVACMODE_DPS] = "cold"
-        self.assertEqual(self.subject.max_temp, 300)
+        self.assertEqual(self.subject.max_temp, 30.0)
         self.dps[HVACMODE_DPS] = "hot"
-        self.assertEqual(self.subject.max_temp, 300)
+        self.assertEqual(self.subject.max_temp, 30.0)
 
     async def test_legacy_set_temperature_with_temperature(self):
         self.dps[HVACMODE_DPS] = "auto"
@@ -96,15 +96,15 @@ class TestHellnarHeatpump(TuyaDeviceTestCase):
     async def test_set_target_temperature_fails_outside_valid_range(self):
         self.dps[HVACMODE_DPS] = "cold"
         with self.assertRaisesRegex(
-            ValueError, "temperature \\(150\\) must be between 170 and 300"
+            ValueError, "temperature \\(15\\) must be between 17.0 and 30.0"
         ):
             await self.subject.async_set_target_temperature(15)
 
         self.dps[HVACMODE_DPS] = "hot"
         with self.assertRaisesRegex(
-            ValueError, "temperature \\(330\\) must be between 0 and 300"
+            ValueError, "temperature \\(31\\) must be between 0.0 and 30.0"
         ):
-            await self.subject.async_set_target_temperature(33)
+            await self.subject.async_set_target_temperature(31)
 
     def test_current_temperature(self):
         self.dps[CURRENTTEMP_DPS] = 25

+ 29 - 0
tests/devices/test_inkbird_itc306a_thermostat.py

@@ -133,6 +133,18 @@ class TestInkbirdThermostat(TuyaDeviceTestCase):
         self.dps[UNIT_DPS] = "C"
         self.assertEqual(self.subject.temperature_unit, TEMP_CELSIUS)
 
+    def test_minimum_target_temperature(self):
+        self.dps[UNIT_DPS] = "C"
+        self.assertEqual(self.subject.min_temp, 20.0)
+        self.dps[UNIT_DPS] = "F"
+        self.assertEqual(self.subject.min_temp, 68.0)
+
+    def test_maximum_target_temperature(self):
+        self.dps[UNIT_DPS] = "C"
+        self.assertEqual(self.subject.max_temp, 35.0)
+        self.dps[UNIT_DPS] = "F"
+        self.assertEqual(self.subject.max_temp, 95.0)
+
     def test_temperature_range(self):
         self.dps[TEMPHIGH_DPS] = 301
         self.dps[TEMPLOW_DPS] = 255
@@ -151,6 +163,23 @@ class TestInkbirdThermostat(TuyaDeviceTestCase):
                 target_temp_high=32.2, target_temp_low=26.6
             )
 
+    async def test_set_target_temperature_fails_outside_valid_range(self):
+        self.dps[UNIT_DPS] = "C"
+        with self.assertRaisesRegex(
+            ValueError, "target_temp_low \\(19.9\\) must be between 20.0 and 35.0"
+        ):
+            await self.subject.async_set_temperature(
+                target_temp_high=32.2, target_temp_low=19.9
+            )
+
+        self.dps[UNIT_DPS] = "F"
+        with self.assertRaisesRegex(
+            ValueError, "target_temp_high \\(95.1\\) must be between 68.0 and 95.0"
+        ):
+            await self.subject.async_set_temperature(
+                target_temp_low=70.0, target_temp_high=95.1
+            )
+
     def test_device_state_attributes(self):
         self.dps[ERROR_DPS] = 1
         self.dps[CALIBRATE_DPS] = 1