diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index b503ba5f787..c14300f2ec8 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 = 17 +STORAGE_VERSION_MINOR = 18 STORAGE_KEY = "core.entity_registry" CLEANUP_INTERVAL = 3600 * 24 @@ -406,12 +406,23 @@ class DeletedRegistryEntry: entity_id: str = attr.ib() unique_id: str = attr.ib() platform: str = attr.ib() + + aliases: set[str] = attr.ib() + area_id: str | None = attr.ib() + categories: dict[str, str] = attr.ib() config_entry_id: str | None = attr.ib() config_subentry_id: str | None = attr.ib() created_at: datetime = attr.ib() + device_class: str | None = attr.ib() + disabled_by: RegistryEntryDisabler | None = attr.ib() domain: str = attr.ib(init=False, repr=False) + hidden_by: RegistryEntryHider | None = attr.ib() + icon: str | None = attr.ib() id: str = attr.ib() + labels: set[str] = attr.ib() modified_at: datetime = attr.ib() + name: str | None = attr.ib() + options: ReadOnlyEntityOptionsType = attr.ib(converter=_protect_entity_options) orphaned_timestamp: float | None = attr.ib() _cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False) @@ -427,12 +438,22 @@ class DeletedRegistryEntry: return json_fragment( json_bytes( { + "aliases": list(self.aliases), + "area_id": self.area_id, + "categories": self.categories, "config_entry_id": self.config_entry_id, "config_subentry_id": self.config_subentry_id, "created_at": self.created_at, + "device_class": self.device_class, + "disabled_by": self.disabled_by, "entity_id": self.entity_id, + "hidden_by": self.hidden_by, + "icon": self.icon, "id": self.id, + "labels": list(self.labels), "modified_at": self.modified_at, + "name": self.name, + "options": self.options, "orphaned_timestamp": self.orphaned_timestamp, "platform": self.platform, "unique_id": self.unique_id, @@ -556,6 +577,20 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): for entity in data["entities"]: entity["suggested_object_id"] = None + if old_minor_version < 18: + # Version 1.18 adds user customizations to deleted entities + for entity in data["deleted_entities"]: + entity["aliases"] = [] + entity["area_id"] = None + entity["categories"] = {} + entity["device_class"] = None + entity["disabled_by"] = None + entity["hidden_by"] = None + entity["icon"] = None + entity["labels"] = [] + entity["name"] = None + entity["options"] = {} + if old_major_version > 1: raise NotImplementedError return data @@ -916,15 +951,40 @@ class EntityRegistry(BaseRegistry): entity_registry_id: str | None = None created_at = utcnow() deleted_entity = self.deleted_entities.pop((domain, platform, unique_id), None) + options: Mapping[str, Mapping[str, Any]] | None if deleted_entity is not None: - # Restore id - entity_registry_id = deleted_entity.id + aliases = deleted_entity.aliases + area_id = deleted_entity.area_id + categories = deleted_entity.categories created_at = deleted_entity.created_at + device_class = deleted_entity.device_class + disabled_by = deleted_entity.disabled_by + # Restore entity_id if it's available + if self._entity_id_available(deleted_entity.entity_id): + entity_id = deleted_entity.entity_id + entity_registry_id = deleted_entity.id + hidden_by = deleted_entity.hidden_by + icon = deleted_entity.icon + labels = deleted_entity.labels + name = deleted_entity.name + options = deleted_entity.options + else: + aliases = set() + area_id = None + categories = {} + device_class = None + icon = None + labels = set() + name = None + options = get_initial_options() if get_initial_options else None - entity_id = self.async_generate_entity_id( - domain, - suggested_object_id or calculated_object_id or f"{platform}_{unique_id}", - ) + if not entity_id: + entity_id = self.async_generate_entity_id( + domain, + suggested_object_id + or calculated_object_id + or f"{platform}_{unique_id}", + ) if ( disabled_by is None @@ -938,21 +998,26 @@ class EntityRegistry(BaseRegistry): """Return None if value is UNDEFINED, otherwise return value.""" return None if value is UNDEFINED else value - initial_options = get_initial_options() if get_initial_options else None - entry = RegistryEntry( + aliases=aliases, + area_id=area_id, + categories=categories, capabilities=none_if_undefined(capabilities), config_entry_id=none_if_undefined(config_entry_id), config_subentry_id=none_if_undefined(config_subentry_id), created_at=created_at, + device_class=device_class, device_id=none_if_undefined(device_id), disabled_by=disabled_by, entity_category=none_if_undefined(entity_category), entity_id=entity_id, hidden_by=hidden_by, has_entity_name=none_if_undefined(has_entity_name) or False, + icon=icon, id=entity_registry_id, - options=initial_options, + labels=labels, + name=name, + options=options, original_device_class=none_if_undefined(original_device_class), original_icon=none_if_undefined(original_icon), original_name=none_if_undefined(original_name), @@ -986,12 +1051,22 @@ class EntityRegistry(BaseRegistry): # If the entity does not belong to a config entry, mark it as orphaned orphaned_timestamp = None if config_entry_id else time.time() self.deleted_entities[key] = DeletedRegistryEntry( + aliases=entity.aliases, + area_id=entity.area_id, + categories=entity.categories, config_entry_id=config_entry_id, config_subentry_id=entity.config_subentry_id, created_at=entity.created_at, + device_class=entity.device_class, + disabled_by=entity.disabled_by, entity_id=entity_id, + hidden_by=entity.hidden_by, + icon=entity.icon, id=entity.id, + labels=entity.labels, modified_at=utcnow(), + name=entity.name, + options=entity.options, orphaned_timestamp=orphaned_timestamp, platform=entity.platform, unique_id=entity.unique_id, @@ -1420,12 +1495,30 @@ class EntityRegistry(BaseRegistry): entity["unique_id"], ) deleted_entities[key] = DeletedRegistryEntry( + aliases=set(entity["aliases"]), + area_id=entity["area_id"], + categories=entity["categories"], config_entry_id=entity["config_entry_id"], config_subentry_id=entity["config_subentry_id"], created_at=datetime.fromisoformat(entity["created_at"]), + device_class=entity["device_class"], + disabled_by=( + RegistryEntryDisabler(entity["disabled_by"]) + if entity["disabled_by"] + else None + ), entity_id=entity["entity_id"], + hidden_by=( + RegistryEntryHider(entity["hidden_by"]) + if entity["hidden_by"] + else None + ), + icon=entity["icon"], id=entity["id"], + labels=set(entity["labels"]), modified_at=datetime.fromisoformat(entity["modified_at"]), + name=entity["name"], + options=entity["options"], orphaned_timestamp=entity["orphaned_timestamp"], platform=entity["platform"], unique_id=entity["unique_id"], @@ -1455,12 +1548,29 @@ class EntityRegistry(BaseRegistry): categories = entry.categories.copy() del categories[scope] self.async_update_entity(entity_id, categories=categories) + for key, deleted_entity in list(self.deleted_entities.items()): + if ( + existing_category_id := deleted_entity.categories.get(scope) + ) and category_id == existing_category_id: + categories = deleted_entity.categories.copy() + del categories[scope] + self.deleted_entities[key] = attr.evolve( + deleted_entity, categories=categories + ) + self.async_schedule_save() @callback def async_clear_label_id(self, label_id: str) -> None: """Clear label from registry entries.""" for entry in self.entities.get_entries_for_label(label_id): self.async_update_entity(entry.entity_id, labels=entry.labels - {label_id}) + for key, deleted_entity in list(self.deleted_entities.items()): + if label_id not in deleted_entity.labels: + continue + self.deleted_entities[key] = attr.evolve( + deleted_entity, labels=deleted_entity.labels - {label_id} + ) + self.async_schedule_save() @callback def async_clear_config_entry(self, config_entry_id: str) -> None: @@ -1525,6 +1635,11 @@ class EntityRegistry(BaseRegistry): """Clear area id from registry entries.""" for entry in self.entities.get_entries_for_area_id(area_id): self.async_update_entity(entry.entity_id, area_id=None) + for key, deleted_entity in list(self.deleted_entities.items()): + if deleted_entity.area_id != area_id: + continue + self.deleted_entities[key] = attr.evolve(deleted_entity, area_id=None) + self.async_schedule_save() @callback diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index ee559ef4235..35a9a0494a6 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1680,6 +1680,7 @@ async def test_rapid_rediscover_unique( "homeassistant/binary_sensor/bla/config", '{ "name": "Beer", "state_topic": "test-topic", "unique_id": "even_uniquer" }', ) + # Removal, immediately followed by rediscover async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "") async_fire_mqtt_message( hass, @@ -1691,8 +1692,10 @@ async def test_rapid_rediscover_unique( assert len(hass.states.async_entity_ids("binary_sensor")) == 2 state = hass.states.get("binary_sensor.ale") assert state is not None - state = hass.states.get("binary_sensor.milk") + state = hass.states.get("binary_sensor.beer") assert state is not None + state = hass.states.get("binary_sensor.milk") + assert state is None assert len(events) == 4 # Add the entity @@ -1702,7 +1705,7 @@ async def test_rapid_rediscover_unique( assert events[2].data["entity_id"] == "binary_sensor.beer" assert events[2].data["new_state"] is None # Add the entity - assert events[3].data["entity_id"] == "binary_sensor.milk" + assert events[3].data["entity_id"] == "binary_sensor.beer" assert events[3].data["old_state"] is None diff --git a/tests/components/pglab/test_switch.py b/tests/components/pglab/test_switch.py index 0f1a2e4bb04..9b0dd7e6008 100644 --- a/tests/components/pglab/test_switch.py +++ b/tests/components/pglab/test_switch.py @@ -166,12 +166,16 @@ async def test_discovery_update( await send_discovery_message(hass, payload) - # be sure that old relay are been removed + # entity id from the old relay configuration should be reused for i in range(8): - assert not hass.states.get(f"switch.first_test_relay_{i}") + state = hass.states.get(f"switch.first_test_relay_{i}") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + for i in range(8): + assert not hass.states.get(f"switch.second_test_relay_{i}") # check new relay - for i in range(16): + for i in range(8, 16): state = hass.states.get(f"switch.second_test_relay_{i}") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index cef52810fa0..10e00e50ee4 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -583,23 +583,43 @@ async def test_load_bad_data( ], "deleted_entities": [ { + "aliases": [], + "area_id": None, + "categories": {}, "config_entry_id": None, "config_subentry_id": None, "created_at": "2024-02-14T12:00:00.900075+00:00", + "device_class": None, + "disabled_by": None, "entity_id": "test.test3", + "hidden_by": None, + "icon": None, "id": "00003", + "labels": [], "modified_at": "2024-02-14T12:00:00.900075+00:00", + "name": None, + "options": None, "orphaned_timestamp": None, "platform": "super_platform", "unique_id": 234, # Should not load }, { + "aliases": [], + "area_id": None, + "categories": {}, "config_entry_id": None, "config_subentry_id": None, "created_at": "2024-02-14T12:00:00.900075+00:00", + "device_class": None, + "disabled_by": None, "entity_id": "test.test4", + "hidden_by": None, + "icon": None, "id": "00004", + "labels": [], "modified_at": "2024-02-14T12:00:00.900075+00:00", + "name": None, + "options": None, "orphaned_timestamp": None, "platform": "super_platform", "unique_id": ["also", "not", "valid"], # Should not load @@ -870,6 +890,33 @@ async def test_removing_area_id(entity_registry: er.EntityRegistry) -> None: assert entry_w_area != entry_wo_area +async def test_removing_area_id_deleted_entity( + entity_registry: er.EntityRegistry, +) -> None: + """Make sure we can clear area id.""" + entry1 = entity_registry.async_get_or_create("light", "hue", "5678") + entry2 = entity_registry.async_get_or_create("light", "hue", "1234") + + entry1_w_area = entity_registry.async_update_entity( + entry1.entity_id, area_id="12345A" + ) + entry2_w_area = entity_registry.async_update_entity( + entry2.entity_id, area_id="12345B" + ) + + entity_registry.async_remove(entry1.entity_id) + entity_registry.async_remove(entry2.entity_id) + + entity_registry.async_clear_area_id("12345A") + entry1_restored = entity_registry.async_get_or_create("light", "hue", "5678") + entry2_restored = entity_registry.async_get_or_create("light", "hue", "1234") + + assert not entry1_restored.area_id + assert entry2_restored.area_id == "12345B" + assert entry1_w_area != entry1_restored + assert entry2_w_area != entry2_restored + + @pytest.mark.parametrize("load_registries", [False]) async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) -> None: """Test migration from version 1.1.""" @@ -1119,12 +1166,22 @@ async def test_migration_1_11( ], "deleted_entities": [ { + "aliases": [], + "area_id": None, + "categories": {}, "config_entry_id": None, "config_subentry_id": None, "created_at": "1970-01-01T00:00:00+00:00", + "device_class": None, + "disabled_by": None, "entity_id": "test.deleted_entity", + "hidden_by": None, + "icon": None, "id": "23456", + "labels": [], "modified_at": "1970-01-01T00:00:00+00:00", + "name": None, + "options": {}, "orphaned_timestamp": None, "platform": "super_duper_platform", "unique_id": "very_very_unique", @@ -2453,7 +2510,7 @@ async def test_restore_entity( entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, ) -> None: - """Make sure entity registry id is stable.""" + """Make sure entity registry id is stable and user configurations are restored.""" update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) config_entry = MockConfigEntry( domain="light", @@ -2511,6 +2568,13 @@ async def test_restore_entity( config_entry=config_entry, config_subentry_id="mock-subentry-id-1-1", ) + entry3 = entity_registry.async_get_or_create( + "light", + "hue", + "abcd", + disabled_by=er.RegistryEntryDisabler.INTEGRATION, + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) # Apply user customizations entry1 = entity_registry.async_update_entity( @@ -2532,8 +2596,9 @@ async def test_restore_entity( entity_registry.async_remove(entry1.entity_id) entity_registry.async_remove(entry2.entity_id) + entity_registry.async_remove(entry3.entity_id) assert len(entity_registry.entities) == 0 - assert len(entity_registry.deleted_entities) == 2 + assert len(entity_registry.deleted_entities) == 3 # Re-add entities, integration has changed entry1_restored = entity_registry.async_get_or_create( @@ -2557,32 +2622,46 @@ async def test_restore_entity( translation_key="translation_key_2", unit_of_measurement="unit_2", ) - entry2_restored = entity_registry.async_get_or_create("light", "hue", "5678") + # Add back the second entity without config entry and with different + # disabled_by and hidden_by settings + entry2_restored = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + disabled_by=er.RegistryEntryDisabler.INTEGRATION, + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + # Add back the third entity with different disabled_by and hidden_by settings + entry3_restored = entity_registry.async_get_or_create("light", "hue", "abcd") - assert len(entity_registry.entities) == 2 + assert len(entity_registry.entities) == 3 assert len(entity_registry.deleted_entities) == 0 assert entry1 != entry1_restored - # entity_id and user customizations are not restored. new integration options are + # entity_id and user customizations are restored. new integration options are # respected. assert entry1_restored == er.RegistryEntry( - entity_id="light.suggested_2", + entity_id="light.custom_1", unique_id="1234", platform="hue", + aliases={"alias1", "alias2"}, + area_id="12345A", + categories={"scope1": "id", "scope2": "id"}, capabilities={"key2": "value2"}, config_entry_id=config_entry.entry_id, config_subentry_id="mock-subentry-id-1-2", created_at=utcnow(), - device_class=None, + device_class="device_class_user", device_id=device_entry_2.id, - disabled_by=er.RegistryEntryDisabler.INTEGRATION, + disabled_by=er.RegistryEntryDisabler.USER, entity_category=EntityCategory.CONFIG, has_entity_name=False, - hidden_by=None, - icon=None, + hidden_by=er.RegistryEntryHider.USER, + icon="icon_user", id=entry1.id, + labels={"label1", "label2"}, modified_at=utcnow(), - name=None, - options={"test_domain": {"key2": "value2"}}, + name="Test Friendly Name", + options={"options_domain": {"key": "value"}, "test_domain": {"key1": "value1"}}, original_device_class="device_class_2", original_icon="original_icon_2", original_name="original_name_2", @@ -2594,14 +2673,21 @@ async def test_restore_entity( assert entry2 != entry2_restored # Config entry and subentry are not restored assert ( - attr.evolve(entry2, config_entry_id=None, config_subentry_id=None) + attr.evolve( + entry2, + config_entry_id=None, + config_subentry_id=None, + disabled_by=None, + hidden_by=None, + ) == entry2_restored ) + assert entry3 == entry3_restored # Remove two of the entities again, then bump time entity_registry.async_remove(entry1_restored.entity_id) entity_registry.async_remove(entry2.entity_id) - assert len(entity_registry.entities) == 0 + assert len(entity_registry.entities) == 1 assert len(entity_registry.deleted_entities) == 2 freezer.tick(timedelta(seconds=er.ORPHANED_ENTITY_KEEP_SECONDS + 1)) async_fire_time_changed(hass) @@ -2612,14 +2698,14 @@ async def test_restore_entity( "light", "hue", "1234", config_entry=config_entry ) entry2_restored = entity_registry.async_get_or_create("light", "hue", "5678") - assert len(entity_registry.entities) == 2 + assert len(entity_registry.entities) == 3 assert len(entity_registry.deleted_entities) == 0 assert entry1.id == entry1_restored.id assert entry2.id != entry2_restored.id # Remove the first entity, then its config entry, finally bump time entity_registry.async_remove(entry1_restored.entity_id) - assert len(entity_registry.entities) == 1 + assert len(entity_registry.entities) == 2 assert len(entity_registry.deleted_entities) == 1 entity_registry.async_clear_config_entry(config_entry.entry_id) freezer.tick(timedelta(seconds=er.ORPHANED_ENTITY_KEEP_SECONDS + 1)) @@ -2630,39 +2716,36 @@ async def test_restore_entity( entry1_restored = entity_registry.async_get_or_create( "light", "hue", "1234", config_entry=config_entry ) - assert len(entity_registry.entities) == 2 + assert len(entity_registry.entities) == 3 assert len(entity_registry.deleted_entities) == 0 assert entry1.id != entry1_restored.id # Check the events await hass.async_block_till_done() - assert len(update_events) == 14 + assert len(update_events) == 17 assert update_events[0].data == { "action": "create", "entity_id": "light.suggested_1", } assert update_events[1].data == {"action": "create", "entity_id": "light.hue_5678"} - assert update_events[2].data["action"] == "update" + assert update_events[2].data == {"action": "create", "entity_id": "light.hue_abcd"} assert update_events[3].data["action"] == "update" - assert update_events[4].data == {"action": "remove", "entity_id": "light.custom_1"} - assert update_events[5].data == {"action": "remove", "entity_id": "light.hue_5678"} + assert update_events[4].data["action"] == "update" + assert update_events[5].data == {"action": "remove", "entity_id": "light.custom_1"} + assert update_events[6].data == {"action": "remove", "entity_id": "light.hue_5678"} + assert update_events[7].data == {"action": "remove", "entity_id": "light.hue_abcd"} # Restore entities the 1st time - assert update_events[6].data == { - "action": "create", - "entity_id": "light.suggested_2", - } - assert update_events[7].data == {"action": "create", "entity_id": "light.hue_5678"} - assert update_events[8].data == { - "action": "remove", - "entity_id": "light.suggested_2", - } - assert update_events[9].data == {"action": "remove", "entity_id": "light.hue_5678"} + assert update_events[8].data == {"action": "create", "entity_id": "light.custom_1"} + assert update_events[9].data == {"action": "create", "entity_id": "light.hue_5678"} + assert update_events[10].data == {"action": "create", "entity_id": "light.hue_abcd"} + assert update_events[11].data == {"action": "remove", "entity_id": "light.custom_1"} + assert update_events[12].data == {"action": "remove", "entity_id": "light.hue_5678"} # Restore entities the 2nd time - assert update_events[10].data == {"action": "create", "entity_id": "light.hue_1234"} - assert update_events[11].data == {"action": "create", "entity_id": "light.hue_5678"} - assert update_events[12].data == {"action": "remove", "entity_id": "light.hue_1234"} + assert update_events[13].data == {"action": "create", "entity_id": "light.custom_1"} + assert update_events[14].data == {"action": "create", "entity_id": "light.hue_5678"} + assert update_events[15].data == {"action": "remove", "entity_id": "light.custom_1"} # Restore entities the 3rd time - assert update_events[13].data == {"action": "create", "entity_id": "light.hue_1234"} + assert update_events[16].data == {"action": "create", "entity_id": "light.hue_1234"} async def test_async_migrate_entry_delete_self( @@ -2763,6 +2846,49 @@ async def test_removing_labels(entity_registry: er.EntityRegistry) -> None: assert not entry_cleared_label2.labels +async def test_removing_labels_deleted_entity( + entity_registry: er.EntityRegistry, +) -> None: + """Make sure we can clear labels.""" + entry1 = entity_registry.async_get_or_create( + domain="light", platform="hue", unique_id="5678" + ) + entry1 = entity_registry.async_update_entity( + entry1.entity_id, labels={"label1", "label2"} + ) + entry2 = entity_registry.async_get_or_create( + domain="light", platform="hue", unique_id="1234" + ) + entry2 = entity_registry.async_update_entity(entry2.entity_id, labels={"label3"}) + + entity_registry.async_remove(entry1.entity_id) + entity_registry.async_remove(entry2.entity_id) + entity_registry.async_clear_label_id("label1") + entry1_cleared_label1 = entity_registry.async_get_or_create( + domain="light", platform="hue", unique_id="5678" + ) + + entity_registry.async_remove(entry1.entity_id) + entity_registry.async_clear_label_id("label2") + entry1_cleared_label2 = entity_registry.async_get_or_create( + domain="light", platform="hue", unique_id="5678" + ) + entry2_restored = entity_registry.async_get_or_create( + domain="light", platform="hue", unique_id="1234" + ) + + assert entry1_cleared_label1 + assert entry1_cleared_label2 + assert entry1 != entry1_cleared_label1 + assert entry1 != entry1_cleared_label2 + assert entry1_cleared_label1 != entry1_cleared_label2 + assert entry1.labels == {"label1", "label2"} + assert entry1_cleared_label1.labels == {"label2"} + assert not entry1_cleared_label2.labels + assert entry2 != entry2_restored + assert entry2_restored.labels == {"label3"} + + async def test_entries_for_label(entity_registry: er.EntityRegistry) -> None: """Test getting entity entries by label.""" entity_registry.async_get_or_create( @@ -2830,6 +2956,39 @@ async def test_removing_categories(entity_registry: er.EntityRegistry) -> None: assert not entry_cleared_scope2.categories +async def test_removing_categories_deleted_entity( + entity_registry: er.EntityRegistry, +) -> None: + """Make sure we can clear categories.""" + entry = entity_registry.async_get_or_create( + domain="light", platform="hue", unique_id="5678" + ) + entry = entity_registry.async_update_entity( + entry.entity_id, categories={"scope1": "id", "scope2": "id"} + ) + + entity_registry.async_remove(entry.entity_id) + entity_registry.async_clear_category_id("scope1", "id") + entry_cleared_scope1 = entity_registry.async_get_or_create( + domain="light", platform="hue", unique_id="5678" + ) + + entity_registry.async_remove(entry.entity_id) + entity_registry.async_clear_category_id("scope2", "id") + entry_cleared_scope2 = entity_registry.async_get_or_create( + domain="light", platform="hue", unique_id="5678" + ) + + assert entry_cleared_scope1 + assert entry_cleared_scope2 + assert entry != entry_cleared_scope1 + assert entry != entry_cleared_scope2 + assert entry_cleared_scope1 != entry_cleared_scope2 + assert entry.categories == {"scope1": "id", "scope2": "id"} + assert entry_cleared_scope1.categories == {"scope2": "id"} + assert not entry_cleared_scope2.categories + + async def test_entries_for_category(entity_registry: er.EntityRegistry) -> None: """Test getting entity entries by category.""" entity_registry.async_get_or_create(