Преглед изворни кода

Migrate unique_ids to be really unique.

- remove friendly_name properties that were not used by HA.
- use more descriptive name properties for entities.
Jason Rumney пре 4 година
родитељ
комит
22ea716df3

+ 38 - 2
custom_components/tuya_local/__init__.py

@@ -10,7 +10,8 @@ import logging
 
 
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import CONF_HOST
 from homeassistant.const import CONF_HOST
-from homeassistant.core import HomeAssistant
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.entity_registry import async_migrate_entries
 
 
 from .const import (
 from .const import (
     CONF_DEVICE_ID,
     CONF_DEVICE_ID,
@@ -41,6 +42,9 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
             device = setup_device(hass, config)
             device = setup_device(hass, config)
             config[CONF_TYPE] = await device.async_inferred_type()
             config[CONF_TYPE] = await device.async_inferred_type()
             if config[CONF_TYPE] is None:
             if config[CONF_TYPE] is None:
+                _LOGGER.error(
+                    f"Unable to determine type for device {config[CONF_DEVICE_ID]}."
+                )
                 return False
                 return False
 
 
         entry.data = {
         entry.data = {
@@ -69,6 +73,9 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
             device = setup_device(hass, config)
             device = setup_device(hass, config)
             config[CONF_TYPE] = await device.async_inferred_type()
             config[CONF_TYPE] = await device.async_inferred_type()
             if config[CONF_TYPE] is None:
             if config[CONF_TYPE] is None:
+                _LOGGER.error(
+                    f"Unable to determine type for device {config[CONF_DEVICE_ID]}."
+                )
                 return False
                 return False
         entry.data = {
         entry.data = {
             CONF_DEVICE_ID: config[CONF_DEVICE_ID],
             CONF_DEVICE_ID: config[CONF_DEVICE_ID],
@@ -127,6 +134,35 @@ async def async_migrate_entry(hass, entry: ConfigEntry):
         entry.options = {**newopts}
         entry.options = {**newopts}
         entry.version = 5
         entry.version = 5
 
 
+    if entry.version == 5:
+        # Migrate unique ids of existing entities to new format
+        old_id = entry.unique_id
+        conf_file = get_config(entry.data[CONF_TYPE])
+        if conf_file is None:
+            _LOGGER.error(f"Configuration file for {entry.data[CONF_TYPE]} not found.")
+            return False
+
+        @callback
+        def update_unique_id(entity_entry):
+            """Update the unique id of an entity entry."""
+            e = conf_file.primary_entity
+            if e.entity != entity_entry.platform:
+                for e in conf_file.secondary_entities():
+                    if e.entity == entity_entry.platform:
+                        break
+            if e.entity == entity_entry.platform:
+                new_id = e.unique_id(old_id)
+                if new_id != old_id:
+                    _LOGGER.info(
+                        f"Migrating {e.entity} unique_id {old_id} to {new_id}."
+                    )
+                    return {
+                        "new_unique_id": entity_entry.unique_id.replace(old_id, new_id)
+                    }
+
+        await async_migrate_entries(hass, entry.entry_id, update_unique_id)
+        entry.version = 6
+
     return True
     return True
 
 
 
 
@@ -134,7 +170,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
     _LOGGER.debug(f"Setting up entry for device: {entry.data[CONF_DEVICE_ID]}")
     _LOGGER.debug(f"Setting up entry for device: {entry.data[CONF_DEVICE_ID]}")
     config = {**entry.data, **entry.options, "name": entry.title}
     config = {**entry.data, **entry.options, "name": entry.title}
     setup_device(hass, config)
     setup_device(hass, config)
-    device_conf = get_config(config[CONF_TYPE])
+    device_conf = get_config(entry.data[CONF_TYPE])
     if device_conf is None:
     if device_conf is None:
         _LOGGER.error(f"Configuration file for {config[CONF_TYPE]} not found.")
         _LOGGER.error(f"Configuration file for {config[CONF_TYPE]} not found.")
         return False
         return False

+ 1 - 1
custom_components/tuya_local/config_flow.py

@@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
 
 
 
 
 class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
 class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
-    VERSION = 5
+    VERSION = 6
     CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
     CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
     device = None
     device = None
     data = {}
     data = {}

+ 3 - 8
custom_components/tuya_local/generic/climate.py

@@ -106,18 +106,13 @@ class TuyaLocalClimate(ClimateEntity):
 
 
     @property
     @property
     def name(self):
     def name(self):
-        """Return the name of the climate device."""
-        return self._device.name
-
-    @property
-    def friendly_name(self):
-        """Return the friendly name of the climate entity for the UI."""
-        return self._config.name
+        """Return the name of the climate entity for the UI."""
+        return self._config.name(self._device.name)
 
 
     @property
     @property
     def unique_id(self):
     def unique_id(self):
         """Return the unique id for this climate device."""
         """Return the unique id for this climate device."""
-        return self._config.unique_id(self._device)
+        return self._config.unique_id(self._device.unique_id)
 
 
     @property
     @property
     def device_info(self):
     def device_info(self):

+ 3 - 8
custom_components/tuya_local/generic/fan.py

@@ -67,18 +67,13 @@ class TuyaLocalFan(FanEntity):
 
 
     @property
     @property
     def name(self):
     def name(self):
-        """Return the name of the climate device."""
-        return f"{self._device.name}"
-
-    @property
-    def friendly_name(self):
-        """Return the friendly name of the climate entity for the UI."""
-        return self._config.name
+        """Return the friendly name of the entity for the UI."""
+        return self._config.name(self._device.name)
 
 
     @property
     @property
     def unique_id(self):
     def unique_id(self):
         """Return the unique id for this climate device."""
         """Return the unique id for this climate device."""
-        return self._config.unique_id(self._device)
+        return self._config.unique_id(self._device.unique_id)
 
 
     @property
     @property
     def device_info(self):
     def device_info(self):

+ 3 - 8
custom_components/tuya_local/generic/humidifier.py

@@ -58,18 +58,13 @@ class TuyaLocalHumidifier(HumidifierEntity):
 
 
     @property
     @property
     def name(self):
     def name(self):
-        """Return the name of the climate device."""
-        return self._device.name
-
-    @property
-    def friendly_name(self):
-        """Return the friendly name of the climate entity for the UI."""
-        return self._config.name
+        """Return the friendly name of the entity for the UI."""
+        return self._config.name(self._device.name)
 
 
     @property
     @property
     def unique_id(self):
     def unique_id(self):
         """Return the unique id for this climate device."""
         """Return the unique id for this climate device."""
-        return self._config.unique_id(self._device)
+        return self._config.unique_id(self._device.unique_id)
 
 
     @property
     @property
     def device_info(self):
     def device_info(self):

+ 2 - 7
custom_components/tuya_local/generic/light.py

@@ -45,18 +45,13 @@ class TuyaLocalLight(LightEntity):
 
 
     @property
     @property
     def name(self):
     def name(self):
-        """Return the name of the light."""
-        return self._device.name
-
-    @property
-    def friendly_name(self):
         """Return the friendly name for this entity."""
         """Return the friendly name for this entity."""
-        return self._config.name
+        return self._config.name(self._device.name)
 
 
     @property
     @property
     def unique_id(self):
     def unique_id(self):
         """Return the unique id for this heater LED display."""
         """Return the unique id for this heater LED display."""
-        return self._config.unique_id(self._device)
+        return self._config.unique_id(self._device.unique_id)
 
 
     @property
     @property
     def device_info(self):
     def device_info(self):

+ 2 - 7
custom_components/tuya_local/generic/lock.py

@@ -36,18 +36,13 @@ class TuyaLocalLock(LockEntity):
 
 
     @property
     @property
     def name(self):
     def name(self):
-        """Return the name for this entity."""
-        return self._device.name
-
-    @property
-    def friendly_name(self):
         """Return the friendly name for this entity."""
         """Return the friendly name for this entity."""
-        return self._config.name
+        return self._config.name(self._device.name)
 
 
     @property
     @property
     def unique_id(self):
     def unique_id(self):
         """Return the device unique ID."""
         """Return the device unique ID."""
-        return self._config.unique_id(self._device)
+        return self._config.unique_id(self._device.unique_id)
 
 
     @property
     @property
     def device_info(self):
     def device_info(self):

+ 2 - 7
custom_components/tuya_local/generic/switch.py

@@ -43,18 +43,13 @@ class TuyaLocalSwitch(SwitchEntity):
 
 
     @property
     @property
     def name(self):
     def name(self):
-        """Return the name of the device."""
-        return self._device.name
-
-    @property
-    def friendly_name(self):
         """Return the friendly name for this entity."""
         """Return the friendly name for this entity."""
-        return self._config.name
+        return self._config.name(self._device.name)
 
 
     @property
     @property
     def unique_id(self):
     def unique_id(self):
         """Return the unique id of the device."""
         """Return the unique id of the device."""
-        return self._config.unique_id(self._device)
+        return self._config.unique_id(self._device.unique_id)
 
 
     @property
     @property
     def device_info(self):
     def device_info(self):

+ 6 - 7
custom_components/tuya_local/helpers/device_config.py

@@ -143,22 +143,21 @@ class TuyaEntityConfig:
         self._device = device
         self._device = device
         self._config = config
         self._config = config
 
 
-    @property
-    def name(self):
+    def name(self, base_name):
         """The friendly name for this entity."""
         """The friendly name for this entity."""
         own_name = self._config.get("name")
         own_name = self._config.get("name")
         if own_name is None:
         if own_name is None:
-            return self._device.name
+            return base_name
         else:
         else:
-            return self._device.name + " " + own_name
+            return base_name + " " + own_name
 
 
-    def unique_id(self, device):
+    def unique_id(self, device_uid):
         """Return a suitable unique_id for this entity."""
         """Return a suitable unique_id for this entity."""
         own_name = self._config.get("name")
         own_name = self._config.get("name")
         if own_name:
         if own_name:
-            return f"{device.unique_id}-{slugify(own_name)}"
+            return f"{device_uid}-{slugify(own_name)}"
         else:
         else:
-            return device.unique_id
+            return device_uid
 
 
     @property
     @property
     def legacy_class(self):
     def legacy_class(self):

+ 5 - 10
tests/devices/base_device_tests.py

@@ -1,5 +1,5 @@
 from unittest import IsolatedAsyncioTestCase
 from unittest import IsolatedAsyncioTestCase
-from unittest.mock import AsyncMock, patch
+from unittest.mock import AsyncMock, patch, PropertyMock
 from uuid import uuid4
 from uuid import uuid4
 
 
 from custom_components.tuya_local.generic.climate import TuyaLocalClimate
 from custom_components.tuya_local.generic.climate import TuyaLocalClimate
@@ -36,9 +36,8 @@ class TuyaDeviceTestCase(IsolatedAsyncioTestCase):
         self.mock_device.get_property.side_effect = lambda id: self.dps[id]
         self.mock_device.get_property.side_effect = lambda id: self.dps[id]
         cfg = TuyaDeviceConfig(config_file)
         cfg = TuyaDeviceConfig(config_file)
         self.conf_type = cfg.legacy_type
         self.conf_type = cfg.legacy_type
-
+        type(self.mock_device).unique_id = PropertyMock(return_value=str(uuid4()))
         self.mock_device.name = cfg.name
         self.mock_device.name = cfg.name
-        self.mock_device.unique_id = str(uuid4())
 
 
         self.entities = {}
         self.entities = {}
         self.entities[cfg.primary_entity.config_id] = self.create_entity(
         self.entities[cfg.primary_entity.config_id] = self.create_entity(
@@ -46,10 +45,10 @@ class TuyaDeviceTestCase(IsolatedAsyncioTestCase):
         )
         )
 
 
         self.names = {}
         self.names = {}
-        self.names[cfg.primary_entity.config_id] = cfg.primary_entity.name
+        self.names[cfg.primary_entity.config_id] = cfg.primary_entity.name(cfg.name)
         for e in cfg.secondary_entities():
         for e in cfg.secondary_entities():
             self.entities[e.config_id] = self.create_entity(e)
             self.entities[e.config_id] = self.create_entity(e)
-            self.names[e.config_id] = e.name
+            self.names[e.config_id] = e.name(cfg.name)
 
 
     def create_entity(self, config):
     def create_entity(self, config):
         """Create an entity to match the config"""
         """Create an entity to match the config"""
@@ -69,12 +68,8 @@ class TuyaDeviceTestCase(IsolatedAsyncioTestCase):
             self.assertTrue(e.should_poll)
             self.assertTrue(e.should_poll)
 
 
     def test_name_returns_device_name(self):
     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:
         for e in self.entities:
-            self.assertEqual(self.entities[e].friendly_name, self.names[e])
+            self.assertEqual(self.entities[e].name, self.names[e])
 
 
     def test_unique_id_contains_device_unique_id(self):
     def test_unique_id_contains_device_unique_id(self):
         entities = {}
         entities = {}

+ 9 - 9
tests/test_config_flow.py

@@ -45,7 +45,7 @@ async def test_init_entry(hass):
     """Test initialisation of the config flow."""
     """Test initialisation of the config flow."""
     entry = MockConfigEntry(
     entry = MockConfigEntry(
         domain=DOMAIN,
         domain=DOMAIN,
-        version=5,
+        version=6,
         title="test",
         title="test",
         data={
         data={
             CONF_DEVICE_ID: "deviceid",
             CONF_DEVICE_ID: "deviceid",
@@ -60,7 +60,7 @@ async def test_init_entry(hass):
     await hass.config_entries.async_setup(entry.entry_id)
     await hass.config_entries.async_setup(entry.entry_id)
     await hass.async_block_till_done()
     await hass.async_block_till_done()
     assert hass.states.get("climate.test")
     assert hass.states.get("climate.test")
-    assert hass.states.get("lock.test")
+    assert hass.states.get("lock.test_child_lock")
 
 
 
 
 @patch("custom_components.tuya_local.setup_device")
 @patch("custom_components.tuya_local.setup_device")
@@ -315,7 +315,7 @@ async def test_flow_choose_entities_creates_config_entry(hass, bypass_setup):
             },
             },
         )
         )
         expected = {
         expected = {
-            "version": 5,
+            "version": 6,
             "type": "create_entry",
             "type": "create_entry",
             "flow_id": ANY,
             "flow_id": ANY,
             "handler": DOMAIN,
             "handler": DOMAIN,
@@ -340,7 +340,7 @@ async def test_options_flow_init(hass):
     """Test config flow options."""
     """Test config flow options."""
     config_entry = MockConfigEntry(
     config_entry = MockConfigEntry(
         domain=DOMAIN,
         domain=DOMAIN,
-        version=5,
+        version=6,
         unique_id="uniqueid",
         unique_id="uniqueid",
         data={
         data={
             CONF_DEVICE_ID: "deviceid",
             CONF_DEVICE_ID: "deviceid",
@@ -377,7 +377,7 @@ async def test_options_flow_modifies_config(mock_test, hass):
 
 
     config_entry = MockConfigEntry(
     config_entry = MockConfigEntry(
         domain=DOMAIN,
         domain=DOMAIN,
-        version=5,
+        version=6,
         unique_id="uniqueid",
         unique_id="uniqueid",
         data={
         data={
             CONF_CLIMATE: True,
             CONF_CLIMATE: True,
@@ -423,7 +423,7 @@ async def test_options_flow_fails_when_connection_fails(mock_test, hass):
 
 
     config_entry = MockConfigEntry(
     config_entry = MockConfigEntry(
         domain=DOMAIN,
         domain=DOMAIN,
-        version=5,
+        version=6,
         unique_id="uniqueid",
         unique_id="uniqueid",
         data={
         data={
             CONF_DEVICE_ID: "deviceid",
             CONF_DEVICE_ID: "deviceid",
@@ -461,7 +461,7 @@ async def test_options_flow_fails_when_config_is_missing(mock_test, hass):
 
 
     config_entry = MockConfigEntry(
     config_entry = MockConfigEntry(
         domain=DOMAIN,
         domain=DOMAIN,
-        version=5,
+        version=6,
         unique_id="uniqueid",
         unique_id="uniqueid",
         data={
         data={
             CONF_DEVICE_ID: "deviceid",
             CONF_DEVICE_ID: "deviceid",
@@ -488,7 +488,7 @@ async def test_async_setup_entry_for_dehumidifier(mock_setup, hass):
     """Test setting up based on a config entry.  Repeats test_init_entry."""
     """Test setting up based on a config entry.  Repeats test_init_entry."""
     config_entry = MockConfigEntry(
     config_entry = MockConfigEntry(
         domain=DOMAIN,
         domain=DOMAIN,
-        version=5,
+        version=6,
         unique_id="uniqueid",
         unique_id="uniqueid",
         data={
         data={
             CONF_CLIMATE: False,
             CONF_CLIMATE: False,
@@ -511,7 +511,7 @@ async def test_async_setup_entry_for_switch(mock_device, hass):
     """Test setting up based on a config entry.  Repeats test_init_entry."""
     """Test setting up based on a config entry.  Repeats test_init_entry."""
     config_entry = MockConfigEntry(
     config_entry = MockConfigEntry(
         domain=DOMAIN,
         domain=DOMAIN,
-        version=5,
+        version=6,
         unique_id="uniqueid",
         unique_id="uniqueid",
         data={
         data={
             CONF_DEVICE_ID: "deviceid",
             CONF_DEVICE_ID: "deviceid",