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

Improvements to goldair dehumidifier config.

Reduced the number of skipped tests from 15 to 5.
Implemented Air-Clean as preset in climate device for compatibility with legacy,
and added separate switch so it can be surfaced when using humidifier/fan entities.
Jason Rumney 4 жил өмнө
parent
commit
17bc730dd1

+ 51 - 15
custom_components/tuya_local/devices/goldair_dehumidifier.yaml

@@ -13,7 +13,7 @@ primary_entity:
           icon_priority: 3
           icon_priority: 3
         - dps_val: true
         - dps_val: true
           icon: "mdi:air-humidifier"
           icon: "mdi:air-humidifier"
-          icon_priority: 5
+          icon_priority: 6
     - id: 2
     - id: 2
       name: mode
       name: mode
       type: string
       type: string
@@ -27,7 +27,7 @@ primary_entity:
         - dps_val: "3"
         - dps_val: "3"
           value: "Dry clothes"
           value: "Dry clothes"
           icon: "mdi:tshirt-crew-outline"
           icon: "mdi:tshirt-crew-outline"
-          icon_priority: 4
+          icon_priority: 5
     - id: 4
     - id: 4
       type: integer
       type: integer
       name: humidity
       name: humidity
@@ -39,8 +39,14 @@ primary_entity:
     - id: 5
     - id: 5
       type: boolean
       type: boolean
       name: air_clean_on
       name: air_clean_on
+      hidden: true
+      mapping:
+        - dps_val: true
+          icon: "mdi:air-purifier"
+          icon_priority: 4
     - id: 11
     - id: 11
       type: bitfield
       type: bitfield
+      name: error
       mapping:
       mapping:
         - dps_val: 8
         - dps_val: 8
           value: "Tank full or missing"
           value: "Tank full or missing"
@@ -48,7 +54,6 @@ primary_entity:
           icon_priority: 1
           icon_priority: 1
         - dps_val: 0
         - dps_val: 0
           value: "OK"
           value: "OK"
-      name: error
       readonly: true
       readonly: true
     - id: 12
     - id: 12
       type: string
       type: string
@@ -109,6 +114,7 @@ secondary_entities:
     dps:
     dps:
       - id: 1
       - id: 1
         type: boolean
         type: boolean
+        name: hvac_mode
         mapping:
         mapping:
           - dps_val: False
           - dps_val: False
             value: "off"
             value: "off"
@@ -117,22 +123,41 @@ secondary_entities:
           - dps_val: True
           - dps_val: True
             value: "dry"
             value: "dry"
             icon: mdi:air-humidifier
             icon: mdi:air-humidifier
-            icon_priority: 5
-        name: hvac_mode
+            icon_priority: 6
       - id: 2
       - id: 2
         type: string
         type: string
+        name: preset_mode
         mapping:
         mapping:
           - dps_val: "0"
           - dps_val: "0"
-            value: "Normal"
+            constraint: air_clean_on
+            conditions:
+              - dps_val: true
+                value: "Air clean"
+              - dps_val: false
+                value: "Normal"
           - dps_val: "1"
           - dps_val: "1"
-            value: "Low"
+            constraint: air_clean_on
+            conditions:
+              - dps_val: true
+                value: "Air clean"
+              - dps_val: false
+                value: "Low"
           - dps_val: "2"
           - dps_val: "2"
-            value: "High"
+            constraint: air_clean_on
+            conditions:
+              - dps_val: true
+                value: "Air clean"
+              - dps_val: false
+                value: "High"
           - dps_val: "3"
           - dps_val: "3"
-            value: "Dry clothes"
-            icon: "mdi:tshirt-crew-outline"
-            icon_priority: 4
-        name: preset_mode
+            constraint: air_clean_on
+            conditions:
+              - dps_val: true
+                value: "Air clean"
+              - dps_val: false
+                value: "Dry clothes"
+                icon: "mdi:tshirt-crew-outline"
+                icon_priority: 5
       - id: 4
       - id: 4
         type: integer
         type: integer
         name: humidity
         name: humidity
@@ -152,16 +177,21 @@ secondary_entities:
       - id: 5
       - id: 5
         type: boolean
         type: boolean
         name: air_clean_on
         name: air_clean_on
+        mapping:
+          - dps_val: true
+            icon: "mdi:air-purifier"
+            icon_priority: 4
       - id: 6
       - id: 6
         type: string
         type: string
+        name: fan_mode
         mapping:
         mapping:
           - dps_val: "1"
           - dps_val: "1"
             value: "low"
             value: "low"
           - dps_val: "3"
           - dps_val: "3"
             value: "high"
             value: "high"
-        name: fan_mode
       - id: 11
       - id: 11
         type: bitfield
         type: bitfield
+        name: error
         mapping:
         mapping:
           - dps_val: 8
           - dps_val: 8
             value: "Tank full or missing"
             value: "Tank full or missing"
@@ -169,7 +199,6 @@ secondary_entities:
             icon_priority: 1
             icon_priority: 1
           - dps_val: 0
           - dps_val: 0
             value: "OK"
             value: "OK"
-        name: error
         readonly: true
         readonly: true
       - id: 12
       - id: 12
         type: string
         type: string
@@ -199,6 +228,7 @@ secondary_entities:
     dps:
     dps:
       - id: 102
       - id: 102
         type: boolean
         type: boolean
+        name: switch
         mapping:
         mapping:
           - dps_val: false
           - dps_val: false
             value: true
             value: true
@@ -206,10 +236,16 @@ secondary_entities:
           - dps_val: true
           - dps_val: true
             value: false
             value: false
             icon: "mdi:led-off"
             icon: "mdi:led-off"
-        name: switch
   - entity: lock
   - entity: lock
     name: Child Lock
     name: Child Lock
     dps:
     dps:
       - id: 7
       - id: 7
         type: boolean
         type: boolean
         name: lock
         name: lock
+  - entity: switch
+    name: Air Clean
+    icon: "mdi:air-purifier"
+    dps:
+      - id: 5
+        type: boolean
+        name: switch

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

@@ -403,15 +403,19 @@ class TuyaDpsConfig:
     def _active_condition(self, mapping, device, value=None):
     def _active_condition(self, mapping, device, value=None):
         constraint = mapping.get("constraint")
         constraint = mapping.get("constraint")
         conditions = mapping.get("conditions")
         conditions = mapping.get("conditions")
+        c_match = None
         if constraint and conditions:
         if constraint and conditions:
             c_dps = self._entity.find_dps(constraint)
             c_dps = self._entity.find_dps(constraint)
             c_val = None if c_dps is None else device.get_property(c_dps.id)
             c_val = None if c_dps is None else device.get_property(c_dps.id)
             for cond in conditions:
             for cond in conditions:
                 if c_val is not None and c_val == cond.get("dps_val"):
                 if c_val is not None and c_val == cond.get("dps_val"):
-                    return cond
+                    c_match = cond
+                # when changing, another condition may become active
+                # return that if it exists over a current condition
                 if value is not None and value == cond.get("value"):
                 if value is not None and value == cond.get("value"):
                     return cond
                     return cond
-        return None
+
+        return c_match
 
 
     def get_values_to_set(self, device, value):
     def get_values_to_set(self, device, value):
         """Return the dps values that would be set when setting to value"""
         """Return the dps values that would be set when setting to value"""

+ 170 - 121
tests/devices/test_goldair_dehumidifier.py

@@ -1,4 +1,5 @@
 from unittest import skip
 from unittest import skip
+from unittest.mock import ANY
 
 
 from homeassistant.components.climate.const import (
 from homeassistant.components.climate.const import (
     FAN_HIGH,
     FAN_HIGH,
@@ -43,15 +44,16 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
 
 
     def setUp(self):
     def setUp(self):
         self.setUpForConfig("goldair_dehumidifier.yaml", DEHUMIDIFIER_PAYLOAD)
         self.setUpForConfig("goldair_dehumidifier.yaml", DEHUMIDIFIER_PAYLOAD)
-        self.subject = self.entities.get("climate")
+        self.subject = self.entities.get("humidifier")
+        self.fan = self.entities.get("fan")
+        self.climate = self.entities.get("climate")
         self.light = self.entities.get("light")
         self.light = self.entities.get("light")
         self.lock = self.entities.get("lock")
         self.lock = self.entities.get("lock")
-        self.humidifier = self.entities.get("humidifier")
-        self.fan = self.entities.get("fan")
+        self.switch = self.entities.get("switch")
 
 
     def test_supported_features(self):
     def test_supported_features(self):
         self.assertEqual(
         self.assertEqual(
-            self.subject.supported_features,
+            self.climate.supported_features,
             SUPPORT_TARGET_HUMIDITY | SUPPORT_PRESET_MODE | SUPPORT_FAN_MODE,
             SUPPORT_TARGET_HUMIDITY | SUPPORT_PRESET_MODE | SUPPORT_FAN_MODE,
         )
         )
 
 
@@ -61,400 +63,406 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
 
 
         self.dps[AIRCLEAN_DPS] = False
         self.dps[AIRCLEAN_DPS] = False
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
-        self.assertEqual(self.subject.icon, "mdi:air-humidifier-off")
+        self.assertEqual(self.climate.icon, "mdi:air-humidifier-off")
 
 
         self.dps[AIRCLEAN_DPS] = True
         self.dps[AIRCLEAN_DPS] = True
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
-        self.assertEqual(self.subject.icon, "mdi:air-humidifier-off")
+        self.assertEqual(self.climate.icon, "mdi:air-humidifier-off")
 
 
-    @skip("Conditions not included in config")
     def test_icon_is_purifier_when_air_clean_is_active(self):
     def test_icon_is_purifier_when_air_clean_is_active(self):
         self.dps[ERROR_DPS] = None
         self.dps[ERROR_DPS] = None
         self.dps[HVACMODE_DPS] = True
         self.dps[HVACMODE_DPS] = True
         self.dps[AIRCLEAN_DPS] = True
         self.dps[AIRCLEAN_DPS] = True
-        self.assertEqual(self.subject.icon, "mdi:air-purifier")
+        self.assertEqual(self.climate.icon, "mdi:air-purifier")
 
 
     def test_icon_is_tshirt_when_dry_clothes_is_active(self):
     def test_icon_is_tshirt_when_dry_clothes_is_active(self):
         self.dps[ERROR_DPS] = None
         self.dps[ERROR_DPS] = None
         self.dps[HVACMODE_DPS] = True
         self.dps[HVACMODE_DPS] = True
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
-        self.assertEqual(self.subject.icon, "mdi:tshirt-crew-outline")
+        self.assertEqual(self.climate.icon, "mdi:tshirt-crew-outline")
 
 
     def test_icon_is_always_melting_snowflake_when_defrosting_and_tank_not_full(self):
     def test_icon_is_always_melting_snowflake_when_defrosting_and_tank_not_full(self):
         self.dps[DEFROST_DPS] = True
         self.dps[DEFROST_DPS] = True
 
 
         self.dps[HVACMODE_DPS] = False
         self.dps[HVACMODE_DPS] = False
-        self.assertEqual(self.subject.icon, "mdi:snowflake-melt")
+        self.assertEqual(self.climate.icon, "mdi:snowflake-melt")
 
 
         self.dps[HVACMODE_DPS] = True
         self.dps[HVACMODE_DPS] = True
-        self.assertEqual(self.subject.icon, "mdi:snowflake-melt")
+        self.assertEqual(self.climate.icon, "mdi:snowflake-melt")
 
 
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
-        self.assertEqual(self.subject.icon, "mdi:snowflake-melt")
+        self.assertEqual(self.climate.icon, "mdi:snowflake-melt")
 
 
         self.dps[AIRCLEAN_DPS] = True
         self.dps[AIRCLEAN_DPS] = True
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
-        self.assertEqual(self.subject.icon, "mdi:snowflake-melt")
+        self.assertEqual(self.climate.icon, "mdi:snowflake-melt")
 
 
     def test_icon_is_always_tank_when_tank_full_error_is_present(self):
     def test_icon_is_always_tank_when_tank_full_error_is_present(self):
         self.dps[ERROR_DPS] = 8
         self.dps[ERROR_DPS] = 8
 
 
         self.dps[HVACMODE_DPS] = False
         self.dps[HVACMODE_DPS] = False
-        self.assertEqual(self.subject.icon, "mdi:cup-water")
+        self.assertEqual(self.climate.icon, "mdi:cup-water")
 
 
         self.dps[HVACMODE_DPS] = True
         self.dps[HVACMODE_DPS] = True
-        self.assertEqual(self.subject.icon, "mdi:cup-water")
+        self.assertEqual(self.climate.icon, "mdi:cup-water")
 
 
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
-        self.assertEqual(self.subject.icon, "mdi:cup-water")
+        self.assertEqual(self.climate.icon, "mdi:cup-water")
 
 
         self.dps[AIRCLEAN_DPS] = True
         self.dps[AIRCLEAN_DPS] = True
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
-        self.assertEqual(self.subject.icon, "mdi:cup-water")
+        self.assertEqual(self.climate.icon, "mdi:cup-water")
 
 
         self.dps[DEFROST_DPS] = True
         self.dps[DEFROST_DPS] = True
-        self.assertEqual(self.subject.icon, "mdi:cup-water")
+        self.assertEqual(self.climate.icon, "mdi:cup-water")
 
 
     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.climate.current_humidity, 47)
 
 
     def test_min_target_humidity(self):
     def test_min_target_humidity(self):
+        self.assertEqual(self.climate.min_humidity, 30)
         self.assertEqual(self.subject.min_humidity, 30)
         self.assertEqual(self.subject.min_humidity, 30)
-        self.assertEqual(self.humidifier.min_humidity, 30)
 
 
     def test_max_target_humidity(self):
     def test_max_target_humidity(self):
+        self.assertEqual(self.climate.max_humidity, 80)
         self.assertEqual(self.subject.max_humidity, 80)
         self.assertEqual(self.subject.max_humidity, 80)
-        self.assertEqual(self.humidifier.max_humidity, 80)
 
 
     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] = 55
         self.dps[HUMIDITY_DPS] = 55
 
 
-        self.assertEqual(self.subject.target_humidity, 55)
+        self.assertEqual(self.climate.target_humidity, 55)
 
 
     def test_target_humidity_in_humidifier(self):
     def test_target_humidity_in_humidifier(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[HUMIDITY_DPS] = 45
         self.dps[HUMIDITY_DPS] = 45
 
 
-        self.assertEqual(self.humidifier.target_humidity, 45)
+        self.assertEqual(self.subject.target_humidity, 45)
 
 
     def test_target_humidity_outside_normal_preset(self):
     def test_target_humidity_outside_normal_preset(self):
         self.dps[HUMIDITY_DPS] = 55
         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.climate.target_humidity, None)
 
 
         self.dps[PRESET_DPS] = PRESET_LOW
         self.dps[PRESET_DPS] = PRESET_LOW
-        self.assertIs(self.subject.target_humidity, None)
+        self.assertIs(self.climate.target_humidity, None)
 
 
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
-        self.assertIs(self.subject.target_humidity, None)
+        self.assertIs(self.climate.target_humidity, None)
 
 
         # self.dps[PRESET_DPS] = PRESET_NORMAL
         # self.dps[PRESET_DPS] = PRESET_NORMAL
         # self.dps[AIRCLEAN_DPS] = True
         # self.dps[AIRCLEAN_DPS] = True
-        # self.assertIs(self.subject.target_humidity, None)
+        # self.assertIs(self.climate.target_humidity, None)
 
 
     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(
-            self.subject._device,
+            self.climate._device,
             {HUMIDITY_DPS: 55},
             {HUMIDITY_DPS: 55},
         ):
         ):
-            await self.subject.async_set_humidity(53)
+            await self.climate.async_set_humidity(53)
 
 
     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
 
 
         async with assert_device_properties_set(
         async with assert_device_properties_set(
-            self.subject._device,
+            self.climate._device,
             {HUMIDITY_DPS: 50},
             {HUMIDITY_DPS: 50},
         ):
         ):
-            await self.subject.async_set_humidity(52)
+            await self.climate.async_set_humidity(52)
 
 
     async def test_set_humidity_in_humidifier_rounds_up_to_5_percent(self):
     async def test_set_humidity_in_humidifier_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(
-            self.humidifier._device,
+            self.subject._device,
             {HUMIDITY_DPS: 45},
             {HUMIDITY_DPS: 45},
         ):
         ):
-            await self.humidifier.async_set_humidity(43)
+            await self.subject.async_set_humidity(43)
 
 
     async def test_set_humidity_in_humidifier_rounds_down_to_5_percent(self):
     async def test_set_humidity_in_humidifier_rounds_down_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(
-            self.humidifier._device,
+            self.subject._device,
             {HUMIDITY_DPS: 40},
             {HUMIDITY_DPS: 40},
         ):
         ):
-            await self.humidifier.async_set_humidity(42)
+            await self.subject.async_set_humidity(42)
 
 
     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(
             AttributeError, "humidity cannot be set at this time"
             AttributeError, "humidity cannot be set at this time"
         ):
         ):
-            await self.subject.async_set_humidity(50)
+            await self.climate.async_set_humidity(50)
 
 
         self.dps[PRESET_DPS] = PRESET_HIGH
         self.dps[PRESET_DPS] = PRESET_HIGH
         with self.assertRaisesRegex(
         with self.assertRaisesRegex(
             AttributeError, "humidity cannot be set at this time"
             AttributeError, "humidity cannot be set at this time"
         ):
         ):
-            await self.subject.async_set_humidity(50)
+            await self.climate.async_set_humidity(50)
 
 
         self.dps[PRESET_DPS] = PRESET_LOW
         self.dps[PRESET_DPS] = PRESET_LOW
         with self.assertRaisesRegex(
         with self.assertRaisesRegex(
             AttributeError, "humidity cannot be set at this time"
             AttributeError, "humidity cannot be set at this time"
         ):
         ):
-            await self.subject.async_set_humidity(50)
+            await self.climate.async_set_humidity(50)
 
 
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         with self.assertRaisesRegex(
         with self.assertRaisesRegex(
             AttributeError, "humidity cannot be set at this time"
             AttributeError, "humidity cannot be set at this time"
         ):
         ):
-            await self.subject.async_set_humidity(50)
+            await self.climate.async_set_humidity(50)
 
 
         # self.dps[PRESET_DPS] = PRESET_NORMAL
         # self.dps[PRESET_DPS] = PRESET_NORMAL
         # self.dps[AIRCLEAN_DPS] = True
         # self.dps[AIRCLEAN_DPS] = True
         # with self.assertRaisesRegex(
         # with self.assertRaisesRegex(
         #     AttributeError, "humidity cannot be set at this time"
         #     AttributeError, "humidity cannot be set at this time"
         # ):
         # ):
-        #     await self.subject.async_set_humidity(50)
+        #     await self.climate.async_set_humidity(50)
 
 
     def test_temperature_unit_returns_device_temperature_unit(self):
     def test_temperature_unit_returns_device_temperature_unit(self):
         self.assertEqual(
         self.assertEqual(
-            self.subject.temperature_unit, self.subject._device.temperature_unit
+            self.climate.temperature_unit, self.climate._device.temperature_unit
         )
         )
 
 
     def test_minimum_target_temperature(self):
     def test_minimum_target_temperature(self):
-        self.assertIs(self.subject.min_temp, None)
+        self.assertIs(self.climate.min_temp, None)
 
 
     def test_maximum_target_temperature(self):
     def test_maximum_target_temperature(self):
-        self.assertIs(self.subject.max_temp, None)
+        self.assertIs(self.climate.max_temp, None)
 
 
     def test_current_temperature(self):
     def test_current_temperature(self):
         self.dps[CURRENTTEMP_DPS] = 25
         self.dps[CURRENTTEMP_DPS] = 25
-        self.assertEqual(self.subject.current_temperature, 25)
+        self.assertEqual(self.climate.current_temperature, 25)
 
 
     def test_hvac_mode(self):
     def test_hvac_mode(self):
         self.dps[HVACMODE_DPS] = True
         self.dps[HVACMODE_DPS] = True
-        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_DRY)
+        self.assertEqual(self.climate.hvac_mode, HVAC_MODE_DRY)
 
 
         self.dps[HVACMODE_DPS] = False
         self.dps[HVACMODE_DPS] = False
-        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+        self.assertEqual(self.climate.hvac_mode, HVAC_MODE_OFF)
 
 
         self.dps[HVACMODE_DPS] = None
         self.dps[HVACMODE_DPS] = None
-        self.assertEqual(self.subject.hvac_mode, STATE_UNAVAILABLE)
+        self.assertEqual(self.climate.hvac_mode, STATE_UNAVAILABLE)
 
 
     def test_hvac_modes(self):
     def test_hvac_modes(self):
-        self.assertCountEqual(self.subject.hvac_modes, [HVAC_MODE_OFF, HVAC_MODE_DRY])
+        self.assertCountEqual(self.climate.hvac_modes, [HVAC_MODE_OFF, HVAC_MODE_DRY])
 
 
     async def test_turn_on(self):
     async def test_turn_on(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
-            self.subject._device, {HVACMODE_DPS: True}
+            self.climate._device, {HVACMODE_DPS: True}
         ):
         ):
-            await self.subject.async_set_hvac_mode(HVAC_MODE_DRY)
+            await self.climate.async_set_hvac_mode(HVAC_MODE_DRY)
 
 
     async def test_turn_off(self):
     async def test_turn_off(self):
 
 
         async with assert_device_properties_set(
         async with assert_device_properties_set(
-            self.subject._device, {HVACMODE_DPS: False}
+            self.climate._device, {HVACMODE_DPS: False}
         ):
         ):
-            await self.subject.async_set_hvac_mode(HVAC_MODE_OFF)
+            await self.climate.async_set_hvac_mode(HVAC_MODE_OFF)
 
 
     def test_humidifier_is_on(self):
     def test_humidifier_is_on(self):
         self.dps[HVACMODE_DPS] = True
         self.dps[HVACMODE_DPS] = True
-        self.assertTrue(self.humidifier.is_on)
+        self.assertTrue(self.subject.is_on)
 
 
         self.dps[HVACMODE_DPS] = False
         self.dps[HVACMODE_DPS] = False
-        self.assertFalse(self.humidifier.is_on)
+        self.assertFalse(self.subject.is_on)
 
 
     async def test_dehumidifier_turn_on(self):
     async def test_dehumidifier_turn_on(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device, {HVACMODE_DPS: True}
             self.subject._device, {HVACMODE_DPS: True}
         ):
         ):
-            await self.humidifier.async_turn_on()
+            await self.subject.async_turn_on()
 
 
     async def test_dehumidifier_turn_off(self):
     async def test_dehumidifier_turn_off(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device, {HVACMODE_DPS: False}
             self.subject._device, {HVACMODE_DPS: False}
         ):
         ):
-            await self.humidifier.async_turn_off()
+            await self.subject.async_turn_off()
 
 
     def test_preset_mode(self):
     def test_preset_mode(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
-        self.assertEqual(self.subject.preset_mode, "Normal")
-        self.assertEqual(self.humidifier.mode, "Normal")
+        self.assertEqual(self.climate.preset_mode, "Normal")
+        self.assertEqual(self.subject.mode, "Normal")
 
 
         self.dps[PRESET_DPS] = PRESET_LOW
         self.dps[PRESET_DPS] = PRESET_LOW
-        self.assertEqual(self.subject.preset_mode, "Low")
-        self.assertEqual(self.humidifier.mode, "Low")
+        self.assertEqual(self.climate.preset_mode, "Low")
+        self.assertEqual(self.subject.mode, "Low")
 
 
         self.dps[PRESET_DPS] = PRESET_HIGH
         self.dps[PRESET_DPS] = PRESET_HIGH
-        self.assertEqual(self.subject.preset_mode, "High")
-        self.assertEqual(self.humidifier.mode, "High")
+        self.assertEqual(self.climate.preset_mode, "High")
+        self.assertEqual(self.subject.mode, "High")
 
 
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
-        self.assertEqual(self.subject.preset_mode, "Dry clothes")
-        self.assertEqual(self.humidifier.mode, "Dry clothes")
+        self.assertEqual(self.climate.preset_mode, "Dry clothes")
+        self.assertEqual(self.subject.mode, "Dry clothes")
 
 
         self.dps[PRESET_DPS] = None
         self.dps[PRESET_DPS] = None
-        self.assertEqual(self.subject.preset_mode, None)
-        self.assertEqual(self.humidifier.mode, None)
+        self.assertEqual(self.climate.preset_mode, None)
+        self.assertEqual(self.subject.mode, None)
 
 
-    @skip("Conditions not included in config")
     def test_air_clean_is_surfaced_in_preset_mode(self):
     def test_air_clean_is_surfaced_in_preset_mode(self):
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[AIRCLEAN_DPS] = True
         self.dps[AIRCLEAN_DPS] = True
 
 
-        self.assertEqual(self.subject.preset_mode, "Air clean")
+        self.assertEqual(self.climate.preset_mode, "Air clean")
 
 
     def test_preset_modes(self):
     def test_preset_modes(self):
         self.assertCountEqual(
         self.assertCountEqual(
-            self.subject.preset_modes,
+            self.climate.preset_modes,
             [
             [
                 "Normal",
                 "Normal",
                 "Low",
                 "Low",
                 "High",
                 "High",
                 "Dry clothes",
                 "Dry clothes",
-                #                "Air clean",
+                "Air clean",
             ],
             ],
         )
         )
 
 
     async def test_set_preset_mode_to_normal(self):
     async def test_set_preset_mode_to_normal(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
-            self.subject._device,
+            self.climate._device,
             {
             {
                 PRESET_DPS: PRESET_NORMAL,
                 PRESET_DPS: PRESET_NORMAL,
+                AIRCLEAN_DPS: False,
             },
             },
         ):
         ):
-            await self.subject.async_set_preset_mode("Normal")
-            self.subject._device.anticipate_property_value.assert_not_called()
+            await self.climate.async_set_preset_mode("Normal")
+            self.climate._device.anticipate_property_value.assert_not_called()
 
 
-    @skip("Conditions not included in config")
     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.climate._device,
             {
             {
                 PRESET_DPS: PRESET_LOW,
                 PRESET_DPS: PRESET_LOW,
+                AIRCLEAN_DPS: False,
             },
             },
         ):
         ):
-            await self.subject.async_set_preset_mode("Low")
-            self.subject._device.anticipate_property_value.assert_called_once_with(
-                FANMODE_DPS, "1"
-            )
+            await self.climate.async_set_preset_mode("Low")
+
+            # No anticipation of device behaviour in generic implementation
+            # self.climate._device.anticipate_property_value.assert_called_once_with(
+            #     FANMODE_DPS, "1"
+            # )
 
 
-    @skip("Conditions not included in config")
     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.climate._device,
             {
             {
                 PRESET_DPS: PRESET_HIGH,
                 PRESET_DPS: PRESET_HIGH,
+                AIRCLEAN_DPS: False,
             },
             },
         ):
         ):
-            await self.subject.async_set_preset_mode("High")
-            self.subject._device.anticipate_property_value.assert_called_once_with(
-                FANMODE_DPS, "3"
-            )
+            await self.climate.async_set_preset_mode("High")
+
+            # No anticipation of device behaviour in generic implementation
+            # self.climate._device.anticipate_property_value.assert_called_once_with(
+            #     FANMODE_DPS, "3"
+            # )
 
 
-    @skip("Conditions not included in config")
     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.climate._device,
             {
             {
                 PRESET_DPS: PRESET_DRY_CLOTHES,
                 PRESET_DPS: PRESET_DRY_CLOTHES,
+                AIRCLEAN_DPS: False,
             },
             },
         ):
         ):
-            await self.subject.async_set_preset_mode("Dry clothes")
-            self.subject._device.anticipate_property_value.assert_called_once_with(
-                FANMODE_DPS, "3"
-            )
+            await self.climate.async_set_preset_mode("Dry clothes")
+
+            # No anticipation of device behaviour in generic implementation
+            # self.climate._device.anticipate_property_value.assert_called_once_with(
+            #     FANMODE_DPS, "3"
+            # )
 
 
-    @skip("Conditions not included in config")
     async def test_set_preset_mode_to_air_clean(self):
     async def test_set_preset_mode_to_air_clean(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
-            self.subject._device, {AIRCLEAN_DPS: True}
+            self.climate._device, {AIRCLEAN_DPS: True, PRESET_DPS: ANY}
         ):
         ):
-            await self.subject.async_set_preset_mode("Air clean")
-            self.subject._device.anticipate_property_value.assert_called_once_with(
-                FANMODE_DPS, "1"
-            )
+            await self.climate.async_set_preset_mode("Air clean")
+
+            # No anticipation of device behaviour in generic implementation
+            # self.climate._device.anticipate_property_value.assert_called_once_with(
+            #     FANMODE_DPS, "1"
+            # )
 
 
     @skip("Conditions not included in config")
     @skip("Conditions not included in config")
     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
-        self.assertEqual(self.subject.fan_mode, FAN_HIGH)
+        self.assertEqual(self.climate.fan_mode, FAN_HIGH)
         self.assertEqual(self.fan.percentage, 100)
         self.assertEqual(self.fan.percentage, 100)
 
 
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
-        self.assertEqual(self.subject.fan_mode, FAN_HIGH)
+        self.assertEqual(self.climate.fan_mode, FAN_HIGH)
         self.assertEqual(self.fan.percentage, 100)
         self.assertEqual(self.fan.percentage, 100)
 
 
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[AIRCLEAN_DPS] = True
         self.dps[AIRCLEAN_DPS] = True
-        self.assertEqual(self.subject.fan_mode, FAN_HIGH)
-        self.assertEqual(self.subject.percentage, 100)
+        self.assertEqual(self.climate.fan_mode, FAN_HIGH)
+        self.assertEqual(self.climate.percentage, 100)
 
 
     @skip("Conditions not included in config")
     @skip("Conditions not included in config")
     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.climate.fan_mode, FAN_LOW)
         self.assertEqual(self.fan.percentage, 50)
         self.assertEqual(self.fan.percentage, 50)
 
 
     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"
-        self.assertEqual(self.subject.fan_mode, FAN_LOW)
+        self.assertEqual(self.climate.fan_mode, FAN_LOW)
         self.assertEqual(self.fan.percentage, 50)
         self.assertEqual(self.fan.percentage, 50)
 
 
         self.dps[FANMODE_DPS] = "3"
         self.dps[FANMODE_DPS] = "3"
-        self.assertEqual(self.subject.fan_mode, FAN_HIGH)
+        self.assertEqual(self.climate.fan_mode, FAN_HIGH)
         self.assertEqual(self.fan.percentage, 100)
         self.assertEqual(self.fan.percentage, 100)
 
 
         self.dps[FANMODE_DPS] = None
         self.dps[FANMODE_DPS] = None
-        self.assertEqual(self.subject.fan_mode, None)
+        self.assertEqual(self.climate.fan_mode, None)
         self.assertEqual(self.fan.percentage, None)
         self.assertEqual(self.fan.percentage, None)
 
 
     @skip("Conditions not included in config")
     @skip("Conditions not included in config")
     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.assertCountEqual(self.subject.fan_modes, [FAN_LOW, FAN_HIGH])
+        self.assertCountEqual(self.climate.fan_modes, [FAN_LOW, FAN_HIGH])
         self.assertEqual(self.fan.speed_count, 2)
         self.assertEqual(self.fan.speed_count, 2)
 
 
         self.dps[PRESET_DPS] = PRESET_LOW
         self.dps[PRESET_DPS] = PRESET_LOW
-        self.assertEqual(self.subject.fan_modes, [FAN_LOW])
+        self.assertEqual(self.climate.fan_modes, [FAN_LOW])
         self.assertEqual(self.fan.speed_count, 0)
         self.assertEqual(self.fan.speed_count, 0)
 
 
         self.dps[PRESET_DPS] = PRESET_HIGH
         self.dps[PRESET_DPS] = PRESET_HIGH
-        self.assertEqual(self.subject.fan_modes, [FAN_HIGH])
+        self.assertEqual(self.climate.fan_modes, [FAN_HIGH])
         self.assertEqual(self.fan.speed_count, 0)
         self.assertEqual(self.fan.speed_count, 0)
 
 
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
-        self.assertEqual(self.subject.fan_modes, [FAN_HIGH])
+        self.assertEqual(self.climate.fan_modes, [FAN_HIGH])
         self.assertEqual(self.fan.speed_count, 0)
         self.assertEqual(self.fan.speed_count, 0)
 
 
         # self.dps[PRESET_DPS] = PRESET_NORMAL
         # self.dps[PRESET_DPS] = PRESET_NORMAL
         # self.dps[AIRCLEAN_DPS] = True
         # self.dps[AIRCLEAN_DPS] = True
-        # self.assertEqual(self.subject.fan_modes, [FAN_HIGH])
+        # self.assertEqual(self.climate.fan_modes, [FAN_HIGH])
         # self.assertEqual(self.fan.speed_count, 0)
         # self.assertEqual(self.fan.speed_count, 0)
 
 
     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(
-            self.subject._device,
+            self.climate._device,
             {FANMODE_DPS: "1"},
             {FANMODE_DPS: "1"},
         ):
         ):
-            await self.subject.async_set_fan_mode(FAN_LOW)
+            await self.climate.async_set_fan_mode(FAN_LOW)
 
 
     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(
-            self.subject._device,
+            self.climate._device,
             {FANMODE_DPS: "3"},
             {FANMODE_DPS: "3"},
         ):
         ):
-            await self.subject.async_set_fan_mode(FAN_HIGH)
+            await self.climate.async_set_fan_mode(FAN_HIGH)
 
 
     async def test_set_fan_50_succeeds_in_normal_preset(self):
     async def test_set_fan_50_succeeds_in_normal_preset(self):
         self.dps[PRESET_DPS] = PRESET_NORMAL
         self.dps[PRESET_DPS] = PRESET_NORMAL
@@ -484,7 +492,7 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
     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.climate.async_set_fan_mode("something")
 
 
     @skip("Conditions not yet supported for setting")
     @skip("Conditions not yet supported for setting")
     async def test_set_fan_mode_fails_outside_normal_preset(self):
     async def test_set_fan_mode_fails_outside_normal_preset(self):
@@ -492,26 +500,26 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
         with self.assertRaisesRegex(
         with self.assertRaisesRegex(
             AttributeError, "fan_mode cannot be set at this time"
             AttributeError, "fan_mode cannot be set at this time"
         ):
         ):
-            await self.subject.async_set_fan_mode(FAN_HIGH)
+            await self.climate.async_set_fan_mode(FAN_HIGH)
 
 
         self.dps[PRESET_DPS] = PRESET_HIGH
         self.dps[PRESET_DPS] = PRESET_HIGH
         with self.assertRaisesRegex(
         with self.assertRaisesRegex(
             AttributeError, "fan_mode cannot be set at this time"
             AttributeError, "fan_mode cannot be set at this time"
         ):
         ):
-            await self.subject.async_set_fan_mode(FAN_HIGH)
+            await self.climate.async_set_fan_mode(FAN_HIGH)
 
 
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         self.dps[PRESET_DPS] = PRESET_DRY_CLOTHES
         with self.assertRaisesRegex(
         with self.assertRaisesRegex(
             AttributeError, "fan_mode cannot be set at this time"
             AttributeError, "fan_mode cannot be set at this time"
         ):
         ):
-            await self.subject.async_set_fan_mode(FAN_HIGH)
+            await self.climate.async_set_fan_mode(FAN_HIGH)
 
 
         # self.dps[PRESET_DPS] = PRESET_NORMAL
         # self.dps[PRESET_DPS] = PRESET_NORMAL
         # self.dps[AIRCLEAN_DPS] = True
         # self.dps[AIRCLEAN_DPS] = True
         # with self.assertRaisesRegex(
         # with self.assertRaisesRegex(
         #     ValueError, "Fan mode can only be changed while in Normal preset mode"
         #     ValueError, "Fan mode can only be changed while in Normal preset mode"
         # ):
         # ):
-        #     await self.subject.async_set_fan_mode(FAN_HIGH)
+        #     await self.climate.async_set_fan_mode(FAN_HIGH)
 
 
     def test_device_state_attributes(self):
     def test_device_state_attributes(self):
         self.dps[ERROR_DPS] = None
         self.dps[ERROR_DPS] = None
@@ -520,7 +528,7 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
         self.dps[UNKNOWN12_DPS] = "something"
         self.dps[UNKNOWN12_DPS] = "something"
         self.dps[UNKNOWN101_DPS] = False
         self.dps[UNKNOWN101_DPS] = False
         self.assertCountEqual(
         self.assertCountEqual(
-            self.subject.device_state_attributes,
+            self.climate.device_state_attributes,
             {
             {
                 "error": STATE_UNAVAILABLE,
                 "error": STATE_UNAVAILABLE,
                 "defrosting": False,
                 "defrosting": False,
@@ -536,7 +544,7 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
         self.dps[UNKNOWN12_DPS] = "something else"
         self.dps[UNKNOWN12_DPS] = "something else"
         self.dps[UNKNOWN101_DPS] = True
         self.dps[UNKNOWN101_DPS] = True
         self.assertCountEqual(
         self.assertCountEqual(
-            self.subject.device_state_attributes,
+            self.climate.device_state_attributes,
             {
             {
                 "error": ERROR_TANK,
                 "error": ERROR_TANK,
                 "defrosting": True,
                 "defrosting": True,
@@ -618,3 +626,44 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
             self.light._device, {LIGHTOFF_DPS: True}
             self.light._device, {LIGHTOFF_DPS: True}
         ):
         ):
             await self.light.async_toggle()
             await self.light.async_toggle()
+
+    def test_switch_icon(self):
+        self.assertEqual(self.switch.icon, "mdi:air-purifier")
+
+    def test_switch_is_on(self):
+        self.dps[AIRCLEAN_DPS] = True
+        self.assertEqual(self.switch.is_on, True)
+
+        self.dps[AIRCLEAN_DPS] = False
+        self.assertEqual(self.switch.is_on, False)
+
+    def test_switch_state_attributes(self):
+        self.assertEqual(self.switch.device_state_attributes, {})
+
+    async def test_switch_turn_on(self):
+        async with assert_device_properties_set(
+            self.switch._device, {AIRCLEAN_DPS: True}
+        ):
+            await self.switch.async_turn_on()
+
+    async def test_switch_turn_off(self):
+        async with assert_device_properties_set(
+            self.switch._device, {AIRCLEAN_DPS: False}
+        ):
+            await self.switch.async_turn_off()
+
+    async def test_toggle_turns_the_switch_on_when_it_was_off(self):
+        self.dps[AIRCLEAN_DPS] = False
+
+        async with assert_device_properties_set(
+            self.switch._device, {AIRCLEAN_DPS: True}
+        ):
+            await self.switch.async_toggle()
+
+    async def test_toggle_turns_the_switch_off_when_it_was_on(self):
+        self.dps[AIRCLEAN_DPS] = True
+
+        async with assert_device_properties_set(
+            self.switch._device, {AIRCLEAN_DPS: False}
+        ):
+            await self.switch.async_toggle()