7979_LOGGER = logging .getLogger (__name__ )
8080
8181STORAGE_VERSION_MAJOR = 1
82- STORAGE_VERSION_MINOR = 18
82+ STORAGE_VERSION_MINOR = 19
8383STORAGE_KEY = "core.entity_registry"
8484
8585CLEANUP_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 )
168179class 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