mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 09:47:13 +00:00
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:
parent
11d9014be0
commit
ce739fd9b6
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user