ソースを参照

Prepare initialisation for future multiple entities of the same type.

Index entities in the HA data store by their config_id instead of hardcoded strings.
Loop through all entities instead of breaking when the first is found.
Currently if there are multiple entities of the same type defined in a config,
this will create them, but override with subsequent ones (orphaning the earlier one, as it is not destroyed).  Further changes are needed before it is safe to create configs with multiple entries of the same type, but this change is being committed first to ensure the current configs are not affected before making the breaking config change and migration to new config_ids.
Jason Rumney 4 年 前
コミット
2d47be7a68

+ 29 - 25
custom_components/tuya_local/climate.py

@@ -19,37 +19,41 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     """Set up the Tuya device according to its type."""
     data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
     device = data["device"]
+    climates = []
 
     cfg = get_config(discovery_info[CONF_TYPE])
     if cfg is None:
         raise ValueError(f"No device config found for {discovery_info}")
     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.")
-    if ecfg.deprecated:
-        _LOGGER.warning(ecfg.deprecation_message)
-
-    legacy_class = ecfg.legacy_class
-    # Transition: generic climate entity exists, but is not complete. More
-    # complex climate devices still need a device specific class.
-    # If legacy_class exists, use it, otherwise use the generic climate class.
-    if legacy_class is not None:
-        data[CONF_CLIMATE] = legacy_class(device)
-    else:
-        data[CONF_CLIMATE] = TuyaLocalClimate(device, ecfg)
-
-    async_add_entities([data[CONF_CLIMATE]])
-    _LOGGER.debug(f"Adding climate device for {discovery_info[CONF_TYPE]}")
+    if ecfg.entity == "climate" and discovery_info.get(ecfg.config_id, False):
+        legacy_class = ecfg.legacy_class
+        if legacy_class is None:
+            data[ecfg.config_id] = TuyaLocalClimate(device, ecfg)
+        else:
+            data[ecfg.config_id] = legacy_class(device)
+        climates.append(data[ecfg.config_id])
+        if ecfg.deprecated:
+            _LOGGER.warning(ecfg.deprecation_message)
+        _LOGGER.debug(f"Adding climate for {ecfg.name}")
+
+    for ecfg in cfg.secondary_entities():
+        if ecfg.entity == "climate" and discovery_info.get(ecfg.config_id, False):
+            legacy_class = ecfg.legacy_class
+            if legacy_class is None:
+                data[ecfg.config_id] = TuyaLocalClimate(device, ecfg)
+            else:
+                data[ecfg.config_id] = legacy_class(device)
+            climates.append(data[ecfg.config_id])
+            if ecfg.deprecated:
+                _LOGGER.warning(ecfg.deprecation_message)
+            _LOGGER.debug(f"Adding climate for {ecfg.name}")
+
+    if not climates:
+        raise ValueError(f"{device.name} does not support use as a climate device.")
+
+    async_add_entities(climates)
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):
     config = {**config_entry.data, **config_entry.options}
-    discovery_info = {
-        CONF_DEVICE_ID: config[CONF_DEVICE_ID],
-        CONF_TYPE: config[CONF_TYPE],
-    }
-    await async_setup_platform(hass, {}, async_add_entities, discovery_info)
+    await async_setup_platform(hass, {}, async_add_entities, config)

+ 18 - 16
custom_components/tuya_local/fan.py

@@ -19,31 +19,33 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     """Set up the Tuya device according to its type."""
     data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
     device = data["device"]
+    fans = []
 
     cfg = get_config(discovery_info[CONF_TYPE])
     if cfg is None:
         raise ValueError(f"No device config found for {discovery_info}")
     ecfg = cfg.primary_entity
-    if ecfg.entity != "fan":
-        for ecfg in cfg.secondary_entities():
-            if ecfg.entity == "fan":
-                break
-        if ecfg.entity != "fan":
-            raise ValueError(f"{device.name} does not support use as a fan device.")
+    if ecfg.entity == "fan" and discovery_info.get(ecfg.config_id, False):
+        data[ecfg.config_id] = TuyaLocalFan(device, ecfg)
+        fans.append(data[ecfg.config_id])
+        if ecfg.deprecated:
+            _LOGGER.warning(ecfg.deprecation_message)
+        _LOGGER.debug(f"Adding fan for {ecfg.name}")
 
-    if ecfg.deprecated:
-        _LOGGER.warning(ecfg.deprecation_message)
+    for ecfg in cfg.secondary_entities():
+        if ecfg.entity == "fan" and discovery_info.get(ecfg.config_id, False):
+            data[ecfg.config_id] = TuyaLocalFan(device, ecfg)
+            fans.append(data[ecfg.config_id])
+            if ecfg.deprecated:
+                _LOGGER.warning(ecfg.deprecation_message)
+            _LOGGER.debug(f"Adding fan for {ecfg.name}")
 
-    data[CONF_FAN] = TuyaLocalFan(device, ecfg)
+    if not fans:
+        raise ValueError(f"{device.name} does not support use as a fan device.")
 
-    async_add_entities([data[CONF_FAN]])
-    _LOGGER.debug(f"Adding fan device for {discovery_info[CONF_TYPE]}")
+    async_add_entities(fans)
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):
     config = {**config_entry.data, **config_entry.options}
-    discovery_info = {
-        CONF_DEVICE_ID: config[CONF_DEVICE_ID],
-        CONF_TYPE: config[CONF_TYPE],
-    }
-    await async_setup_platform(hass, {}, async_add_entities, discovery_info)
+    await async_setup_platform(hass, {}, async_add_entities, config)

+ 19 - 18
custom_components/tuya_local/humidifier.py

@@ -19,32 +19,33 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     """Set up the Tuya device according to its type."""
     data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
     device = data["device"]
+    humidifiers = []
 
     cfg = get_config(discovery_info[CONF_TYPE])
     if cfg is None:
         raise ValueError(f"No device config found for {discovery_info}")
     ecfg = cfg.primary_entity
-    if ecfg.entity != "humidifier":
-        for ecfg in cfg.secondary_entities():
-            if ecfg.entity == "humidifier":
-                break
-        if ecfg.entity != "humidifier":
-            raise ValueError(
-                f"{device.name} does not support use as a humidifier device."
-            )
-    if ecfg.deprecated:
-        _LOGGER.warning(ecfg.deprecation_message)
+    if ecfg.entity == "humidifier" and discovery_info.get(ecfg.config_id, False):
+        data[ecfg.config_id] = TuyaLocalHumidifier(device, ecfg)
+        humidifiers.append(data[ecfg.config_id])
+        if ecfg.deprecated:
+            _LOGGER.warning(ecfg.deprecation_message)
+        _LOGGER.debug(f"Adding humidifier for {ecfg.name}")
 
-    data[CONF_HUMIDIFIER] = TuyaLocalHumidifier(device, ecfg)
+    for ecfg in cfg.secondary_entities():
+        if ecfg.entity == "humidifier" and discovery_info.get(ecfg.config_id, False):
+            data[ecfg.config_id] = TuyaLocalHumidifier(device, ecfg)
+            humidifiers.append(data[ecfg.config_id])
+            if ecfg.deprecated:
+                _LOGGER.warning(ecfg.deprecation_message)
+            _LOGGER.debug(f"Adding humidifier for {ecfg.name}")
 
-    async_add_entities([data[CONF_HUMIDIFIER]])
-    _LOGGER.debug(f"Adding humidifier device for {discovery_info[CONF_TYPE]}")
+    if not humidifiers:
+        raise ValueError(f"{device.name} does not support use as a humidifier device.")
+
+    async_add_entities(humidifiers)
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):
     config = {**config_entry.data, **config_entry.options}
-    discovery_info = {
-        CONF_DEVICE_ID: config[CONF_DEVICE_ID],
-        CONF_TYPE: config[CONF_TYPE],
-    }
-    await async_setup_platform(hass, {}, async_add_entities, discovery_info)
+    await async_setup_platform(hass, {}, async_add_entities, config)

+ 20 - 16
custom_components/tuya_local/light.py

@@ -19,29 +19,33 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     """Set up the light device according to its type."""
     data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
     device = data["device"]
+    lights = []
 
     cfg = get_config(discovery_info[CONF_TYPE])
     if cfg is None:
         raise ValueError(f"No device config found for {discovery_info}")
     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.")
-    if ecfg.deprecated:
-        _LOGGER.warning(ecfg.deprecation_message)
+    if ecfg.entity == "light" and discovery_info.get(ecfg.config_id, False):
+        data[ecfg.config_id] = TuyaLocalLight(device, ecfg)
+        lights.append(data[ecfg.config_id])
+        if ecfg.deprecated:
+            _LOGGER.warning(ecfg.deprecation_message)
+        _LOGGER.debug(f"Adding light for {device.name}/{ecfg.name}")
 
-    data[CONF_LIGHT] = TuyaLocalLight(device, ecfg)
-    async_add_entities([data[CONF_LIGHT]])
-    _LOGGER.debug(f"Adding light for {discovery_info[CONF_TYPE]}")
+    for ecfg in cfg.secondary_entities():
+        if ecfg.entity == "light" and discovery_info.get(ecfg.config_id, False):
+            data[ecfg.config_id] = TuyaLocalLight(device, ecfg)
+            lights.append(data[ecfg.config_id])
+            if ecfg.deprecated:
+                _LOGGER.warning(ecfg.deprecation_message)
+            _LOGGER.debug(f"Adding light for {ecfg.name}")
+
+    if not lights:
+        raise ValueError(f"{device.name} does not support use as a light device.")
+
+    async_add_entities(lights)
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):
     config = {**config_entry.data, **config_entry.options}
-    discovery_info = {
-        CONF_DEVICE_ID: config[CONF_DEVICE_ID],
-        CONF_TYPE: config[CONF_TYPE],
-    }
-    await async_setup_platform(hass, {}, async_add_entities, discovery_info)
+    await async_setup_platform(hass, {}, async_add_entities, config)

+ 20 - 18
custom_components/tuya_local/lock.py

@@ -6,7 +6,6 @@ import logging
 from . import DOMAIN
 from .const import (
     CONF_DEVICE_ID,
-    CONF_LOCK,
     CONF_TYPE,
 )
 from .generic.lock import TuyaLocalLock
@@ -20,29 +19,32 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     _LOGGER.debug(f"Domain data: {hass.data[DOMAIN]}")
     data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
     device = data["device"]
+    locks = []
 
     cfg = get_config(discovery_info[CONF_TYPE])
     if cfg is None:
         raise ValueError(f"No device config found for {discovery_info}")
     ecfg = cfg.primary_entity
-    if ecfg.entity != "lock":
-        for ecfg in cfg.secondary_entities():
-            if ecfg.entity == "lock":
-                break
-        if ecfg.entity != "lock":
-            raise ValueError(f"{device.name} does not support use as a lock device.")
-    if ecfg.deprecated:
-        _LOGGER.warning(ecfg.deprecation_message)
-
-    data[CONF_LOCK] = TuyaLocalLock(device, ecfg)
-    async_add_entities([data[CONF_LOCK]])
-    _LOGGER.debug(f"Adding lock for {discovery_info[CONF_TYPE]}")
+    if ecfg.entity == "lock" and discovery_info.get(ecfg.config_id, False):
+        data[ecfg.config_id] = TuyaLocalLock(device, ecfg)
+        locks.append(data[ecfg.config_id])
+        if ecfg.deprecated:
+            _LOGGER.warning(ecfg.deprecation_message)
+        _LOGGER.debug(f"Adding lock for {ecfg.name}")
+
+    for ecfg in cfg.secondary_entities():
+        if ecfg.entity == "lock" and discovery_info.get(ecfg.config_id, False):
+            data[ecfg.config_id] = TuyaLocalLock(device, ecfg)
+            locks.append(data[ecfg.config_id])
+            if ecfg.deprecated:
+                _LOGGER.warning(ecfg.deprecation_message)
+            _LOGGER.debug(f"Adding lock for {ecfg.name}")
+
+    if not locks:
+        raise ValueError(f"{device.name} does not support use as a lock device.")
+    async_add_entities(locks)
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):
     config = {**config_entry.data, **config_entry.options}
-    discovery_info = {
-        CONF_DEVICE_ID: config[CONF_DEVICE_ID],
-        CONF_TYPE: config[CONF_TYPE],
-    }
-    await async_setup_platform(hass, {}, async_add_entities, discovery_info)
+    await async_setup_platform(hass, {}, async_add_entities, config)

+ 2 - 2
custom_components/tuya_local/manifest.json

@@ -2,11 +2,11 @@
     "domain": "tuya_local",
     "iot_class": "local_polling",
     "name": "Tuya Local",
-    "version": "0.11.3",
+    "version": "0.12.0",
     "documentation": "https://github.com/make-all/tuya-local",
     "issue_tracker": "https://github.com/make-all/tuya-local/issues",
     "dependencies": [],
     "codeowners": ["@make-all"],
-    "requirements": ["pycryptodome==3.10.4","tinytuya==1.2.9"],
+    "requirements": ["pycryptodome==3.11.0","tinytuya==1.2.9"],
     "config_flow": true
 }

+ 19 - 17
custom_components/tuya_local/switch.py

@@ -6,7 +6,6 @@ import logging
 from . import DOMAIN
 from .const import (
     CONF_DEVICE_ID,
-    CONF_SWITCH,
     CONF_TYPE,
 )
 from .generic.switch import TuyaLocalSwitch
@@ -19,30 +18,33 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     """Set up the switch device according to its type."""
     data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
     device = data["device"]
+    switches = []
 
     cfg = get_config(discovery_info[CONF_TYPE])
     if cfg is None:
         raise ValueError(f"No device config found for {discovery_info}")
     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.")
+    if ecfg.entity == "switch" and discovery_info.get(ecfg.config_id, False):
+        data[ecfg.config_id] = TuyaLocalSwitch(device, ecfg)
+        switches.append(data[ecfg.config_id])
+        if ecfg.deprecated:
+            _LOGGER.warning(ecfg.deprecation_message)
+        _LOGGER.debug(f"Adding switch for {discovery_info[ecfg.config_id]}")
 
-    if ecfg.deprecated:
-        _LOGGER.warning(ecfg.deprecation_message)
+    for ecfg in cfg.secondary_entities():
+        if ecfg.entity == "switch" and discovery_info.get(ecfg.config_id, False):
+            data[ecfg.config_id] = TuyaLocalSwitch(device, ecfg)
+            switches.append(data[ecfg.config_id])
+            if ecfg.deprecated:
+                _LOGGER.warning(ecfg.deprecation_message)
+            _LOGGER.debug(f"Adding switch for {discovery_info[ecfg.config_id]}")
 
-    data[CONF_SWITCH] = TuyaLocalSwitch(device, ecfg)
-    async_add_entities([data[CONF_SWITCH]])
-    _LOGGER.debug(f"Adding switch for {discovery_info[CONF_TYPE]}")
+    if not switches:
+        raise ValueError(f"{device.name} does not support use as a switch device.")
+
+    async_add_entities(switches)
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):
     config = {**config_entry.data, **config_entry.options}
-    discovery_info = {
-        CONF_DEVICE_ID: config[CONF_DEVICE_ID],
-        CONF_TYPE: config[CONF_TYPE],
-    }
-    await async_setup_platform(hass, {}, async_add_entities, discovery_info)
+    await async_setup_platform(hass, {}, async_add_entities, config)

+ 1 - 1
requirements-dev.txt

@@ -5,5 +5,5 @@ pytest-homeassistant-custom-component
 pytest
 pytest-asyncio
 pytest-cov
-pycryptodome==3.10.4
+pycryptodome==3.11.0
 tinytuya==1.2.9

+ 1 - 1
requirements-first.txt

@@ -1 +1 @@
-pycryptodome==3.10.4
+pycryptodome==3.11.0

+ 1 - 1
requirements.txt

@@ -1,2 +1,2 @@
-pycryptodome~=3.10.4
+pycryptodome~=3.11.0
 tinytuya~=1.2.9

+ 4 - 4
tests/test_climate.py

@@ -17,7 +17,7 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "heater", CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "heater", CONF_DEVICE_ID: "dummy", CONF_CLIMATE: True},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -38,7 +38,7 @@ async def test_init_entry_as_secondary(hass):
     """Test initialisation when fan is a secondary entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "dehumidifier", CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "dehumidifier", CONF_DEVICE_ID: "dummy", CONF_CLIMATE: True},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -59,7 +59,7 @@ async def test_init_entry_fails_if_device_has_no_climate(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "kogan_switch", CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "kogan_switch", CONF_DEVICE_ID: "dummy", CONF_CLIMATE: True},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -82,7 +82,7 @@ async def test_init_entry_fails_if_config_is_missing(hass):
     """Test initialisation when device has no matching entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy", CONF_CLIMATE: True},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use

+ 2 - 2
tests/test_fan.py

@@ -16,7 +16,7 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "fan", CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "fan", CONF_DEVICE_ID: "dummy", CONF_FAN: True},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -37,7 +37,7 @@ async def test_init_entry_as_secondary(hass):
     """Test initialisation when fan is a secondary entity"""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "dehumidifier", CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "dehumidifier", CONF_DEVICE_ID: "dummy", CONF_FAN: True},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use

+ 5 - 1
tests/test_humidifier.py

@@ -16,7 +16,11 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "dehumidifier", CONF_DEVICE_ID: "dummy"},
+        data={
+            CONF_TYPE: "dehumidifier",
+            CONF_DEVICE_ID: "dummy",
+            CONF_HUMIDIFIER: True,
+        },
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use

+ 1 - 1
tests/test_light.py

@@ -16,7 +16,7 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "heater", CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "heater", CONF_DEVICE_ID: "dummy", CONF_LIGHT: True},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use

+ 1 - 1
tests/test_lock.py

@@ -16,7 +16,7 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "heater", CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "heater", CONF_DEVICE_ID: "dummy", CONF_LOCK: True},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use

+ 2 - 2
tests/test_switch.py

@@ -16,7 +16,7 @@ async def test_init_entry(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "kogan_switch", CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "kogan_switch", CONF_DEVICE_ID: "dummy", CONF_SWITCH: True},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use
@@ -37,7 +37,7 @@ async def test_init_entry_as_secondary(hass):
     """Test the initialisation."""
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={CONF_TYPE: "deta_fan", CONF_DEVICE_ID: "dummy"},
+        data={CONF_TYPE: "deta_fan", CONF_DEVICE_ID: "dummy", CONF_SWITCH: True},
     )
     # although async, the async_add_entities function passed to
     # async_setup_entry is called truly asynchronously. If we use