Skip to content

Commit 75d6c0b

Browse files
emontnemeryfrenck
authored andcommitted
Improve migration to entity registry version 1.18 (home-assistant#151570)
1 parent 7500406 commit 75d6c0b

File tree

2 files changed

+631
-29
lines changed

2 files changed

+631
-29
lines changed

homeassistant/helpers/entity_registry.py

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
_LOGGER = logging.getLogger(__name__)
8080

8181
STORAGE_VERSION_MAJOR = 1
82-
STORAGE_VERSION_MINOR = 18
82+
STORAGE_VERSION_MINOR = 19
8383
STORAGE_KEY = "core.entity_registry"
8484

8585
CLEANUP_INTERVAL = 3600 * 24
@@ -164,6 +164,17 @@ def _protect_entity_options(
164164
return ReadOnlyDict({key: ReadOnlyDict(val) for key, val in data.items()})
165165

166166

167+
def _protect_optional_entity_options(
168+
data: EntityOptionsType | UndefinedType | None,
169+
) -> ReadOnlyEntityOptionsType | UndefinedType:
170+
"""Protect entity options from being modified."""
171+
if data is UNDEFINED:
172+
return UNDEFINED
173+
if data is None:
174+
return ReadOnlyDict({})
175+
return ReadOnlyDict({key: ReadOnlyDict(val) for key, val in data.items()})
176+
177+
167178
@attr.s(frozen=True, kw_only=True, slots=True)
168179
class RegistryEntry:
169180
"""Entity Registry Entry."""
@@ -414,15 +425,17 @@ class DeletedRegistryEntry:
414425
config_subentry_id: str | None = attr.ib()
415426
created_at: datetime = attr.ib()
416427
device_class: str | None = attr.ib()
417-
disabled_by: RegistryEntryDisabler | None = attr.ib()
428+
disabled_by: RegistryEntryDisabler | UndefinedType | None = attr.ib()
418429
domain: str = attr.ib(init=False, repr=False)
419-
hidden_by: RegistryEntryHider | None = attr.ib()
430+
hidden_by: RegistryEntryHider | UndefinedType | None = attr.ib()
420431
icon: str | None = attr.ib()
421432
id: str = attr.ib()
422433
labels: set[str] = attr.ib()
423434
modified_at: datetime = attr.ib()
424435
name: str | None = attr.ib()
425-
options: ReadOnlyEntityOptionsType = attr.ib(converter=_protect_entity_options)
436+
options: ReadOnlyEntityOptionsType | UndefinedType = attr.ib(
437+
converter=_protect_optional_entity_options
438+
)
426439
orphaned_timestamp: float | None = attr.ib()
427440

428441
_cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False)
@@ -445,15 +458,22 @@ def as_storage_fragment(self) -> json_fragment:
445458
"config_subentry_id": self.config_subentry_id,
446459
"created_at": self.created_at,
447460
"device_class": self.device_class,
448-
"disabled_by": self.disabled_by,
461+
"disabled_by": self.disabled_by
462+
if self.disabled_by is not UNDEFINED
463+
else None,
464+
"disabled_by_undefined": self.disabled_by is UNDEFINED,
449465
"entity_id": self.entity_id,
450-
"hidden_by": self.hidden_by,
466+
"hidden_by": self.hidden_by
467+
if self.hidden_by is not UNDEFINED
468+
else None,
469+
"hidden_by_undefined": self.hidden_by is UNDEFINED,
451470
"icon": self.icon,
452471
"id": self.id,
453472
"labels": list(self.labels),
454473
"modified_at": self.modified_at,
455474
"name": self.name,
456-
"options": self.options,
475+
"options": self.options if self.options is not UNDEFINED else {},
476+
"options_undefined": self.options is UNDEFINED,
457477
"orphaned_timestamp": self.orphaned_timestamp,
458478
"platform": self.platform,
459479
"unique_id": self.unique_id,
@@ -590,6 +610,14 @@ async def _async_migrate_func( # noqa: C901
590610
entity["labels"] = []
591611
entity["name"] = None
592612
entity["options"] = {}
613+
if old_minor_version < 19:
614+
# Version 1.19 adds undefined flags to deleted entities, this is a bugfix
615+
# of version 1.18
616+
set_to_undefined = old_minor_version < 18
617+
for entity in data["deleted_entities"]:
618+
entity["disabled_by_undefined"] = set_to_undefined
619+
entity["hidden_by_undefined"] = set_to_undefined
620+
entity["options_undefined"] = set_to_undefined
593621

594622
if old_major_version > 1:
595623
raise NotImplementedError
@@ -958,25 +986,30 @@ def async_get_or_create(
958986
categories = deleted_entity.categories
959987
created_at = deleted_entity.created_at
960988
device_class = deleted_entity.device_class
961-
disabled_by = deleted_entity.disabled_by
962-
# Adjust disabled_by based on config entry state
963-
if config_entry and config_entry is not UNDEFINED:
964-
if config_entry.disabled_by:
965-
if disabled_by is None:
966-
disabled_by = RegistryEntryDisabler.CONFIG_ENTRY
989+
if deleted_entity.disabled_by is not UNDEFINED:
990+
disabled_by = deleted_entity.disabled_by
991+
# Adjust disabled_by based on config entry state
992+
if config_entry and config_entry is not UNDEFINED:
993+
if config_entry.disabled_by:
994+
if disabled_by is None:
995+
disabled_by = RegistryEntryDisabler.CONFIG_ENTRY
996+
elif disabled_by == RegistryEntryDisabler.CONFIG_ENTRY:
997+
disabled_by = None
967998
elif disabled_by == RegistryEntryDisabler.CONFIG_ENTRY:
968999
disabled_by = None
969-
elif disabled_by == RegistryEntryDisabler.CONFIG_ENTRY:
970-
disabled_by = None
9711000
# Restore entity_id if it's available
9721001
if self._entity_id_available(deleted_entity.entity_id):
9731002
entity_id = deleted_entity.entity_id
9741003
entity_registry_id = deleted_entity.id
975-
hidden_by = deleted_entity.hidden_by
1004+
if deleted_entity.hidden_by is not UNDEFINED:
1005+
hidden_by = deleted_entity.hidden_by
9761006
icon = deleted_entity.icon
9771007
labels = deleted_entity.labels
9781008
name = deleted_entity.name
979-
options = deleted_entity.options
1009+
if deleted_entity.options is not UNDEFINED:
1010+
options = deleted_entity.options
1011+
else:
1012+
options = get_initial_options() if get_initial_options else None
9801013
else:
9811014
aliases = set()
9821015
area_id = None
@@ -1529,6 +1562,20 @@ async def async_load(self) -> None:
15291562
previous_unique_id=entity["previous_unique_id"],
15301563
unit_of_measurement=entity["unit_of_measurement"],
15311564
)
1565+
1566+
def get_optional_enum[_EnumT: StrEnum](
1567+
cls: type[_EnumT], value: str | None, undefined: bool
1568+
) -> _EnumT | UndefinedType | None:
1569+
"""Convert string to the passed enum, UNDEFINED or None."""
1570+
if undefined:
1571+
return UNDEFINED
1572+
if value is None:
1573+
return None
1574+
try:
1575+
return cls(value)
1576+
except ValueError:
1577+
return None
1578+
15321579
for entity in data["deleted_entities"]:
15331580
try:
15341581
domain = split_entity_id(entity["entity_id"])[0]
@@ -1554,23 +1601,25 @@ async def async_load(self) -> None:
15541601
config_subentry_id=entity["config_subentry_id"],
15551602
created_at=datetime.fromisoformat(entity["created_at"]),
15561603
device_class=entity["device_class"],
1557-
disabled_by=(
1558-
RegistryEntryDisabler(entity["disabled_by"])
1559-
if entity["disabled_by"]
1560-
else None
1604+
disabled_by=get_optional_enum(
1605+
RegistryEntryDisabler,
1606+
entity["disabled_by"],
1607+
entity["disabled_by_undefined"],
15611608
),
15621609
entity_id=entity["entity_id"],
1563-
hidden_by=(
1564-
RegistryEntryHider(entity["hidden_by"])
1565-
if entity["hidden_by"]
1566-
else None
1610+
hidden_by=get_optional_enum(
1611+
RegistryEntryHider,
1612+
entity["hidden_by"],
1613+
entity["hidden_by_undefined"],
15671614
),
15681615
icon=entity["icon"],
15691616
id=entity["id"],
15701617
labels=set(entity["labels"]),
15711618
modified_at=datetime.fromisoformat(entity["modified_at"]),
15721619
name=entity["name"],
1573-
options=entity["options"],
1620+
options=entity["options"]
1621+
if not entity["options_undefined"]
1622+
else UNDEFINED,
15741623
orphaned_timestamp=entity["orphaned_timestamp"],
15751624
platform=entity["platform"],
15761625
unique_id=entity["unique_id"],

0 commit comments

Comments
 (0)