Jelajahi Sumber

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 tahun lalu
induk
melakukan
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.const import (
     ATTR_PRESET_MODE,
-    DEFAULT_MIN_TEMP,
+    DEFAULT_MAX_HUMIDITY,
     DEFAULT_MAX_TEMP,
+    DEFAULT_MIN_HUMIDITY,
+    DEFAULT_MIN_TEMP,
     HVAC_MODE_HEAT,
+    SUPPORT_FAN_MODE,
     SUPPORT_PRESET_MODE,
+    SUPPORT_SWING_MODE,
+    SUPPORT_TARGET_HUMIDITY,
     SUPPORT_TARGET_TEMPERATURE,
 )
 from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
@@ -35,7 +40,11 @@ class TuyaLocalClimate(ClimateEntity):
         self._support_flags = 0
         self._current_temperature_dps = None
         self._temperature_dps = None
+        self._current_humidity_dps = None
+        self._humidity_dps = None
         self._preset_mode_dps = None
+        self._swing_mode_dps = None
+        self._fan_mode_dps = None
         self._hvac_mode_dps = None
         self._attr_dps = []
         self._temperature_step = 1
@@ -46,12 +55,22 @@ class TuyaLocalClimate(ClimateEntity):
             elif d.name == "temperature":
                 self._temperature_dps = d
                 self._support_flags |= SUPPORT_TARGET_TEMPERATURE
-
             elif d.name == "current_temperature":
                 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":
                 self._preset_mode_dps = d
                 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:
                 self._attr_dps.append(d)
 
@@ -113,15 +132,19 @@ class TuyaLocalClimate(ClimateEntity):
     @property
     def min_temp(self):
         """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 self._temperature_dps.range["min"]
 
     @property
     def max_temp(self):
         """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"]
 
     async def async_set_temperature(self, **kwargs):
@@ -136,21 +159,54 @@ class TuyaLocalClimate(ClimateEntity):
             raise NotImplementedError()
 
         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)
 
     @property
     def current_temperature(self):
-        """Return this current temperature."""
+        """Return the current measured temperature."""
         if self._current_temperature_dps is None:
             return None
         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
     def hvac_mode(self):
         """Return current HVAC mode."""
@@ -193,6 +249,46 @@ class TuyaLocalClimate(ClimateEntity):
             raise NotImplementedError()
         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
     def device_state_attributes(self):
         """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
         replaced = False
         scale = 1
+        step = None
         if "mapping" in self._config.keys():
             for map in self._config["mapping"]:
 
@@ -253,12 +254,33 @@ class TuyaDpsConfig:
                     result = map["dps_val"]
                     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"]
+                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)):
             result = result / scale
             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:
             _LOGGER.debug(

+ 29 - 57
tests/devices/test_goldair_dehumidifier.py

@@ -28,6 +28,8 @@ AIRCLEAN_DPS = "5"
 FANMODE_DPS = "6"
 LOCK_DPS = "7"
 ERROR_DPS = "11"
+UNKNOWN12_DPS = "12"
+UNKNOWN101_DPS = "101"
 LIGHTOFF_DPS = "102"
 CURRENTTEMP_DPS = "103"
 CURRENTHUMID_DPS = "104"
@@ -66,7 +68,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps = DEHUMIDIFIER_PAYLOAD.copy()
         self.subject._device.get_property.side_effect = lambda id: self.dps[id]
 
-    @skip("Humidity and fan not supported yet")
     def test_supported_features(self):
         self.assertEqual(
             self.subject.supported_features,
@@ -162,29 +163,25 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[DEFROST_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:cup-water")
 
-    @skip("Humidity not supported yet")
     def test_current_humidity(self):
         self.dps[CURRENTHUMID_DPS] = 47
         self.assertEqual(self.subject.current_humidity, 47)
 
-    @skip("Humidity not supported yet")
     def test_min_target_humidity(self):
         self.assertEqual(self.subject.min_humidity, 30)
 
-    @skip("Humidity not supported yet")
     def test_max_target_humidity(self):
         self.assertEqual(self.subject.max_humidity, 80)
 
-    @skip("Humidity not supported yet")
     def test_target_humidity_in_normal_preset(self):
         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):
-        self.dps[HUMIDITY_DPS] = 53
+        self.dps[HUMIDITY_DPS] = 55
 
         self.dps[PRESET_DPS] = PRESET_HIGH
         self.assertIs(self.subject.target_humidity, None)
@@ -199,7 +196,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[AIRCLEAN_DPS] = True
         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):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         async with assert_device_properties_set(
@@ -208,7 +204,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         ):
             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):
         self.dps[PRESET_DPS] = PRESET_NORMAL
 
@@ -218,7 +213,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         ):
             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):
         self.dps[PRESET_DPS] = PRESET_LOW
         with self.assertRaisesRegex(
@@ -337,7 +332,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
             await self.subject.async_set_preset_mode("Normal")
             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 with assert_device_properties_set(
             self.subject._device,
@@ -350,7 +345,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
                 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 with assert_device_properties_set(
             self.subject._device,
@@ -363,7 +358,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
                 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 with assert_device_properties_set(
             self.subject._device,
@@ -386,7 +381,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
                 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):
         self.dps[FANMODE_DPS] = "1"
         self.dps[PRESET_DPS] = PRESET_HIGH
@@ -399,14 +394,14 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[AIRCLEAN_DPS] = True
         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):
         self.dps[FANMODE_DPS] = "3"
         self.dps[PRESET_DPS] = PRESET_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):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[FANMODE_DPS] = "1"
@@ -418,7 +413,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[FANMODE_DPS] = 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):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.assertEqual(self.subject.fan_modes, [FAN_LOW, FAN_HIGH])
@@ -440,7 +435,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[AIRCLEAN_DPS] = False
         self.assertEqual(self.subject.fan_modes, [])
 
-    @skip("Fan not supported yet")
     async def test_set_fan_mode_to_low_succeeds_in_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         async with assert_device_properties_set(
@@ -449,7 +443,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         ):
             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):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         async with assert_device_properties_set(
@@ -458,13 +451,13 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         ):
             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):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         with self.assertRaisesRegex(ValueError, "Invalid 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):
         self.dps[PRESET_DPS] = PRESET_LOW
         with self.assertRaisesRegex(
@@ -491,7 +484,7 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         ):
             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):
         self.dps[ERROR_DPS] = None
         self.assertEqual(self.subject.tank_full_or_missing, False)
@@ -499,57 +492,36 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         self.dps[ERROR_DPS] = 8
         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):
         self.dps[ERROR_DPS] = None
         self.dps[DEFROST_DPS] = False
+        self.dps[AIRCLEAN_DPS] = False
+        self.dps[UNKNOWN12_DPS] = "something"
+        self.dps[UNKNOWN101_DPS] = False
         self.assertCountEqual(
             self.subject.device_state_attributes,
             {
-                "error_code": None,
                 "error": STATE_UNAVAILABLE,
                 "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[DEFROST_DPS] = True
+        self.dps[AIRCLEAN_DPS] = True
+        self.dps[UNKNOWN12_DPS] = "something else"
+        self.dps[UNKNOWN101_DPS] = True
         self.assertCountEqual(
             self.subject.device_state_attributes,
             {
                 "error": ERROR_TANK,
-                "error_code": 8,
                 "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.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):
         self.assertEqual(
             self.subject.supported_features,
@@ -149,7 +148,6 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
             await self.subject.async_set_preset_mode(PRESET_SLEEP)
 
-    @skip("Swing mode not supported yet")
     def test_swing_mode(self):
         self.dps[SWING_DPS] = False
         self.assertEqual(self.subject.swing_mode, SWING_OFF)
@@ -160,11 +158,9 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         self.dps[SWING_DPS] = None
         self.assertIs(self.subject.swing_mode, None)
 
-    @skip("Swing mode not supported yet")
     def test_swing_modes(self):
         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 with assert_device_properties_set(
             self.subject._device,
@@ -172,7 +168,6 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
             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 with assert_device_properties_set(
             self.subject._device,
@@ -180,7 +175,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
             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):
         self.dps[PRESET_DPS] = "normal"
         self.assertCountEqual(self.subject.fan_modes, list(range(1, 13)))
@@ -194,7 +189,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         self.dps[PRESET_DPS] = None
         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):
         self.dps[PRESET_DPS] = "normal"
 
@@ -210,7 +205,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         self.dps[FANMODE_DPS] = 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):
         self.dps[PRESET_DPS] = "normal"
 
@@ -220,7 +215,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
             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):
         self.dps[PRESET_DPS] = "nature"
 
@@ -236,7 +231,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         self.dps[FANMODE_DPS] = 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):
         self.dps[PRESET_DPS] = "nature"
 
@@ -246,7 +241,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
             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):
         self.dps[PRESET_DPS] = PRESET_SLEEP
 
@@ -262,7 +257,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         self.dps[FANMODE_DPS] = 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):
         self.dps[PRESET_DPS] = PRESET_SLEEP
 
@@ -272,7 +267,7 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
             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):
         self.dps[PRESET_DPS] = None
 
@@ -281,6 +276,12 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         ):
             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):
         result = AsyncMock()
         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.subject._device.get_property.side_effect = lambda id: self.dps[id]
 
-    @skip("Swing mode not supported yet")
     def test_supported_features(self):
         self.assertEqual(
             self.subject.supported_features,
@@ -304,7 +303,7 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         ):
             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):
         self.dps[SWING_DPS] = "user"
 
@@ -317,7 +316,6 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         self.dps[POWERLEVEL_DPS] = None
         self.assertIs(self.subject.swing_mode, None)
 
-    @skip("Swing mode not supported yet")
     def test_non_user_swing_mode(self):
         self.dps[SWING_DPS] = "stop"
         self.assertEqual(self.subject.swing_mode, "Stop")
@@ -328,14 +326,14 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         self.dps[SWING_DPS] = 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):
         self.assertCountEqual(
             self.subject.swing_modes,
             ["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 with assert_device_properties_set(
             self.subject._device,
@@ -343,7 +341,7 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         ):
             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 with assert_device_properties_set(
             self.subject._device,
@@ -351,7 +349,7 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         ):
             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 with assert_device_properties_set(
             self.subject._device,
@@ -359,7 +357,7 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         ):
             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):
         with self.assertRaisesRegex(ValueError, "Invalid power level: 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.subject._device.get_property.side_effect = lambda id: self.dps[id]
 
-    @skip("Swing mode not supported yet")
     def test_supported_features(self):
         self.assertEqual(
             self.subject.supported_features,