Prechádzať zdrojové kódy

Refactor climate, light, switch to lookup legacy_class in config.

Remove all the if/else statements dealing with explicit lists of classes, replacing them with the transitional legacy_class field from config files.
Jason Rumney 4 rokov pred
rodič
commit
53a150bed6

+ 20 - 46
custom_components/tuya_local/climate.py

@@ -1,34 +1,18 @@
 """
 Setup for different kinds of Tuya climate devices
 """
+import logging
+
 from . import DOMAIN
 from .const import (
     CONF_CLIMATE,
     CONF_DEVICE_ID,
     CONF_TYPE,
     CONF_TYPE_AUTO,
-    CONF_TYPE_DEHUMIDIFIER,
-    CONF_TYPE_FAN,
-    CONF_TYPE_GECO_HEATER,
-    CONF_TYPE_EUROM_600_HEATER,
-    CONF_TYPE_GPCV_HEATER,
-    CONF_TYPE_GPPH_HEATER,
-    CONF_TYPE_GSH_HEATER,
-    CONF_TYPE_KOGAN_HEATER,
-    CONF_TYPE_GARDENPAC_HEATPUMP,
-    CONF_TYPE_PURLINE_M100_HEATER,
-    CONF_CLIMATE,
 )
-from .dehumidifier.climate import GoldairDehumidifier
-from .fan.climate import GoldairFan
-from .geco_heater.climate import GoldairGECOHeater
-from .eurom_600_heater.climate import EuromMonSoleil600Heater
-from .gpcv_heater.climate import GoldairGPCVHeater
-from .heater.climate import GoldairHeater
-from .kogan_heater.climate import KoganHeater
-from .gardenpac_heatpump.climate import GardenPACPoolHeatpump
-from .purline_m100_heater.climate import PurlineM100Heater
-from .gsh_heater.climate import AnderssonGSHHeater
+from .helpers.device_config import config_for_legacy_use
+
+_LOGGER = logging.getLogger(__name__)
 
 
 async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -42,31 +26,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         if discovery_info[CONF_TYPE] is None:
             raise ValueError(f"Unable to detect type for device {device.name}")
 
-    if discovery_info[CONF_TYPE] == CONF_TYPE_GPPH_HEATER:
-        data[CONF_CLIMATE] = GoldairHeater(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_DEHUMIDIFIER:
-        data[CONF_CLIMATE] = GoldairDehumidifier(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_FAN:
-        data[CONF_CLIMATE] = GoldairFan(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_GECO_HEATER:
-        data[CONF_CLIMATE] = GoldairGECOHeater(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_EUROM_600_HEATER:
-        data[CONF_CLIMATE] = EuromMonSoleil600Heater(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_GPCV_HEATER:
-        data[CONF_CLIMATE] = GoldairGPCVHeater(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_KOGAN_HEATER:
-        data[CONF_CLIMATE] = KoganHeater(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_GSH_HEATER:
-        data[CONF_CLIMATE] = AnderssonGSHHeater(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_GARDENPAC_HEATPUMP:
-        data[CONF_CLIMATE] = GardenPACPoolHeatpump(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_PURLINE_M100_HEATER:
-        data[CONF_CLIMATE] = PurlineM100Heater(device)
-    else:
-        raise ValueError("This device does not support working as a climate device")
-
-    if CONF_CLIMATE in data:
-        async_add_entities([data[CONF_CLIMATE]])
+    cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
+    ecfg = cfg.primary_entity
+    if ecfg.entity != "climate":
+        for ecfg in cfg.secondary_entities():
+            if ecfg.entity == "climate":
+                break
+        if ecfg.entity != "climate":
+            raise ValueError(f"{device.name} does not support use as a climate device.")
+
+    legacy_class = ecfg.legacy_class
+    # Instantiate it: Sonarcloud thinks this is a blocker bug, and legacy_class
+    # is not callable, but the unit tests show the object is created...
+    data[CONF_CLIMATE] = legacy_class(device)
+    async_add_entities([data[CONF_CLIMATE]])
+    _LOGGER.debug(f"Adding climate device for {discovery_info[CONF_TYPE]}")
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):

+ 1 - 1
custom_components/tuya_local/devices/goldair_dehumidifier.yaml

@@ -73,7 +73,7 @@ primary_entity:
       readonly: true
 secondary_entities:
   - entity: light
-    legacy_class: ".dehumidifier.light.GoldairDehumifierLedDisplayLight"
+    legacy_class: ".dehumidifier.light.GoldairDehumidifierLedDisplayLight"
     name: Panel Light
     dps:
       - id: 102

+ 8 - 4
custom_components/tuya_local/helpers/device_config.py

@@ -1,11 +1,12 @@
 """
 Config parser for Tuya Local devices.
 """
+from fnmatch import fnmatch
 import logging
-
 from os import walk
 from os.path import join, dirname
-from fnmatch import fnmatch
+from pydoc import locate
+
 from homeassistant.util.yaml import load_yaml
 
 import custom_components.tuya_local.devices as config_dir
@@ -110,12 +111,15 @@ class TuyaEntityConfig:
     @property
     def name(self):
         """The friendly name for this entity."""
-        return self.__config.get("name", self.__device_name)
+        return self.__config.get("name", self.__device.name)
 
     @property
     def legacy_class(self):
         """Return the legacy device corresponding to this config."""
-        return "custom_components.tuya_local" + self.__config.get("legacy_class", None)
+        name = self.__config.get("legacy_class", None)
+        if name is None:
+            return None
+        return locate("custom_components.tuya_local" + name)
 
     @property
     def entity(self):

+ 21 - 22
custom_components/tuya_local/light.py

@@ -1,25 +1,22 @@
 """
 Setup for different kinds of Tuya climate devices
 """
+import logging
+
 from . import DOMAIN
 from .const import (
     CONF_DEVICE_ID,
     CONF_DISPLAY_LIGHT,
     CONF_TYPE,
     CONF_TYPE_AUTO,
-    CONF_TYPE_DEHUMIDIFIER,
-    CONF_TYPE_FAN,
-    CONF_TYPE_GPPH_HEATER,
-    CONF_TYPE_PURLINE_M100_HEATER,
 )
-from .dehumidifier.light import GoldairDehumidifierLedDisplayLight
-from .fan.light import GoldairFanLedDisplayLight
-from .heater.light import GoldairHeaterLedDisplayLight
-from .purline_m100_heater.light import PurlineM100HeaterLedDisplayLight
+from .helpers.device_config import config_for_legacy_use
+
+_LOGGER = logging.getLogger(__name__)
 
 
 async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
-    """Set up the Goldair climate device according to its type."""
+    """Set up the light device according to its type."""
     data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
     device = data["device"]
 
@@ -29,19 +26,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         if discovery_info[CONF_TYPE] is None:
             raise ValueError(f"Unable to detect type for device {device.name}")
 
-    if discovery_info[CONF_TYPE] == CONF_TYPE_GPPH_HEATER:
-        data[CONF_DISPLAY_LIGHT] = GoldairHeaterLedDisplayLight(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_DEHUMIDIFIER:
-        data[CONF_DISPLAY_LIGHT] = GoldairDehumidifierLedDisplayLight(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_FAN:
-        data[CONF_DISPLAY_LIGHT] = GoldairFanLedDisplayLight(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_PURLINE_M100_HEATER:
-        dataa[CONF_DISPLAY_LIGHT] = PurlineM100HeaterLedDisplayLight(device)
-    else:
-        raise ValueError("This device does not support panel lighting control.")
-
-    if CONF_DISPLAY_LIGHT in data:
-        async_add_entities([data[CONF_DISPLAY_LIGHT]])
+    cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
+    ecfg = cfg.primary_entity
+    if ecfg.entity != "light":
+        for ecfg in cfg.secondary_entities():
+            if ecfg.entity == "light":
+                break
+        if ecfg.entity != "light":
+            raise ValueError(f"{device.name} does not support use as a light device.")
+
+    legacy_class = ecfg.legacy_class
+    # Instantiate it: Sonarcloud thinks this is a blocker bug, and legacy_class
+    # is not callable, but the unit tests show the object is created...
+    data[CONF_DISPLAY_LIGHT] = legacy_class(device)
+    async_add_entities([data[CONF_DISPLAY_LIGHT]])
+    _LOGGER.debug(f"Adding light for {discovery_info[CONF_TYPE]}")
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):

+ 2 - 2
custom_components/tuya_local/lock.py

@@ -2,7 +2,6 @@
 Setup for different kinds of Tuya climate devices
 """
 import logging
-from pydoc import locate
 
 from . import DOMAIN
 from .const import (
@@ -37,11 +36,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         if ecfg.entity != "lock":
             raise ValueError(f"{device.name} does not support use as a lock device.")
 
-    legacy_class = locate(ecfg.legacy_class)
+    legacy_class = ecfg.legacy_class
     # Instantiate it: Sonarcloud thinks this is a blocker bug, and legacy_class
     # is not callable, but the unit tests show the object is created...
     data[CONF_CHILD_LOCK] = legacy_class(device)
     async_add_entities([data[CONF_CHILD_LOCK]])
+    _LOGGER.debug(f"Adding lock for {discovery_info[CONF_TYPE]}")
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):

+ 21 - 14
custom_components/tuya_local/switch.py

@@ -1,17 +1,18 @@
 """
 Setup for different kinds of Tuya switch devices
 """
+import logging
+
 from . import DOMAIN
 from .const import (
     CONF_DEVICE_ID,
+    CONF_SWITCH,
     CONF_TYPE,
-    CONF_TYPE_KOGAN_SWITCH,
-    CONF_TYPE_PURLINE_M100_HEATER,
     CONF_TYPE_AUTO,
-    CONF_SWITCH,
 )
-from .kogan_socket.switch import KoganSocketSwitch
-from .purline_m100_heater.switch import PurlineM100OpenWindowDetector
+from .helpers.device_config import config_for_legacy_use
+
+_LOGGER = logging.getLogger(__name__)
 
 
 async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -25,15 +26,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         if discovery_info[CONF_TYPE] is None:
             raise ValueError(f"Unable to detect type for device {device.name}")
 
-    if discovery_info[CONF_TYPE] == CONF_TYPE_KOGAN_SWITCH:
-        data[CONF_SWITCH] = KoganSocketSwitch(device)
-    elif discovery_info[CONF_TYPE] == CONF_TYPE_PURLINE_M100_HEATER:
-        data[CONF_SWITCH] = PurlineM100OpenWindowDetector(device)
-    else:
-        raise ValueError("This device does not support working as a switch")
-
-    if CONF_SWITCH in data:
-        async_add_entities([data[CONF_SWITCH]])
+    cfg = config_for_legacy_use(discovery_info[CONF_TYPE])
+    ecfg = cfg.primary_entity
+    if ecfg.entity != "switch":
+        for ecfg in cfg.secondary_entities():
+            if ecfg.entity == "switch":
+                break
+        if ecfg.entity != "switch":
+            raise ValueError(f"{device.name} does not support use as a switch device.")
+
+    legacy_class = ecfg.legacy_class
+    # Instantiate it: Sonarcloud thinks this is a blocker bug, and legacy_class
+    # is not callable, but the unit tests show the object is created...
+    data[CONF_SWITCH] = legacy_class(device)
+    async_add_entities([data[CONF_SWITCH]])
+    _LOGGER.debug(f"Adding switch for {discovery_info[CONF_TYPE]}")
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):

+ 37 - 0
tests/test_climate.py

@@ -0,0 +1,37 @@
+"""Tests for the light entity."""
+import pytest
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+from unittest.mock import AsyncMock, Mock
+
+from custom_components.tuya_local.const import (
+    CONF_CLIMATE,
+    CONF_DEVICE_ID,
+    CONF_TYPE,
+    CONF_TYPE_AUTO,
+    CONF_TYPE_GPPH_HEATER,
+    DOMAIN,
+)
+from custom_components.tuya_local.heater.climate import GoldairHeater
+from custom_components.tuya_local.climate import async_setup_entry
+
+
+async def test_init_entry(hass):
+    """Test the initialisation."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={CONF_TYPE: CONF_TYPE_AUTO, CONF_DEVICE_ID: "dummy"},
+    )
+    # although async, the async_add_entities function passed to
+    # async_setup_entry is called truly asynchronously. If we use
+    # AsyncMock, it expects us to await the result.
+    m_add_entities = Mock()
+    m_device = AsyncMock()
+    m_device.async_inferred_type = AsyncMock(return_value=CONF_TYPE_GPPH_HEATER)
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+
+    await async_setup_entry(hass, entry, m_add_entities)
+    assert type(hass.data[DOMAIN]["dummy"][CONF_CLIMATE]) == GoldairHeater
+    m_add_entities.assert_called_once()

+ 23 - 20
tests/test_device_config.py

@@ -67,9 +67,14 @@ class TestDeviceConfig(unittest.TestCase):
             parsed = TuyaDeviceConfig(cfg)
             self.assertIsNotNone(parsed.legacy_type)
             self.assertIsNotNone(parsed.primary_entity)
-            self.assertIsNotNone(parsed.primary_entity.legacy_class)
+            self.assertIsNotNone(
+                parsed.primary_entity.legacy_class,
+                f"No class for {parsed.legacy_type}/primary entity",
+            )
             for e in parsed.secondary_entities():
-                self.assertIsNotNone(e.legacy_class)
+                self.assertIsNotNone(
+                    e.legacy_class, f"No class for {parsed.legacy_type}/{e.name}"
+                )
 
     def _test_detect(self, payload, legacy_type, legacy_class):
         """Test that payload is detected as the correct type and class."""
@@ -83,8 +88,8 @@ class TestDeviceConfig(unittest.TestCase):
                 matched = True
                 quality = cfg.match_quality(payload)
                 self.assertEqual(
-                    cfg.primary_entity.legacy_class,
-                    "custom_components.tuya_local" + legacy_class,
+                    cfg.primary_entity.legacy_class.__name__,
+                    legacy_class,
                 )
             else:
                 false_matches.append(cfg)
@@ -105,22 +110,20 @@ class TestDeviceConfig(unittest.TestCase):
         # Ensure the same correct config is returned when looked up by type
         cfg = config_for_legacy_use(legacy_type)
         self.assertEqual(
-            cfg.primary_entity.legacy_class,
-            "custom_components.tuya_local" + legacy_class,
+            cfg.primary_entity.legacy_class.__name__,
+            legacy_class,
         )
 
     def test_gpph_heater_detection(self):
         """Test that GPPH heater can be detected from its sample payload."""
-        self._test_detect(
-            GPPH_HEATER_PAYLOAD, CONF_TYPE_GPPH_HEATER, ".heater.climate.GoldairHeater"
-        )
+        self._test_detect(GPPH_HEATER_PAYLOAD, CONF_TYPE_GPPH_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,
             CONF_TYPE_GPCV_HEATER,
-            ".gpcv_heater.climate.GoldairGPCVHeater",
+            "GoldairGPCVHeater",
         )
 
     def test_eurom_heater_detection(self):
@@ -128,7 +131,7 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             EUROM_600_HEATER_PAYLOAD,
             CONF_TYPE_EUROM_600_HEATER,
-            ".eurom_600_heater.climate.EuromMonSoleil600Heater",
+            "EuromMonSoleil600Heater",
         )
 
     def test_geco_heater_detection(self):
@@ -136,7 +139,7 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             GECO_HEATER_PAYLOAD,
             CONF_TYPE_GECO_HEATER,
-            ".geco_heater.climate.GoldairGECOHeater",
+            "GoldairGECOHeater",
         )
 
     def test_kogan_heater_detection(self):
@@ -144,7 +147,7 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             KOGAN_HEATER_PAYLOAD,
             CONF_TYPE_KOGAN_HEATER,
-            ".kogan_heater.climate.KoganHeater",
+            "KoganHeater",
         )
 
     def test_goldair_dehumidifier_detection(self):
@@ -152,19 +155,19 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             DEHUMIDIFIER_PAYLOAD,
             CONF_TYPE_DEHUMIDIFIER,
-            ".dehumidifier.climate.GoldairDehumidifier",
+            "GoldairDehumidifier",
         )
 
     def test_goldair_fan_detection(self):
         """Test that Goldair fan can be detected from its sample payload."""
-        self._test_detect(FAN_PAYLOAD, CONF_TYPE_FAN, ".fan.climate.GoldairFan")
+        self._test_detect(FAN_PAYLOAD, CONF_TYPE_FAN, "GoldairFan")
 
     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,
             CONF_TYPE_KOGAN_SWITCH,
-            ".kogan_socket.switch.KoganSocketSwitch",
+            "KoganSocketSwitch",
         )
 
     def test_kogan_socket2_detection(self):
@@ -172,7 +175,7 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             KOGAN_SOCKET_PAYLOAD2,
             CONF_TYPE_KOGAN_SWITCH,
-            ".kogan_socket.switch.KoganSocketSwitch",
+            "KoganSocketSwitch",
         )
 
     def test_gsh_heater_detection(self):
@@ -180,7 +183,7 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             GSH_HEATER_PAYLOAD,
             CONF_TYPE_GSH_HEATER,
-            ".gsh_heater.climate.AnderssonGSHHeater",
+            "AnderssonGSHHeater",
         )
 
     def test_gardenpac_heatpump_detection(self):
@@ -188,7 +191,7 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             GARDENPAC_HEATPUMP_PAYLOAD,
             CONF_TYPE_GARDENPAC_HEATPUMP,
-            ".gardenpac_heatpump.climate.GardenPACPoolHeatpump",
+            "GardenPACPoolHeatpump",
         )
 
     def test_purline_heater_detection(self):
@@ -196,5 +199,5 @@ class TestDeviceConfig(unittest.TestCase):
         self._test_detect(
             PURLINE_M100_HEATER_PAYLOAD,
             CONF_TYPE_PURLINE_M100_HEATER,
-            ".purline_m100_heater.climate.PurlineM100Heater",
+            "PurlineM100Heater",
         )

+ 40 - 0
tests/test_light.py

@@ -0,0 +1,40 @@
+"""Tests for the light entity."""
+import pytest
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+from unittest.mock import AsyncMock, Mock
+
+from custom_components.tuya_local.const import (
+    CONF_DISPLAY_LIGHT,
+    CONF_DEVICE_ID,
+    CONF_TYPE,
+    CONF_TYPE_AUTO,
+    CONF_TYPE_GPPH_HEATER,
+    DOMAIN,
+)
+from custom_components.tuya_local.heater.light import GoldairHeaterLedDisplayLight
+from custom_components.tuya_local.light import async_setup_entry
+
+
+async def test_init_entry(hass):
+    """Test the initialisation."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={CONF_TYPE: CONF_TYPE_AUTO, CONF_DEVICE_ID: "dummy"},
+    )
+    # although async, the async_add_entities function passed to
+    # async_setup_entry is called truly asynchronously. If we use
+    # AsyncMock, it expects us to await the result.
+    m_add_entities = Mock()
+    m_device = AsyncMock()
+    m_device.async_inferred_type = AsyncMock(return_value=CONF_TYPE_GPPH_HEATER)
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+
+    await async_setup_entry(hass, entry, m_add_entities)
+    assert (
+        type(hass.data[DOMAIN]["dummy"][CONF_DISPLAY_LIGHT])
+        == GoldairHeaterLedDisplayLight
+    )
+    m_add_entities.assert_called_once()

+ 37 - 0
tests/test_switch.py

@@ -0,0 +1,37 @@
+"""Tests for the switch entity."""
+import pytest
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+from unittest.mock import AsyncMock, Mock
+
+from custom_components.tuya_local.const import (
+    CONF_DEVICE_ID,
+    CONF_SWITCH,
+    CONF_TYPE,
+    CONF_TYPE_AUTO,
+    CONF_TYPE_KOGAN_SWITCH,
+    DOMAIN,
+)
+from custom_components.tuya_local.kogan_socket.switch import KoganSocketSwitch
+from custom_components.tuya_local.switch import async_setup_entry
+
+
+async def test_init_entry(hass):
+    """Test the initialisation."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={CONF_TYPE: CONF_TYPE_AUTO, CONF_DEVICE_ID: "dummy"},
+    )
+    # although async, the async_add_entities function passed to
+    # async_setup_entry is called truly asynchronously. If we use
+    # AsyncMock, it expects us to await the result.
+    m_add_entities = Mock()
+    m_device = AsyncMock()
+    m_device.async_inferred_type = AsyncMock(return_value=CONF_TYPE_KOGAN_SWITCH)
+
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN]["dummy"] = {}
+    hass.data[DOMAIN]["dummy"]["device"] = m_device
+
+    await async_setup_entry(hass, entry, m_add_entities)
+    assert type(hass.data[DOMAIN]["dummy"][CONF_SWITCH]) == KoganSocketSwitch
+    m_add_entities.assert_called_once()