Selaa lähdekoodia

fix (migration): correct past migrations to invalid unique ids.

Some time ago, HA made the unique_id of config entries optional and
stopped automatically populating them at device level (entities are
still populated since our entity has a unique_id attribute.

Now the config flow has to explicitly set the unique_id.

Since this has not been done, some migrations have picked up
incorrect (None) device ids instead of the actual device id, and used
them to generate new entity unique ids during migration.

Repair this damage, and ensure that we get the device id in future.

Issue #4130
Jason Rumney 2 kuukautta sitten
vanhempi
commit
964cf443f4

+ 47 - 11
custom_components/tuya_local/__init__.py

@@ -17,6 +17,7 @@ from homeassistant.helpers.entity_registry import async_migrate_entries
 from homeassistant.util import slugify
 
 from .const import (
+    CONF_DEVICE_CID,
     CONF_DEVICE_ID,
     CONF_LOCAL_KEY,
     CONF_POLL_ONLY,
@@ -60,6 +61,15 @@ def replace_unique_ids(entity_entry, device_id, conf_file, replacements):
                 }
 
 
+def get_device_unique_id(entry: ConfigEntry):
+    """Get the unique id for the device from the config entry."""
+    return (
+        entry.unique_id
+        or entry.data.get(CONF_DEVICE_CID)
+        or entry.data.get(CONF_DEVICE_ID)
+    )
+
+
 async def async_migrate_entry(hass, entry: ConfigEntry):
     """Migrate to latest config format."""
 
@@ -157,7 +167,7 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
 
     if entry.version <= 5:
         # Migrate unique ids of existing entities to new format
-        old_id = entry.unique_id
+        old_id = get_device_unique_id(entry)
         conf_file = await hass.async_add_executor_job(
             get_config,
             entry.data[CONF_TYPE],
@@ -241,7 +251,7 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
 
     if entry.version <= 11:
         # Migrate unique ids of existing entities to new format
-        device_id = entry.unique_id
+        device_id = get_device_unique_id(entry)
         conf_file = await hass.async_add_executor_job(
             get_config,
             entry.data[CONF_TYPE],
@@ -288,7 +298,7 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
     if entry.version <= 12:
         # Migrate unique ids of existing entities to new format taking into
         # account device_class if name is missing.
-        device_id = entry.unique_id
+        device_id = get_device_unique_id(entry)
         conf_file = await hass.async_add_executor_job(
             get_config,
             entry.data[CONF_TYPE],
@@ -348,7 +358,7 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
     if entry.version == 13 and entry.minor_version < 2:
         # Migrate unique ids of existing entities to new id taking into
         # account translation_key, and standardising naming
-        device_id = entry.unique_id
+        device_id = get_device_unique_id(entry)
         conf_file = await hass.async_add_executor_job(
             get_config,
             entry.data[CONF_TYPE],
@@ -396,7 +406,7 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
     if entry.version == 13 and entry.minor_version < 3:
         # Migrate unique ids of existing entities to new id taking into
         # account translation_key, and standardising naming
-        device_id = entry.unique_id
+        device_id = get_device_unique_id(entry)
         conf_file = await hass.async_add_executor_job(
             get_config,
             entry.data[CONF_TYPE],
@@ -463,7 +473,7 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
     if entry.version == 13 and entry.minor_version < 5:
         # Migrate unique ids of existing entities to new id taking into
         # account translation_key, and standardising naming
-        device_id = entry.unique_id
+        device_id = get_device_unique_id(entry)
         conf_file = await hass.async_add_executor_job(
             get_config,
             entry.data[CONF_TYPE],
@@ -493,7 +503,7 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
     if entry.version == 13 and entry.minor_version < 6:
         # Migrate unique ids of existing entities to new id taking into
         # account translation_key, and standardising naming
-        device_id = entry.unique_id
+        device_id = get_device_unique_id(entry)
         conf_file = await hass.async_add_executor_job(
             get_config,
             entry.data[CONF_TYPE],
@@ -534,7 +544,7 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
     if entry.version == 13 and entry.minor_version < 7:
         # Migrate unique ids of existing entities to new id taking into
         # account translation_key, and standardising naming
-        device_id = entry.unique_id
+        device_id = get_device_unique_id(entry)
         conf_file = await hass.async_add_executor_job(
             get_config,
             entry.data[CONF_TYPE],
@@ -561,7 +571,7 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
     if entry.version == 13 and entry.minor_version < 8:
         # Migrate unique ids of existing entities to new id taking into
         # account translation_key, and standardising naming
-        device_id = entry.unique_id
+        device_id = get_device_unique_id(entry)
         conf_file = await hass.async_add_executor_job(
             get_config,
             entry.data[CONF_TYPE],
@@ -612,7 +622,7 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
     if entry.version == 13 and entry.minor_version < 9:
         # Migrate unique ids of existing entities to new id taking into
         # account translation_key, and standardising naming
-        device_id = entry.unique_id
+        device_id = get_device_unique_id(entry)
         conf_file = await hass.async_add_executor_job(
             get_config,
             entry.data[CONF_TYPE],
@@ -639,7 +649,7 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
     if entry.version == 13 and entry.minor_version < 10:
         # Migrate unique ids of existing entities to new id taking into
         # account translation_key, and standardising naming
-        device_id = entry.unique_id
+        device_id = get_device_unique_id(entry)
         conf_file = await hass.async_add_executor_job(
             get_config,
             entry.data[CONF_TYPE],
@@ -666,6 +676,32 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
 
         await async_migrate_entries(hass, entry.entry_id, update_unique_id13_10)
         hass.config_entries.async_update_entry(entry, minor_version=10)
+
+    if entry.version == 13 and entry.minor_version < 11:
+        # at some point HA stopped populating unique_id for device entries,
+        # and our migrations have been using the wrong value. Migration
+        # to correct that
+        unique_id = entry.unique_id
+        device_id = get_device_unique_id(entry)
+        if unique_id is None:
+            hass.config_entries.async_update_entry(
+                entry,
+                unique_id=device_id,
+            )
+
+        @callback
+        def update_unique_id3_11(entity_entry):
+            """Update the unique id of badly migrated entities."""
+            old_id = entity_entry.unique_id
+            if old_id.startswith("None-"):
+                new_id = f"{device_id}-{old_id[5:]}"
+                return {
+                    "new_unique_id": new_id,
+                }
+
+        await async_migrate_entries(hass, entry.entry_id, update_unique_id3_11)
+        hass.config_entries.async_update_entry(entry, minor_version=11)
+
     return True
 
 

+ 5 - 2
custom_components/tuya_local/config_flow.py

@@ -47,7 +47,7 @@ _LOGGER = logging.getLogger(__name__)
 
 class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
     VERSION = 13
-    MINOR_VERSION = 10
+    MINOR_VERSION = 11
     CONNECTION_CLASS = CONN_CLASS_LOCAL_PUSH
     device = None
     data = {}
@@ -354,7 +354,10 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
                         self.device.set_detected_product_id(
                             self.__cloud_device.get("local_product_id")
                         )
-
+                await self.async_set_unique_id(
+                    user_input.get(CONF_DEVICE_CID, user_input[CONF_DEVICE_ID])
+                )
+                self._abort_if_unique_id_configured()
                 return await self.async_step_select_type()
             else:
                 errors["base"] = "connection"

+ 18 - 0
tests/test_config_flow.py

@@ -11,6 +11,7 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry
 from custom_components.tuya_local import (
     async_migrate_entry,
     config_flow,
+    get_device_unique_id,
 )
 from custom_components.tuya_local.const import (
     CONF_DEVICE_CID,
@@ -695,3 +696,20 @@ async def test_options_flow_fails_when_config_is_missing(mock_test, hass):
     result = await hass.config_entries.options.async_init(config_entry.entry_id)
     assert result["type"] == "abort"
     assert result["reason"] == "not_supported"
+
+
+def test_migration_gets_correct_device_id():
+    """Test that migration gets the correct device id."""
+    # Normal device
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=1,
+        title="test",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: TESTKEY,
+            CONF_TYPE: "auto",
+        },
+    )
+    assert get_device_unique_id(entry) == "deviceid"