From 22200d6804ff034eaabc4679a8e8f7da3ce463b6 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 1 Dec 2025 07:55:13 +0100 Subject: [PATCH] Fix subentry ID is not updated when renaming the entity ID (#157498) --- homeassistant/helpers/entity.py | 4 +- tests/helpers/snapshots/test_entity.ambr | 188 +++++++++++++++++++++++ tests/helpers/test_entity.py | 66 +++++++- 3 files changed, 256 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 2abbd30a598..9a52a8edace 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1553,7 +1553,9 @@ class Entity( # Clear the remove future to handle entity added again after entity id change self.__remove_future = None self._platform_state = EntityPlatformState.NOT_ADDED - await self.platform.async_add_entities([self]) + await self.platform.async_add_entities( + [self], config_subentry_id=registry_entry.config_subentry_id + ) @callback def _async_unsubscribe_device_updates(self) -> None: diff --git a/tests/helpers/snapshots/test_entity.ambr b/tests/helpers/snapshots/test_entity.ambr index 70f86feaf79..0e2596fb755 100644 --- a/tests/helpers/snapshots/test_entity.ambr +++ b/tests/helpers/snapshots/test_entity.ambr @@ -1,4 +1,192 @@ # serializer version: 1 +# name: test_change_entity_id_config_entry[None] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'test_domain', + 'entity_category': None, + 'entity_id': 'test_domain.test_5678', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'test', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '5678', + 'unit_of_measurement': None, + }) +# --- +# name: test_change_entity_id_config_entry[None].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + }), + 'context': , + 'entity_id': 'test_domain.test_5678', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_change_entity_id_config_entry[None].2 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'test_domain', + 'entity_category': None, + 'entity_id': 'test_domain.test2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'test', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '5678', + 'unit_of_measurement': None, + }) +# --- +# name: test_change_entity_id_config_entry[None].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + }), + 'context': , + 'entity_id': 'test_domain.test2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_change_entity_id_config_entry[mock-subentry-id-1] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'test_domain', + 'entity_category': None, + 'entity_id': 'test_domain.test_5678', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'test', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '5678', + 'unit_of_measurement': None, + }) +# --- +# name: test_change_entity_id_config_entry[mock-subentry-id-1].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + }), + 'context': , + 'entity_id': 'test_domain.test_5678', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_change_entity_id_config_entry[mock-subentry-id-1].2 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'test_domain', + 'entity_category': None, + 'entity_id': 'test_domain.test2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'test', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '5678', + 'unit_of_measurement': None, + }) +# --- +# name: test_change_entity_id_config_entry[mock-subentry-id-1].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + }), + 'context': , + 'entity_id': 'test_domain.test2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_entity_description_as_dataclass dict({ 'device_class': 'test', diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 3064d8d4260..b2d91d3fbe5 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -16,7 +16,7 @@ from pytest_unordered import unordered from syrupy.assertion import SnapshotAssertion import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigSubentryData from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, @@ -1911,6 +1911,70 @@ async def test_change_entity_id( assert ent._platform_state == entity.EntityPlatformState.ADDED +@pytest.mark.parametrize("config_subentry_id", [None, "mock-subentry-id-1"]) +async def test_change_entity_id_config_entry( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + config_subentry_id: str | None, +) -> None: + """Test changing entity id does not effect the config entry.""" + + class MockEntity(entity.Entity): + _attr_unique_id = "5678" + + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, + ) -> None: + """Mock setup entry method.""" + async_add_entities([MockEntity()], config_subentry_id=config_subentry_id) + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry( + entry_id="super-mock-id", + subentries_data=[ + ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ], + ) + config_entry.add_to_hass(hass) + entity_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + + ent = entity_registry.async_get(next(iter(hass.states.async_entity_ids()))) + assert ent == snapshot + # The snapshot check asserts on any (sub)entry ID + assert ent.config_entry_id == config_entry.entry_id + assert ent.config_subentry_id == config_subentry_id + + state = hass.states.async_all()[0] + assert state == snapshot + + entity_registry.async_update_entity( + ent.entity_id, new_entity_id="test_domain.test2" + ) + await hass.async_block_till_done(wait_background_tasks=True) + new_ent = entity_registry.async_get("test_domain.test2") + assert new_ent == snapshot + # The snapshot check asserts on any (sub)entry ID + assert new_ent.config_entry_id == config_entry.entry_id + assert new_ent.config_subentry_id == config_subentry_id + + new_state = hass.states.get("test_domain.test2") + assert new_state == snapshot + + def test_entity_description_as_dataclass(snapshot: SnapshotAssertion) -> None: """Test EntityDescription behaves like a dataclass."""