From 9f039002ff0b5bdf0e2cd22ff1c94deaa218933a Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 28 Apr 2025 08:29:53 +0200 Subject: [PATCH] Add WS command to help reset custom entity_id --- .../components/config/entity_registry.py | 35 ++++++++++++ homeassistant/helpers/entity_registry.py | 11 +++- tests/common.py | 1 + .../components/config/test_entity_registry.py | 55 +++++++++++++++++++ tests/helpers/test_entity_platform.py | 1 + tests/helpers/test_entity_registry.py | 7 +++ 6 files changed, 109 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index b987f249a33..94b7f0851e4 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -22,6 +22,7 @@ from homeassistant.helpers.json import json_dumps def async_setup(hass: HomeAssistant) -> bool: """Enable the Entity Registry views.""" + websocket_api.async_register_command(hass, websocket_get_automatic_entity_ids) websocket_api.async_register_command(hass, websocket_get_entities) websocket_api.async_register_command(hass, websocket_get_entity) websocket_api.async_register_command(hass, websocket_list_entities_for_display) @@ -316,3 +317,37 @@ def websocket_remove_entity( registry.async_remove(msg["entity_id"]) connection.send_message(websocket_api.result_message(msg["id"])) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "config/entity_registry/get_automatic_entity_ids", + vol.Required("entity_ids"): cv.entity_ids, + } +) +@callback +def websocket_get_automatic_entity_ids( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Return the automatic entity IDs for the given entity IDs. + + This is used to help user reset entity IDs which have been customized by the user. + """ + registry = er.async_get(hass) + + entity_ids = msg["entity_ids"] + automatic_entity_ids: dict[str, str | None] = {} + for entity_id in entity_ids: + if not (entry := registry.entities.get(entity_id)): + automatic_entity_ids[entity_id] = None + continue + automatic_entity_ids[entity_id] = registry.async_generate_entity_id( + entry.domain, + entry.suggested_object_id or f"{entry.platform}_{entry.unique_id}", + ) + + connection.send_message( + websocket_api.result_message(msg["id"], automatic_entity_ids) + ) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 78a65acf290..530f1f24be5 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -79,7 +79,7 @@ EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = Event _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 16 +STORAGE_VERSION_MINOR = 17 STORAGE_KEY = "core.entity_registry" CLEANUP_INTERVAL = 3600 * 24 @@ -198,6 +198,7 @@ class RegistryEntry: original_device_class: str | None = attr.ib() original_icon: str | None = attr.ib() original_name: str | None = attr.ib() + suggested_object_id: str | None = attr.ib() supported_features: int = attr.ib() translation_key: str | None = attr.ib() unit_of_measurement: str | None = attr.ib() @@ -359,6 +360,7 @@ class RegistryEntry: "original_icon": self.original_icon, "original_name": self.original_name, "platform": self.platform, + "suggested_object_id": self.suggested_object_id, "supported_features": self.supported_features, "translation_key": self.translation_key, "unique_id": self.unique_id, @@ -548,6 +550,11 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): for entity in data["deleted_entities"]: entity["config_subentry_id"] = None + if old_minor_version < 17: + # Version 1.17 adds suggested_object_id + for entity in data["entities"]: + entity["suggested_object_id"] = None + if old_major_version > 1: raise NotImplementedError return data @@ -942,6 +949,7 @@ class EntityRegistry(BaseRegistry): original_icon=none_if_undefined(original_icon), original_name=none_if_undefined(original_name), platform=platform, + suggested_object_id=suggested_object_id, supported_features=none_if_undefined(supported_features) or 0, translation_key=none_if_undefined(translation_key), unique_id=unique_id, @@ -1378,6 +1386,7 @@ class EntityRegistry(BaseRegistry): original_icon=entity["original_icon"], original_name=entity["original_name"], platform=entity["platform"], + suggested_object_id=entity["suggested_object_id"], supported_features=entity["supported_features"], translation_key=entity["translation_key"], unique_id=entity["unique_id"], diff --git a/tests/common.py b/tests/common.py index d439021a9df..bfc8ef65063 100644 --- a/tests/common.py +++ b/tests/common.py @@ -669,6 +669,7 @@ class RegistryEntryWithDefaults(er.RegistryEntry): original_device_class: str | None = attr.ib(default=None) original_icon: str | None = attr.ib(default=None) original_name: str | None = attr.ib(default=None) + suggested_object_id: str | None = attr.ib(default=None) supported_features: int = attr.ib(default=0) translation_key: str | None = attr.ib(default=None) unit_of_measurement: str | None = attr.ib(default=None) diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index ea7a65f25d3..3b5a933f963 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -1288,3 +1288,58 @@ async def test_remove_non_existing_entity( msg = await client.receive_json() assert not msg["success"] + + +async def test_get_automatic_entity_ids( + hass: HomeAssistant, client: MockHAClientWebSocket +) -> None: + """Test get_automatic_entity_ids.""" + mock_registry( + hass, + { + "test_domain.test_1": RegistryEntryWithDefaults( + entity_id="test_domain.test_1", + unique_id="uniq1", + platform="test_platform", + ), + "test_domain.test_2": RegistryEntryWithDefaults( + entity_id="test_domain.test_2", + unique_id="uniq2", + platform="test_platform", + suggested_object_id="blabla", + ), + "test_domain.test_3": RegistryEntryWithDefaults( + entity_id="test_domain.test_3", + unique_id="uniq3", + platform="test_platform", + suggested_object_id="collision", + ), + "test_domain.collision": RegistryEntryWithDefaults( + entity_id="test_domain.collision", + unique_id="uniq_collision", + platform="test_platform", + ), + }, + ) + + await client.send_json_auto_id( + { + "type": "config/entity_registry/get_automatic_entity_ids", + "entity_ids": [ + "test_domain.test_1", + "test_domain.test_2", + "test_domain.test_3", + "test_domain.unknown", + ], + } + ) + + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "test_domain.test_1": "test_domain.test_platform_uniq1", # platform + unique_id + "test_domain.test_2": "test_domain.blabla", # suggested_object_id + "test_domain.test_3": "test_domain.collision_2", # suggested_object_id + _2 + "test_domain.unknown": None, # no test_domain.unknown in registry + } diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 77ac85ed4ed..49839abc2fb 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1550,6 +1550,7 @@ async def test_entity_info_added_to_entity_registry( original_icon="nice:icon", original_name="best name", options=None, + suggested_object_id="best name", supported_features=5, translation_key="my_translation_key", unit_of_measurement=PERCENTAGE, diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 7df7bb398e8..72628debb58 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -144,6 +144,7 @@ def test_get_or_create_updates_data( original_device_class="mock-device-class", original_icon="initial-original_icon", original_name="initial-original_name", + suggested_object_id=None, supported_features=5, translation_key="initial-translation_key", unit_of_measurement="initial-unit_of_measurement", @@ -202,6 +203,7 @@ def test_get_or_create_updates_data( original_device_class="new-mock-device-class", original_icon="updated-original_icon", original_name="updated-original_name", + suggested_object_id=None, supported_features=10, translation_key="updated-translation_key", unit_of_measurement="updated-unit_of_measurement", @@ -254,6 +256,7 @@ def test_get_or_create_updates_data( original_device_class=None, original_icon=None, original_name=None, + suggested_object_id=None, supported_features=0, # supported_features is stored as an int translation_key=None, unit_of_measurement=None, @@ -537,6 +540,7 @@ async def test_load_bad_data( "original_name": None, "platform": "super_platform", "previous_unique_id": None, + "suggested_object_id": None, "supported_features": 0, "translation_key": None, "unique_id": 123, # Should trigger warning @@ -568,6 +572,7 @@ async def test_load_bad_data( "original_name": None, "platform": "super_platform", "previous_unique_id": None, + "suggested_object_id": None, "supported_features": 0, "translation_key": None, "unique_id": ["not", "valid"], # Should not load @@ -922,6 +927,7 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) "original_name": None, "platform": "super_platform", "previous_unique_id": None, + "suggested_object_id": None, "supported_features": 0, "translation_key": None, "unique_id": "very_unique", @@ -1101,6 +1107,7 @@ async def test_migration_1_11( "original_name": None, "platform": "super_platform", "previous_unique_id": None, + "suggested_object_id": None, "supported_features": 0, "translation_key": None, "unique_id": "very_unique",