소스 검색

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 5 년 전
부모
커밋
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()