Revert "Add support for subentries to config entries" (#133470)

Revert "Add support for subentries to config entries (#117355)"

This reverts commit ad15786115.
This commit is contained in:
Erik Montnemery
2024-12-18 13:51:05 +01:00
committed by GitHub
parent 2aba1d399b
commit ecb3bf79f3
95 changed files with 33 additions and 1774 deletions

View File

@ -15,7 +15,6 @@ from collections.abc import (
)
from contextvars import ContextVar
from copy import deepcopy
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum, StrEnum
import functools
@ -23,7 +22,7 @@ from functools import cache
import logging
from random import randint
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Generic, Self, TypedDict, cast
from typing import TYPE_CHECKING, Any, Generic, Self, cast
from async_interrupt import interrupt
from propcache import cached_property
@ -129,7 +128,7 @@ HANDLERS: Registry[str, type[ConfigFlow]] = Registry()
STORAGE_KEY = "core.config_entries"
STORAGE_VERSION = 1
STORAGE_VERSION_MINOR = 5
STORAGE_VERSION_MINOR = 4
SAVE_DELAY = 1
@ -257,10 +256,6 @@ class UnknownEntry(ConfigError):
"""Unknown entry specified."""
class UnknownSubEntry(ConfigError):
"""Unknown subentry specified."""
class OperationNotAllowed(ConfigError):
"""Raised when a config entry operation is not allowed."""
@ -305,7 +300,6 @@ class ConfigFlowResult(FlowResult[ConfigFlowContext, str], total=False):
minor_version: int
options: Mapping[str, Any]
subentries: Iterable[ConfigSubentryData]
version: int
@ -319,51 +313,6 @@ def _validate_item(*, disabled_by: ConfigEntryDisabler | Any | None = None) -> N
)
class ConfigSubentryData(TypedDict):
"""Container for configuration subentry data.
Returned by integrations, a subentry_id will be assigned automatically.
"""
data: Mapping[str, Any]
title: str
unique_id: str | None
class ConfigSubentryDataWithId(ConfigSubentryData):
"""Container for configuration subentry data.
This type is used when loading existing subentries from storage.
"""
subentry_id: str
class SubentryFlowResult(FlowResult[FlowContext, tuple[str, str]], total=False):
"""Typed result dict for subentry flow."""
unique_id: str | None
@dataclass(frozen=True, kw_only=True)
class ConfigSubentry:
"""Container for a configuration subentry."""
data: MappingProxyType[str, Any]
subentry_id: str = field(default_factory=ulid_util.ulid_now)
title: str
unique_id: str | None
def as_dict(self) -> ConfigSubentryDataWithId:
"""Return dictionary version of this subentry."""
return {
"data": dict(self.data),
"subentry_id": self.subentry_id,
"title": self.title,
"unique_id": self.unique_id,
}
class ConfigEntry(Generic[_DataT]):
"""Hold a configuration entry."""
@ -373,7 +322,6 @@ class ConfigEntry(Generic[_DataT]):
data: MappingProxyType[str, Any]
runtime_data: _DataT
options: MappingProxyType[str, Any]
subentries: MappingProxyType[str, ConfigSubentry]
unique_id: str | None
state: ConfigEntryState
reason: str | None
@ -389,7 +337,6 @@ class ConfigEntry(Generic[_DataT]):
supports_remove_device: bool | None
_supports_options: bool | None
_supports_reconfigure: bool | None
_supported_subentries: tuple[str, ...] | None
update_listeners: list[UpdateListenerType]
_async_cancel_retry_setup: Callable[[], Any] | None
_on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None
@ -419,7 +366,6 @@ class ConfigEntry(Generic[_DataT]):
pref_disable_polling: bool | None = None,
source: str,
state: ConfigEntryState = ConfigEntryState.NOT_LOADED,
subentries_data: Iterable[ConfigSubentryData | ConfigSubentryDataWithId] | None,
title: str,
unique_id: str | None,
version: int,
@ -445,24 +391,6 @@ class ConfigEntry(Generic[_DataT]):
# Entry options
_setter(self, "options", MappingProxyType(options or {}))
# Subentries
subentries_data = subentries_data or ()
subentries = {}
for subentry_data in subentries_data:
subentry_kwargs = {}
if "subentry_id" in subentry_data:
# If subentry_data has key "subentry_id", we're loading from storage
subentry_kwargs["subentry_id"] = subentry_data["subentry_id"] # type: ignore[typeddict-item]
subentry = ConfigSubentry(
data=MappingProxyType(subentry_data["data"]),
title=subentry_data["title"],
unique_id=subentry_data.get("unique_id"),
**subentry_kwargs,
)
subentries[subentry.subentry_id] = subentry
_setter(self, "subentries", MappingProxyType(subentries))
# Entry system options
if pref_disable_new_entities is None:
pref_disable_new_entities = False
@ -499,9 +427,6 @@ class ConfigEntry(Generic[_DataT]):
# Supports reconfigure
_setter(self, "_supports_reconfigure", None)
# Supports subentries
_setter(self, "_supported_subentries", None)
# Listeners to call on update
_setter(self, "update_listeners", [])
@ -574,18 +499,6 @@ class ConfigEntry(Generic[_DataT]):
)
return self._supports_reconfigure or False
@property
def supported_subentries(self) -> tuple[str, ...]:
"""Return supported subentries."""
if self._supported_subentries is None and (
handler := HANDLERS.get(self.domain)
):
# work out sub entries supported by the handler
object.__setattr__(
self, "_supported_subentries", handler.async_supported_subentries(self)
)
return self._supported_subentries or ()
def clear_state_cache(self) -> None:
"""Clear cached properties that are included in as_json_fragment."""
self.__dict__.pop("as_json_fragment", None)
@ -605,14 +518,12 @@ class ConfigEntry(Generic[_DataT]):
"supports_remove_device": self.supports_remove_device or False,
"supports_unload": self.supports_unload or False,
"supports_reconfigure": self.supports_reconfigure,
"supported_subentries": self.supported_subentries,
"pref_disable_new_entities": self.pref_disable_new_entities,
"pref_disable_polling": self.pref_disable_polling,
"disabled_by": self.disabled_by,
"reason": self.reason,
"error_reason_translation_key": self.error_reason_translation_key,
"error_reason_translation_placeholders": self.error_reason_translation_placeholders,
"num_subentries": len(self.subentries),
}
return json_fragment(json_bytes(json_repr))
@ -1107,7 +1018,6 @@ class ConfigEntry(Generic[_DataT]):
"pref_disable_new_entities": self.pref_disable_new_entities,
"pref_disable_polling": self.pref_disable_polling,
"source": self.source,
"subentries": [subentry.as_dict() for subentry in self.subentries.values()],
"title": self.title,
"unique_id": self.unique_id,
"version": self.version,
@ -1593,7 +1503,6 @@ class ConfigEntriesFlowManager(
minor_version=result["minor_version"],
options=result["options"],
source=flow.context["source"],
subentries_data=result["subentries"],
title=result["title"],
unique_id=flow.unique_id,
version=result["version"],
@ -1884,11 +1793,6 @@ class ConfigEntryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
for entry in data["entries"]:
entry["discovery_keys"] = {}
if old_minor_version < 5:
# Version 1.4 adds config subentries
for entry in data["entries"]:
entry.setdefault("subentries", entry.get("subentries", {}))
if old_major_version > 1:
raise NotImplementedError
return data
@ -1905,7 +1809,6 @@ class ConfigEntries:
self.hass = hass
self.flow = ConfigEntriesFlowManager(hass, self, hass_config)
self.options = OptionsFlowManager(hass)
self.subentries = ConfigSubentryFlowManager(hass)
self._hass_config = hass_config
self._entries = ConfigEntryItems(hass)
self._store = ConfigEntryStore(hass)
@ -2108,7 +2011,6 @@ class ConfigEntries:
pref_disable_new_entities=entry["pref_disable_new_entities"],
pref_disable_polling=entry["pref_disable_polling"],
source=entry["source"],
subentries_data=entry["subentries"],
title=entry["title"],
unique_id=entry["unique_id"],
version=entry["version"],
@ -2268,44 +2170,6 @@ class ConfigEntries:
If the entry was changed, the update_listeners are
fired and this function returns True
If the entry was not changed, the update_listeners are
not fired and this function returns False
"""
return self._async_update_entry(
entry,
data=data,
discovery_keys=discovery_keys,
minor_version=minor_version,
options=options,
pref_disable_new_entities=pref_disable_new_entities,
pref_disable_polling=pref_disable_polling,
title=title,
unique_id=unique_id,
version=version,
)
@callback
def _async_update_entry(
self,
entry: ConfigEntry,
*,
data: Mapping[str, Any] | UndefinedType = UNDEFINED,
discovery_keys: MappingProxyType[str, tuple[DiscoveryKey, ...]]
| UndefinedType = UNDEFINED,
minor_version: int | UndefinedType = UNDEFINED,
options: Mapping[str, Any] | UndefinedType = UNDEFINED,
pref_disable_new_entities: bool | UndefinedType = UNDEFINED,
pref_disable_polling: bool | UndefinedType = UNDEFINED,
subentries: dict[str, ConfigSubentry] | UndefinedType = UNDEFINED,
title: str | UndefinedType = UNDEFINED,
unique_id: str | None | UndefinedType = UNDEFINED,
version: int | UndefinedType = UNDEFINED,
) -> bool:
"""Update a config entry.
If the entry was changed, the update_listeners are
fired and this function returns True
If the entry was not changed, the update_listeners are
not fired and this function returns False
"""
@ -2368,11 +2232,6 @@ class ConfigEntries:
changed = True
_setter(entry, "options", MappingProxyType(options))
if subentries is not UNDEFINED:
if entry.subentries != subentries:
changed = True
_setter(entry, "subentries", MappingProxyType(subentries))
if not changed:
return False
@ -2390,37 +2249,6 @@ class ConfigEntries:
self._async_dispatch(ConfigEntryChange.UPDATED, entry)
return True
@callback
def async_add_subentry(self, entry: ConfigEntry, subentry: ConfigSubentry) -> bool:
"""Add a subentry to a config entry."""
self._raise_if_subentry_unique_id_exists(entry, subentry.unique_id)
return self._async_update_entry(
entry,
subentries=entry.subentries | {subentry.subentry_id: subentry},
)
@callback
def async_remove_subentry(self, entry: ConfigEntry, subentry_id: str) -> bool:
"""Remove a subentry from a config entry."""
subentries = dict(entry.subentries)
try:
subentries.pop(subentry_id)
except KeyError as err:
raise UnknownSubEntry from err
return self._async_update_entry(entry, subentries=subentries)
def _raise_if_subentry_unique_id_exists(
self, entry: ConfigEntry, unique_id: str | None
) -> None:
"""Raise if a subentry with the same unique_id exists."""
if unique_id is None:
return
for existing_subentry in entry.subentries.values():
if existing_subentry.unique_id == unique_id:
raise data_entry_flow.AbortFlow("already_configured")
@callback
def _async_dispatch(
self, change_type: ConfigEntryChange, entry: ConfigEntry
@ -2757,20 +2585,6 @@ class ConfigFlow(ConfigEntryBaseFlow):
"""Return options flow support for this handler."""
return cls.async_get_options_flow is not ConfigFlow.async_get_options_flow
@staticmethod
@callback
def async_get_subentry_flow(
config_entry: ConfigEntry, subentry_type: str
) -> ConfigSubentryFlow:
"""Get the subentry flow for this handler."""
raise NotImplementedError
@classmethod
@callback
def async_supported_subentries(cls, config_entry: ConfigEntry) -> tuple[str, ...]:
"""Return subentries supported by this handler."""
return ()
@callback
def _async_abort_entries_match(
self, match_dict: dict[str, Any] | None = None
@ -3079,7 +2893,6 @@ class ConfigFlow(ConfigEntryBaseFlow):
description: str | None = None,
description_placeholders: Mapping[str, str] | None = None,
options: Mapping[str, Any] | None = None,
subentries: Iterable[ConfigSubentryData] | None = None,
) -> ConfigFlowResult:
"""Finish config flow and create a config entry."""
if self.source in {SOURCE_REAUTH, SOURCE_RECONFIGURE}:
@ -3099,7 +2912,6 @@ class ConfigFlow(ConfigEntryBaseFlow):
result["minor_version"] = self.MINOR_VERSION
result["options"] = options or {}
result["subentries"] = subentries or ()
result["version"] = self.VERSION
return result
@ -3214,126 +3026,17 @@ class ConfigFlow(ConfigEntryBaseFlow):
)
class _ConfigSubFlowManager:
"""Mixin class for flow managers which manage flows tied to a config entry."""
class OptionsFlowManager(
data_entry_flow.FlowManager[ConfigFlowContext, ConfigFlowResult]
):
"""Flow to set options for a configuration entry."""
hass: HomeAssistant
_flow_result = ConfigFlowResult
def _async_get_config_entry(self, config_entry_id: str) -> ConfigEntry:
"""Return config entry or raise if not found."""
return self.hass.config_entries.async_get_known_entry(config_entry_id)
class ConfigSubentryFlowManager(
data_entry_flow.FlowManager[FlowContext, SubentryFlowResult, tuple[str, str]],
_ConfigSubFlowManager,
):
"""Manage all the config subentry flows that are in progress."""
_flow_result = SubentryFlowResult
async def async_create_flow(
self,
handler_key: tuple[str, str],
*,
context: FlowContext | None = None,
data: dict[str, Any] | None = None,
) -> ConfigSubentryFlow:
"""Create a subentry flow for a config entry.
The entry_id and flow.handler[0] is the same thing to map entry with flow.
"""
if not context or "source" not in context:
raise KeyError("Context not set or doesn't have a source set")
entry_id, subentry_type = handler_key
entry = self._async_get_config_entry(entry_id)
handler = await _async_get_flow_handler(self.hass, entry.domain, {})
if subentry_type not in handler.async_supported_subentries(entry):
raise data_entry_flow.UnknownHandler(
f"Config entry '{entry.domain}' does not support subentry '{subentry_type}'"
)
subentry_flow = handler.async_get_subentry_flow(entry, subentry_type)
subentry_flow.init_step = context["source"]
return subentry_flow
async def async_finish_flow(
self,
flow: data_entry_flow.FlowHandler[
FlowContext, SubentryFlowResult, tuple[str, str]
],
result: SubentryFlowResult,
) -> SubentryFlowResult:
"""Finish a subentry flow and add a new subentry to the configuration entry.
The flow.handler[0] and entry_id is the same thing to map flow with entry.
"""
flow = cast(ConfigSubentryFlow, flow)
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
return result
entry_id = flow.handler[0]
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is None:
raise UnknownEntry(entry_id)
unique_id = result.get("unique_id")
if unique_id is not None and not isinstance(unique_id, str):
raise HomeAssistantError("unique_id must be a string")
self.hass.config_entries.async_add_subentry(
entry,
ConfigSubentry(
data=MappingProxyType(result["data"]),
title=result["title"],
unique_id=unique_id,
),
)
result["result"] = True
return result
class ConfigSubentryFlow(
data_entry_flow.FlowHandler[FlowContext, SubentryFlowResult, tuple[str, str]]
):
"""Base class for config subentry flows."""
_flow_result = SubentryFlowResult
handler: tuple[str, str]
@callback
def async_create_entry(
self,
*,
title: str | None = None,
data: Mapping[str, Any],
description: str | None = None,
description_placeholders: Mapping[str, str] | None = None,
unique_id: str | None = None,
) -> SubentryFlowResult:
"""Finish config flow and create a config entry."""
result = super().async_create_entry(
title=title,
data=data,
description=description,
description_placeholders=description_placeholders,
)
result["unique_id"] = unique_id
return result
class OptionsFlowManager(
data_entry_flow.FlowManager[ConfigFlowContext, ConfigFlowResult],
_ConfigSubFlowManager,
):
"""Manage all the config entry option flows that are in progress."""
_flow_result = ConfigFlowResult
async def async_create_flow(
self,
handler_key: str,
@ -3343,7 +3046,7 @@ class OptionsFlowManager(
) -> OptionsFlow:
"""Create an options flow for a config entry.
The entry_id and the flow.handler is the same thing to map entry with flow.
Entry_id and flow.handler is the same thing to map entry with flow.
"""
entry = self._async_get_config_entry(handler_key)
handler = await _async_get_flow_handler(self.hass, entry.domain, {})
@ -3359,7 +3062,7 @@ class OptionsFlowManager(
This method is called when a flow step returns FlowResultType.ABORT or
FlowResultType.CREATE_ENTRY.
The flow.handler and the entry_id is the same thing to map flow with entry.
Flow.handler and entry_id is the same thing to map flow with entry.
"""
flow = cast(OptionsFlow, flow)