Sfoglia il codice sorgente

Full test coverage for dehumidifiers

Nik Rolls 5 anni fa
parent
commit
6ad85dcfd0

+ 586 - 0
tests/dehumidifier/test_climate.py

@@ -0,0 +1,586 @@
+from unittest import IsolatedAsyncioTestCase
+from unittest.mock import AsyncMock, patch
+
+from homeassistant.components.climate.const import (
+    ATTR_FAN_MODE,
+    ATTR_HUMIDITY,
+    ATTR_HVAC_MODE,
+    ATTR_PRESET_MODE,
+    FAN_HIGH,
+    FAN_LOW,
+    HVAC_MODE_DRY,
+    HVAC_MODE_OFF,
+    SUPPORT_FAN_MODE,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_HUMIDITY,
+)
+from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
+
+from custom_components.goldair_climate.dehumidifier.climate import GoldairDehumidifier
+from custom_components.goldair_climate.dehumidifier.const import (
+    ATTR_AIR_CLEAN_ON,
+    ATTR_DEFROSTING,
+    ATTR_ERROR,
+    ATTR_TARGET_HUMIDITY,
+    ERROR_CODE_TO_DPS_CODE,
+    ERROR_TANK,
+    FAN_MODE_TO_DPS_MODE,
+    HVAC_MODE_TO_DPS_MODE,
+    PRESET_AIR_CLEAN,
+    PRESET_DRY_CLOTHES,
+    PRESET_HIGH,
+    PRESET_LOW,
+    PRESET_MODE_TO_DPS_MODE,
+    PRESET_NORMAL,
+    PROPERTY_TO_DPS_ID,
+)
+
+from ..const import DEHUMIDIFIER_PAYLOAD
+from ..helpers import assert_device_properties_set
+
+
+class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
+    def setUp(self):
+        device_patcher = patch(
+            "custom_components.goldair_climate.device.GoldairTuyaDevice"
+        )
+        self.addCleanup(device_patcher.stop)
+        self.mock_device = device_patcher.start()
+
+        self.subject = GoldairDehumidifier(self.mock_device())
+
+        self.dps = DEHUMIDIFIER_PAYLOAD.copy()
+        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+
+    def test_supported_features(self):
+        self.assertEqual(
+            self.subject.supported_features,
+            SUPPORT_TARGET_HUMIDITY | SUPPORT_PRESET_MODE | SUPPORT_FAN_MODE,
+        )
+
+    def test_should_poll(self):
+        self.assertTrue(self.subject.should_poll)
+
+    def test_name_returns_device_name(self):
+        self.assertEqual(self.subject.name, self.subject._device.name)
+
+    def test_unique_id_returns_device_unique_id(self):
+        self.assertEqual(self.subject.unique_id, self.subject._device.unique_id)
+
+    def test_device_info_returns_device_info_from_device(self):
+        self.assertEqual(self.subject.device_info, self.subject._device.device_info)
+
+    def test_icon_is_always_standard_when_off_without_error(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_ERROR]] = None
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = False
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]] = False
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_DRY_CLOTHES
+        ]
+        self.assertEqual(self.subject.icon, "mdi:air-humidifier")
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]] = True
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        self.assertEqual(self.subject.icon, "mdi:air-humidifier")
+
+    def test_icon_is_purifier_when_air_clean_is_active(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_ERROR]] = None
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = True
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]] = True
+
+        self.assertEqual(self.subject.icon, "mdi:air-purifier")
+
+    def test_icon_is_tshirt_when_dry_clothes_is_active(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_ERROR]] = None
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = True
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_DRY_CLOTHES
+        ]
+
+        self.assertEqual(self.subject.icon, "mdi:tshirt-crew-outline")
+
+    def test_icon_is_always_melting_snowflake_when_defrosting_and_tank_not_full(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_DEFROSTING]] = True
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = False
+        self.assertEqual(self.subject.icon, "mdi:snowflake-melt")
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = True
+        self.assertEqual(self.subject.icon, "mdi:snowflake-melt")
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_DRY_CLOTHES
+        ]
+        self.assertEqual(self.subject.icon, "mdi:snowflake-melt")
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]] = True
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        self.assertEqual(self.subject.icon, "mdi:snowflake-melt")
+
+    def test_icon_is_always_tank_when_tank_full_error_is_present(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_ERROR]] = ERROR_CODE_TO_DPS_CODE[ERROR_TANK]
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = False
+        self.assertEqual(self.subject.icon, "mdi:cup-water")
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = True
+        self.assertEqual(self.subject.icon, "mdi:cup-water")
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_DRY_CLOTHES
+        ]
+        self.assertEqual(self.subject.icon, "mdi:cup-water")
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]] = True
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        self.assertEqual(self.subject.icon, "mdi:cup-water")
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_DEFROSTING]] = True
+        self.assertEqual(self.subject.icon, "mdi:cup-water")
+
+    def test_current_humidity(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HUMIDITY]] = 47
+        self.assertEqual(self.subject.current_humidity, 47)
+
+    def test_min_target_humidity(self):
+        self.assertEqual(self.subject.min_humidity, 30)
+
+    def test_max_target_humidity(self):
+        self.assertEqual(self.subject.max_humidity, 80)
+
+    def test_target_humidity_in_normal_preset(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_TARGET_HUMIDITY]] = 53
+
+        self.assertEqual(self.subject.target_humidity, 53)
+
+    def test_target_humidity_outside_normal_preset(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_TARGET_HUMIDITY]] = 53
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_HIGH
+        ]
+        self.assertIs(self.subject.target_humidity, None)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_LOW
+        ]
+        self.assertIs(self.subject.target_humidity, None)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_DRY_CLOTHES
+        ]
+        self.assertIs(self.subject.target_humidity, None)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]] = True
+        self.assertIs(self.subject.target_humidity, None)
+
+    async def test_set_target_humidity_in_normal_preset_rounds_up_to_5_percent(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+
+        async with assert_device_properties_set(
+            self.subject._device, {PROPERTY_TO_DPS_ID[ATTR_TARGET_HUMIDITY]: 55},
+        ):
+            await self.subject.async_set_humidity(53)
+
+    async def test_set_target_humidity_in_normal_preset_rounds_down_to_5_percent(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+
+        async with assert_device_properties_set(
+            self.subject._device, {PROPERTY_TO_DPS_ID[ATTR_TARGET_HUMIDITY]: 50},
+        ):
+            await self.subject.async_set_humidity(52)
+
+    async def test_set_target_humidity_raises_error_outside_of_normal_preset(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_LOW
+        ]
+        with self.assertRaisesRegex(
+            ValueError, "Target humidity can only be changed while in Normal mode"
+        ):
+            await self.subject.async_set_humidity(50)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_HIGH
+        ]
+        with self.assertRaisesRegex(
+            ValueError, "Target humidity can only be changed while in Normal mode"
+        ):
+            await self.subject.async_set_humidity(50)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_LOW
+        ]
+        with self.assertRaisesRegex(
+            ValueError, "Target humidity can only be changed while in Normal mode"
+        ):
+            await self.subject.async_set_humidity(50)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_DRY_CLOTHES
+        ]
+        with self.assertRaisesRegex(
+            ValueError, "Target humidity can only be changed while in Normal mode"
+        ):
+            await self.subject.async_set_humidity(50)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]] = True
+        with self.assertRaisesRegex(
+            ValueError, "Target humidity can only be changed while in Normal mode"
+        ):
+            await self.subject.async_set_humidity(50)
+
+    def test_temperature_unit_returns_device_temperature_unit(self):
+        self.assertEqual(
+            self.subject.temperature_unit, self.subject._device.temperature_unit
+        )
+
+    def test_minimum_target_temperature(self):
+        self.assertIs(self.subject.min_temp, None)
+
+    def test_maximum_target_temperature(self):
+        self.assertIs(self.subject.max_temp, None)
+
+    def test_current_temperature(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_TEMPERATURE]] = 25
+        self.assertEqual(self.subject.current_temperature, 25)
+
+    def test_hvac_mode(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = True
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_DRY)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = False
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]] = None
+        self.assertEqual(self.subject.hvac_mode, STATE_UNAVAILABLE)
+
+    def test_hvac_modes(self):
+        self.assertEqual(self.subject.hvac_modes, [HVAC_MODE_OFF, HVAC_MODE_DRY])
+
+    async def test_turn_on(self):
+        async with assert_device_properties_set(
+            self.subject._device, {PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]: True}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_DRY)
+
+    async def test_turn_off(self):
+        async with assert_device_properties_set(
+            self.subject._device, {PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]: False}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_OFF)
+
+    def test_preset_mode(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        self.assertEqual(self.subject.preset_mode, PRESET_NORMAL)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_HIGH
+        ]
+        self.assertEqual(self.subject.preset_mode, PRESET_HIGH)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_LOW
+        ]
+        self.assertEqual(self.subject.preset_mode, PRESET_LOW)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_DRY_CLOTHES
+        ]
+        self.assertEqual(self.subject.preset_mode, PRESET_DRY_CLOTHES)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = None
+        self.assertEqual(self.subject.preset_mode, None)
+
+    def test_air_clean_is_surfaced_in_preset_mode(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_DRY_CLOTHES
+        ]
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]] = True
+
+        self.assertEqual(self.subject.preset_mode, PRESET_AIR_CLEAN)
+
+    def test_preset_modes(self):
+        self.assertEqual(
+            self.subject.preset_modes,
+            [
+                PRESET_NORMAL,
+                PRESET_LOW,
+                PRESET_HIGH,
+                PRESET_DRY_CLOTHES,
+                PRESET_AIR_CLEAN,
+            ],
+        )
+
+    async def test_set_test_preset_mode_to_normal(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]: PRESET_MODE_TO_DPS_MODE[
+                    PRESET_NORMAL
+                ],
+                PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]: False,
+            },
+        ):
+            await self.subject.async_set_preset_mode(PRESET_NORMAL)
+            self.subject._device.anticipate_property_value.assert_not_called()
+
+    async def test_set_test_preset_mode_to_low(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]: PRESET_MODE_TO_DPS_MODE[
+                    PRESET_LOW
+                ],
+                PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]: False,
+            },
+        ):
+            await self.subject.async_set_preset_mode(PRESET_LOW)
+            self.subject._device.anticipate_property_value.assert_called_once_with(
+                PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], FAN_LOW
+            )
+
+    async def test_set_test_preset_mode_to_high(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]: PRESET_MODE_TO_DPS_MODE[
+                    PRESET_HIGH
+                ],
+                PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]: False,
+            },
+        ):
+            await self.subject.async_set_preset_mode(PRESET_HIGH)
+            self.subject._device.anticipate_property_value.assert_called_once_with(
+                PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], FAN_HIGH
+            )
+
+    async def test_set_test_preset_mode_to_dry_clothes(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]: PRESET_MODE_TO_DPS_MODE[
+                    PRESET_DRY_CLOTHES
+                ],
+                PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]: False,
+            },
+        ):
+            await self.subject.async_set_preset_mode(PRESET_DRY_CLOTHES)
+            self.subject._device.anticipate_property_value.assert_called_once_with(
+                PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], FAN_HIGH
+            )
+
+    async def test_set_test_preset_mode_to_air_clean(self):
+        async with assert_device_properties_set(
+            self.subject._device, {PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]: True}
+        ):
+            await self.subject.async_set_preset_mode(PRESET_AIR_CLEAN)
+            self.subject._device.anticipate_property_value.assert_called_once_with(
+                PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], FAN_HIGH
+            )
+
+    def test_fan_mode_is_forced_to_high_in_high_dry_clothes_air_clean_presets(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_FAN_MODE]] = FAN_MODE_TO_DPS_MODE[FAN_LOW]
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_HIGH
+        ]
+        self.assertEqual(self.subject.fan_mode, FAN_HIGH)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_DRY_CLOTHES
+        ]
+        self.assertEqual(self.subject.fan_mode, FAN_HIGH)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]] = True
+        self.assertEqual(self.subject.fan_mode, FAN_HIGH)
+
+    def test_fan_mode_is_forced_to_low_in_low_preset(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_FAN_MODE]] = FAN_MODE_TO_DPS_MODE[FAN_HIGH]
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_LOW
+        ]
+
+        self.assertEqual(self.subject.fan_mode, FAN_LOW)
+
+    def test_fan_mode_reflects_dps_mode_in_normal_preset(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_FAN_MODE]] = FAN_MODE_TO_DPS_MODE[FAN_LOW]
+        self.assertEqual(self.subject.fan_mode, FAN_LOW)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_FAN_MODE]] = FAN_MODE_TO_DPS_MODE[FAN_HIGH]
+        self.assertEqual(self.subject.fan_mode, FAN_HIGH)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_FAN_MODE]] = None
+        self.assertEqual(self.subject.fan_mode, None)
+
+    def test_fan_modes_reflect_preset_mode(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        self.assertEqual(self.subject.fan_modes, [FAN_LOW, FAN_HIGH])
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_LOW
+        ]
+        self.assertEqual(self.subject.fan_modes, [FAN_LOW])
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_HIGH
+        ]
+        self.assertEqual(self.subject.fan_modes, [FAN_HIGH])
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_DRY_CLOTHES
+        ]
+        self.assertEqual(self.subject.fan_modes, [FAN_HIGH])
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]] = True
+        self.assertEqual(self.subject.fan_modes, [FAN_HIGH])
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = None
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]] = False
+        self.assertEqual(self.subject.fan_modes, [])
+
+    async def test_set_fan_mode_to_low_succeeds_in_normal_preset(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PROPERTY_TO_DPS_ID[ATTR_FAN_MODE]: FAN_MODE_TO_DPS_MODE[FAN_LOW]},
+        ):
+            await self.subject.async_set_fan_mode(FAN_LOW)
+
+    async def test_set_fan_mode_to_high_succeeds_in_normal_preset(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PROPERTY_TO_DPS_ID[ATTR_FAN_MODE]: FAN_MODE_TO_DPS_MODE[FAN_HIGH]},
+        ):
+            await self.subject.async_set_fan_mode(FAN_HIGH)
+
+    async def test_set_fan_mode_fails_with_invalid_mode(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        with self.assertRaisesRegex(ValueError, "Invalid fan mode: something"):
+            await self.subject.async_set_fan_mode("something")
+
+    async def test_set_fan_mode_fails_outside_normal_preset(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_LOW
+        ]
+        with self.assertRaisesRegex(
+            ValueError, "Fan mode can only be changed while in Normal preset mode"
+        ):
+            await self.subject.async_set_fan_mode(FAN_HIGH)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_HIGH
+        ]
+        with self.assertRaisesRegex(
+            ValueError, "Fan mode can only be changed while in Normal preset mode"
+        ):
+            await self.subject.async_set_fan_mode(FAN_HIGH)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_DRY_CLOTHES
+        ]
+        with self.assertRaisesRegex(
+            ValueError, "Fan mode can only be changed while in Normal preset mode"
+        ):
+            await self.subject.async_set_fan_mode(FAN_HIGH)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]] = PRESET_MODE_TO_DPS_MODE[
+            PRESET_NORMAL
+        ]
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON]] = True
+        with self.assertRaisesRegex(
+            ValueError, "Fan mode can only be changed while in Normal preset mode"
+        ):
+            await self.subject.async_set_fan_mode(FAN_HIGH)
+
+    def test_tank_full_or_missing(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_ERROR]] = None
+        self.assertEqual(self.subject.tank_full_or_missing, False)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_ERROR]] = ERROR_CODE_TO_DPS_CODE[ERROR_TANK]
+        self.assertEqual(self.subject.tank_full_or_missing, True)
+
+    def test_defrosting(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_DEFROSTING]] = False
+        self.assertEqual(self.subject.defrosting, False)
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_DEFROSTING]] = True
+        self.assertEqual(self.subject.defrosting, True)
+
+    def test_device_state_attributes(self):
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_ERROR]] = None
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_DEFROSTING]] = False
+        self.assertEqual(
+            self.subject.device_state_attributes,
+            {ATTR_ERROR: None, ATTR_DEFROSTING: False},
+        )
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_ERROR]] = ERROR_CODE_TO_DPS_CODE[ERROR_TANK]
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_DEFROSTING]] = False
+        self.assertEqual(
+            self.subject.device_state_attributes,
+            {ATTR_ERROR: ERROR_TANK, ATTR_DEFROSTING: False},
+        )
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_ERROR]] = None
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_DEFROSTING]] = True
+        self.assertEqual(
+            self.subject.device_state_attributes,
+            {ATTR_ERROR: None, ATTR_DEFROSTING: True},
+        )
+
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_ERROR]] = ERROR_CODE_TO_DPS_CODE[ERROR_TANK]
+        self.dps[PROPERTY_TO_DPS_ID[ATTR_DEFROSTING]] = True
+        self.assertEqual(
+            self.subject.device_state_attributes,
+            {ATTR_ERROR: ERROR_TANK, ATTR_DEFROSTING: True},
+        )
+
+    async def test_update(self):
+        result = AsyncMock()
+        self.subject._device.async_refresh.return_value = result()
+
+        await self.subject.async_update()
+
+        self.subject._device.async_refresh.assert_called_once()
+        result.assert_awaited()

+ 2 - 2
tests/geco_heater/test_climate.py

@@ -101,12 +101,12 @@ class TestGoldairGECOHeater(IsolatedAsyncioTestCase):
 
     async def test_set_target_temperature_fails_outside_valid_range(self):
         with self.assertRaisesRegex(
-            ValueError, "Target temperature \(14\) must be between 15 and 35"
+            ValueError, "Target temperature \\(14\\) must be between 15 and 35"
         ):
             await self.subject.async_set_target_temperature(14)
 
         with self.assertRaisesRegex(
-            ValueError, "Target temperature \(36\) must be between 15 and 35"
+            ValueError, "Target temperature \\(36\\) must be between 15 and 35"
         ):
             await self.subject.async_set_target_temperature(36)
 

+ 2 - 2
tests/gpcv_heater/test_climate.py

@@ -127,12 +127,12 @@ class TestGoldairGPCVHeater(IsolatedAsyncioTestCase):
 
     async def test_set_target_temperature_fails_outside_valid_range(self):
         with self.assertRaisesRegex(
-            ValueError, "Target temperature \(14\) must be between 15 and 35"
+            ValueError, "Target temperature \\(14\\) must be between 15 and 35"
         ):
             await self.subject.async_set_target_temperature(14)
 
         with self.assertRaisesRegex(
-            ValueError, "Target temperature \(36\) must be between 15 and 35"
+            ValueError, "Target temperature \\(36\\) must be between 15 and 35"
         ):
             await self.subject.async_set_target_temperature(36)