Ver código fonte

Implement swing mode, fan mode and humidity in generic climate class.

This completes the basic functionality of climate devices.  Unfortunately it does not yet enable any more devices, as all devices it benefits have complex behaviour involving multiple DPS values interacting.
Jason Rumney 5 anos atrás
pai
commit
548a596931

+ 107 - 11
custom_components/tuya_local/generic/climate.py

@@ -6,10 +6,15 @@ import logging
 from homeassistant.components.climate import ClimateEntity
 from homeassistant.components.climate import ClimateEntity
 from homeassistant.components.climate.const import (
 from homeassistant.components.climate.const import (
     ATTR_PRESET_MODE,
     ATTR_PRESET_MODE,
-    DEFAULT_MIN_TEMP,
+    DEFAULT_MAX_HUMIDITY,
     DEFAULT_MAX_TEMP,
     DEFAULT_MAX_TEMP,
+    DEFAULT_MIN_HUMIDITY,
+    DEFAULT_MIN_TEMP,
     HVAC_MODE_HEAT,
     HVAC_MODE_HEAT,
+    SUPPORT_FAN_MODE,
     SUPPORT_PRESET_MODE,
     SUPPORT_PRESET_MODE,
+    SUPPORT_SWING_MODE,
+    SUPPORT_TARGET_HUMIDITY,
     SUPPORT_TARGET_TEMPERATURE,
     SUPPORT_TARGET_TEMPERATURE,
 )
 )
 from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
 from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
@@ -35,7 +40,11 @@ class TuyaLocalClimate(ClimateEntity):
         self._support_flags = 0
         self._support_flags = 0
         self._current_temperature_dps = None
         self._current_temperature_dps = None
         self._temperature_dps = None
         self._temperature_dps = None
+        self._current_humidity_dps = None
+        self._humidity_dps = None
         self._preset_mode_dps = None
         self._preset_mode_dps = None
+        self._swing_mode_dps = None
+        self._fan_mode_dps = None
         self._hvac_mode_dps = None
         self._hvac_mode_dps = None
         self._attr_dps = []
         self._attr_dps = []
         self._temperature_step = 1
         self._temperature_step = 1
@@ -46,12 +55,22 @@ class TuyaLocalClimate(ClimateEntity):
             elif d.name == "temperature":
             elif d.name == "temperature":
                 self._temperature_dps = d
                 self._temperature_dps = d
                 self._support_flags |= SUPPORT_TARGET_TEMPERATURE
                 self._support_flags |= SUPPORT_TARGET_TEMPERATURE
-
             elif d.name == "current_temperature":
             elif d.name == "current_temperature":
                 self._current_temperature_dps = d
                 self._current_temperature_dps = d
+            elif d.name == "humidity":
+                self._humidity_dps = d
+                self._support_flags |= SUPPORT_TARGET_HUMIDITY
+            elif d.name == "current_humidity":
+                self._current_humidity_dps = d
             elif d.name == "preset_mode":
             elif d.name == "preset_mode":
                 self._preset_mode_dps = d
                 self._preset_mode_dps = d
                 self._support_flags |= SUPPORT_PRESET_MODE
                 self._support_flags |= SUPPORT_PRESET_MODE
+            elif d.name == "swing_mode":
+                self._swing_mode_dps = d
+                self._support_flags |= SUPPORT_SWING_MODE
+            elif d.name == "fan_mode":
+                self._fan_mode_dps = d
+                self._support_flags |= SUPPORT_FAN_MODE
             else:
             else:
                 self._attr_dps.append(d)
                 self._attr_dps.append(d)
 
 
@@ -113,15 +132,19 @@ class TuyaLocalClimate(ClimateEntity):
     @property
     @property
     def min_temp(self):
     def min_temp(self):
         """Return the minimum supported target temperature."""
         """Return the minimum supported target temperature."""
-        if self._temperature_dps is None or self._temperature_dps.range is None:
+        if self._temperature_dps is None:
+            return None
+        if self._temperature_dps.range is None:
             return DEFAULT_MIN_TEMP
             return DEFAULT_MIN_TEMP
         return self._temperature_dps.range["min"]
         return self._temperature_dps.range["min"]
 
 
     @property
     @property
     def max_temp(self):
     def max_temp(self):
         """Return the maximum supported target temperature."""
         """Return the maximum supported target temperature."""
-        if self._temperature_dps is None or self._temperature_dps.range is None:
-            return DEFAULT_MIN_TEMP
+        if self._temperature_dps is None:
+            return None
+        if self._temperature_dps.range is None:
+            return DEFAULT_MAX_TEMP
         return self._temperature_dps.range["max"]
         return self._temperature_dps.range["max"]
 
 
     async def async_set_temperature(self, **kwargs):
     async def async_set_temperature(self, **kwargs):
@@ -136,21 +159,54 @@ class TuyaLocalClimate(ClimateEntity):
             raise NotImplementedError()
             raise NotImplementedError()
 
 
         target_temperature = int(round(target_temperature))
         target_temperature = int(round(target_temperature))
-        if not self.min_temp <= target_temperature <= self.max_temp:
-            raise ValueError(
-                f"Target temperature ({target_temperature}) must be between "
-                f"{self.min_temp} and {self.max_temp}."
-            )
 
 
         await self._temperature_dps.async_set_value(self._device, target_temperature)
         await self._temperature_dps.async_set_value(self._device, target_temperature)
 
 
     @property
     @property
     def current_temperature(self):
     def current_temperature(self):
-        """Return this current temperature."""
+        """Return the current measured temperature."""
         if self._current_temperature_dps is None:
         if self._current_temperature_dps is None:
             return None
             return None
         return self._current_temperature_dps.get_value(self._device)
         return self._current_temperature_dps.get_value(self._device)
 
 
+    @property
+    def target_humidity(self):
+        """Return the currently set target humidity."""
+        if self._humidity_dps is None:
+            raise NotImplementedError()
+        return self._humidity_dps.get_value(self._device)
+
+    @property
+    def min_humidity(self):
+        """Return the minimum supported target humidity."""
+        if self._humidity_dps is None:
+            return None
+        if self._humidity_dps.range is None:
+            return DEFAULT_MIN_HUMIDITY
+        return self._humidity_dps.range["min"]
+
+    @property
+    def max_humidity(self):
+        """Return the maximum supported target humidity."""
+        if self._humidity_dps is None:
+            return None
+        if self._humidity_dps.range is None:
+            return DEFAULT_MAX_HUMIDITY
+        return self._humidity_dps.range["max"]
+
+    async def async_set_humidity(self, target_humidity):
+        if self._humidity_dps is None:
+            raise NotImplementedError()
+
+        await self._humidity_dps.async_set_value(self._device, target_humidity)
+
+    @property
+    def current_humidity(self):
+        """Return the current measured humidity."""
+        if self._current_humidity_dps is None:
+            return None
+        return self._current_humidity_dps.get_value(self._device)
+
     @property
     @property
     def hvac_mode(self):
     def hvac_mode(self):
         """Return current HVAC mode."""
         """Return current HVAC mode."""
@@ -193,6 +249,46 @@ class TuyaLocalClimate(ClimateEntity):
             raise NotImplementedError()
             raise NotImplementedError()
         await self._preset_mode_dps.async_set_value(self._device, preset_mode)
         await self._preset_mode_dps.async_set_value(self._device, preset_mode)
 
 
+    @property
+    def swing_mode(self):
+        """Return the current swing mode."""
+        if self._swing_mode_dps is None:
+            raise NotImplementedError()
+        return self._swing_mode_dps.get_value(self._device)
+
+    @property
+    def swing_modes(self):
+        """Return the list of swing modes that this device supports."""
+        if self._swing_mode_dps is None:
+            return None
+        return self._swing_mode_dps.values
+
+    async def async_set_swing_mode(self, swing_mode):
+        """Set the preset mode."""
+        if self._swing_mode_dps is None:
+            raise NotImplementedError()
+        await self._swing_mode_dps.async_set_value(self._device, swing_mode)
+
+    @property
+    def fan_mode(self):
+        """Return the current swing mode."""
+        if self._fan_mode_dps is None:
+            raise NotImplementedError()
+        return self._fan_mode_dps.get_value(self._device)
+
+    @property
+    def fan_modes(self):
+        """Return the list of swing modes that this device supports."""
+        if self._fan_mode_dps is None:
+            return None
+        return self._fan_mode_dps.values
+
+    async def async_set_fan_mode(self, fan_mode):
+        """Set the preset mode."""
+        if self._fan_mode_dps is None:
+            raise NotImplementedError()
+        await self._fan_mode_dps.async_set_value(self._device, fan_mode)
+
     @property
     @property
     def device_state_attributes(self):
     def device_state_attributes(self):
         """Get additional attributes that the integration itself does not support."""
         """Get additional attributes that the integration itself does not support."""

+ 23 - 1
custom_components/tuya_local/helpers/device_config.py

@@ -242,6 +242,7 @@ class TuyaDpsConfig:
         result = value
         result = value
         replaced = False
         replaced = False
         scale = 1
         scale = 1
+        step = None
         if "mapping" in self._config.keys():
         if "mapping" in self._config.keys():
             for map in self._config["mapping"]:
             for map in self._config["mapping"]:
 
 
@@ -253,12 +254,33 @@ class TuyaDpsConfig:
                     result = map["dps_val"]
                     result = map["dps_val"]
                     replaced = True
                     replaced = True
 
 
-                if "scale" in map and "value" not in map:
+                if (
+                    "scale" in map
+                    and "value" not in map
+                    and isinstance(map["scale"], (int, float))
+                ):
                     scale = map["scale"]
                     scale = map["scale"]
+                if (
+                    "step" in map
+                    and "value" not in map
+                    and isinstance(map["step"], (int, float))
+                ):
+                    step = map["step"]
 
 
         if scale != 1 and isinstance(result, (int, float)):
         if scale != 1 and isinstance(result, (int, float)):
             result = result / scale
             result = result / scale
             replaced = True
             replaced = True
+        if step is not None and isinstance(result, (int, float)):
+            result = step * round(float(result) / step)
+            replaced = True
+
+        if self.range is not None:
+            min = self.range["min"]
+            max = self.range["max"]
+            if result < min or result > max:
+                raise ValueError(
+                    f"Target {self.name} ({value}) must be between {min} and {max}"
+                )
 
 
         if replaced:
         if replaced:
             _LOGGER.debug(
             _LOGGER.debug(

+ 29 - 57
tests/devices/test_goldair_dehumidifier.py

@@ -28,6 +28,8 @@ AIRCLEAN_DPS = "5"
 FANMODE_DPS = "6"
 FANMODE_DPS = "6"
 LOCK_DPS = "7"
 LOCK_DPS = "7"
 ERROR_DPS = "11"
 ERROR_DPS = "11"
+UNKNOWN12_DPS = "12"
+UNKNOWN101_DPS = "101"
 LIGHTOFF_DPS = "102"
 LIGHTOFF_DPS = "102"
 CURRENTTEMP_DPS = "103"
 CURRENTTEMP_DPS = "103"
 CURRENTHUMID_DPS = "104"
 CURRENTHUMID_DPS = "104"
@@ -66,7 +68,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps = DEHUMIDIFIER_PAYLOAD.copy()
         self.dps = DEHUMIDIFIER_PAYLOAD.copy()
         self.subject._device.get_property.side_effect = lambda id: self.dps[id]
         self.subject._device.get_property.side_effect = lambda id: self.dps[id]
 
 
-    @skip("Humidity and fan not supported yet")
     def test_supported_features(self):
     def test_supported_features(self):
         self.assertEqual(
         self.assertEqual(
             self.subject.supported_features,
             self.subject.supported_features,
@@ -162,29 +163,25 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[DEFROST_DPS] = True
         self.dps[DEFROST_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:cup-water")
         self.assertEqual(self.subject.icon, "mdi:cup-water")
 
 
-    @skip("Humidity not supported yet")
     def test_current_humidity(self):
     def test_current_humidity(self):
         self.dps[CURRENTHUMID_DPS] = 47
         self.dps[CURRENTHUMID_DPS] = 47
         self.assertEqual(self.subject.current_humidity, 47)
         self.assertEqual(self.subject.current_humidity, 47)
 
 
-    @skip("Humidity not supported yet")
     def test_min_target_humidity(self):
     def test_min_target_humidity(self):
         self.assertEqual(self.subject.min_humidity, 30)
         self.assertEqual(self.subject.min_humidity, 30)
 
 
-    @skip("Humidity not supported yet")
     def test_max_target_humidity(self):
     def test_max_target_humidity(self):
         self.assertEqual(self.subject.max_humidity, 80)
         self.assertEqual(self.subject.max_humidity, 80)
 
 
-    @skip("Humidity not supported yet")
     def test_target_humidity_in_normal_preset(self):
     def test_target_humidity_in_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
-        self.dps[HUMIDITY_DPS] = 53
+        self.dps[HUMIDITY_DPS] = 55
 
 
-        self.assertEqual(self.subject.target_humidity, 53)
+        self.assertEqual(self.subject.target_humidity, 55)
 
 
-    @skip("Humidity not supported yet")
+    @skip("Conditions not supported yet")
     def test_target_humidity_outside_normal_preset(self):
     def test_target_humidity_outside_normal_preset(self):
-        self.dps[HUMIDITY_DPS] = 53
+        self.dps[HUMIDITY_DPS] = 55
 
 
         self.dps[PRESET_DPS] = PRESET_HIGH
         self.dps[PRESET_DPS] = PRESET_HIGH
         self.assertIs(self.subject.target_humidity, None)
         self.assertIs(self.subject.target_humidity, None)
@@ -199,7 +196,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[AIRCLEAN_DPS] = True
         self.dps[AIRCLEAN_DPS] = True
         self.assertIs(self.subject.target_humidity, None)
         self.assertIs(self.subject.target_humidity, None)
 
 
-    @skip("Humidity not supported yet")
     async def test_set_target_humidity_in_normal_preset_rounds_up_to_5_percent(self):
     async def test_set_target_humidity_in_normal_preset_rounds_up_to_5_percent(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
         async with assert_device_properties_set(
         async with assert_device_properties_set(
@@ -208,7 +204,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_humidity(53)
             await self.subject.async_set_humidity(53)
 
 
-    @skip("Humidity not supported yet")
     async def test_set_target_humidity_in_normal_preset_rounds_down_to_5_percent(self):
     async def test_set_target_humidity_in_normal_preset_rounds_down_to_5_percent(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
 
 
@@ -218,7 +213,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_humidity(52)
             await self.subject.async_set_humidity(52)
 
 
-    @skip("Humidity not supported yet")
+    @skip("Conditions not supported yet")
     async def test_set_target_humidity_raises_error_outside_of_normal_preset(self):
     async def test_set_target_humidity_raises_error_outside_of_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_LOW
         self.dps[PRESET_DPS] = PRESET_LOW
         with self.assertRaisesRegex(
         with self.assertRaisesRegex(
@@ -337,7 +332,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
             await self.subject.async_set_preset_mode("Normal")
             await self.subject.async_set_preset_mode("Normal")
             self.subject._device.anticipate_property_value.assert_not_called()
             self.subject._device.anticipate_property_value.assert_not_called()
 
 
-    @skip("Fan and conditions not supported yet")
+    @skip("Conditions not supported yet")
     async def test_set_preset_mode_to_low(self):
     async def test_set_preset_mode_to_low(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
@@ -350,7 +345,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
                 FANMODE_DPS, "1"
                 FANMODE_DPS, "1"
             )
             )
 
 
-    @skip("Fan and conditions not supported yet")
+    @skip("Conditions not supported yet")
     async def test_set_preset_mode_to_high(self):
     async def test_set_preset_mode_to_high(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
@@ -363,7 +358,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
                 FANMODE_DPS, "3"
                 FANMODE_DPS, "3"
             )
             )
 
 
-    @skip("Fan and conditions not supported yet")
+    @skip("Conditions not supported yet")
     async def test_set_preset_mode_to_dry_clothes(self):
     async def test_set_preset_mode_to_dry_clothes(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
@@ -386,7 +381,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
                 FANMODE_DPS, "1"
                 FANMODE_DPS, "1"
             )
             )
 
 
-    @skip("Fan and conditions not supported yet")
+    @skip("Conditions not supported yet")
     def test_fan_mode_is_forced_to_high_in_high_dry_clothes_air_clean_presets(self):
     def test_fan_mode_is_forced_to_high_in_high_dry_clothes_air_clean_presets(self):
         self.dps[FANMODE_DPS] = "1"
         self.dps[FANMODE_DPS] = "1"
         self.dps[PRESET_DPS] = PRESET_HIGH
         self.dps[PRESET_DPS] = PRESET_HIGH
@@ -399,14 +394,14 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[AIRCLEAN_DPS] = True
         self.dps[AIRCLEAN_DPS] = True
         self.assertEqual(self.subject.fan_mode, FAN_HIGH)
         self.assertEqual(self.subject.fan_mode, FAN_HIGH)
 
 
-    @skip("Fan and conditions not supported yet")
+    @skip("Conditions not supported yet")
     def test_fan_mode_is_forced_to_low_in_low_preset(self):
     def test_fan_mode_is_forced_to_low_in_low_preset(self):
         self.dps[FANMODE_DPS] = "3"
         self.dps[FANMODE_DPS] = "3"
         self.dps[PRESET_DPS] = PRESET_LOW
         self.dps[PRESET_DPS] = PRESET_LOW
 
 
         self.assertEqual(self.subject.fan_mode, FAN_LOW)
         self.assertEqual(self.subject.fan_mode, FAN_LOW)
 
 
-    @skip("Fan and conditions not supported yet")
+    @skip("Conditions not supported yet")
     def test_fan_mode_reflects_dps_mode_in_normal_preset(self):
     def test_fan_mode_reflects_dps_mode_in_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[FANMODE_DPS] = "1"
         self.dps[FANMODE_DPS] = "1"
@@ -418,7 +413,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[FANMODE_DPS] = None
         self.dps[FANMODE_DPS] = None
         self.assertEqual(self.subject.fan_mode, None)
         self.assertEqual(self.subject.fan_mode, None)
 
 
-    @skip("Fan and conditions not supported yet")
+    @skip("Conditions not supported yet")
     def test_fan_modes_reflect_preset_mode(self):
     def test_fan_modes_reflect_preset_mode(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.assertEqual(self.subject.fan_modes, [FAN_LOW, FAN_HIGH])
         self.assertEqual(self.subject.fan_modes, [FAN_LOW, FAN_HIGH])
@@ -440,7 +435,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[AIRCLEAN_DPS] = False
         self.dps[AIRCLEAN_DPS] = False
         self.assertEqual(self.subject.fan_modes, [])
         self.assertEqual(self.subject.fan_modes, [])
 
 
-    @skip("Fan not supported yet")
     async def test_set_fan_mode_to_low_succeeds_in_normal_preset(self):
     async def test_set_fan_mode_to_low_succeeds_in_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
         async with assert_device_properties_set(
         async with assert_device_properties_set(
@@ -449,7 +443,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_fan_mode(FAN_LOW)
             await self.subject.async_set_fan_mode(FAN_LOW)
 
 
-    @skip("Fan not supported yet")
     async def test_set_fan_mode_to_high_succeeds_in_normal_preset(self):
     async def test_set_fan_mode_to_high_succeeds_in_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
         async with assert_device_properties_set(
         async with assert_device_properties_set(
@@ -458,13 +451,13 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_fan_mode(FAN_HIGH)
             await self.subject.async_set_fan_mode(FAN_HIGH)
 
 
-    @skip("Fan not supported yet")
+    @skip("Restriction to listed options not supported yet")
     async def test_set_fan_mode_fails_with_invalid_mode(self):
     async def test_set_fan_mode_fails_with_invalid_mode(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
         with self.assertRaisesRegex(ValueError, "Invalid fan mode: something"):
         with self.assertRaisesRegex(ValueError, "Invalid fan mode: something"):
             await self.subject.async_set_fan_mode("something")
             await self.subject.async_set_fan_mode("something")
 
 
-    @skip("Fan and conditions not supported yet")
+    @skip("Conditions not supported yet")
     async def test_set_fan_mode_fails_outside_normal_preset(self):
     async def test_set_fan_mode_fails_outside_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_LOW
         self.dps[PRESET_DPS] = PRESET_LOW
         with self.assertRaisesRegex(
         with self.assertRaisesRegex(
@@ -491,7 +484,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_fan_mode(FAN_HIGH)
             await self.subject.async_set_fan_mode(FAN_HIGH)
 
 
-    @skip("Redirection supported yet")
+    @skip("Redirection not supported yet")
     def test_tank_full_or_missing(self):
     def test_tank_full_or_missing(self):
         self.dps[ERROR_DPS] = None
         self.dps[ERROR_DPS] = None
         self.assertEqual(self.subject.tank_full_or_missing, False)
         self.assertEqual(self.subject.tank_full_or_missing, False)
@@ -499,57 +492,36 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[ERROR_DPS] = 8
         self.dps[ERROR_DPS] = 8
         self.assertEqual(self.subject.tank_full_or_missing, True)
         self.assertEqual(self.subject.tank_full_or_missing, True)
 
 
-    @skip("Defrosting not supported yet")
-    def test_defrosting(self):
-        self.dps[DEFROST_DPS] = False
-        self.assertEqual(self.subject.defrosting, False)
-
-        self.dps[DEFROST_DPS] = True
-        self.assertEqual(self.subject.defrosting, True)
-
-    @skip("Virtual attributes not supported yet.")
     def test_device_state_attributes(self):
     def test_device_state_attributes(self):
         self.dps[ERROR_DPS] = None
         self.dps[ERROR_DPS] = None
         self.dps[DEFROST_DPS] = False
         self.dps[DEFROST_DPS] = False
+        self.dps[AIRCLEAN_DPS] = False
+        self.dps[UNKNOWN12_DPS] = "something"
+        self.dps[UNKNOWN101_DPS] = False
         self.assertCountEqual(
         self.assertCountEqual(
             self.subject.device_state_attributes,
             self.subject.device_state_attributes,
             {
             {
-                "error_code": None,
                 "error": STATE_UNAVAILABLE,
                 "error": STATE_UNAVAILABLE,
                 "defrosting": False,
                 "defrosting": False,
-            },
-        )
-
-        self.dps[ERROR_DPS] = 8
-        self.dps[DEFROST_DPS] = False
-        self.assertCountEqual(
-            self.subject.device_state_attributes,
-            {
-                "error": ERROR_TANK,
-                "error_code": 8,
-                "defrosting": False,
-            },
-        )
-
-        self.dps[ERROR_DPS] = None
-        self.dps[DEFROST_DPS] = True
-        self.assertCountEqual(
-            self.subject.device_state_attributes,
-            {
-                "error_code": None,
-                "error": STATE_UNAVAILABLE,
-                "defrosting": True,
+                "air_clean_on": False,
+                "unknown_12": "something",
+                "unknown_101": False,
             },
             },
         )
         )
 
 
         self.dps[ERROR_DPS] = 8
         self.dps[ERROR_DPS] = 8
         self.dps[DEFROST_DPS] = True
         self.dps[DEFROST_DPS] = True
+        self.dps[AIRCLEAN_DPS] = True
+        self.dps[UNKNOWN12_DPS] = "something else"
+        self.dps[UNKNOWN101_DPS] = True
         self.assertCountEqual(
         self.assertCountEqual(
             self.subject.device_state_attributes,
             self.subject.device_state_attributes,
             {
             {
                 "error": ERROR_TANK,
                 "error": ERROR_TANK,
-                "error_code": 8,
                 "defrosting": True,
                 "defrosting": True,
+                "air_clean_on": True,
+                "unknown_12": "something else",
+                "unknown_101": True,
             },
             },
         )
         )
 
 

+ 14 - 13
tests/devices/test_goldair_fan.py

@@ -49,7 +49,6 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         self.dps = FAN_PAYLOAD.copy()
         self.dps = FAN_PAYLOAD.copy()
         self.subject._device.get_property.side_effect = lambda id: self.dps[id]
         self.subject._device.get_property.side_effect = lambda id: self.dps[id]
 
 
-    @skip("Fan and swing modes are not supported yet.")
     def test_supported_features(self):
     def test_supported_features(self):
         self.assertEqual(
         self.assertEqual(
             self.subject.supported_features,
             self.subject.supported_features,
@@ -149,7 +148,6 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_preset_mode(PRESET_SLEEP)
             await self.subject.async_set_preset_mode(PRESET_SLEEP)
 
 
-    @skip("Swing mode not supported yet")
     def test_swing_mode(self):
     def test_swing_mode(self):
         self.dps[SWING_DPS] = False
         self.dps[SWING_DPS] = False
         self.assertEqual(self.subject.swing_mode, SWING_OFF)
         self.assertEqual(self.subject.swing_mode, SWING_OFF)
@@ -160,11 +158,9 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         self.dps[SWING_DPS] = None
         self.dps[SWING_DPS] = None
         self.assertIs(self.subject.swing_mode, None)
         self.assertIs(self.subject.swing_mode, None)
 
 
-    @skip("Swing mode not supported yet")
     def test_swing_modes(self):
     def test_swing_modes(self):
         self.assertCountEqual(self.subject.swing_modes, [SWING_OFF, SWING_HORIZONTAL])
         self.assertCountEqual(self.subject.swing_modes, [SWING_OFF, SWING_HORIZONTAL])
 
 
-    @skip("Swing mode not supported yet")
     async def test_set_swing_mode_to_off(self):
     async def test_set_swing_mode_to_off(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
@@ -172,7 +168,6 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_swing_mode(SWING_OFF)
             await self.subject.async_set_swing_mode(SWING_OFF)
 
 
-    @skip("Swing mode not supported yet")
     async def test_set_swing_mode_to_horizontal(self):
     async def test_set_swing_mode_to_horizontal(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
@@ -180,7 +175,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_swing_mode(SWING_HORIZONTAL)
             await self.subject.async_set_swing_mode(SWING_HORIZONTAL)
 
 
-    @skip("Fan modes and conditions not supported yet")
+    @skip("Conditions not supported yet")
     def test_fan_modes(self):
     def test_fan_modes(self):
         self.dps[PRESET_DPS] = "normal"
         self.dps[PRESET_DPS] = "normal"
         self.assertCountEqual(self.subject.fan_modes, list(range(1, 13)))
         self.assertCountEqual(self.subject.fan_modes, list(range(1, 13)))
@@ -194,7 +189,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         self.dps[PRESET_DPS] = None
         self.dps[PRESET_DPS] = None
         self.assertEqual(self.subject.fan_modes, [])
         self.assertEqual(self.subject.fan_modes, [])
 
 
-    @skip("Fan modes and conditions not supported yet")
+    @skip("Conditions not supported yet")
     def test_fan_mode_for_normal_preset(self):
     def test_fan_mode_for_normal_preset(self):
         self.dps[PRESET_DPS] = "normal"
         self.dps[PRESET_DPS] = "normal"
 
 
@@ -210,7 +205,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         self.dps[FANMODE_DPS] = None
         self.dps[FANMODE_DPS] = None
         self.assertEqual(self.subject.fan_mode, None)
         self.assertEqual(self.subject.fan_mode, None)
 
 
-    @skip("Fan modes and conditions not supported yet")
+    @skip("Conditions not supported yet")
     async def test_set_fan_mode_for_normal_preset(self):
     async def test_set_fan_mode_for_normal_preset(self):
         self.dps[PRESET_DPS] = "normal"
         self.dps[PRESET_DPS] = "normal"
 
 
@@ -220,7 +215,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_fan_mode(6)
             await self.subject.async_set_fan_mode(6)
 
 
-    @skip("Fan modes and conditions not supported yet")
+    @skip("Conditions not supported yet")
     def test_fan_mode_for_eco_preset(self):
     def test_fan_mode_for_eco_preset(self):
         self.dps[PRESET_DPS] = "nature"
         self.dps[PRESET_DPS] = "nature"
 
 
@@ -236,7 +231,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         self.dps[FANMODE_DPS] = None
         self.dps[FANMODE_DPS] = None
         self.assertEqual(self.subject.fan_mode, None)
         self.assertEqual(self.subject.fan_mode, None)
 
 
-    @skip("Fan modes and conditions not supported yet")
+    @skip("Conditions not supported yet")
     async def test_set_fan_mode_for_eco_preset(self):
     async def test_set_fan_mode_for_eco_preset(self):
         self.dps[PRESET_DPS] = "nature"
         self.dps[PRESET_DPS] = "nature"
 
 
@@ -246,7 +241,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_fan_mode(1)
             await self.subject.async_set_fan_mode(1)
 
 
-    @skip("Fan modes and conditions not supported yet")
+    @skip("Conditions not supported yet")
     def test_fan_mode_for_sleep_preset(self):
     def test_fan_mode_for_sleep_preset(self):
         self.dps[PRESET_DPS] = PRESET_SLEEP
         self.dps[PRESET_DPS] = PRESET_SLEEP
 
 
@@ -262,7 +257,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         self.dps[FANMODE_DPS] = None
         self.dps[FANMODE_DPS] = None
         self.assertEqual(self.subject.fan_mode, None)
         self.assertEqual(self.subject.fan_mode, None)
 
 
-    @skip("Fan modes and conditions not supported yet")
+    @skip("Conditions not supported yet")
     async def test_set_fan_mode_for_sleep_preset(self):
     async def test_set_fan_mode_for_sleep_preset(self):
         self.dps[PRESET_DPS] = PRESET_SLEEP
         self.dps[PRESET_DPS] = PRESET_SLEEP
 
 
@@ -272,7 +267,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_fan_mode(2)
             await self.subject.async_set_fan_mode(2)
 
 
-    @skip("Fan modes and conditions not supported yet")
+    @skip("Conditions not supported yet")
     async def test_set_fan_mode_does_nothing_when_preset_mode_is_not_set(self):
     async def test_set_fan_mode_does_nothing_when_preset_mode_is_not_set(self):
         self.dps[PRESET_DPS] = None
         self.dps[PRESET_DPS] = None
 
 
@@ -281,6 +276,12 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_fan_mode(2)
             await self.subject.async_set_fan_mode(2)
 
 
+    def test_device_state_attributes(self):
+        self.dps[UNKNOWN_DPS] = "something"
+        self.assertEqual(
+            self.subject.device_state_attributes, {"unknown_11": "something"}
+        )
+
     async def test_update(self):
     async def test_update(self):
         result = AsyncMock()
         result = AsyncMock()
         self.subject._device.async_refresh.return_value = result()
         self.subject._device.async_refresh.return_value = result()

+ 6 - 8
tests/devices/test_goldair_gpph_heater.py

@@ -59,7 +59,6 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         self.dps = GPPH_HEATER_PAYLOAD.copy()
         self.dps = GPPH_HEATER_PAYLOAD.copy()
         self.subject._device.get_property.side_effect = lambda id: self.dps[id]
         self.subject._device.get_property.side_effect = lambda id: self.dps[id]
 
 
-    @skip("Swing mode not supported yet")
     def test_supported_features(self):
     def test_supported_features(self):
         self.assertEqual(
         self.assertEqual(
             self.subject.supported_features,
             self.subject.supported_features,
@@ -304,7 +303,7 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_preset_mode("away")
             await self.subject.async_set_preset_mode("away")
 
 
-    @skip("Swing mode and conditional redirection not yet supported")
+    @skip("Conditional redirection not yet supported")
     def test_power_level_returns_user_power_level(self):
     def test_power_level_returns_user_power_level(self):
         self.dps[SWING_DPS] = "user"
         self.dps[SWING_DPS] = "user"
 
 
@@ -317,7 +316,6 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         self.dps[POWERLEVEL_DPS] = None
         self.dps[POWERLEVEL_DPS] = None
         self.assertIs(self.subject.swing_mode, None)
         self.assertIs(self.subject.swing_mode, None)
 
 
-    @skip("Swing mode not supported yet")
     def test_non_user_swing_mode(self):
     def test_non_user_swing_mode(self):
         self.dps[SWING_DPS] = "stop"
         self.dps[SWING_DPS] = "stop"
         self.assertEqual(self.subject.swing_mode, "Stop")
         self.assertEqual(self.subject.swing_mode, "Stop")
@@ -328,14 +326,14 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         self.dps[SWING_DPS] = None
         self.dps[SWING_DPS] = None
         self.assertIs(self.subject.swing_mode, None)
         self.assertIs(self.subject.swing_mode, None)
 
 
-    @skip("Swing mode and conditional redirection not supported yet")
+    @skip("Conditional redirection not supported yet")
     def test_swing_modes(self):
     def test_swing_modes(self):
         self.assertCountEqual(
         self.assertCountEqual(
             self.subject.swing_modes,
             self.subject.swing_modes,
             ["Stop", "1", "2", "3", "4", "5", "Auto"],
             ["Stop", "1", "2", "3", "4", "5", "Auto"],
         )
         )
 
 
-    @skip("Swing mode and conditional redirection not supported yet")
+    @skip("Conditional redirection not supported yet")
     async def test_set_power_level_to_stop(self):
     async def test_set_power_level_to_stop(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
@@ -343,7 +341,7 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_swing_mode("Stop")
             await self.subject.async_set_swing_mode("Stop")
 
 
-    @skip("Swing mode and conditional redirection not supported yet")
+    @skip("Conditional redirection not supported yet")
     async def test_set_swing_mode_to_auto(self):
     async def test_set_swing_mode_to_auto(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
@@ -351,7 +349,7 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_swing_mode("Auto")
             await self.subject.async_set_swing_mode("Auto")
 
 
-    @skip("Swing mode and conditional redirection not supported yet")
+    @skip("Conditional redirection not supported yet")
     async def test_set_power_level_to_numeric_value(self):
     async def test_set_power_level_to_numeric_value(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
@@ -359,7 +357,7 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         ):
         ):
             await self.subject.async_set_swing_mode("3")
             await self.subject.async_set_swing_mode("3")
 
 
-    @skip("Swing mode and conditional redirection not supported yet")
+    @skip("Conditional redirection not supported yet")
     async def test_set_power_level_to_invalid_value_raises_error(self):
     async def test_set_power_level_to_invalid_value_raises_error(self):
         with self.assertRaisesRegex(ValueError, "Invalid power level: unknown"):
         with self.assertRaisesRegex(ValueError, "Invalid power level: unknown"):
             await self.subject.async_set_swing_mode("unknown")
             await self.subject.async_set_swing_mode("unknown")

+ 0 - 1
tests/devices/test_purline_m100_heater.py

@@ -60,7 +60,6 @@ class TestPulineM100Heater(IsolatedAsyncioTestCase):
         self.dps = PURLINE_M100_HEATER_PAYLOAD.copy()
         self.dps = PURLINE_M100_HEATER_PAYLOAD.copy()
         self.subject._device.get_property.side_effect = lambda id: self.dps[id]
         self.subject._device.get_property.side_effect = lambda id: self.dps[id]
 
 
-    @skip("Swing mode not supported yet")
     def test_supported_features(self):
     def test_supported_features(self):
         self.assertEqual(
         self.assertEqual(
             self.subject.supported_features,
             self.subject.supported_features,