4
0
Эх сурвалжийг харах

Improvements to conditional behaviour to cover more functionality.

Another step towards fully generic components for the original Goldair devices.
Jason Rumney 4 жил өмнө
parent
commit
1569b673a3

+ 8 - 0
custom_components/tuya_local/devices/goldair_dehumidifier.yaml

@@ -141,6 +141,14 @@ secondary_entities:
           max: 80
         mapping:
           - step: 5
+            constraint: preset_mode
+            conditions:
+              - dps_val: "1"
+                invalid: true
+              - dps_val: "2"
+                invalid: true
+              - dps_val: "3"
+                invalid: true
       - id: 5
         type: boolean
         name: air_clean_on

+ 3 - 5
custom_components/tuya_local/devices/goldair_fan.yaml

@@ -17,11 +17,9 @@ primary_entity:
           constraint: preset_mode
           conditions:
             - dps_val: nature
-              mapping:
-                - step: 4
+              step: 4
             - dps_val: sleep
-              mapping:
-                - step: 4
+              step: 4
     - id: 3
       type: string
       mapping:
@@ -57,7 +55,7 @@ secondary_entities:
           min: 1
           max: 12
         mapping:
-          - constraint: present_mode
+          - constraint: preset_mode
             conditions:
               - dps_val: nature
                 step: 4

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

@@ -272,8 +272,17 @@ class TuyaDpsConfig:
         scale = 1
         mapping = self._find_map_for_dps(device.get_property(self.id))
         if mapping is not None:
+            _LOGGER.debug(f"Considering mapping for step of {self.name}")
             step = mapping.get("step", 1)
             scale = mapping.get("scale", 1)
+            cond = self._active_condition(mapping, device)
+            if cond is not None:
+                constraint = mapping.get("constraint")
+                _LOGGER.debug(f"Considering condition on {constraint}")
+                step = cond.get("step", step)
+                scale = cond.get("scale", scale)
+        if step != 1 or scale != 1:
+            _LOGGER.info(f"Step for {self.name} is {step} with scale {scale}")
         return step / scale
 
     @property
@@ -314,9 +323,16 @@ class TuyaDpsConfig:
             result = mapping.get("value", result)
             cond = self._active_condition(mapping, device)
             if cond is not None:
+                if cond.get("invalid", False):
+                    return None
                 replaced = replaced or "value" in cond
                 result = cond.get("value", result)
                 scale = cond.get("scale", scale)
+                if "mapping" in cond:
+                    for m in cond["mapping"]:
+                        if str(m.get("dps_val")) == str(result):
+                            replaced = "value" in m
+                            result = m.get("value", result)
 
             if scale != 1 and isinstance(result, (int, float)):
                 result = result / scale
@@ -378,9 +394,12 @@ class TuyaDpsConfig:
                 replaced = True
             # Conditions may have side effect of setting another value.
             cond = self._active_condition(mapping, device)
-            if cond is not None and cond.get("value") == value:
-                c_dps = self._entity.find_dps(mapping["constraint"])
-                dps_map.update(c_dps.get_values_to_set(device, cond["dps_val"]))
+            if cond is not None:
+                if cond.get("value") == value:
+                    c_dps = self._entity.find_dps(mapping["constraint"])
+                    dps_map.update(c_dps.get_values_to_set(device, cond["dps_val"]))
+                scale = cond.get("scale", scale)
+                step = cond.get("step", step)
 
             if scale != 1 and isinstance(result, (int, float)):
                 _LOGGER.debug(f"Scaling {result} by {scale}")

+ 8 - 0
tests/const.py

@@ -143,3 +143,11 @@ INKBIRD_THERMOSTAT_PAYLOAD = {
     "119": False,
     "120": False,
 }
+
+ANKO_FAN_PAYLOAD = {
+    "1": True,
+    "2": "normal",
+    "3": 1,
+    "4": "off",
+    "6": 0,
+}

+ 37 - 41
tests/devices/test_goldair_dehumidifier.py

@@ -10,9 +10,8 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_HUMIDITY,
 )
-from homeassistant.components.humidifier.const import SUPPORT_MODES
 from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
-from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
+from homeassistant.const import STATE_UNAVAILABLE
 
 from custom_components.tuya_local.generic.climate import TuyaLocalClimate
 from custom_components.tuya_local.generic.fan import TuyaLocalFan
@@ -210,7 +209,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
 
         self.assertEqual(self.humidifier.target_humidity, 45)
 
-    @skip("Conditions not supported yet")
     def test_target_humidity_outside_normal_preset(self):
         self.dps[HUMIDITY_DPS] = 55
 
@@ -223,9 +221,9 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.assertIs(self.subject.target_humidity, None)
 
-        self.dps[PRESET_DPS] = PRESET_NORMAL
-        self.dps[AIRCLEAN_DPS] = True
-        self.assertIs(self.subject.target_humidity, None)
+        # self.dps[PRESET_DPS] = PRESET_NORMAL
+        # self.dps[AIRCLEAN_DPS] = True
+        # self.assertIs(self.subject.target_humidity, None)
 
     async def test_set_target_humidity_in_normal_preset_rounds_up_to_5_percent(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
@@ -260,38 +258,37 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         ):
             await self.humidifier.async_set_humidity(42)
 
-    @skip("Conditions not supported yet")
     async def test_set_target_humidity_raises_error_outside_of_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_LOW
         with self.assertRaisesRegex(
-            AttributeError, "target_humidity cannot be set at this time"
+            AttributeError, "humidity cannot be set at this time"
         ):
             await self.subject.async_set_humidity(50)
 
         self.dps[PRESET_DPS] = PRESET_HIGH
         with self.assertRaisesRegex(
-            AttributeError, "target_humidity cannot be set at this time"
+            AttributeError, "humidity cannot be set at this time"
         ):
             await self.subject.async_set_humidity(50)
 
         self.dps[PRESET_DPS] = PRESET_LOW
         with self.assertRaisesRegex(
-            AttributeError, "target_humidity cannot be set at this time"
+            AttributeError, "humidity cannot be set at this time"
         ):
             await self.subject.async_set_humidity(50)
 
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         with self.assertRaisesRegex(
-            AttributeError, "target_humidity cannot be set at this time"
+            AttributeError, "humidity cannot be set at this time"
         ):
             await self.subject.async_set_humidity(50)
 
-        self.dps[PRESET_DPS] = PRESET_NORMAL
-        self.dps[AIRCLEAN_DPS] = True
-        with self.assertRaisesRegex(
-            AttributeError, "target_humidity cannot be set at this time"
-        ):
-            await self.subject.async_set_humidity(50)
+        # self.dps[PRESET_DPS] = PRESET_NORMAL
+        # self.dps[AIRCLEAN_DPS] = True
+        # with self.assertRaisesRegex(
+        #     AttributeError, "humidity cannot be set at this time"
+        # ):
+        #     await self.subject.async_set_humidity(50)
 
     def test_temperature_unit_returns_device_temperature_unit(self):
         self.assertEqual(
@@ -374,14 +371,13 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.assertEqual(self.subject.preset_mode, None)
         self.assertEqual(self.humidifier.mode, None)
 
-    @skip("Conditions not supported yet")
+    @skip("Conditions not included in config")
     def test_air_clean_is_surfaced_in_preset_mode(self):
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[AIRCLEAN_DPS] = True
 
         self.assertEqual(self.subject.preset_mode, "Air clean")
 
-    @skip("Conditions not supported yet")
     def test_preset_modes(self):
         self.assertCountEqual(
             self.subject.preset_modes,
@@ -390,7 +386,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
                 "Low",
                 "High",
                 "Dry clothes",
-                "Air clean",
+                #                "Air clean",
             ],
         )
 
@@ -404,7 +400,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
             await self.subject.async_set_preset_mode("Normal")
             self.subject._device.anticipate_property_value.assert_not_called()
 
-    @skip("Conditions not supported yet")
+    @skip("Conditions not included in config")
     async def test_set_preset_mode_to_low(self):
         async with assert_device_properties_set(
             self.subject._device,
@@ -417,7 +413,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
                 FANMODE_DPS, "1"
             )
 
-    @skip("Conditions not supported yet")
+    @skip("Conditions not included in config")
     async def test_set_preset_mode_to_high(self):
         async with assert_device_properties_set(
             self.subject._device,
@@ -430,7 +426,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
                 FANMODE_DPS, "3"
             )
 
-    @skip("Conditions not supported yet")
+    @skip("Conditions not included in config")
     async def test_set_preset_mode_to_dry_clothes(self):
         async with assert_device_properties_set(
             self.subject._device,
@@ -443,7 +439,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
                 FANMODE_DPS, "3"
             )
 
-    @skip("Conditions not supported yet")
+    @skip("Conditions not included in config")
     async def test_set_preset_mode_to_air_clean(self):
         async with assert_device_properties_set(
             self.subject._device, {AIRCLEAN_DPS: True}
@@ -453,7 +449,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
                 FANMODE_DPS, "1"
             )
 
-    @skip("Conditions not supported yet")
+    @skip("Conditions not included in config")
     def test_fan_mode_is_forced_to_high_in_high_dry_clothes_air_clean_presets(self):
         self.dps[FANMODE_DPS] = "1"
         self.dps[PRESET_DPS] = PRESET_HIGH
@@ -469,7 +465,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.assertEqual(self.subject.fan_mode, FAN_HIGH)
         self.assertEqual(self.subject.percentage, 100)
 
-    @skip("Conditions not supported yet")
+    @skip("Conditions not included in config")
     def test_fan_mode_is_forced_to_low_in_low_preset(self):
         self.dps[FANMODE_DPS] = "3"
         self.dps[PRESET_DPS] = PRESET_LOW
@@ -491,7 +487,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.assertEqual(self.subject.fan_mode, None)
         self.assertEqual(self.fan.percentage, None)
 
-    @skip("Conditions not supported yet")
+    @skip("Conditions not included in config")
     def test_fan_modes_reflect_preset_mode(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.assertCountEqual(self.subject.fan_modes, [FAN_LOW, FAN_HIGH])
@@ -509,10 +505,10 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.assertEqual(self.subject.fan_modes, [FAN_HIGH])
         self.assertEqual(self.fan.speed_count, 0)
 
-        self.dps[PRESET_DPS] = PRESET_NORMAL
-        self.dps[AIRCLEAN_DPS] = True
-        self.assertEqual(self.subject.fan_modes, [FAN_HIGH])
-        self.assertEqual(self.fan.speed_count, 0)
+        # self.dps[PRESET_DPS] = PRESET_NORMAL
+        # self.dps[AIRCLEAN_DPS] = True
+        # self.assertEqual(self.subject.fan_modes, [FAN_HIGH])
+        # self.assertEqual(self.fan.speed_count, 0)
 
     async def test_set_fan_mode_to_low_succeeds_in_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
@@ -560,32 +556,32 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         with self.assertRaisesRegex(ValueError, "Invalid fan mode: something"):
             await self.subject.async_set_fan_mode("something")
 
-    @skip("Conditions not supported yet")
+    @skip("Conditions not yet supported for setting")
     async def test_set_fan_mode_fails_outside_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_LOW
         with self.assertRaisesRegex(
-            ValueError, "Fan mode can only be changed while in Normal preset mode"
+            AttributeError, "fan_mode cannot be set at this time"
         ):
             await self.subject.async_set_fan_mode(FAN_HIGH)
 
         self.dps[PRESET_DPS] = PRESET_HIGH
         with self.assertRaisesRegex(
-            ValueError, "Fan mode can only be changed while in Normal preset mode"
+            AttributeError, "fan_mode cannot be set at this time"
         ):
             await self.subject.async_set_fan_mode(FAN_HIGH)
 
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         with self.assertRaisesRegex(
-            ValueError, "Fan mode can only be changed while in Normal preset mode"
+            AttributeError, "fan_mode cannot be set at this time"
         ):
             await self.subject.async_set_fan_mode(FAN_HIGH)
 
-        self.dps[PRESET_DPS] = PRESET_NORMAL
-        self.dps[AIRCLEAN_DPS] = True
-        with self.assertRaisesRegex(
-            ValueError, "Fan mode can only be changed while in Normal preset mode"
-        ):
-            await self.subject.async_set_fan_mode(FAN_HIGH)
+        # self.dps[PRESET_DPS] = PRESET_NORMAL
+        # self.dps[AIRCLEAN_DPS] = True
+        # with self.assertRaisesRegex(
+        #     ValueError, "Fan mode can only be changed while in Normal preset mode"
+        # ):
+        #     await self.subject.async_set_fan_mode(FAN_HIGH)
 
     @skip("Redirection not supported yet")
     def test_tank_full_or_missing(self):

+ 11 - 13
tests/devices/test_goldair_fan.py

@@ -315,23 +315,22 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
             await self.climate.async_set_fan_mode(6)
 
-    @skip("Complex conditions not supported yet")
     def test_climate_fan_mode_for_eco_preset(self):
         self.dps[PRESET_DPS] = "nature"
 
         self.dps[FANMODE_DPS] = 4
-        self.assertEqual(self.climate.fan_mode, 1)
+        self.assertEqual(self.climate.fan_mode, "low")
 
         self.dps[FANMODE_DPS] = 8
-        self.assertEqual(self.climate.fan_mode, 2)
+        self.assertEqual(self.climate.fan_mode, "medium")
 
         self.dps[FANMODE_DPS] = 12
-        self.assertEqual(self.climate.fan_mode, 3)
+        self.assertEqual(self.climate.fan_mode, "high")
 
         self.dps[FANMODE_DPS] = None
         self.assertEqual(self.climate.fan_mode, None)
 
-    @skip("Complex conditions not supported yet")
+    @skip("Complex conditions not yet supported for setting")
     async def test_climate_set_fan_mode_for_eco_preset(self):
         self.dps[PRESET_DPS] = "nature"
 
@@ -339,25 +338,24 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
             self.climate._device,
             {FANMODE_DPS: "4"},
         ):
-            await self.climate.async_set_fan_mode(1)
+            await self.climate.async_set_fan_mode("low")
 
-    @skip("Complex conditions not supported yet")
     def test_climate_fan_mode_for_sleep_preset(self):
         self.dps[PRESET_DPS] = PRESET_SLEEP
 
         self.dps[FANMODE_DPS] = "4"
-        self.assertEqual(self.climate.fan_mode, 1)
+        self.assertEqual(self.climate.fan_mode, "low")
 
         self.dps[FANMODE_DPS] = "8"
-        self.assertEqual(self.climate.fan_mode, 2)
+        self.assertEqual(self.climate.fan_mode, "medium")
 
         self.dps[FANMODE_DPS] = "12"
-        self.assertEqual(self.climate.fan_mode, 3)
+        self.assertEqual(self.climate.fan_mode, "high")
 
         self.dps[FANMODE_DPS] = None
         self.assertEqual(self.climate.fan_mode, None)
 
-    @skip("Complex conditions not supported yet")
+    @skip("Complex conditions not yet supported for setting")
     async def test_climate_set_fan_mode_for_sleep_preset(self):
         self.dps[PRESET_DPS] = PRESET_SLEEP
 
@@ -367,7 +365,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
             await self.climate.async_set_fan_mode(2)
 
-    @skip("Complex conditions not supported yet")
+    @skip("Complex conditions not yet supported for setting")
     async def test_climate_set_fan_mode_does_nothing_when_preset_mode_is_not_set(self):
         self.dps[PRESET_DPS] = None
 
@@ -379,7 +377,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
     def test_device_state_attributes(self):
         self.dps[TIMER_DPS] = "5"
         self.assertEqual(self.climate.device_state_attributes, {"timer": "5"})
-        self.assertEqual(self.climate.device_state_attributes, {"timer": "5"})
+        self.assertEqual(self.subject.device_state_attributes, {"timer": "5"})
 
     async def test_update(self):
         result = AsyncMock()

+ 7 - 0
tests/helpers.py

@@ -29,6 +29,8 @@ async def assert_device_properties_set(device: TuyaLocalDevice, properties: dict
         assert len(provided) == len(properties.keys())
         for p in properties:
             assert p in provided
+            assert properties[p] == provided[p]
+
         for result in results:
             result.assert_awaited()
 
@@ -64,5 +66,10 @@ async def assert_device_properties_set_optional(
         )
         for p in properties:
             assert p in provided
+            assert properties[p] == provided[p]
+        for p in optional_properties:
+            if p in provided:
+                assert optional_properties[p] == provided[p]
+
         for result in results:
             result.assert_awaited()

+ 27 - 0
tests/test_device.py

@@ -31,8 +31,13 @@ from .const import (
     GSH_HEATER_PAYLOAD,
     KOGAN_HEATER_PAYLOAD,
     KOGAN_SOCKET_PAYLOAD,
+    KOGAN_SOCKET_PAYLOAD2,
     GARDENPAC_HEATPUMP_PAYLOAD,
     PURLINE_M100_HEATER_PAYLOAD,
+    REMORA_HEATPUMP_PAYLOAD,
+    EANONS_HUMIDIFIER_PAYLOAD,
+    INKBIRD_THERMOSTAT_PAYLOAD,
+    ANKO_FAN_PAYLOAD,
 )
 
 
@@ -145,6 +150,12 @@ class TestDevice(IsolatedAsyncioTestCase):
             await self.subject.async_inferred_type(), CONF_TYPE_KOGAN_SWITCH
         )
 
+    async def test_detects_kogan_socket_payload2(self):
+        self.subject._cached_state = KOGAN_SOCKET_PAYLOAD2
+        self.assertEqual(
+            await self.subject.async_inferred_type(), CONF_TYPE_KOGAN_SWITCH
+        )
+
     async def test_detects_gsh_heater_payload(self):
         self.subject._cached_state = GSH_HEATER_PAYLOAD
         self.assertEqual(await self.subject.async_inferred_type(), CONF_TYPE_GSH_HEATER)
@@ -161,6 +172,22 @@ class TestDevice(IsolatedAsyncioTestCase):
             await self.subject.async_inferred_type(), CONF_TYPE_PURLINE_M100_HEATER
         )
 
+    async def test_detects_remora_heatpump_payload(self):
+        self.subject._cached_state = REMORA_HEATPUMP_PAYLOAD
+        self.assertEqual(await self.subject.async_inferred_type(), "remora_heatpump")
+
+    async def test_detects_eanons_humidifier_payload(self):
+        self.subject._cached_state = EANONS_HUMIDIFIER_PAYLOAD
+        self.assertEqual(await self.subject.async_inferred_type(), "eanons_humidifier")
+
+    async def test_detects_inkbird_thermostat_payload(self):
+        self.subject._cached_state = INKBIRD_THERMOSTAT_PAYLOAD
+        self.assertEqual(await self.subject.async_inferred_type(), "inkbird_thermostat")
+
+    async def test_detects_anko_fan_payload(self):
+        self.subject._cached_state = ANKO_FAN_PAYLOAD
+        self.assertEqual(await self.subject.async_inferred_type(), "anko_fan")
+
     async def test_detection_returns_none_when_device_type_could_not_be_detected(self):
         self.subject._cached_state = {"1": False, "updated_at": datetime.now()}
         self.assertEqual(await self.subject.async_inferred_type(), None)

+ 5 - 0
tests/test_device_config.py

@@ -43,6 +43,7 @@ from .const import (
     REMORA_HEATPUMP_PAYLOAD,
     EANONS_HUMIDIFIER_PAYLOAD,
     INKBIRD_THERMOSTAT_PAYLOAD,
+    ANKO_FAN_PAYLOAD,
 )
 
 
@@ -231,3 +232,7 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             INKBIRD_THERMOSTAT_PAYLOAD, CONF_TYPE_INKBIRD_THERMOSTAT, None
         )
+
+    def test_anko_fan(self):
+        """Test that Anko fan can be detected from its sample payload."""
+        self._test_detect(ANKO_FAN_PAYLOAD, "anko_fan", None)