Restore entity ID and user customizations of deleted entities (#145278)

* Restore entity ID and user customizations of deleted entities

* Clear removed areas, categories and labels from deleted entities

* Correct test

* Fix logic for disabled_by and hidden_by

* Improve test coverage

* Fix sorting

* Always restore disabled_by and hidden_by

* Update mqtt test

* Update pglab tests
This commit is contained in:
Erik Montnemery 2025-06-10 11:47:54 +02:00 committed by GitHub
parent 11d9014be0
commit ce739fd9b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 331 additions and 50 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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(