Quellcode durchsuchen

Refactor tests for devices.

Remove a lot of duplication for general device tests by using a base class.

This should reduce the cut and paste effort to add a new device with unit
tests.  Only tests for actual configured functionality should be required,
and the setUp is mostly implemented in the base class so there is no need
to mess with patching in every device specific test module.
Jason Rumney vor 4 Jahren
Ursprung
Commit
c62a1e4ad6

+ 92 - 0
tests/devices/base_device_tests.py

@@ -0,0 +1,92 @@
+from unittest import IsolatedAsyncioTestCase
+from unittest.mock import AsyncMock, patch
+
+from custom_components.tuya_local.generic.climate import TuyaLocalClimate
+from custom_components.tuya_local.generic.fan import TuyaLocalFan
+from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
+from custom_components.tuya_local.generic.light import TuyaLocalLight
+from custom_components.tuya_local.generic.lock import TuyaLocalLock
+from custom_components.tuya_local.generic.switch import TuyaLocalSwitch
+
+from custom_components.tuya_local.helpers.device_config import (
+    TuyaDeviceConfig,
+    possible_matches,
+)
+
+DEVICE_TYPES = {
+    "climate": TuyaLocalClimate,
+    "fan": TuyaLocalFan,
+    "humidifier": TuyaLocalHumidifier,
+    "light": TuyaLocalLight,
+    "lock": TuyaLocalLock,
+    "switch": TuyaLocalSwitch,
+}
+
+
+class TuyaDeviceTestCase(IsolatedAsyncioTestCase):
+    __test__ = False
+
+    def setUpForConfig(self, config_file, payload):
+        """Perform setup tasks for every test."""
+        device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
+        self.addCleanup(device_patcher.stop)
+        self.mock_device = device_patcher.start()
+        self.dps = payload.copy()
+        self.mock_device.get_property.side_effect = lambda id: self.dps[id]
+        cfg = TuyaDeviceConfig(config_file)
+        self.conf_type = cfg.legacy_type
+
+        self.mock_device.name = cfg.name
+
+        self.entities = {}
+        self.entities[cfg.primary_entity.entity] = self.create_entity(
+            cfg.primary_entity
+        )
+
+        self.names = {}
+        self.names[cfg.primary_entity.entity] = cfg.primary_entity.name
+        for e in cfg.secondary_entities():
+            self.entities[e.entity] = self.create_entity(e)
+            self.names[e.entity] = e.name
+
+    def create_entity(self, config):
+        """Create an entity to match the config"""
+        dev_type = DEVICE_TYPES[config.entity]
+        if dev_type:
+            return dev_type(self.mock_device, config)
+
+    def test_config_matched(self):
+        for cfg in possible_matches(self.dps):
+            if cfg.legacy_type == self.conf_type:
+                self.assertEqual(cfg.match_quality(self.dps), 100.0)
+                return
+        self.fail()
+
+    def test_should_poll(self):
+        for e in self.entities.values():
+            self.assertTrue(e.should_poll)
+
+    def test_name_returns_device_name(self):
+        for e in self.entities.values():
+            self.assertEqual(e.name, self.mock_device.name)
+
+    def test_friendly_name_returns_config_name(self):
+        for e in self.entities:
+            self.assertEqual(self.entities[e].friendly_name, self.names[e])
+
+    def test_unique_id_returns_device_unique_id(self):
+        for e in self.entities.values():
+            self.assertEqual(e.unique_id, self.mock_device.unique_id)
+
+    def test_device_info_returns_device_info_from_device(self):
+        for e in self.entities.values():
+            self.assertEqual(e.device_info, self.mock_device.device_info)
+
+    async def test_update(self):
+        for e in self.entities.values():
+            result = AsyncMock()
+            self.mock_device.async_refresh.return_value = result()
+            self.mock_device.async_refresh.reset_mock()
+            await e.async_update()
+            self.mock_device.async_refresh.assert_called_once()
+            result.assert_awaited()

+ 6 - 38
tests/devices/test_andersson_gsh_heater.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -9,11 +6,9 @@ from homeassistant.components.climate.const import (
 )
 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 GSH_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 TEMPERATURE_DPS = "2"
@@ -22,18 +17,12 @@ PRESET_DPS = "4"
 ERROR_DPS = "12"
 
 
-class TestAnderssonGSHHeater(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("andersson_gsh_heater.yaml")
-        climate = cfg.primary_entity
-        self.climate_name = climate.name
-        self.subject = TuyaLocalClimate(self.mock_device, climate)
-        self.dps = GSH_HEATER_PAYLOAD.copy()
+class TestAnderssonGSHHeater(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("andersson_gsh_heater.yaml", GSH_HEATER_PAYLOAD)
+        self.subject = self.entities["climate"]
 
     def test_supported_features(self):
         self.assertEqual(
@@ -41,18 +30,6 @@ class TestAnderssonGSHHeater(IsolatedAsyncioTestCase):
             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):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:radiator")
@@ -197,12 +174,3 @@ class TestAnderssonGSHHeater(IsolatedAsyncioTestCase):
         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()

+ 6 - 46
tests/devices/test_anko_fan.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.fan import (
     SUPPORT_OSCILLATE,
     SUPPORT_PRESET_MODE,
@@ -8,11 +5,10 @@ from homeassistant.components.fan import (
 )
 
 from homeassistant.const import STATE_UNAVAILABLE
-from custom_components.tuya_local.generic.fan import TuyaLocalFan
-from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
 
 from ..const import ANKO_FAN_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 PRESET_DPS = "2"
@@ -21,24 +17,12 @@ OSCILLATE_DPS = "4"
 TIMER_DPS = "6"
 
 
-class TestAnkoFan(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("anko_fan.yaml")
-        entities = {}
-        entities[cfg.primary_entity.entity] = cfg.primary_entity
-        for e in cfg.secondary_entities():
-            entities[e.entity] = e
-
-        self.fan_name = (
-            "missing" if "fan" not in entities.keys() else entities["fan"].name
-        )
+class TestAnkoFan(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.subject = TuyaLocalFan(self.mock_device(), entities.get("fan"))
-        self.dps = ANKO_FAN_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("anko_fan.yaml", ANKO_FAN_PAYLOAD)
+        self.subject = self.entities["fan"]
 
     def test_supported_features(self):
         self.assertEqual(
@@ -46,21 +30,6 @@ class TestAnkoFan(IsolatedAsyncioTestCase):
             SUPPORT_OSCILLATE | SUPPORT_PRESET_MODE | SUPPORT_SET_SPEED,
         )
 
-    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.fan_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_is_on(self):
         self.dps[SWITCH_DPS] = True
         self.assertTrue(self.subject.is_on)
@@ -164,12 +133,3 @@ class TestAnkoFan(IsolatedAsyncioTestCase):
     def test_device_state_attributes(self):
         self.dps[TIMER_DPS] = "5"
         self.assertEqual(self.subject.device_state_attributes, {"timer": 5})
-
-    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()

+ 6 - 38
tests/devices/test_bwt_heatpump.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -9,11 +6,9 @@ from homeassistant.components.climate.const import (
 )
 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 BWT_HEATPUMP_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 TEMPERATURE_DPS = "2"
@@ -22,18 +17,12 @@ PRESET_DPS = "4"
 ERROR_DPS = "9"
 
 
-class TestBWTHeatpump(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("bwt_heatpump.yaml")
-        climate = cfg.primary_entity
-        self.climate_name = climate.name
-        self.subject = TuyaLocalClimate(self.mock_device, climate)
-        self.dps = BWT_HEATPUMP_PAYLOAD.copy()
+class TestBWTHeatpump(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("bwt_heatpump.yaml", BWT_HEATPUMP_PAYLOAD)
+        self.subject = self.entities["climate"]
 
     def test_supported_features(self):
         self.assertEqual(
@@ -41,18 +30,6 @@ class TestBWTHeatpump(IsolatedAsyncioTestCase):
             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):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:hot-tub")
@@ -256,12 +233,3 @@ class TestBWTHeatpump(IsolatedAsyncioTestCase):
             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()

+ 8 - 65
tests/devices/test_deta_fan.py

@@ -1,18 +1,12 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.fan import (
     SUPPORT_SET_SPEED,
 )
 
 from homeassistant.const import STATE_UNAVAILABLE
-from custom_components.tuya_local.generic.fan import TuyaLocalFan
-from custom_components.tuya_local.generic.light import TuyaLocalLight
-from custom_components.tuya_local.generic.switch import TuyaLocalSwitch
-from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
 
 from ..const import DETA_FAN_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 SPEED_DPS = "3"
@@ -22,31 +16,14 @@ TIMER_DPS = "102"
 LIGHT_TIMER_DPS = "103"
 
 
-class TestDetaFan(IsolatedAsyncioTestCase):
+class TestDetaFan(TuyaDeviceTestCase):
+    __test__ = True
+
     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("deta_fan.yaml")
-        entities = {}
-        entities[cfg.primary_entity.entity] = cfg.primary_entity
-        for e in cfg.secondary_entities():
-            entities[e.entity] = e
-
-        self.fan_name = (
-            "missing" if "fan" not in entities.keys() else entities["fan"].name
-        )
-        self.light_name = (
-            "missing" if "light" not in entities.keys() else entities["light"].name
-        )
-        self.switch_name = (
-            "missing" if "switch" not in entities.keys() else entities["switch"].name
-        )
-        self.subject = TuyaLocalFan(self.mock_device(), entities.get("fan"))
-        self.light = TuyaLocalLight(self.mock_device(), entities.get("light"))
-        self.switch = TuyaLocalSwitch(self.mock_device(), entities.get("switch"))
-        self.dps = DETA_FAN_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+        self.setUpForConfig("deta_fan.yaml", DETA_FAN_PAYLOAD)
+        self.subject = self.entities["fan"]
+        self.light = self.entities["light"]
+        self.switch = self.entities["switch"]
 
     def test_supported_features(self):
         self.assertEqual(
@@ -54,31 +31,6 @@ class TestDetaFan(IsolatedAsyncioTestCase):
             SUPPORT_SET_SPEED,
         )
 
-    def test_should_poll(self):
-        self.assertTrue(self.subject.should_poll)
-        self.assertTrue(self.light.should_poll)
-        self.assertTrue(self.switch.should_poll)
-
-    def test_name_returns_device_name(self):
-        self.assertEqual(self.subject.name, self.subject._device.name)
-        self.assertEqual(self.light.name, self.subject._device.name)
-        self.assertEqual(self.switch.name, self.subject._device.name)
-
-    def test_friendly_name_returns_config_name(self):
-        self.assertEqual(self.subject.friendly_name, self.fan_name)
-        self.assertEqual(self.light.friendly_name, self.light_name)
-        self.assertEqual(self.switch.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)
-        self.assertEqual(self.light.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.switch.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.light.device_info, self.subject._device.device_info)
-        self.assertEqual(self.switch.device_info, self.subject._device.device_info)
-
     def test_is_on(self):
         self.dps[SWITCH_DPS] = True
         self.assertTrue(self.subject.is_on)
@@ -164,12 +116,3 @@ class TestDetaFan(IsolatedAsyncioTestCase):
             self.light._device, {MASTER_DPS: False}
         ):
             await self.switch.async_turn_off()
-
-    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()

+ 9 - 74
tests/devices/test_eanons_humidifier.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     FAN_HIGH,
     FAN_MEDIUM,
@@ -23,14 +20,9 @@ from homeassistant.components.humidifier.const import (
 from homeassistant.components.switch import DEVICE_CLASS_SWITCH
 from homeassistant.const import STATE_UNAVAILABLE
 
-from custom_components.tuya_local.generic.climate import TuyaLocalClimate
-from custom_components.tuya_local.generic.fan import TuyaLocalFan
-from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
-from custom_components.tuya_local.generic.switch import TuyaLocalSwitch
-from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
-
 from ..const import EANONS_HUMIDIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 FANMODE_DPS = "2"
 TIMERHR_DPS = "3"
@@ -43,36 +35,15 @@ CURRENTHUMID_DPS = "16"
 SWITCH_DPS = "22"
 
 
-class TestEanonsHumidifier(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("eanons_humidifier.yaml")
-        entities = {}
-        entities[cfg.primary_entity.entity] = cfg.primary_entity
-        for e in cfg.secondary_entities():
-            entities[e.entity] = e
-
-        self.climate_name = (
-            "missing" if "climate" not in entities else entities["climate"].name
-        )
-        self.switch_name = (
-            "missing" if "switch" not in entities else entities["switch"].name
-        )
-        self.humidifier_name = (
-            "missing" if "humidifier" not in entities else entities["humidifier"].name
-        )
-        self.fan_name = "missing" if "fan" not in entities else entities["fan"].name
-        self.subject = TuyaLocalHumidifier(
-            self.mock_device(), entities.get("humidifier")
-        )
-        self.climate = TuyaLocalClimate(self.mock_device(), entities.get("climate"))
-        self.switch = TuyaLocalSwitch(self.mock_device(), entities.get("switch"))
-        self.fan = TuyaLocalFan(self.mock_device(), entities.get("fan"))
+class TestEanonsHumidifier(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.dps = EANONS_HUMIDIFIER_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("eanons_humidifier.yaml", EANONS_HUMIDIFIER_PAYLOAD)
+        self.subject = self.entities["humidifier"]
+        self.climate = self.entities["climate"]
+        self.switch = self.entities["switch"]
+        self.fan = self.entities["fan"]
 
     def test_supported_features(self):
         self.assertEqual(
@@ -82,36 +53,6 @@ class TestEanonsHumidifier(IsolatedAsyncioTestCase):
         self.assertEqual(self.subject.supported_features, SUPPORT_MODES)
         self.assertEqual(self.fan.supported_features, SUPPORT_SET_SPEED)
 
-    def test_shouldPoll(self):
-        self.assertTrue(self.subject.should_poll)
-        self.assertTrue(self.climate.should_poll)
-        self.assertTrue(self.fan.should_poll)
-        self.assertTrue(self.switch.should_poll)
-
-    def test_name_returns_device_name(self):
-        self.assertEqual(self.subject.name, self.subject._device.name)
-        self.assertEqual(self.climate.name, self.subject._device.name)
-        self.assertEqual(self.fan.name, self.subject._device.name)
-        self.assertEqual(self.switch.name, self.subject._device.name)
-
-    def test_friendly_name_returns_config_name(self):
-        self.assertEqual(self.subject.friendly_name, self.humidifier_name)
-        self.assertEqual(self.climate.friendly_name, self.climate_name)
-        self.assertEqual(self.fan.friendly_name, self.fan_name)
-        self.assertEqual(self.switch.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)
-        self.assertEqual(self.climate.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.fan.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.switch.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.climate.device_info, self.subject._device.device_info)
-        self.assertEqual(self.fan.device_info, self.subject._device.device_info)
-        self.assertEqual(self.switch.device_info, self.subject._device.device_info)
-
     def test_climate_icon_is_humidifier(self):
         """Test that the icon is as expected."""
         self.dps[HVACMODE_DPS] = True
@@ -391,12 +332,6 @@ class TestEanonsHumidifier(IsolatedAsyncioTestCase):
         ):
             await self.climate.async_set_fan_mode(FAN_LOW)
 
-    def test_switch_was_created(self):
-        self.assertIsInstance(self.switch, TuyaLocalSwitch)
-
-    def test_switch_is_same_device(self):
-        self.assertEqual(self.switch._device, self.climate._device)
-
     def test_switch_class_is_switch(self):
         self.assertEqual(self.switch.device_class, DEVICE_CLASS_SWITCH)
 

+ 12 - 116
tests/devices/test_electriq_dehumidifier.py

@@ -1,21 +1,10 @@
-from unittest import IsolatedAsyncioTestCase
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.fan import SUPPORT_PRESET_MODE
 from homeassistant.components.humidifier import SUPPORT_MODES
-from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
-from homeassistant.components.switch import DEVICE_CLASS_SWITCH
 from homeassistant.const import STATE_UNAVAILABLE
 
-from custom_components.tuya_local.generic.fan import TuyaLocalFan
-from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
-from custom_components.tuya_local.generic.light import TuyaLocalLight
-from custom_components.tuya_local.generic.lock import TuyaLocalLock
-from custom_components.tuya_local.generic.switch import TuyaLocalSwitch
-from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
-
 from ..const import ELECTRIQ_DEHUMIDIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 MODE_DPS = "2"
@@ -28,90 +17,21 @@ CURRENTTEMP_DPS = "103"
 IONIZER_DPS = "104"
 
 
-class TestElectriqDehumidifier(IsolatedAsyncioTestCase):
+class TestElectriqDehumidifier(TuyaDeviceTestCase):
+    __test__ = True
+
     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("electriq_dehumidifier.yaml")
-        entities = {}
-        entities[cfg.primary_entity.entity] = cfg.primary_entity
-        for e in cfg.secondary_entities():
-            entities[e.entity] = e
-
-        self.humidifier_name = (
-            "missing" if "humidifier" not in entities else entities["humidifier"].name
-        )
-        self.fan_name = "missing" if "fan" not in entities else entities["fan"].name
-        self.light_name = (
-            "missing" if "light" not in entities else entities["light"].name
-        )
-        self.lock_name = "missing" if "lock" not in entities else entities["lock"].name
-        self.switch_name = (
-            "missing" if "switch" not in entities else entities["switch"].name
-        )
-        self.subject = TuyaLocalHumidifier(
-            self.mock_device(), entities.get("humidifier")
-        )
-        self.fan = TuyaLocalFan(self.mock_device(), entities.get("fan"))
-        self.light = TuyaLocalLight(self.mock_device(), entities.get("light"))
-        self.lock = TuyaLocalLock(self.mock_device(), entities.get("lock"))
-        self.switch = TuyaLocalSwitch(self.mock_device(), entities.get("switch"))
-        self.dps = ELECTRIQ_DEHUMIDIFIER_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+        self.setUpForConfig("electriq_dehumidifier.yaml", ELECTRIQ_DEHUMIDIFIER_PAYLOAD)
+        self.subject = self.entities.get("humidifier")
+        self.fan = self.entities.get("fan")
+        self.light = self.entities.get("light")
+        self.lock = self.entities.get("lock")
+        self.switch = self.entities.get("switch")
 
     def test_supported_features(self):
         self.assertEqual(self.subject.supported_features, SUPPORT_MODES)
         self.assertEqual(self.fan.supported_features, SUPPORT_PRESET_MODE)
 
-    def test_should_poll(self):
-        self.assertTrue(self.subject.should_poll)
-        self.assertTrue(self.fan.should_poll)
-        self.assertTrue(self.light.should_poll)
-        self.assertTrue(self.lock.should_poll)
-        self.assertTrue(self.switch.should_poll)
-
-    def test_name_returns_device_name(self):
-        self.assertEqual(self.subject.name, self.subject._device.name)
-        self.assertEqual(self.fan.name, self.subject._device.name)
-        self.assertEqual(self.light.name, self.subject._device.name)
-        self.assertEqual(self.lock.name, self.subject._device.name)
-        self.assertEqual(self.switch.name, self.subject._device.name)
-
-    def test_friendly_name_returns_config_name(self):
-        self.assertEqual(self.subject.friendly_name, self.humidifier_name)
-        self.assertEqual(self.fan.friendly_name, self.fan_name)
-        self.assertEqual(self.light.friendly_name, self.light_name)
-        self.assertEqual(self.lock.friendly_name, self.lock_name)
-        self.assertEqual(self.switch.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)
-        self.assertEqual(self.fan.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.light.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.lock.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.switch.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.fan.device_info, self.subject._device.device_info)
-        self.assertEqual(self.light.device_info, self.subject._device.device_info)
-        self.assertEqual(self.lock.device_info, self.subject._device.device_info)
-        self.assertEqual(self.switch.device_info, self.subject._device.device_info)
-
-    def test_entities_created(self):
-        self.assertIsInstance(self.subject, TuyaLocalHumidifier)
-        self.assertIsInstance(self.fan, TuyaLocalFan)
-        self.assertIsInstance(self.light, TuyaLocalLight)
-        self.assertIsInstance(self.lock, TuyaLocalLock)
-        self.assertIsInstance(self.switch, TuyaLocalSwitch)
-
-    def test_entities_are_same_device(self):
-        self.assertEqual(self.fan._device, self.subject._device)
-        self.assertEqual(self.light._device, self.subject._device)
-        self.assertEqual(self.lock._device, self.subject._device)
-        self.assertEqual(self.switch._device, self.subject._device)
-
     def test_icon(self):
         """Test that the icon is as expected."""
         self.dps[SWITCH_DPS] = True
@@ -176,7 +96,7 @@ class TestElectriqDehumidifier(IsolatedAsyncioTestCase):
         ):
             await self.subject.async_turn_on()
 
-    async def test_turn_on(self):
+    async def test_turn_off(self):
         async with assert_device_properties_set(
             self.subject._device, {SWITCH_DPS: False}
         ):
@@ -188,7 +108,7 @@ class TestElectriqDehumidifier(IsolatedAsyncioTestCase):
         ):
             await self.fan.async_turn_on()
 
-    async def test_fan_turn_on(self):
+    async def test_fan_turn_off(self):
         async with assert_device_properties_set(
             self.subject._device, {SWITCH_DPS: False}
         ):
@@ -302,14 +222,6 @@ class TestElectriqDehumidifier(IsolatedAsyncioTestCase):
         async with assert_device_properties_set(self.lock._device, {LOCK_DPS: False}):
             await self.lock.async_unlock()
 
-    async def 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()
-
     def test_light_is_on(self):
         self.dps[LIGHT_DPS] = True
         self.assertTrue(self.light.is_on)
@@ -335,14 +247,6 @@ class TestElectriqDehumidifier(IsolatedAsyncioTestCase):
         async with assert_device_properties_set(self.light._device, {LIGHT_DPS: False}):
             await self.light.async_toggle()
 
-    async def test_light_update(self):
-        result = AsyncMock()
-        self.light._device.async_refresh.return_value = result()
-
-        await self.light.async_update()
-        self.light._device.async_refresh.assert_called_once()
-        result.assert_awaited()
-
     def test_switch_is_on(self):
         self.dps[IONIZER_DPS] = True
         self.assertTrue(self.switch.is_on)
@@ -376,14 +280,6 @@ class TestElectriqDehumidifier(IsolatedAsyncioTestCase):
         ):
             await self.switch.async_toggle()
 
-    async def test_switch_update(self):
-        result = AsyncMock()
-        self.switch._device.async_refresh.return_value = result()
-
-        await self.switch.async_update()
-        self.switch._device.async_refresh.assert_called_once()
-        result.assert_awaited()
-
     def test_state_attributes(self):
         self.dps[CURRENTHUMID_DPS] = 50
         self.dps[CURRENTTEMP_DPS] = 21

+ 6 - 39
tests/devices/test_eurom_600_heater.py

@@ -1,19 +1,13 @@
-from unittest import IsolatedAsyncioTestCase, skip
-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
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 TEMPERATURE_DPS = "2"
@@ -21,18 +15,12 @@ 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()
+class TestEurom600Heater(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("eurom_600_heater.yaml", EUROM_600_HEATER_PAYLOAD)
+        self.subject = self.entities.get("climate")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -40,18 +28,6 @@ class TestEurom600Heater(IsolatedAsyncioTestCase):
             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")
@@ -147,12 +123,3 @@ class TestEurom600Heater(IsolatedAsyncioTestCase):
         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()

+ 6 - 39
tests/devices/test_gardenpac_heatpump.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -13,11 +10,9 @@ from homeassistant.const import (
     TEMP_FAHRENHEIT,
 )
 
-from custom_components.tuya_local.generic.climate import TuyaLocalClimate
-from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
-
 from ..const import GARDENPAC_HEATPUMP_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 CURRENTTEMP_DPS = "102"
@@ -32,19 +27,12 @@ UNKNOWN116_DPS = "116"
 PRESET_DPS = "117"
 
 
-class TestGardenPACPoolHeatpump(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("gardenpac_heatpump.yaml")
-        climate = cfg.primary_entity
-        self.climate_name = climate.name
-
-        self.subject = TuyaLocalClimate(self.mock_device(), climate)
+class TestGardenPACPoolHeatpump(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.dps = GARDENPAC_HEATPUMP_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("gardenpac_heatpump.yaml", GARDENPAC_HEATPUMP_PAYLOAD)
+        self.subject = self.entities.get("climate")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -52,18 +40,6 @@ class TestGardenPACPoolHeatpump(IsolatedAsyncioTestCase):
             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):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:hot-tub")
@@ -206,12 +182,3 @@ class TestGardenPACPoolHeatpump(IsolatedAsyncioTestCase):
                 "unknown_116": 4,
             },
         )
-
-    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()

+ 11 - 116
tests/devices/test_goldair_dehumidifier.py

@@ -1,5 +1,4 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
+from unittest import skip
 
 from homeassistant.components.climate.const import (
     FAN_HIGH,
@@ -13,15 +12,9 @@ from homeassistant.components.climate.const import (
 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.fan import TuyaLocalFan
-from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
-from custom_components.tuya_local.generic.light import TuyaLocalLight
-from custom_components.tuya_local.generic.lock import TuyaLocalLock
-from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
-
 from ..const import DEHUMIDIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 PRESET_DPS = "2"
@@ -45,40 +38,16 @@ PRESET_DRY_CLOTHES = "3"
 ERROR_TANK = "Tank full or missing"
 
 
-class TestGoldairDehumidifier(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_dehumidifier.yaml")
-        entities = {}
-        entities[cfg.primary_entity.entity] = cfg.primary_entity
-
-        for e in cfg.secondary_entities():
-            entities[e.entity] = e
-
-        self.climate_name = (
-            "missing" if "climate" not in entities else entities["climate"].name
-        )
-        self.light_name = (
-            "missing" if "light" not in entities else entities["light"].name
-        )
-        self.lock_name = "missing" if "lock" not in entities else entities["lock"].name
-        self.humidifier_name = (
-            "missing" if "humidifier" not in entities else entities["humidifier"].name
-        )
-        self.fan_name = "missing" if "fan" not in entities else entities["fan"].name
+class TestGoldairDehumidifier(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.subject = TuyaLocalClimate(self.mock_device(), entities.get("climate"))
-        self.light = TuyaLocalLight(self.mock_device(), entities.get("light"))
-        self.lock = TuyaLocalLock(self.mock_device(), entities.get("lock"))
-        self.humidifier = TuyaLocalHumidifier(
-            self.mock_device(), entities.get("humidifier")
-        )
-        self.fan = TuyaLocalFan(self.mock_device(), entities.get("fan"))
-
-        self.dps = DEHUMIDIFIER_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("goldair_dehumidifier.yaml", DEHUMIDIFIER_PAYLOAD)
+        self.subject = self.entities.get("climate")
+        self.light = self.entities.get("light")
+        self.lock = self.entities.get("lock")
+        self.humidifier = self.entities.get("humidifier")
+        self.fan = self.entities.get("fan")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -86,41 +55,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
             SUPPORT_TARGET_HUMIDITY | SUPPORT_PRESET_MODE | SUPPORT_FAN_MODE,
         )
 
-    def test_should_poll(self):
-        self.assertTrue(self.subject.should_poll)
-        self.assertTrue(self.fan.should_poll)
-        self.assertTrue(self.light.should_poll)
-        self.assertTrue(self.lock.should_poll)
-        self.assertTrue(self.humidifier.should_poll)
-
-    def test_name_returns_device_name(self):
-        self.assertEqual(self.subject.name, self.subject._device.name)
-        self.assertEqual(self.fan.name, self.subject._device.name)
-        self.assertEqual(self.light.name, self.subject._device.name)
-        self.assertEqual(self.lock.name, self.subject._device.name)
-        self.assertEqual(self.humidifier.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.fan.friendly_name, self.fan_name)
-        self.assertEqual(self.light.friendly_name, self.light_name)
-        self.assertEqual(self.lock.friendly_name, self.lock_name)
-        self.assertEqual(self.humidifier.friendly_name, self.humidifier_name)
-
-    def test_unique_id_returns_device_unique_id(self):
-        self.assertEqual(self.subject.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.fan.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.light.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.lock.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.humidifier.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.fan.device_info, self.subject._device.device_info)
-        self.assertEqual(self.light.device_info, self.subject._device.device_info)
-        self.assertEqual(self.lock.device_info, self.subject._device.device_info)
-        self.assertEqual(self.humidifier.device_info, self.subject._device.device_info)
-
     def test_icon_is_always_standard_when_off_without_error(self):
         self.dps[ERROR_DPS] = None
         self.dps[HVACMODE_DPS] = False
@@ -612,21 +546,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
             },
         )
 
-    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)
@@ -655,21 +574,6 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
         async with assert_device_properties_set(self.lock._device, {LOCK_DPS: False}):
             await self.lock.async_unlock()
 
-    async def 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()
-
-    def test_light_was_created(self):
-        self.assertIsInstance(self.light, TuyaLocalLight)
-
-    def test_light_is_same_device(self):
-        self.assertEqual(self.light._device, self.subject._device)
-
     def test_light_icon(self):
         self.dps[LIGHTOFF_DPS] = False
         self.assertEqual(self.light.icon, "mdi:led-on")
@@ -714,12 +618,3 @@ class TestGoldairDehumidifier(IsolatedAsyncioTestCase):
             self.light._device, {LIGHTOFF_DPS: True}
         ):
             await self.light.async_toggle()
-
-    async def test_light_update(self):
-        result = AsyncMock()
-        self.light._device.async_refresh.return_value = result()
-
-        await self.light.async_update()
-
-        self.light._device.async_refresh.assert_called_once()
-        result.assert_awaited()

+ 8 - 82
tests/devices/test_goldair_fan.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     HVAC_MODE_FAN_ONLY,
     HVAC_MODE_OFF,
@@ -19,13 +16,10 @@ from homeassistant.components.fan import (
 )
 
 from homeassistant.const import STATE_UNAVAILABLE
-from custom_components.tuya_local.generic.climate import TuyaLocalClimate
-from custom_components.tuya_local.generic.fan import TuyaLocalFan
-from custom_components.tuya_local.generic.light import TuyaLocalLight
-from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
 
 from ..const import FAN_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 FANMODE_DPS = "2"
@@ -35,33 +29,14 @@ TIMER_DPS = "11"
 LIGHT_DPS = "101"
 
 
-class TestGoldairFan(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_fan.yaml")
-        entities = {}
-        entities[cfg.primary_entity.entity] = cfg.primary_entity
-        for e in cfg.secondary_entities():
-            entities[e.entity] = e
-
-        self.climate_name = (
-            "missing" if "climate" not in entities.keys() else entities["climate"].name
-        )
-        self.fan_name = (
-            "missing" if "fan" not in entities.keys() else entities["fan"].name
-        )
-        self.light_name = (
-            "missing" if "light" not in entities.keys() else entities["light"].name
-        )
-
-        self.subject = TuyaLocalFan(self.mock_device(), entities.get("fan"))
-        self.climate = TuyaLocalClimate(self.mock_device(), entities.get("climate"))
-        self.light = TuyaLocalLight(self.mock_device(), entities.get("light"))
+class TestGoldairFan(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.dps = FAN_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("goldair_fan.yaml", FAN_PAYLOAD)
+        self.subject = self.entities.get("fan")
+        self.climate = self.entities.get("climate")
+        self.light = self.entities.get("light")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -73,31 +48,6 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
             SUPPORT_FAN_MODE | SUPPORT_CLIMATE_PRESET | SUPPORT_SWING_MODE,
         )
 
-    def test_should_poll(self):
-        self.assertTrue(self.subject.should_poll)
-        self.assertTrue(self.climate.should_poll)
-        self.assertTrue(self.light.should_poll)
-
-    def test_name_returns_device_name(self):
-        self.assertEqual(self.subject.name, self.subject._device.name)
-        self.assertEqual(self.climate.name, self.subject._device.name)
-        self.assertEqual(self.light.name, self.subject._device.name)
-
-    def test_friendly_name_returns_config_name(self):
-        self.assertEqual(self.subject.friendly_name, self.fan_name)
-        self.assertEqual(self.climate.friendly_name, self.climate_name)
-        self.assertEqual(self.light.friendly_name, self.light_name)
-
-    def test_unique_id_returns_device_unique_id(self):
-        self.assertEqual(self.subject.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.climate.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.light.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.climate.device_info, self.subject._device.device_info)
-        self.assertEqual(self.light.device_info, self.subject._device.device_info)
-
     def test_climate_icon_is_fan(self):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.climate.icon, "mdi:fan")
@@ -369,21 +319,6 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
         self.assertEqual(self.climate.device_state_attributes, {"timer": "5"})
         self.assertEqual(self.subject.device_state_attributes, {"timer": "5"})
 
-    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_light_was_created(self):
-        self.assertIsInstance(self.light, TuyaLocalLight)
-
-    def test_light_is_same_device(self):
-        self.assertEqual(self.light._device, self.subject._device)
-
     def test_light_icon(self):
         self.dps[LIGHT_DPS] = True
         self.assertEqual(self.light.icon, "mdi:led-on")
@@ -420,12 +355,3 @@ class TestGoldairFan(IsolatedAsyncioTestCase):
 
         async with assert_device_properties_set(self.light._device, {LIGHT_DPS: False}):
             await self.light.async_toggle()
-
-    async def test_light_update(self):
-        result = AsyncMock()
-        self.light._device.async_refresh.return_value = result()
-
-        await self.light.async_update()
-
-        self.light._device.async_refresh.assert_called_once()
-        result.assert_awaited()

+ 7 - 71
tests/devices/test_goldair_geco_heater.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -9,13 +6,9 @@ from homeassistant.components.climate.const import (
 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
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 LOCK_DPS = "2"
@@ -25,26 +18,13 @@ 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()
+class TestGoldairGECOHeater(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("goldair_geco_heater.yaml", GECO_HEATER_PAYLOAD)
+        self.subject = self.entities.get("climate")
+        self.lock = self.entities.get("lock")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -52,26 +32,6 @@ class TestGoldairGECOHeater(IsolatedAsyncioTestCase):
             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")
@@ -174,21 +134,6 @@ class TestGoldairGECOHeater(IsolatedAsyncioTestCase):
             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)
@@ -216,12 +161,3 @@ class TestGoldairGECOHeater(IsolatedAsyncioTestCase):
     async def test_lock_unlocks(self):
         async with assert_device_properties_set(self.lock._device, {LOCK_DPS: False}):
             await self.lock.async_unlock()
-
-    async def 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()

+ 7 - 71
tests/devices/test_goldair_gpcv_heater.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -10,13 +7,9 @@ from homeassistant.components.climate.const import (
 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
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 LOCK_DPS = "2"
@@ -27,26 +20,13 @@ 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()
+class TestGoldairGPCVHeater(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("goldair_gpcv_heater.yaml", GPCV_HEATER_PAYLOAD)
+        self.subject = self.entities.get("climate")
+        self.lock = self.entities.get("lock")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -54,26 +34,6 @@ class TestGoldairGPCVHeater(IsolatedAsyncioTestCase):
             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")
@@ -215,21 +175,6 @@ class TestGoldairGPCVHeater(IsolatedAsyncioTestCase):
             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)
@@ -257,12 +202,3 @@ class TestGoldairGPCVHeater(IsolatedAsyncioTestCase):
     async def test_lock_unlocks(self):
         async with assert_device_properties_set(self.lock._device, {LOCK_DPS: False}):
             await self.lock.async_unlock()
-
-    async def 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()

+ 9 - 94
tests/devices/test_goldair_gpph_heater.py

@@ -1,5 +1,4 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
+from unittest import skip
 
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
@@ -12,13 +11,9 @@ 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.light import TuyaLocalLight
-from custom_components.tuya_local.generic.lock import TuyaLocalLock
-from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
-
 from ..const import GPPH_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 TEMPERATURE_DPS = "2"
@@ -34,30 +29,14 @@ SWING_DPS = "105"
 ECOTEMP_DPS = "106"
 
 
-class TestGoldairHeater(IsolatedAsyncioTestCase):
+class TestGoldairHeater(TuyaDeviceTestCase):
+    __test__ = True
+
     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_gpph_heater.yaml")
-        climate = cfg.primary_entity
-        light = None
-        lock = None
-        for e in cfg.secondary_entities():
-            if e.entity == "light":
-                light = e
-            elif e.entity == "lock":
-                lock = e
-        self.climate_name = climate.name
-        self.light_name = "missing" if light is None else light.name
-        self.lock_name = "missing" if lock is None else lock.name
-
-        self.subject = TuyaLocalClimate(self.mock_device(), climate)
-        self.light = TuyaLocalLight(self.mock_device(), light)
-        self.lock = TuyaLocalLock(self.mock_device(), lock)
-
-        self.dps = GPPH_HEATER_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+        self.setUpForConfig("goldair_gpph_heater.yaml", GPPH_HEATER_PAYLOAD)
+        self.subject = self.entities.get("climate")
+        self.light = self.entities.get("light")
+        self.lock = self.entities.get("lock")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -65,31 +44,6 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
             SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_SWING_MODE,
         )
 
-    def test_should_poll(self):
-        self.assertTrue(self.subject.should_poll)
-        self.assertTrue(self.light.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.light.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.light.friendly_name, self.light_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.light.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.light.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")
@@ -368,21 +322,6 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
             },
         )
 
-    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)
@@ -411,21 +350,6 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
         async with assert_device_properties_set(self.lock._device, {LOCK_DPS: False}):
             await self.lock.async_unlock()
 
-    async def 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()
-
-    def test_light_was_created(self):
-        self.assertIsInstance(self.light, TuyaLocalLight)
-
-    def test_light_is_same_device(self):
-        self.assertEqual(self.light._device, self.subject._device)
-
     def test_light_icon(self):
         self.dps[LIGHT_DPS] = True
         self.assertEqual(self.light.icon, "mdi:led-on")
@@ -462,12 +386,3 @@ class TestGoldairHeater(IsolatedAsyncioTestCase):
 
         async with assert_device_properties_set(self.light._device, {LIGHT_DPS: False}):
             await self.light.async_toggle()
-
-    async def test_light_update(self):
-        result = AsyncMock()
-        self.light._device.async_refresh.return_value = result()
-
-        await self.light.async_update()
-
-        self.light._device.async_refresh.assert_called_once()
-        result.assert_awaited()

+ 6 - 47
tests/devices/test_inkbird_thermostat.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE_RANGE,
@@ -8,11 +5,9 @@ from homeassistant.components.climate.const import (
 from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
 
 
-from custom_components.tuya_local.generic.climate import TuyaLocalClimate
-from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
-
 from ..const import INKBIRD_THERMOSTAT_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 ERROR_DPS = "12"
 UNIT_DPS = "101"
@@ -35,24 +30,12 @@ UNKNOWN119_DPS = "119"
 UNKNOWN120_DPS = "120"
 
 
-class TestInkbirdThermostat(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("inkbird_thermostat.yaml")
-        entities = {}
-        entities[cfg.primary_entity.entity] = cfg.primary_entity
-        for e in cfg.secondary_entities():
-            entities[e.entity] = e
-
-        self.climate_name = (
-            "missing" if "climate" not in entities else entities["climate"].name
-        )
+class TestInkbirdThermostat(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.subject = TuyaLocalClimate(self.mock_device(), entities.get("climate"))
-        self.dps = INKBIRD_THERMOSTAT_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("inkbird_thermostat.yaml", INKBIRD_THERMOSTAT_PAYLOAD)
+        self.subject = self.entities.get("climate")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -60,21 +43,6 @@ class TestInkbirdThermostat(IsolatedAsyncioTestCase):
             SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_PRESET_MODE,
         )
 
-    def test_shouldPoll(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.climate_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):
         """Test that the icon is as expected."""
         self.dps[ALARM_HIGH_DPS] = False
@@ -216,12 +184,3 @@ class TestInkbirdThermostat(IsolatedAsyncioTestCase):
                 "unknown_120": False,
             },
         )
-
-    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()

+ 7 - 71
tests/devices/test_kogan_heater.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -10,13 +7,9 @@ from homeassistant.components.climate.const import (
 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
+from .base_device_tests import TuyaDeviceTestCase
 
 TEMPERATURE_DPS = "2"
 CURRENTTEMP_DPS = "3"
@@ -26,26 +19,13 @@ 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()
+class TestGoldairKoganHeater(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("kogan_heater.yaml", KOGAN_HEATER_PAYLOAD)
+        self.subject = self.entities.get("climate")
+        self.lock = self.entities.get("lock")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -53,26 +33,6 @@ class TestGoldairKoganHeater(IsolatedAsyncioTestCase):
             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")
@@ -206,21 +166,6 @@ class TestGoldairKoganHeater(IsolatedAsyncioTestCase):
         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)
@@ -248,12 +193,3 @@ class TestGoldairKoganHeater(IsolatedAsyncioTestCase):
     async def test_lock_unlocks(self):
         async with assert_device_properties_set(self.lock._device, {LOCK_DPS: False}):
             await self.lock.async_unlock()
-
-    async def 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()

+ 6 - 41
tests/devices/test_kogan_switch.py

@@ -1,15 +1,10 @@
 """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
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 TIMER_DPS = "2"
@@ -18,33 +13,12 @@ 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)
+class TestKoganSwitch(TuyaDeviceTestCase):
+    __test__ = True
 
-    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 setUp(self):
+        self.setUpForConfig("kogan_switch.yaml", KOGAN_SOCKET_PAYLOAD)
+        self.subject = self.entities.get("switch")
 
     def test_device_class_is_outlet(self):
         self.assertEqual(self.subject.device_class, DEVICE_CLASS_OUTLET)
@@ -120,12 +94,3 @@ class TestKoganSwitch(IsolatedAsyncioTestCase):
                 "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()

+ 6 - 41
tests/devices/test_kogan_switch2.py

@@ -1,15 +1,10 @@
 """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_PAYLOAD2
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 TIMER_DPS = "9"
@@ -18,33 +13,12 @@ POWER_DPS = "19"
 VOLTAGE_DPS = "20"
 
 
-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_switch2.yaml")
-        switch = cfg.primary_entity
-        self.switch_name = switch.name
-        self.subject = TuyaLocalSwitch(self.mock_device(), switch)
-        self.dps = KOGAN_SOCKET_PAYLOAD2.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)
+class TestKoganSwitch(TuyaDeviceTestCase):
+    __test__ = True
 
-    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 setUp(self):
+        self.setUpForConfig("kogan_switch2.yaml", KOGAN_SOCKET_PAYLOAD2)
+        self.subject = self.entities.get("switch")
 
     def test_device_class_is_outlet(self):
         self.assertEqual(self.subject.device_class, DEVICE_CLASS_OUTLET)
@@ -120,12 +94,3 @@ class TestKoganSwitch(IsolatedAsyncioTestCase):
                 "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()

+ 6 - 39
tests/devices/test_madimack_heatpump.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -13,11 +10,9 @@ from homeassistant.const import (
     TEMP_FAHRENHEIT,
 )
 
-from custom_components.tuya_local.generic.climate import TuyaLocalClimate
-from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
-
 from ..const import MADIMACK_HEATPUMP_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 CURRENTTEMP_DPS = "102"
@@ -47,19 +42,12 @@ UNKNOWN140_DPS = "140"
 PRESET_DPS = "117"
 
 
-class TestMadimackPoolHeatpump(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("madimack_heatpump.yaml")
-        climate = cfg.primary_entity
-        self.climate_name = climate.name
-
-        self.subject = TuyaLocalClimate(self.mock_device(), climate)
+class TestMadimackPoolHeatpump(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.dps = MADIMACK_HEATPUMP_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("madimack_heatpump.yaml", MADIMACK_HEATPUMP_PAYLOAD)
+        self.subject = self.entities.get("climate")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -67,18 +55,6 @@ class TestMadimackPoolHeatpump(IsolatedAsyncioTestCase):
             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):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:hot-tub")
@@ -251,12 +227,3 @@ class TestMadimackPoolHeatpump(IsolatedAsyncioTestCase):
                 "unknown_140": "test",
             },
         )
-
-    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()

+ 8 - 37
tests/devices/test_poolex_silverline_heatpump.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -9,11 +6,9 @@ from homeassistant.components.climate.const import (
 )
 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 POOLEX_SILVERLINE_HEATPUMP_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 TEMPERATURE_DPS = "2"
@@ -22,17 +17,14 @@ PRESET_DPS = "4"
 ERROR_DPS = "13"
 
 
-class TestPoolexSilverlineHeatpump(IsolatedAsyncioTestCase):
+class TestPoolexSilverlineHeatpump(TuyaDeviceTestCase):
+    __test__ = True
+
     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("poolex_silverline_heatpump.yaml")
-        climate = cfg.primary_entity
-        self.climate_name = climate.name
-        self.subject = TuyaLocalClimate(self.mock_device, climate)
-        self.dps = POOLEX_SILVERLINE_HEATPUMP_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+        self.setUpForConfig(
+            "poolex_silverline_heatpump.yaml", POOLEX_SILVERLINE_HEATPUMP_PAYLOAD
+        )
+        self.subject = self.entities.get("climate")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -40,18 +32,6 @@ class TestPoolexSilverlineHeatpump(IsolatedAsyncioTestCase):
             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):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:hot-tub")
@@ -220,12 +200,3 @@ class TestPoolexSilverlineHeatpump(IsolatedAsyncioTestCase):
             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()

+ 8 - 37
tests/devices/test_poolex_vertigo_heatpump.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -9,11 +6,9 @@ from homeassistant.components.climate.const import (
 )
 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 POOLEX_VERTIGO_HEATPUMP_PAYLOAD
 from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 TEMPERATURE_DPS = "2"
@@ -22,17 +17,14 @@ PRESET_DPS = "4"
 ERROR_DPS = "9"
 
 
-class TestPoolexVertigoHeatpump(IsolatedAsyncioTestCase):
+class TestPoolexVertigoHeatpump(TuyaDeviceTestCase):
+    __test__ = True
+
     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("poolex_vertigo_heatpump.yaml")
-        climate = cfg.primary_entity
-        self.climate_name = climate.name
-        self.subject = TuyaLocalClimate(self.mock_device, climate)
-        self.dps = POOLEX_VERTIGO_HEATPUMP_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+        self.setUpForConfig(
+            "poolex_vertigo_heatpump.yaml", POOLEX_VERTIGO_HEATPUMP_PAYLOAD
+        )
+        self.subject = self.entities.get("climate")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -40,18 +32,6 @@ class TestPoolexVertigoHeatpump(IsolatedAsyncioTestCase):
             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):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:hot-tub")
@@ -209,12 +189,3 @@ class TestPoolexVertigoHeatpump(IsolatedAsyncioTestCase):
             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()

+ 8 - 85
tests/devices/test_purline_m100_heater.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     HVAC_MODE_FAN_ONLY,
     HVAC_MODE_HEAT,
@@ -14,16 +11,12 @@ from homeassistant.components.climate.const import (
 from homeassistant.components.switch import DEVICE_CLASS_SWITCH
 from homeassistant.const import STATE_UNAVAILABLE
 
-from custom_components.tuya_local.generic.climate import TuyaLocalClimate
-from custom_components.tuya_local.generic.light import TuyaLocalLight
-from custom_components.tuya_local.generic.switch import TuyaLocalSwitch
-from custom_components.tuya_local.helpers.device_config import TuyaDeviceConfig
-
 from ..const import PURLINE_M100_HEATER_PAYLOAD
 from ..helpers import (
     assert_device_properties_set,
     assert_device_properties_set_optional,
 )
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 TEMPERATURE_DPS = "2"
@@ -36,29 +29,14 @@ SWITCH_DPS = "101"
 SWING_DPS = "102"
 
 
-class TestPulineM100Heater(IsolatedAsyncioTestCase):
+class TestPulineM100Heater(TuyaDeviceTestCase):
+    __test__ = True
+
     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("purline_m100_heater.yaml")
-        climate = cfg.primary_entity
-        light = None
-        switch = None
-        for e in cfg.secondary_entities():
-            if e.entity == "light":
-                light = e
-            elif e.entity == "switch":
-                switch = e
-        self.climate_name = climate.name
-        self.light_name = "missing" if light is None else light.name
-        self.switch_name = "missing" if switch is None else switch.name
-        self.subject = TuyaLocalClimate(self.mock_device(), climate)
-        self.light = TuyaLocalLight(self.mock_device(), light)
-        self.switch = TuyaLocalSwitch(self.mock_device(), switch)
-
-        self.dps = PURLINE_M100_HEATER_PAYLOAD.copy()
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+        self.setUpForConfig("purline_m100_heater.yaml", PURLINE_M100_HEATER_PAYLOAD)
+        self.subject = self.entities.get("climate")
+        self.light = self.entities.get("light")
+        self.switch = self.entities.get("switch")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -66,31 +44,6 @@ class TestPulineM100Heater(IsolatedAsyncioTestCase):
             SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_SWING_MODE,
         )
 
-    def test_should_poll(self):
-        self.assertTrue(self.subject.should_poll)
-        self.assertTrue(self.light.should_poll)
-        self.assertTrue(self.switch.should_poll)
-
-    def test_name_returns_device_name(self):
-        self.assertEqual(self.subject.name, self.subject._device.name)
-        self.assertEqual(self.light.name, self.subject._device.name)
-        self.assertEqual(self.switch.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.light.friendly_name, self.light_name)
-        self.assertEqual(self.switch.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)
-        self.assertEqual(self.light.unique_id, self.subject._device.unique_id)
-        self.assertEqual(self.switch.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.light.device_info, self.subject._device.device_info)
-        self.assertEqual(self.switch.device_info, self.subject._device.device_info)
-
     def test_icon(self):
         self.dps[HVACMODE_DPS] = True
         self.dps[PRESET_DPS] = "auto"
@@ -253,21 +206,6 @@ class TestPulineM100Heater(IsolatedAsyncioTestCase):
         ):
             await self.subject.async_set_swing_mode(SWING_OFF)
 
-    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_light_was_created(self):
-        self.assertIsInstance(self.light, TuyaLocalLight)
-
-    def test_light_is_same_device(self):
-        self.assertEqual(self.light._device, self.subject._device)
-
     def test_light_icon(self):
         self.dps[LIGHTOFF_DPS] = False
         self.assertEqual(self.light.icon, "mdi:led-on")
@@ -313,21 +251,6 @@ class TestPulineM100Heater(IsolatedAsyncioTestCase):
         ):
             await self.light.async_toggle()
 
-    async def test_light_update(self):
-        result = AsyncMock()
-        self.light._device.async_refresh.return_value = result()
-
-        await self.light.async_update()
-
-        self.light._device.async_refresh.assert_called_once()
-        result.assert_awaited()
-
-    def test_switch_was_created(self):
-        self.assertIsInstance(self.switch, TuyaLocalSwitch)
-
-    def test_switch_is_same_device(self):
-        self.assertEqual(self.switch._device, self.subject._device)
-
     def test_switch_class_is_switch(self):
         self.assertEqual(self.switch.device_class, DEVICE_CLASS_SWITCH)
 

+ 6 - 38
tests/devices/test_remora_heatpump.py

@@ -1,6 +1,3 @@
-from unittest import IsolatedAsyncioTestCase, skip
-from unittest.mock import AsyncMock, patch
-
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -9,11 +6,9 @@ from homeassistant.components.climate.const import (
 )
 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
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 TEMPERATURE_DPS = "2"
@@ -22,18 +17,12 @@ 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()
+class TestRemoraHeatpump(TuyaDeviceTestCase):
+    __test__ = True
 
-        self.subject._device.get_property.side_effect = lambda id: self.dps[id]
+    def setUp(self):
+        self.setUpForConfig("remora_heatpump.yaml", REMORA_HEATPUMP_PAYLOAD)
+        self.subject = self.entities.get("climate")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -41,18 +30,6 @@ class TestRemoraHeatpump(IsolatedAsyncioTestCase):
             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):
         self.dps[HVACMODE_DPS] = True
         self.assertEqual(self.subject.icon, "mdi:hot-tub")
@@ -245,12 +222,3 @@ class TestRemoraHeatpump(IsolatedAsyncioTestCase):
             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()

+ 0 - 122
tests/test_device.py

@@ -7,30 +7,9 @@ from unittest.mock import AsyncMock, call, patch
 from homeassistant.const import TEMP_CELSIUS
 
 from custom_components.tuya_local.device import TuyaLocalDevice
-from custom_components.tuya_local.helpers.device_config import possible_matches
 
 from .const import (
-    DEHUMIDIFIER_PAYLOAD,
-    FAN_PAYLOAD,
-    GECO_HEATER_PAYLOAD,
     EUROM_600_HEATER_PAYLOAD,
-    GPCV_HEATER_PAYLOAD,
-    GPPH_HEATER_PAYLOAD,
-    GSH_HEATER_PAYLOAD,
-    KOGAN_HEATER_PAYLOAD,
-    KOGAN_SOCKET_PAYLOAD,
-    KOGAN_SOCKET_PAYLOAD2,
-    GARDENPAC_HEATPUMP_PAYLOAD,
-    MADIMACK_HEATPUMP_PAYLOAD,
-    PURLINE_M100_HEATER_PAYLOAD,
-    REMORA_HEATPUMP_PAYLOAD,
-    BWT_HEATPUMP_PAYLOAD,
-    EANONS_HUMIDIFIER_PAYLOAD,
-    INKBIRD_THERMOSTAT_PAYLOAD,
-    ANKO_FAN_PAYLOAD,
-    ELECTRIQ_DEHUMIDIFIER_PAYLOAD,
-    POOLEX_SILVERLINE_HEATPUMP_PAYLOAD,
-    POOLEX_VERTIGO_HEATPUMP_PAYLOAD,
 )
 
 
@@ -92,107 +71,6 @@ class TestDevice(IsolatedAsyncioTestCase):
 
         self.subject.async_refresh.assert_awaited()
 
-    async def test_detects_eurom_600_heater_payload(self):
-        self.subject._cached_state = EUROM_600_HEATER_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "eurom_heater")
-
-    async def test_detects_geco_heater_payload(self):
-        self.subject._cached_state = GECO_HEATER_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "geco_heater")
-
-    async def test_detects_gpcv_heater_payload(self):
-        self.subject._cached_state = GPCV_HEATER_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "gpcv_heater")
-
-    async def test_detects_gpph_heater_payload(self):
-        self.subject._cached_state = GPPH_HEATER_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "heater")
-
-    async def test_detects_dehumidifier_payload(self):
-        self.subject._cached_state = DEHUMIDIFIER_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "dehumidifier")
-
-    async def test_detects_fan_payload(self):
-        self.subject._cached_state = FAN_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "fan")
-
-    async def test_detects_kogan_heater_payload(self):
-        self.subject._cached_state = KOGAN_HEATER_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "kogan_heater")
-
-    async def test_detects_kogan_socket_payload(self):
-        self.subject._cached_state = KOGAN_SOCKET_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "kogan_switch")
-
-    async def test_detects_kogan_socket_payload2(self):
-        self.subject._cached_state = KOGAN_SOCKET_PAYLOAD2
-        self.assertEqual(await self.subject.async_inferred_type(), "kogan_switch")
-
-    async def test_detects_gsh_heater_payload(self):
-        self.subject._cached_state = GSH_HEATER_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "gsh_heater")
-
-    async def test_detects_gardenpac_heatpump_payload(self):
-        self.subject._cached_state = GARDENPAC_HEATPUMP_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "gardenpac_heatpump")
-
-    async def test_detects_madimack_heatpump_payload(self):
-        self.subject._cached_state = MADIMACK_HEATPUMP_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "madimack_heatpump")
-
-    async def test_detects_purline_m100_heater_payload(self):
-        self.subject._cached_state = PURLINE_M100_HEATER_PAYLOAD
-        self.assertEqual(
-            await self.subject.async_inferred_type(), "purline_m100_heater"
-        )
-
-    async def test_detects_remora_heatpump_payload(self):
-        for cfg in possible_matches(REMORA_HEATPUMP_PAYLOAD):
-            if cfg.legacy_type == "remora_heatpump":
-                self.assertEqual(cfg.match_quality(REMORA_HEATPUMP_PAYLOAD), 100.0)
-                return
-        self.fail()
-
-    async def test_detects_bwt_heatpump_payload(self):
-        for cfg in possible_matches(BWT_HEATPUMP_PAYLOAD):
-            if cfg.legacy_type == "bwt_heatpump":
-                self.assertEqual(cfg.match_quality(BWT_HEATPUMP_PAYLOAD), 100.0)
-                return
-        self.fail()
-
-    async def test_detects_eanons_humidifier_payload(self):
-        self.subject._cached_state = EANONS_HUMIDIFIER_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "eanons_humidifier")
-
-    async def test_detects_inkbird_thermostat_payload(self):
-        self.subject._cached_state = INKBIRD_THERMOSTAT_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "inkbird_thermostat")
-
-    async def test_detects_anko_fan_payload(self):
-        self.subject._cached_state = ANKO_FAN_PAYLOAD
-        self.assertEqual(await self.subject.async_inferred_type(), "anko_fan")
-
-    async def test_detects_electriq_humidifier_payload(self):
-        self.subject._cached_state = ELECTRIQ_DEHUMIDIFIER_PAYLOAD
-        self.assertEqual(
-            await self.subject.async_inferred_type(), "electriq_dehumidifier"
-        )
-
-    async def test_detects_poolex_silverline_heatpump_payload(self):
-        self.subject._cached_state = POOLEX_SILVERLINE_HEATPUMP_PAYLOAD
-        self.assertEqual(
-            await self.subject.async_inferred_type(), "poolex_silverline_heatpump"
-        )
-
-    async def test_detects_poolex_vertigo_heatpump_payload(self):
-        for cfg in possible_matches(POOLEX_VERTIGO_HEATPUMP_PAYLOAD):
-            if cfg.legacy_type == "poolex_vertigo_heatpump":
-                self.assertEqual(
-                    cfg.match_quality(POOLEX_VERTIGO_HEATPUMP_PAYLOAD), 100.0
-                )
-                return
-        self.fail()
-
     async def test_detection_returns_none_when_device_type_could_not_be_detected(self):
         self.subject._cached_state = {"2": False, "updated_at": datetime.now()}
         self.assertEqual(await self.subject.async_inferred_type(), None)

+ 6 - 94
tests/test_device_config.py

@@ -1,5 +1,6 @@
 """Test the config parser"""
-import unittest
+from unittest import IsolatedAsyncioTestCase
+from unittest.mock import MagicMock
 
 from warnings import warn
 
@@ -12,26 +13,12 @@ from custom_components.tuya_local.helpers.device_config import (
 
 from .const import (
     DEHUMIDIFIER_PAYLOAD,
-    EUROM_600_HEATER_PAYLOAD,
-    FAN_PAYLOAD,
-    GARDENPAC_HEATPUMP_PAYLOAD,
-    GECO_HEATER_PAYLOAD,
-    GPCV_HEATER_PAYLOAD,
     GPPH_HEATER_PAYLOAD,
-    GSH_HEATER_PAYLOAD,
     KOGAN_HEATER_PAYLOAD,
-    KOGAN_SOCKET_PAYLOAD,
-    KOGAN_SOCKET_PAYLOAD2,
-    PURLINE_M100_HEATER_PAYLOAD,
-    REMORA_HEATPUMP_PAYLOAD,
-    BWT_HEATPUMP_PAYLOAD,
-    EANONS_HUMIDIFIER_PAYLOAD,
-    INKBIRD_THERMOSTAT_PAYLOAD,
-    ANKO_FAN_PAYLOAD,
 )
 
 
-class TestDeviceConfig(unittest.TestCase):
+class TestDeviceConfig(IsolatedAsyncioTestCase):
     """Test the device config parser"""
 
     def test_can_find_config_files(self):
@@ -84,11 +71,12 @@ class TestDeviceConfig(unittest.TestCase):
         with self.assertRaises(TypeError):
             await voltage.async_set_value(mock_device, 230)
 
-    async def test_dps_values_returns_none_with_no_mapping(self):
+    def test_dps_values_returns_none_with_no_mapping(self):
         """Test that a dps with no mapping returns None as its possible values"""
+        mock_device = MagicMock()
         cfg = config_for_legacy_use("kogan_switch")
         voltage = cfg.primary_entity.find_dps("voltage_v")
-        self.assertIsNone(voltage.values)
+        self.assertIsNone(voltage.values(mock_device))
 
     # Test detection of all devices.
 
@@ -148,38 +136,6 @@ class TestDeviceConfig(unittest.TestCase):
         """Test that GPPH heater can be detected from its sample payload."""
         self._test_detect(GPPH_HEATER_PAYLOAD, "heater", "GoldairHeater")
 
-    def test_gpcv_heater_detection(self):
-        """Test that GPCV heater can be detected from its sample payload."""
-        self._test_detect(
-            GPCV_HEATER_PAYLOAD,
-            "gpcv_heater",
-            None,
-        )
-
-    def test_eurom_heater_detection(self):
-        """Test that Eurom heater can be detected from its sample payload."""
-        self._test_detect(
-            EUROM_600_HEATER_PAYLOAD,
-            "eurom_heater",
-            None,
-        )
-
-    def test_geco_heater_detection(self):
-        """Test that GECO heater can be detected from its sample payload."""
-        self._test_detect(
-            GECO_HEATER_PAYLOAD,
-            "geco_heater",
-            None,
-        )
-
-    def test_kogan_heater_detection(self):
-        """Test that Kogan heater can be detected from its sample payload."""
-        self._test_detect(
-            KOGAN_HEATER_PAYLOAD,
-            "kogan_heater",
-            None,
-        )
-
     def test_goldair_dehumidifier_detection(self):
         """Test that Goldair dehumidifier can be detected from its sample payload."""
         self._test_detect(
@@ -188,49 +144,5 @@ class TestDeviceConfig(unittest.TestCase):
             "GoldairDehumidifier",
         )
 
-    def test_goldair_fan_detection(self):
-        """Test that Goldair fan can be detected from its sample payload."""
-        self._test_detect(FAN_PAYLOAD, "fan", None)
-
-    def test_kogan_socket_detection(self):
-        """Test that 1st gen Kogan Socket can be detected from its sample payload."""
-        self._test_detect(
-            KOGAN_SOCKET_PAYLOAD,
-            "kogan_switch",
-            None,
-        )
-
-    def test_kogan_socket2_detection(self):
-        """Test that 2nd gen Kogan Socket can be detected from its sample payload."""
-        self._test_detect(
-            KOGAN_SOCKET_PAYLOAD2,
-            "kogan_switch",
-            None,
-        )
-
-    def test_gsh_heater_detection(self):
-        """Test that GSH heater can be detected from its sample payload."""
-        self._test_detect(
-            GSH_HEATER_PAYLOAD,
-            "gsh_heater",
-            None,
-        )
-
-    def test_gardenpac_heatpump_detection(self):
-        """Test that GardenPac heatpump can be detected from its sample payload."""
-        self._test_detect(
-            GARDENPAC_HEATPUMP_PAYLOAD,
-            "gardenpac_heatpump",
-            None,
-        )
-
-    def test_purline_heater_detection(self):
-        """Test that Purline heater can be detected from its sample payload."""
-        self._test_detect(
-            PURLINE_M100_HEATER_PAYLOAD,
-            "purline_m100_heater",
-            None,
-        )
-
     # Non-legacy devices endup being the same as the tests in test_device.py, so
     # skip them.