Przeglądaj źródła

Add back tests for devices converted to generic classes.

To confirm that the generic classes are meeting the requirements, add back device
specific tests for all supported devices.
Jason Rumney 4 lat temu
rodzic
commit
1977147dfc

+ 2 - 0
tests/devices/test_andersson_gsh_heater.py

@@ -195,6 +195,8 @@ class TestAnderssonGSHHeater(IsolatedAsyncioTestCase):
         # they are discovered
         self.dps[ERROR_DPS] = "something"
         self.assertEqual(self.subject.device_state_attributes, {"error": "something"})
+        self.dps[ERROR_DPS] = "0"
+        self.assertEqual(self.subject.device_state_attributes, {"error": "OK"})
 
     async def test_update(self):
         result = AsyncMock()

+ 158 - 0
tests/devices/test_eurom_600_heater.py

@@ -0,0 +1,158 @@
+from unittest import IsolatedAsyncioTestCase
+from unittest.mock import AsyncMock, patch
+
+from homeassistant.components.climate.const import (
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import STATE_UNAVAILABLE
+
+from custom_components.tuya_local.generic.climate import TuyaLocalClimate
+from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
+
+from ..const import EUROM_600_HEATER_PAYLOAD
+from ..helpers import assert_device_properties_set
+
+HVACMODE_DPS = "1"
+TEMPERATURE_DPS = "2"
+CURRENTTEMP_DPS = "5"
+ERROR_DPS = "6"
+
+
+class TestEurom600Heater(IsolatedAsyncioTestCase):
+    def setUp(self):
+        device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
+        self.addCleanup(device_patcher.stop)
+        self.mock_device = device_patcher.start()
+        cfg = TuyaDeviceConfig("eurom_600_heater.yaml")
+        climate = cfg.primary_entity
+        self.climate_name = climate.name
+        self.subject = TuyaLocalClimate(self.mock_device, climate)
+        self.dps = EUROM_600_HEATER_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_TEMPERATURE,
+        )
+
+    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(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:radiator")
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.icon, "mdi:radiator-disabled")
+
+    def test_temperature_unit_returns_device_temperature_unit(self):
+        self.assertEqual(
+            self.subject.temperature_unit, self.subject._device.temperature_unit
+        )
+
+    def test_target_temperature(self):
+        self.dps[TEMPERATURE_DPS] = 25
+        self.assertEqual(self.subject.target_temperature, 25)
+
+    def test_target_temperature_step(self):
+        self.assertEqual(self.subject.target_temperature_step, 1)
+
+    def test_minimum_target_temperature(self):
+        self.assertEqual(self.subject.min_temp, 15)
+
+    def test_maximum_target_temperature(self):
+        self.assertEqual(self.subject.max_temp, 35)
+
+    async def test_legacy_set_temperature_with_temperature(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 24}
+        ):
+            await self.subject.async_set_temperature(temperature=24)
+
+    async def test_legacy_set_temperature_with_no_valid_properties(self):
+        await self.subject.async_set_temperature(something="else")
+        self.subject._device.async_set_property.assert_not_called
+
+    async def test_set_target_temperature_succeeds_within_valid_range(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {TEMPERATURE_DPS: 25},
+        ):
+            await self.subject.async_set_target_temperature(25)
+
+    async def test_set_target_temperature_rounds_value_to_closest_integer(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 23}
+        ):
+            await self.subject.async_set_target_temperature(22.6)
+
+    async def test_set_target_temperature_fails_outside_valid_range(self):
+        with self.assertRaisesRegex(
+            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"
+        ):
+            await self.subject.async_set_target_temperature(36)
+
+    def test_current_temperature(self):
+        self.dps[CURRENTTEMP_DPS] = 25
+        self.assertEqual(self.subject.current_temperature, 25)
+
+    def test_hvac_mode(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_HEAT)
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+
+        self.dps[HVACMODE_DPS] = None
+        self.assertEqual(self.subject.hvac_mode, STATE_UNAVAILABLE)
+
+    def test_hvac_modes(self):
+        self.assertCountEqual(self.subject.hvac_modes, [HVAC_MODE_OFF, HVAC_MODE_HEAT])
+
+    async def test_turn_on(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: True}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_HEAT)
+
+    async def test_turn_off(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: False}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_OFF)
+
+    def test_error_state(self):
+        # There are currently no known error states; update this as
+        # they are discovered
+        self.dps[ERROR_DPS] = "something"
+        self.assertEqual(self.subject.device_state_attributes, {"error": "something"})
+        self.dps[ERROR_DPS] = "0"
+        self.assertEqual(self.subject.device_state_attributes, {"error": "OK"})
+
+    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()

+ 227 - 0
tests/devices/test_goldair_geco_heater.py

@@ -0,0 +1,227 @@
+from unittest import IsolatedAsyncioTestCase
+from unittest.mock import AsyncMock, patch
+
+from homeassistant.components.climate.const import (
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
+from homeassistant.const import STATE_UNAVAILABLE
+
+from custom_components.tuya_local.generic.climate import TuyaLocalClimate
+from custom_components.tuya_local.generic.lock import TuyaLocalLock
+
+from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
+
+from ..const import GECO_HEATER_PAYLOAD
+from ..helpers import assert_device_properties_set
+
+HVACMODE_DPS = "1"
+LOCK_DPS = "2"
+TEMPERATURE_DPS = "3"
+CURRENTTEMP_DPS = "4"
+TIMER_DPS = "5"
+ERROR_DPS = "6"
+
+
+class TestGoldairGECOHeater(IsolatedAsyncioTestCase):
+    def setUp(self):
+        device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
+        self.addCleanup(device_patcher.stop)
+        self.mock_device = device_patcher.start()
+        cfg = TuyaDeviceConfig("goldair_geco_heater.yaml")
+        climate = cfg.primary_entity
+        lock = None
+        for e in cfg.secondary_entities():
+            if e.entity == "lock":
+                lock = e
+        self.climate_name = climate.name
+        self.lock_name = "missing" if lock is None else lock.name
+
+        self.subject = TuyaLocalClimate(self.mock_device, climate)
+        self.lock = None if lock is None else TuyaLocalLock(self.mock_device, lock)
+
+        self.dps = GECO_HEATER_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_TEMPERATURE,
+        )
+
+    def test_should_poll(self):
+        self.assertTrue(self.subject.should_poll)
+        self.assertTrue(self.lock.should_poll)
+
+    def test_name_returns_device_name(self):
+        self.assertEqual(self.subject.name, self.subject._device.name)
+        self.assertEqual(self.lock.name, self.subject._device.name)
+
+    def test_friendly_name_returns_config_name(self):
+        self.assertEqual(self.subject.friendly_name, self.climate_name)
+        self.assertEqual(self.lock.friendly_name, self.lock_name)
+
+    def test_unique_id_returns_device_unique_id(self):
+        self.assertEqual(self.subject.unique_id, self.subject._device.unique_id)
+        self.assertEqual(self.lock.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)
+        self.assertEqual(self.lock.device_info, self.subject._device.device_info)
+
+    def test_icon(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:radiator")
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.icon, "mdi:radiator-disabled")
+
+    def test_temperature_unit_returns_device_temperature_unit(self):
+        self.assertEqual(
+            self.subject.temperature_unit, self.subject._device.temperature_unit
+        )
+
+    def test_target_temperature(self):
+        self.dps[TEMPERATURE_DPS] = 25
+        self.assertEqual(self.subject.target_temperature, 25)
+
+    def test_target_temperature_step(self):
+        self.assertEqual(self.subject.target_temperature_step, 1)
+
+    def test_minimum_target_temperature(self):
+        self.assertEqual(self.subject.min_temp, 15)
+
+    def test_maximum_target_temperature(self):
+        self.assertEqual(self.subject.max_temp, 35)
+
+    async def test_legacy_set_temperature_with_temperature(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 24}
+        ):
+            await self.subject.async_set_temperature(temperature=24)
+
+    async def test_legacy_set_temperature_with_no_valid_properties(self):
+        await self.subject.async_set_temperature(something="else")
+        self.subject._device.async_set_property.assert_not_called
+
+    async def test_set_target_temperature_succeeds_within_valid_range(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {TEMPERATURE_DPS: 25},
+        ):
+            await self.subject.async_set_target_temperature(25)
+
+    async def test_set_target_temperature_rounds_value_to_closest_integer(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 23}
+        ):
+            await self.subject.async_set_target_temperature(22.6)
+
+    async def test_set_target_temperature_fails_outside_valid_range(self):
+        with self.assertRaisesRegex(
+            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"
+        ):
+            await self.subject.async_set_target_temperature(36)
+
+    def test_current_temperature(self):
+        self.dps[CURRENTTEMP_DPS] = 25
+        self.assertEqual(self.subject.current_temperature, 25)
+
+    def test_hvac_mode(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_HEAT)
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+
+        self.dps[HVACMODE_DPS] = None
+        self.assertEqual(self.subject.hvac_mode, STATE_UNAVAILABLE)
+
+    def test_hvac_modes(self):
+        self.assertCountEqual(self.subject.hvac_modes, [HVAC_MODE_OFF, HVAC_MODE_HEAT])
+
+    async def test_turn_on(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: True}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_HEAT)
+
+    async def test_turn_off(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: False}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_OFF)
+
+    def test_state_attributes(self):
+        # There are currently no known error states; update this as
+        # they are discovered
+        self.dps[ERROR_DPS] = "something"
+        self.dps[TIMER_DPS] = 10
+        self.assertCountEqual(
+            self.subject.device_state_attributes, {"error": "something", "timer": 10}
+        )
+        self.dps[ERROR_DPS] = "0"
+        self.dps[TIMER_DPS] = 0
+        self.assertCountEqual(
+            self.subject.device_state_attributes, {"error": "OK", "timer": 0}
+        )
+
+    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()
+
+    def test_lock_was_created(self):
+        self.assertIsInstance(self.lock, TuyaLocalLock)
+
+    def test_lock_is_same_device(self):
+        self.assertEqual(self.lock._device, self.subject._device)
+
+    def test_lock_state(self):
+        self.dps[LOCK_DPS] = True
+        self.assertEqual(self.lock.state, STATE_LOCKED)
+
+        self.dps[LOCK_DPS] = False
+        self.assertEqual(self.lock.state, STATE_UNLOCKED)
+
+        self.dps[LOCK_DPS] = None
+        self.assertEqual(self.lock.state, STATE_UNAVAILABLE)
+
+    def test_lock_is_locked(self):
+        self.dps[LOCK_DPS] = True
+        self.assertTrue(self.lock.is_locked)
+
+        self.dps[LOCK_DPS] = False
+        self.assertFalse(self.lock.is_locked)
+
+        self.dps[LOCK_DPS] = None
+        self.assertFalse(self.lock.is_locked)
+
+    async def async_test_lock_locks(self):
+        async with assert_device_properties_set(self.lock._device, {LOCK_DPS: True}):
+            await self.subject.async_lock()
+
+    async def async_test_lock_unlocks(self):
+        async with assert_device_properties_set(self.lock._device, {LOCK_DPS: False}):
+            await self.subject.async_unlock()
+
+    async def async_test_lock_update(self):
+        result = AsyncMock()
+        self.lock._device.async_refresh.return_value = result()
+
+        await self.lock.async_update()
+
+        self.lock._device.async_refresh.assert_called_once()
+        result.assert_awaited()

+ 268 - 0
tests/devices/test_goldair_gpcv_heater.py

@@ -0,0 +1,268 @@
+from unittest import IsolatedAsyncioTestCase
+from unittest.mock import AsyncMock, patch
+
+from homeassistant.components.climate.const import (
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
+from homeassistant.const import STATE_UNAVAILABLE
+
+from custom_components.tuya_local.generic.climate import TuyaLocalClimate
+from custom_components.tuya_local.generic.lock import TuyaLocalLock
+
+from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
+
+from ..const import GPCV_HEATER_PAYLOAD
+from ..helpers import assert_device_properties_set
+
+HVACMODE_DPS = "1"
+LOCK_DPS = "2"
+TEMPERATURE_DPS = "3"
+CURRENTTEMP_DPS = "4"
+TIMER_DPS = "5"
+ERROR_DPS = "6"
+PRESET_DPS = "7"
+
+
+class TestGoldairGPCVHeater(IsolatedAsyncioTestCase):
+    def setUp(self):
+        device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
+        self.addCleanup(device_patcher.stop)
+        self.mock_device = device_patcher.start()
+        cfg = TuyaDeviceConfig("goldair_gpcv_heater.yaml")
+        climate = cfg.primary_entity
+        lock = None
+        for e in cfg.secondary_entities():
+            if e.entity == "lock":
+                lock = e
+        self.climate_name = climate.name
+        self.lock_name = "missing" if lock is None else lock.name
+
+        self.subject = TuyaLocalClimate(self.mock_device, climate)
+        self.lock = None if lock is None else TuyaLocalLock(self.mock_device, lock)
+
+        self.dps = GPCV_HEATER_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_TEMPERATURE | SUPPORT_PRESET_MODE,
+        )
+
+    def test_should_poll(self):
+        self.assertTrue(self.subject.should_poll)
+        self.assertTrue(self.lock.should_poll)
+
+    def test_name_returns_device_name(self):
+        self.assertEqual(self.subject.name, self.subject._device.name)
+        self.assertEqual(self.lock.name, self.subject._device.name)
+
+    def test_friendly_name_returns_config_name(self):
+        self.assertEqual(self.subject.friendly_name, self.climate_name)
+        self.assertEqual(self.lock.friendly_name, self.lock_name)
+
+    def test_unique_id_returns_device_unique_id(self):
+        self.assertEqual(self.subject.unique_id, self.subject._device.unique_id)
+        self.assertEqual(self.lock.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)
+        self.assertEqual(self.lock.device_info, self.subject._device.device_info)
+
+    def test_icon(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:radiator")
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.icon, "mdi:radiator-disabled")
+
+    def test_temperature_unit_returns_device_temperature_unit(self):
+        self.assertEqual(
+            self.subject.temperature_unit, self.subject._device.temperature_unit
+        )
+
+    def test_target_temperature(self):
+        self.dps[TEMPERATURE_DPS] = 25
+        self.assertEqual(self.subject.target_temperature, 25)
+
+    def test_target_temperature_step(self):
+        self.assertEqual(self.subject.target_temperature_step, 1)
+
+    def test_minimum_target_temperature(self):
+        self.assertEqual(self.subject.min_temp, 15)
+
+    def test_maximum_target_temperature(self):
+        self.assertEqual(self.subject.max_temp, 35)
+
+    async def test_legacy_set_temperature_with_temperature(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 24}
+        ):
+            await self.subject.async_set_temperature(temperature=24)
+
+    async def test_legacy_set_temperature_with_preset_mode(self):
+        async with assert_device_properties_set(
+            self.subject._device, {PRESET_DPS: "Low"}
+        ):
+            await self.subject.async_set_temperature(preset_mode="Low")
+
+    async def test_legacy_set_temperature_with_both_properties(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 26, PRESET_DPS: "High"}
+        ):
+            await self.subject.async_set_temperature(temperature=26, preset_mode="High")
+
+    async def test_legacy_set_temperature_with_no_valid_properties(self):
+        await self.subject.async_set_temperature(something="else")
+        self.subject._device.async_set_property.assert_not_called
+
+    async def test_set_target_temperature_succeeds_within_valid_range(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {TEMPERATURE_DPS: 25},
+        ):
+            await self.subject.async_set_target_temperature(25)
+
+    async def test_set_target_temperature_rounds_value_to_closest_integer(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 23}
+        ):
+            await self.subject.async_set_target_temperature(22.6)
+
+    async def test_set_target_temperature_fails_outside_valid_range(self):
+        with self.assertRaisesRegex(
+            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"
+        ):
+            await self.subject.async_set_target_temperature(36)
+
+    def test_current_temperature(self):
+        self.dps[CURRENTTEMP_DPS] = 25
+        self.assertEqual(self.subject.current_temperature, 25)
+
+    def test_hvac_mode(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_HEAT)
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+
+        self.dps[HVACMODE_DPS] = None
+        self.assertEqual(self.subject.hvac_mode, STATE_UNAVAILABLE)
+
+    def test_hvac_modes(self):
+        self.assertCountEqual(self.subject.hvac_modes, [HVAC_MODE_OFF, HVAC_MODE_HEAT])
+
+    async def test_turn_on(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: True}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_HEAT)
+
+    async def test_turn_off(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: False}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_OFF)
+
+    def test_preset_mode(self):
+        self.dps[PRESET_DPS] = "Low"
+        self.assertEqual(self.subject.preset_mode, "Low")
+
+        self.dps[PRESET_DPS] = "High"
+        self.assertEqual(self.subject.preset_mode, "High")
+
+        self.dps[PRESET_DPS] = None
+        self.assertIs(self.subject.preset_mode, None)
+
+    def test_preset_modes(self):
+        self.assertCountEqual(self.subject.preset_modes, ["Low", "High"])
+
+    async def test_set_preset_mode_to_low(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "Low"},
+        ):
+            await self.subject.async_set_preset_mode("Low")
+
+    async def test_set_preset_mode_to_high(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "High"},
+        ):
+            await self.subject.async_set_preset_mode("High")
+
+    def test_state_attributes(self):
+        # There are currently no known error states; update this as
+        # they are discovered
+        self.dps[ERROR_DPS] = "something"
+        self.dps[TIMER_DPS] = 10
+        self.assertCountEqual(
+            self.subject.device_state_attributes, {"error": "something", "timer": 10}
+        )
+        self.dps[ERROR_DPS] = "0"
+        self.dps[TIMER_DPS] = 0
+        self.assertCountEqual(
+            self.subject.device_state_attributes, {"error": "OK", "timer": 0}
+        )
+
+    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()
+
+    def test_lock_was_created(self):
+        self.assertIsInstance(self.lock, TuyaLocalLock)
+
+    def test_lock_is_same_device(self):
+        self.assertEqual(self.lock._device, self.subject._device)
+
+    def test_lock_state(self):
+        self.dps[LOCK_DPS] = True
+        self.assertEqual(self.lock.state, STATE_LOCKED)
+
+        self.dps[LOCK_DPS] = False
+        self.assertEqual(self.lock.state, STATE_UNLOCKED)
+
+        self.dps[LOCK_DPS] = None
+        self.assertEqual(self.lock.state, STATE_UNAVAILABLE)
+
+    def test_lock_is_locked(self):
+        self.dps[LOCK_DPS] = True
+        self.assertTrue(self.lock.is_locked)
+
+        self.dps[LOCK_DPS] = False
+        self.assertFalse(self.lock.is_locked)
+
+        self.dps[LOCK_DPS] = None
+        self.assertFalse(self.lock.is_locked)
+
+    async def async_test_lock_locks(self):
+        async with assert_device_properties_set(self.lock._device, {LOCK_DPS: True}):
+            await self.subject.async_lock()
+
+    async def async_test_lock_unlocks(self):
+        async with assert_device_properties_set(self.lock._device, {LOCK_DPS: False}):
+            await self.subject.async_unlock()
+
+    async def async_test_lock_update(self):
+        result = AsyncMock()
+        self.lock._device.async_refresh.return_value = result()
+
+        await self.lock.async_update()
+
+        self.lock._device.async_refresh.assert_called_once()
+        result.assert_awaited()

+ 259 - 0
tests/devices/test_kogan_heater.py

@@ -0,0 +1,259 @@
+from unittest import IsolatedAsyncioTestCase
+from unittest.mock import AsyncMock, patch
+
+from homeassistant.components.climate.const import (
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
+from homeassistant.const import STATE_UNAVAILABLE
+
+from custom_components.tuya_local.generic.climate import TuyaLocalClimate
+from custom_components.tuya_local.generic.lock import TuyaLocalLock
+
+from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
+
+from ..const import KOGAN_HEATER_PAYLOAD
+from ..helpers import assert_device_properties_set
+
+TEMPERATURE_DPS = "2"
+CURRENTTEMP_DPS = "3"
+PRESET_DPS = "4"
+LOCK_DPS = "6"
+HVACMODE_DPS = "7"
+TIMER_DPS = "8"
+
+
+class TestGoldairKoganHeater(IsolatedAsyncioTestCase):
+    def setUp(self):
+        device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
+        self.addCleanup(device_patcher.stop)
+        self.mock_device = device_patcher.start()
+        cfg = TuyaDeviceConfig("kogan_heater.yaml")
+        climate = cfg.primary_entity
+        lock = None
+        for e in cfg.secondary_entities():
+            if e.entity == "lock":
+                lock = e
+        self.climate_name = climate.name
+        self.lock_name = "missing" if lock is None else lock.name
+
+        self.subject = TuyaLocalClimate(self.mock_device, climate)
+        self.lock = None if lock is None else TuyaLocalLock(self.mock_device, lock)
+
+        self.dps = KOGAN_HEATER_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_TEMPERATURE | SUPPORT_PRESET_MODE,
+        )
+
+    def test_should_poll(self):
+        self.assertTrue(self.subject.should_poll)
+        self.assertTrue(self.lock.should_poll)
+
+    def test_name_returns_device_name(self):
+        self.assertEqual(self.subject.name, self.subject._device.name)
+        self.assertEqual(self.lock.name, self.subject._device.name)
+
+    def test_friendly_name_returns_config_name(self):
+        self.assertEqual(self.subject.friendly_name, self.climate_name)
+        self.assertEqual(self.lock.friendly_name, self.lock_name)
+
+    def test_unique_id_returns_device_unique_id(self):
+        self.assertEqual(self.subject.unique_id, self.subject._device.unique_id)
+        self.assertEqual(self.lock.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)
+        self.assertEqual(self.lock.device_info, self.subject._device.device_info)
+
+    def test_icon(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:radiator")
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.icon, "mdi:radiator-disabled")
+
+    def test_temperature_unit_returns_device_temperature_unit(self):
+        self.assertEqual(
+            self.subject.temperature_unit, self.subject._device.temperature_unit
+        )
+
+    def test_target_temperature(self):
+        self.dps[TEMPERATURE_DPS] = 25
+        self.assertEqual(self.subject.target_temperature, 25)
+
+    def test_target_temperature_step(self):
+        self.assertEqual(self.subject.target_temperature_step, 1)
+
+    def test_minimum_target_temperature(self):
+        self.assertEqual(self.subject.min_temp, 15)
+
+    def test_maximum_target_temperature(self):
+        self.assertEqual(self.subject.max_temp, 35)
+
+    async def test_legacy_set_temperature_with_temperature(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 24}
+        ):
+            await self.subject.async_set_temperature(temperature=24)
+
+    async def test_legacy_set_temperature_with_preset_mode(self):
+        async with assert_device_properties_set(
+            self.subject._device, {PRESET_DPS: "Low"}
+        ):
+            await self.subject.async_set_temperature(preset_mode="Low")
+
+    async def test_legacy_set_temperature_with_both_properties(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 26, PRESET_DPS: "High"}
+        ):
+            await self.subject.async_set_temperature(temperature=26, preset_mode="High")
+
+    async def test_legacy_set_temperature_with_no_valid_properties(self):
+        await self.subject.async_set_temperature(something="else")
+        self.subject._device.async_set_property.assert_not_called
+
+    async def test_set_target_temperature_succeeds_within_valid_range(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {TEMPERATURE_DPS: 25},
+        ):
+            await self.subject.async_set_target_temperature(25)
+
+    async def test_set_target_temperature_rounds_value_to_closest_integer(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 23}
+        ):
+            await self.subject.async_set_target_temperature(22.6)
+
+    async def test_set_target_temperature_fails_outside_valid_range(self):
+        with self.assertRaisesRegex(
+            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"
+        ):
+            await self.subject.async_set_target_temperature(36)
+
+    def test_current_temperature(self):
+        self.dps[CURRENTTEMP_DPS] = 25
+        self.assertEqual(self.subject.current_temperature, 25)
+
+    def test_hvac_mode(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_HEAT)
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+
+        self.dps[HVACMODE_DPS] = None
+        self.assertEqual(self.subject.hvac_mode, STATE_UNAVAILABLE)
+
+    def test_hvac_modes(self):
+        self.assertCountEqual(self.subject.hvac_modes, [HVAC_MODE_OFF, HVAC_MODE_HEAT])
+
+    async def test_turn_on(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: True}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_HEAT)
+
+    async def test_turn_off(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: False}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_OFF)
+
+    def test_preset_mode(self):
+        self.dps[PRESET_DPS] = "Low"
+        self.assertEqual(self.subject.preset_mode, "Low")
+
+        self.dps[PRESET_DPS] = "High"
+        self.assertEqual(self.subject.preset_mode, "High")
+
+        self.dps[PRESET_DPS] = None
+        self.assertIs(self.subject.preset_mode, None)
+
+    def test_preset_modes(self):
+        self.assertCountEqual(self.subject.preset_modes, ["Low", "High"])
+
+    async def test_set_preset_mode_to_low(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "Low"},
+        ):
+            await self.subject.async_set_preset_mode("Low")
+
+    async def test_set_preset_mode_to_high(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "High"},
+        ):
+            await self.subject.async_set_preset_mode("High")
+
+    def test_state_attributes(self):
+        self.dps[TIMER_DPS] = 10
+        self.assertCountEqual(self.subject.device_state_attributes, {"timer": 10})
+        self.dps[TIMER_DPS] = 0
+        self.assertCountEqual(self.subject.device_state_attributes, {"timer": 0})
+
+    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()
+
+    def test_lock_was_created(self):
+        self.assertIsInstance(self.lock, TuyaLocalLock)
+
+    def test_lock_is_same_device(self):
+        self.assertEqual(self.lock._device, self.subject._device)
+
+    def test_lock_state(self):
+        self.dps[LOCK_DPS] = True
+        self.assertEqual(self.lock.state, STATE_LOCKED)
+
+        self.dps[LOCK_DPS] = False
+        self.assertEqual(self.lock.state, STATE_UNLOCKED)
+
+        self.dps[LOCK_DPS] = None
+        self.assertEqual(self.lock.state, STATE_UNAVAILABLE)
+
+    def test_lock_is_locked(self):
+        self.dps[LOCK_DPS] = True
+        self.assertTrue(self.lock.is_locked)
+
+        self.dps[LOCK_DPS] = False
+        self.assertFalse(self.lock.is_locked)
+
+        self.dps[LOCK_DPS] = None
+        self.assertFalse(self.lock.is_locked)
+
+    async def async_test_lock_locks(self):
+        async with assert_device_properties_set(self.lock._device, {LOCK_DPS: True}):
+            await self.subject.async_lock()
+
+    async def async_test_lock_unlocks(self):
+        async with assert_device_properties_set(self.lock._device, {LOCK_DPS: False}):
+            await self.subject.async_unlock()
+
+    async def async_test_lock_update(self):
+        result = AsyncMock()
+        self.lock._device.async_refresh.return_value = result()
+
+        await self.lock.async_update()
+
+        self.lock._device.async_refresh.assert_called_once()
+        result.assert_awaited()

+ 131 - 0
tests/devices/test_kogan_switch.py

@@ -0,0 +1,131 @@
+"""Tests for the switch entity."""
+from unittest import IsolatedAsyncioTestCase
+from unittest.mock import AsyncMock, patch
+
+from homeassistant.components.switch import DEVICE_CLASS_OUTLET
+from homeassistant.const import STATE_UNAVAILABLE
+
+from custom_components.tuya_local.generic.switch import TuyaLocalSwitch
+from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
+
+from ..const import KOGAN_SOCKET_PAYLOAD
+from ..helpers import assert_device_properties_set
+
+SWITCH_DPS = "1"
+TIMER_DPS = "2"
+CURRENT_DPS = "4"
+POWER_DPS = "5"
+VOLTAGE_DPS = "6"
+
+
+class TestKoganSwitch(IsolatedAsyncioTestCase):
+    def setUp(self):
+        device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
+        self.addCleanup(device_patcher.stop)
+        self.mock_device = device_patcher.start()
+        cfg = TuyaDeviceConfig("kogan_switch.yaml")
+        switch = cfg.primary_entity
+        self.switch_name = switch.name
+        self.subject = TuyaLocalSwitch(self.mock_device(), switch)
+        self.dps = KOGAN_SOCKET_PAYLOAD.copy()
+
+        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+
+    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_friendly_name_returns_config_name(self):
+        self.assertEqual(self.subject.friendly_name, self.switch_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_device_class_is_outlet(self):
+        self.assertEqual(self.subject.device_class, DEVICE_CLASS_OUTLET)
+
+    def test_is_on(self):
+        self.dps[SWITCH_DPS] - True
+        self.assertTrue(self.subject.is_on)
+
+        self.dps[SWITCH_DPS] = False
+        self.assertFalse(self.subject.is_on)
+
+    def test_is_on_when_unavailable(self):
+        self.dps[SWITCH_DPS] = None
+        self.assertEqual(self.subject.is_on, STATE_UNAVAILABLE)
+
+    async def test_turn_on(self):
+        async with assert_device_properties_set(
+            self.subject._device, {SWITCH_DPS: True}
+        ):
+            await self.subject.async_turn_on()
+
+    async def test_turn_off(self):
+        async with assert_device_properties_set(
+            self.subject._device, {SWITCH_DPS: False}
+        ):
+            await self.subject.async_turn_off()
+
+    async def test_toggle_turns_the_switch_on_when_it_was_off(self):
+        self.dps[SWITCH_DPS] = False
+
+        async with assert_device_properties_set(
+            self.subject._device, {SWITCH_DPS: True}
+        ):
+            await self.subject.async_toggle()
+
+    async def test_toggle_turns_the_switch_off_when_it_was_on(self):
+        self.dps[SWITCH_DPS] = True
+
+        async with assert_device_properties_set(
+            self.subject._device, {SWITCH_DPS: False}
+        ):
+            await self.subject.async_toggle()
+
+    def test_current_power_w(self):
+        self.dps[POWER_DPS] = 1234
+        self.assertEqual(self.subject.current_power_w, 123.4)
+
+    def test_device_state_attributes_set(self):
+        self.dps[TIMER_DPS] = 1
+        self.dps[VOLTAGE_DPS] = 2350
+        self.dps[CURRENT_DPS] = 1234
+        self.dps[POWER_DPS] = 5678
+        self.assertEqual(
+            self.subject.device_state_attributes,
+            {
+                "timer": 1,
+                "current_a": 1.234,
+                "voltage_v": 235.0,
+                "current_power_w": 567.8,
+            },
+        )
+
+        self.dps[TIMER_DPS] = 0
+        self.dps[CURRENT_DPS] = None
+        self.dps[VOLTAGE_DPS] = None
+        self.dps[POWER_DPS] = None
+        self.assertEqual(
+            self.subject.device_state_attributes,
+            {
+                "timer": 0,
+                "current_a": None,
+                "voltage_v": None,
+                "current_power_w": None,
+            },
+        )
+
+    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()

+ 260 - 0
tests/devices/test_remora_heatpump.py

@@ -0,0 +1,260 @@
+from unittest import IsolatedAsyncioTestCase
+from unittest.mock import AsyncMock, patch
+
+from homeassistant.components.climate.const import (
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import STATE_UNAVAILABLE
+
+from custom_components.tuya_local.generic.climate import TuyaLocalClimate
+from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
+
+from ..const import REMORA_HEATPUMP_PAYLOAD
+from ..helpers import assert_device_properties_set
+
+HVACMODE_DPS = "1"
+TEMPERATURE_DPS = "2"
+CURRENTTEMP_DPS = "3"
+PRESET_DPS = "4"
+ERROR_DPS = "9"
+
+
+class TestRemoraHeatpump(IsolatedAsyncioTestCase):
+    def setUp(self):
+        device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
+        self.addCleanup(device_patcher.stop)
+        self.mock_device = device_patcher.start()
+        cfg = TuyaDeviceConfig("remora_heatpump.yaml")
+        climate = cfg.primary_entity
+        self.climate_name = climate.name
+        self.subject = TuyaLocalClimate(self.mock_device, climate)
+        self.dps = REMORA_HEATPUMP_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_TEMPERATURE | SUPPORT_PRESET_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(self):
+        # Temporary: since proper icon parsing from config files is not yet
+        # implemented, the icons are fixed to defaults
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:radiator")
+        # self.assertEqual(self.subject.icon, "mdi:hot-tub")
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.icon, "mdi:radiator-disabled")
+        # selt.assertEqual(self.subject.icon, "mdi:hvac_off")
+
+    def test_temperature_unit_returns_device_temperature_unit(self):
+        self.assertEqual(
+            self.subject.temperature_unit, self.subject._device.temperature_unit
+        )
+
+    def test_target_temperature(self):
+        self.dps[TEMPERATURE_DPS] = 25
+        self.assertEqual(self.subject.target_temperature, 25)
+
+    def test_target_temperature_step(self):
+        self.assertEqual(self.subject.target_temperature_step, 1)
+
+    def test_minimum_target_temperature(self):
+        self.assertEqual(self.subject.min_temp, 5)
+
+    def test_maximum_target_temperature(self):
+        self.assertEqual(self.subject.max_temp, 40)
+
+    async def test_legacy_set_temperature_with_temperature(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 24}
+        ):
+            await self.subject.async_set_temperature(temperature=24)
+
+    async def test_legacy_set_temperature_with_preset_mode(self):
+        async with assert_device_properties_set(
+            self.subject._device, {PRESET_DPS: "cool"}
+        ):
+            await self.subject.async_set_temperature(preset_mode="Smart Cooling")
+
+    async def test_legacy_set_temperature_with_both_properties(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 26, PRESET_DPS: "heat"}
+        ):
+            await self.subject.async_set_temperature(
+                temperature=26, preset_mode="Smart Heating"
+            )
+
+    async def test_legacy_set_temperature_with_no_valid_properties(self):
+        await self.subject.async_set_temperature(something="else")
+        self.subject._device.async_set_property.assert_not_called
+
+    async def test_set_target_temperature_succeeds_within_valid_range(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {TEMPERATURE_DPS: 25},
+        ):
+            await self.subject.async_set_target_temperature(25)
+
+    async def test_set_target_temperature_rounds_value_to_closest_integer(self):
+        async with assert_device_properties_set(
+            self.subject._device, {TEMPERATURE_DPS: 23}
+        ):
+            await self.subject.async_set_target_temperature(22.6)
+
+    async def test_set_target_temperature_fails_outside_valid_range(self):
+        with self.assertRaisesRegex(
+            ValueError, "Target temperature \\(4\\) must be between 5 and 40"
+        ):
+            await self.subject.async_set_target_temperature(4)
+
+        with self.assertRaisesRegex(
+            ValueError, "Target temperature \\(41\\) must be between 5 and 40"
+        ):
+            await self.subject.async_set_target_temperature(41)
+
+    def test_current_temperature(self):
+        self.dps[CURRENTTEMP_DPS] = 25
+        self.assertEqual(self.subject.current_temperature, 25)
+
+    def test_hvac_mode(self):
+        self.dps[HVACMODE_DPS] = True
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_HEAT)
+
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.hvac_mode, HVAC_MODE_OFF)
+
+        self.dps[HVACMODE_DPS] = None
+        self.assertEqual(self.subject.hvac_mode, STATE_UNAVAILABLE)
+
+    def test_hvac_modes(self):
+        self.assertCountEqual(self.subject.hvac_modes, [HVAC_MODE_OFF, HVAC_MODE_HEAT])
+
+    async def test_turn_on(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: True}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_HEAT)
+
+    async def test_turn_off(self):
+        async with assert_device_properties_set(
+            self.subject._device, {HVACMODE_DPS: False}
+        ):
+            await self.subject.async_set_hvac_mode(HVAC_MODE_OFF)
+
+    def test_preset_mode(self):
+        self.dps[PRESET_DPS] = "heat"
+        self.assertEqual(self.subject.preset_mode, "Smart Heating")
+
+        self.dps[PRESET_DPS] = "cool"
+        self.assertEqual(self.subject.preset_mode, "Smart Cooling")
+
+        self.dps[PRESET_DPS] = "h_powerful"
+        self.assertEqual(self.subject.preset_mode, "Powerful Heating")
+
+        self.dps[PRESET_DPS] = "c_powerful"
+        self.assertEqual(self.subject.preset_mode, "Powerful Cooling")
+
+        self.dps[PRESET_DPS] = "h_silent"
+        self.assertEqual(self.subject.preset_mode, "Silent Heating")
+
+        self.dps[PRESET_DPS] = "c_silent"
+        self.assertEqual(self.subject.preset_mode, "Silent Cooling")
+
+        self.dps[PRESET_DPS] = None
+        self.assertIs(self.subject.preset_mode, None)
+
+    def test_preset_modes(self):
+        self.assertCountEqual(
+            self.subject.preset_modes,
+            [
+                "Smart Heating",
+                "Powerful Heating",
+                "Silent Heating",
+                "Smart Cooling",
+                "Powerful Cooling",
+                "Silent Cooling",
+            ],
+        )
+
+    async def test_set_preset_mode_to_heat(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "heat"},
+        ):
+            await self.subject.async_set_preset_mode("Smart Heating")
+
+    async def test_set_preset_mode_to_cool(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "cool"},
+        ):
+            await self.subject.async_set_preset_mode("Smart Cooling")
+
+    async def test_set_preset_mode_to_h_powerful(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "h_powerful"},
+        ):
+            await self.subject.async_set_preset_mode("Powerful Heating")
+
+    async def test_set_preset_mode_to_c_powerful(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "c_powerful"},
+        ):
+            await self.subject.async_set_preset_mode("Powerful Cooling")
+
+    async def test_set_preset_mode_to_h_silent(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "h_silent"},
+        ):
+            await self.subject.async_set_preset_mode("Silent Heating")
+
+    async def test_set_preset_mode_to_c_silent(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "c_silent"},
+        ):
+            await self.subject.async_set_preset_mode("Silent Cooling")
+
+    def test_error_state(self):
+        self.dps[ERROR_DPS] = 0
+        self.assertEqual(self.subject.device_state_attributes, {"error": "OK"})
+
+        self.dps[ERROR_DPS] = 1
+        self.assertEqual(
+            self.subject.device_state_attributes,
+            {"error": "Water Flow Protection"},
+        )
+        self.dps[ERROR_DPS] = 2
+        self.assertEqual(
+            self.subject.device_state_attributes,
+            {"error": 2},
+        )
+
+    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()

+ 4 - 2
tests/test_device_config.py

@@ -90,14 +90,16 @@ class TestDeviceConfig(unittest.TestCase):
 
         self.assertTrue(matched)
         if quality < 100:
-            warn(f"{legacy_type} detected with quality {quality}")
+            warn(f"{legacy_type} detected with imperfect quality {quality}%")
 
         best_q = 0
         for cfg in false_matches:
             q = cfg.match_quality(payload)
             if q > best_q:
                 best_q = q
-            warn(f"{legacy_type} also detectable as {cfg.legacy_type} with quality {q}")
+            warn(
+                f"{legacy_type} also detectable as {cfg.legacy_type} with quality {q}%"
+            )
 
         self.assertGreater(quality, best_q)
 

+ 1 - 128
tests/test_switch.py

@@ -1,10 +1,6 @@
 """Tests for the switch entity."""
 from pytest_homeassistant_custom_component.common import MockConfigEntry
-from unittest import IsolatedAsyncioTestCase
-from unittest.mock import AsyncMock, Mock, patch
-
-from homeassistant.components.switch import DEVICE_CLASS_OUTLET
-from homeassistant.const import STATE_UNAVAILABLE
+from unittest.mock import AsyncMock, Mock
 
 from custom_components.tuya_local.const import (
     CONF_DEVICE_ID,
@@ -15,18 +11,8 @@ from custom_components.tuya_local.const import (
     DOMAIN,
 )
 from custom_components.tuya_local.generic.switch import TuyaLocalSwitch
-from custom_components.tuya_local.helpers.device_config import config_for_legacy_use
 from custom_components.tuya_local.switch import async_setup_entry
 
-from .const import KOGAN_SOCKET_PAYLOAD
-from .helpers import assert_device_properties_set
-
-KOGAN_SWITCH_DPS = "1"
-KOGAN_TIMER_DPS = "2"
-KOGAN_CURRENT_DPS = "4"
-KOGAN_POWER_DPS = "5"
-KOGAN_VOLTAGE_DPS = "6"
-
 
 async def test_init_entry(hass):
     """Test the initialisation."""
@@ -48,116 +34,3 @@ async def test_init_entry(hass):
     await async_setup_entry(hass, entry, m_add_entities)
     assert type(hass.data[DOMAIN]["dummy"][CONF_SWITCH]) == TuyaLocalSwitch
     m_add_entities.assert_called_once()
-
-
-class TestTuyaLocalSwitch(IsolatedAsyncioTestCase):
-    def setUp(self):
-        device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
-        self.addCleanup(device_patcher.stop)
-        self.mock_device = device_patcher.start()
-        kogan_switch_config = config_for_legacy_use(CONF_TYPE_KOGAN_SWITCH)
-        switch = kogan_switch_config.primary_entity
-        self.switch_name = switch.name
-        self.subject = TuyaLocalSwitch(self.mock_device(), switch)
-        self.dps = KOGAN_SOCKET_PAYLOAD.copy()
-
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
-
-    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_friendly_name_returns_config_name(self):
-        self.assertEqual(self.subject.friendly_name, self.switch_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_device_class_is_outlet(self):
-        self.assertEqual(self.subject.device_class, DEVICE_CLASS_OUTLET)
-
-    def test_is_on(self):
-        self.dps[KOGAN_SWITCH_DPS] - True
-        self.assertTrue(self.subject.is_on)
-
-        self.dps[KOGAN_SWITCH_DPS] = False
-        self.assertFalse(self.subject.is_on)
-
-    def test_is_on_when_unavailable(self):
-        self.dps[KOGAN_SWITCH_DPS] = None
-        self.assertEqual(self.subject.is_on, STATE_UNAVAILABLE)
-
-    async def test_turn_on(self):
-        async with assert_device_properties_set(
-            self.subject._device, {KOGAN_SWITCH_DPS: True}
-        ):
-            await self.subject.async_turn_on()
-
-    async def test_turn_off(self):
-        async with assert_device_properties_set(
-            self.subject._device, {KOGAN_SWITCH_DPS: False}
-        ):
-            await self.subject.async_turn_off()
-
-    async def test_toggle_turns_the_switch_on_when_it_was_off(self):
-        self.dps[KOGAN_SWITCH_DPS] = False
-
-        async with assert_device_properties_set(
-            self.subject._device, {KOGAN_SWITCH_DPS: True}
-        ):
-            await self.subject.async_toggle()
-
-    async def test_toggle_turns_the_switch_off_when_it_was_on(self):
-        self.dps[KOGAN_SWITCH_DPS] = True
-
-        async with assert_device_properties_set(
-            self.subject._device, {KOGAN_SWITCH_DPS: False}
-        ):
-            await self.subject.async_toggle()
-
-    def test_current_power_w(self):
-        self.dps[KOGAN_POWER_DPS] = 1234
-        self.assertEqual(self.subject.current_power_w, 123.4)
-
-    def test_device_state_attributes_set(self):
-        self.dps[KOGAN_TIMER_DPS] = 1
-        self.dps[KOGAN_VOLTAGE_DPS] = 2350
-        self.dps[KOGAN_CURRENT_DPS] = 1234
-        self.dps[KOGAN_POWER_DPS] = 5678
-        self.assertEqual(
-            self.subject.device_state_attributes,
-            {
-                "timer": 1,
-                "current_a": 1.234,
-                "voltage_v": 235.0,
-                "current_power_w": 567.8,
-            },
-        )
-
-        self.dps[KOGAN_TIMER_DPS] = 0
-        self.dps[KOGAN_CURRENT_DPS] = None
-        self.dps[KOGAN_VOLTAGE_DPS] = None
-        self.dps[KOGAN_POWER_DPS] = None
-        self.assertEqual(
-            self.subject.device_state_attributes,
-            {
-                "timer": 0,
-                "current_a": None,
-                "voltage_v": None,
-                "current_power_w": None,
-            },
-        )
-
-    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()