Compare commits

...
Sign in to create a new pull request.

13 commits

Author SHA1 Message Date
Erik
2462dfbc47 Adjust tests 2024-11-07 12:21:38 +01:00
Erik
2246f86d4b Update test snapshots 2024-11-07 12:17:43 +01:00
Erik
02a5fed2e4 Improve ConfigSubentryData typed dict 2024-11-07 10:11:12 +01:00
Erik
498773bad0 Add methods to ConfigEntries to add and remove subentry 2024-11-07 10:11:12 +01:00
Erik
b2922334aa Typing tweaks 2024-11-07 10:11:12 +01:00
Erik
1a06a2b6a6 Address review comments 2024-11-07 10:11:12 +01:00
Erik
55bf543648 Fix stale docstring 2024-11-07 10:11:12 +01:00
Erik
df13bed5b5 Update tests 2024-11-07 10:11:12 +01:00
Erik
a298091968 Tweak 2024-11-07 10:10:27 +01:00
Erik
977c8311c9 Auto-generate subentry_id and add optional unique_id 2024-11-07 10:10:27 +01:00
Erik
40d1c164c4 Include subentry_id in subentry containers 2024-11-07 10:10:27 +01:00
Erik
a3077513e4 Improve error handling and test coverage 2024-11-07 10:10:27 +01:00
Erik
e15cf8b633 Add support for subentries to config entries 2024-11-07 10:10:27 +01:00
95 changed files with 1565 additions and 27 deletions

View file

@ -46,6 +46,13 @@ def async_setup(hass: HomeAssistant) -> bool:
hass.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options)) hass.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options))
hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options)) hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options))
hass.http.register_view(
SubentryManagerFlowIndexView(hass.config_entries.subentries)
)
hass.http.register_view(
SubentryManagerFlowResourceView(hass.config_entries.subentries)
)
websocket_api.async_register_command(hass, config_entries_get) websocket_api.async_register_command(hass, config_entries_get)
websocket_api.async_register_command(hass, config_entry_disable) websocket_api.async_register_command(hass, config_entry_disable)
websocket_api.async_register_command(hass, config_entry_get_single) websocket_api.async_register_command(hass, config_entry_get_single)
@ -54,6 +61,9 @@ def async_setup(hass: HomeAssistant) -> bool:
websocket_api.async_register_command(hass, config_entries_progress) websocket_api.async_register_command(hass, config_entries_progress)
websocket_api.async_register_command(hass, ignore_config_flow) websocket_api.async_register_command(hass, ignore_config_flow)
websocket_api.async_register_command(hass, config_subentry_delete)
websocket_api.async_register_command(hass, config_subentry_list)
return True return True
@ -285,6 +295,48 @@ class OptionManagerFlowResourceView(
return await super().post(request, flow_id) return await super().post(request, flow_id)
class SubentryManagerFlowIndexView(
FlowManagerIndexView[config_entries.ConfigSubentryFlowManager]
):
"""View to create subentry flows."""
url = "/api/config/config_entries/subentries/flow"
name = "api:config:config_entries:subentries:flow"
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def post(self, request: web.Request) -> web.Response:
"""Handle a POST request.
handler in request is entry_id.
"""
return await super().post(request)
class SubentryManagerFlowResourceView(
FlowManagerResourceView[config_entries.ConfigSubentryFlowManager]
):
"""View to interact with the subentry flow manager."""
url = "/api/config/config_entries/subentries/flow/{flow_id}"
name = "api:config:config_entries:subentries:flow:resource"
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
"""Get the current state of a data_entry_flow."""
return await super().get(request, flow_id)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def post(self, request: web.Request, flow_id: str) -> web.Response:
"""Handle a POST request."""
return await super().post(request, flow_id)
@websocket_api.require_admin @websocket_api.require_admin
@websocket_api.websocket_command({"type": "config_entries/flow/progress"}) @websocket_api.websocket_command({"type": "config_entries/flow/progress"})
def config_entries_progress( def config_entries_progress(
@ -588,3 +640,62 @@ async def _async_matching_config_entries_json_fragments(
) )
or (filter_is_not_helper and entry.domain not in integrations) or (filter_is_not_helper and entry.domain not in integrations)
] ]
@websocket_api.require_admin
@websocket_api.websocket_command(
{
"type": "config_entries/subentries/list",
"entry_id": str,
}
)
@websocket_api.async_response
async def config_subentry_list(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""List subentries of a config entry."""
entry = get_entry(hass, connection, msg["entry_id"], msg["id"])
if entry is None:
return
result = [
{
"subentry_id": subentry.subentry_id,
"title": subentry.title,
"unique_id": subentry.unique_id,
}
for subentry_id, subentry in entry.subentries.items()
]
connection.send_result(msg["id"], result)
@websocket_api.require_admin
@websocket_api.websocket_command(
{
"type": "config_entries/subentries/delete",
"entry_id": str,
"subentry_id": str,
}
)
@websocket_api.async_response
async def config_subentry_delete(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Delete a subentry of a config entry."""
entry = get_entry(hass, connection, msg["entry_id"], msg["id"])
if entry is None:
return
try:
hass.config_entries.async_remove_subentry(entry, msg["subentry_id"])
except config_entries.UnknownSubEntry:
connection.send_error(
msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config subentry not found"
)
return
connection.send_result(msg["id"])

View file

@ -15,6 +15,7 @@ from collections.abc import (
) )
from contextvars import ContextVar from contextvars import ContextVar
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from enum import Enum, StrEnum from enum import Enum, StrEnum
import functools import functools
@ -22,7 +23,7 @@ from functools import cache
import logging import logging
from random import randint from random import randint
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Generic, Self, cast from typing import TYPE_CHECKING, Any, Generic, Self, TypedDict, cast
from async_interrupt import interrupt from async_interrupt import interrupt
from propcache import cached_property from propcache import cached_property
@ -123,7 +124,7 @@ HANDLERS: Registry[str, type[ConfigFlow]] = Registry()
STORAGE_KEY = "core.config_entries" STORAGE_KEY = "core.config_entries"
STORAGE_VERSION = 1 STORAGE_VERSION = 1
STORAGE_VERSION_MINOR = 4 STORAGE_VERSION_MINOR = 5
SAVE_DELAY = 1 SAVE_DELAY = 1
@ -251,6 +252,10 @@ class UnknownEntry(ConfigError):
"""Unknown entry specified.""" """Unknown entry specified."""
class UnknownSubEntry(ConfigError):
"""Unknown subentry specified."""
class OperationNotAllowed(ConfigError): class OperationNotAllowed(ConfigError):
"""Raised when a config entry operation is not allowed.""" """Raised when a config entry operation is not allowed."""
@ -295,6 +300,7 @@ class ConfigFlowResult(FlowResult[ConfigFlowContext, str], total=False):
minor_version: int minor_version: int
options: Mapping[str, Any] options: Mapping[str, Any]
subentries: Iterable[ConfigSubentryData]
version: int version: int
@ -308,6 +314,51 @@ 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, 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]): class ConfigEntry(Generic[_DataT]):
"""Hold a configuration entry.""" """Hold a configuration entry."""
@ -317,6 +368,7 @@ class ConfigEntry(Generic[_DataT]):
data: MappingProxyType[str, Any] data: MappingProxyType[str, Any]
runtime_data: _DataT runtime_data: _DataT
options: MappingProxyType[str, Any] options: MappingProxyType[str, Any]
subentries: MappingProxyType[str, ConfigSubentry]
unique_id: str | None unique_id: str | None
state: ConfigEntryState state: ConfigEntryState
reason: str | None reason: str | None
@ -332,6 +384,7 @@ class ConfigEntry(Generic[_DataT]):
supports_remove_device: bool | None supports_remove_device: bool | None
_supports_options: bool | None _supports_options: bool | None
_supports_reconfigure: bool | None _supports_reconfigure: bool | None
_supports_subentries: bool | None
update_listeners: list[UpdateListenerType] update_listeners: list[UpdateListenerType]
_async_cancel_retry_setup: Callable[[], Any] | None _async_cancel_retry_setup: Callable[[], Any] | None
_on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None _on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None
@ -361,6 +414,7 @@ class ConfigEntry(Generic[_DataT]):
pref_disable_polling: bool | None = None, pref_disable_polling: bool | None = None,
source: str, source: str,
state: ConfigEntryState = ConfigEntryState.NOT_LOADED, state: ConfigEntryState = ConfigEntryState.NOT_LOADED,
subentries_data: Iterable[ConfigSubentryData | ConfigSubentryDataWithId] | None,
title: str, title: str,
unique_id: str | None, unique_id: str | None,
version: int, version: int,
@ -386,6 +440,24 @@ class ConfigEntry(Generic[_DataT]):
# Entry options # Entry options
_setter(self, "options", MappingProxyType(options or {})) _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 # Entry system options
if pref_disable_new_entities is None: if pref_disable_new_entities is None:
pref_disable_new_entities = False pref_disable_new_entities = False
@ -422,6 +494,9 @@ class ConfigEntry(Generic[_DataT]):
# Supports reconfigure # Supports reconfigure
_setter(self, "_supports_reconfigure", None) _setter(self, "_supports_reconfigure", None)
# Supports subentries
_setter(self, "_supports_subentries", None)
# Listeners to call on update # Listeners to call on update
_setter(self, "update_listeners", []) _setter(self, "update_listeners", [])
@ -494,6 +569,16 @@ class ConfigEntry(Generic[_DataT]):
) )
return self._supports_reconfigure or False return self._supports_reconfigure or False
@property
def supports_subentries(self) -> bool:
"""Return if entry supports adding and removing sub entries."""
if self._supports_subentries is None and (handler := HANDLERS.get(self.domain)):
# work out if handler has support for sub entries
object.__setattr__(
self, "_supports_subentries", handler.async_supports_subentries(self)
)
return self._supports_subentries or False
def clear_state_cache(self) -> None: def clear_state_cache(self) -> None:
"""Clear cached properties that are included in as_json_fragment.""" """Clear cached properties that are included in as_json_fragment."""
self.__dict__.pop("as_json_fragment", None) self.__dict__.pop("as_json_fragment", None)
@ -513,6 +598,7 @@ class ConfigEntry(Generic[_DataT]):
"supports_remove_device": self.supports_remove_device or False, "supports_remove_device": self.supports_remove_device or False,
"supports_unload": self.supports_unload or False, "supports_unload": self.supports_unload or False,
"supports_reconfigure": self.supports_reconfigure, "supports_reconfigure": self.supports_reconfigure,
"supports_subentries": self.supports_subentries,
"pref_disable_new_entities": self.pref_disable_new_entities, "pref_disable_new_entities": self.pref_disable_new_entities,
"pref_disable_polling": self.pref_disable_polling, "pref_disable_polling": self.pref_disable_polling,
"disabled_by": self.disabled_by, "disabled_by": self.disabled_by,
@ -1013,6 +1099,7 @@ class ConfigEntry(Generic[_DataT]):
"pref_disable_new_entities": self.pref_disable_new_entities, "pref_disable_new_entities": self.pref_disable_new_entities,
"pref_disable_polling": self.pref_disable_polling, "pref_disable_polling": self.pref_disable_polling,
"source": self.source, "source": self.source,
"subentries": [subentry.as_dict() for subentry in self.subentries.values()],
"title": self.title, "title": self.title,
"unique_id": self.unique_id, "unique_id": self.unique_id,
"version": self.version, "version": self.version,
@ -1502,6 +1589,7 @@ class ConfigEntriesFlowManager(
minor_version=result["minor_version"], minor_version=result["minor_version"],
options=result["options"], options=result["options"],
source=flow.context["source"], source=flow.context["source"],
subentries_data=result["subentries"],
title=result["title"], title=result["title"],
unique_id=flow.unique_id, unique_id=flow.unique_id,
version=result["version"], version=result["version"],
@ -1786,6 +1874,11 @@ class ConfigEntryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
for entry in data["entries"]: for entry in data["entries"]:
entry["discovery_keys"] = {} 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: if old_major_version > 1:
raise NotImplementedError raise NotImplementedError
return data return data
@ -1802,6 +1895,7 @@ class ConfigEntries:
self.hass = hass self.hass = hass
self.flow = ConfigEntriesFlowManager(hass, self, hass_config) self.flow = ConfigEntriesFlowManager(hass, self, hass_config)
self.options = OptionsFlowManager(hass) self.options = OptionsFlowManager(hass)
self.subentries = ConfigSubentryFlowManager(hass)
self._hass_config = hass_config self._hass_config = hass_config
self._entries = ConfigEntryItems(hass) self._entries = ConfigEntryItems(hass)
self._store = ConfigEntryStore(hass) self._store = ConfigEntryStore(hass)
@ -1981,6 +2075,7 @@ class ConfigEntries:
pref_disable_new_entities=entry["pref_disable_new_entities"], pref_disable_new_entities=entry["pref_disable_new_entities"],
pref_disable_polling=entry["pref_disable_polling"], pref_disable_polling=entry["pref_disable_polling"],
source=entry["source"], source=entry["source"],
subentries_data=entry["subentries"],
title=entry["title"], title=entry["title"],
unique_id=entry["unique_id"], unique_id=entry["unique_id"],
version=entry["version"], version=entry["version"],
@ -2145,6 +2240,44 @@ class ConfigEntries:
If the entry was changed, the update_listeners are If the entry was changed, the update_listeners are
fired and this function returns True 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 If the entry was not changed, the update_listeners are
not fired and this function returns False not fired and this function returns False
""" """
@ -2202,6 +2335,11 @@ class ConfigEntries:
changed = True changed = True
_setter(entry, "options", MappingProxyType(options)) _setter(entry, "options", MappingProxyType(options))
if subentries is not UNDEFINED:
if entry.subentries != subentries:
changed = True
_setter(entry, "subentries", MappingProxyType(subentries))
if not changed: if not changed:
return False return False
@ -2219,6 +2357,25 @@ class ConfigEntries:
self._async_dispatch(ConfigEntryChange.UPDATED, entry) self._async_dispatch(ConfigEntryChange.UPDATED, entry)
return True return True
@callback
def async_add_subentry(self, entry: ConfigEntry, subentry: ConfigSubentry) -> bool:
"""Add a subentry to a config entry."""
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)
@callback @callback
def _async_dispatch( def _async_dispatch(
self, change_type: ConfigEntryChange, entry: ConfigEntry self, change_type: ConfigEntryChange, entry: ConfigEntry
@ -2539,6 +2696,18 @@ class ConfigFlow(ConfigEntryBaseFlow):
"""Return options flow support for this handler.""" """Return options flow support for this handler."""
return cls.async_get_options_flow is not ConfigFlow.async_get_options_flow return cls.async_get_options_flow is not ConfigFlow.async_get_options_flow
@staticmethod
@callback
def async_get_subentry_flow(config_entry: ConfigEntry) -> ConfigSubentryFlow:
"""Get the subentry flow for this handler."""
raise data_entry_flow.UnknownHandler
@classmethod
@callback
def async_supports_subentries(cls, config_entry: ConfigEntry) -> bool:
"""Return subentry support for this handler."""
return cls.async_get_subentry_flow is not ConfigFlow.async_get_subentry_flow
@callback @callback
def _async_abort_entries_match( def _async_abort_entries_match(
self, match_dict: dict[str, Any] | None = None self, match_dict: dict[str, Any] | None = None
@ -2847,6 +3016,7 @@ class ConfigFlow(ConfigEntryBaseFlow):
description: str | None = None, description: str | None = None,
description_placeholders: Mapping[str, str] | None = None, description_placeholders: Mapping[str, str] | None = None,
options: Mapping[str, Any] | None = None, options: Mapping[str, Any] | None = None,
subentries: Iterable[ConfigSubentryData] | None = None,
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Finish config flow and create a config entry.""" """Finish config flow and create a config entry."""
if self.source in {SOURCE_REAUTH, SOURCE_RECONFIGURE}: if self.source in {SOURCE_REAUTH, SOURCE_RECONFIGURE}:
@ -2872,6 +3042,7 @@ class ConfigFlow(ConfigEntryBaseFlow):
result["minor_version"] = self.MINOR_VERSION result["minor_version"] = self.MINOR_VERSION
result["options"] = options or {} result["options"] = options or {}
result["subentries"] = subentries or ()
result["version"] = self.VERSION result["version"] = self.VERSION
return result return result
@ -2990,12 +3161,10 @@ class ConfigFlow(ConfigEntryBaseFlow):
raise UnknownEntry raise UnknownEntry
class OptionsFlowManager( class _ConfigSubFlowManager:
data_entry_flow.FlowManager[ConfigFlowContext, ConfigFlowResult] """Mixin class for flow managers which manage flows tied to a config entry."""
):
"""Flow to set options for a configuration entry."""
_flow_result = ConfigFlowResult hass: HomeAssistant
def _async_get_config_entry(self, config_entry_id: str) -> ConfigEntry: def _async_get_config_entry(self, config_entry_id: str) -> ConfigEntry:
"""Return config entry or raise if not found.""" """Return config entry or raise if not found."""
@ -3005,6 +3174,106 @@ class OptionsFlowManager(
return entry return entry
class ConfigSubentryFlowManager(
data_entry_flow.FlowManager[FlowContext, SubentryFlowResult], _ConfigSubFlowManager
):
"""Manage all the config subentry flows that are in progress."""
_flow_result = SubentryFlowResult
async def async_create_flow(
self,
handler_key: str,
*,
context: FlowContext | None = None,
data: dict[str, Any] | None = None,
) -> ConfigSubentryFlow:
"""Create a subentry flow for a config entry.
The entry_id and the 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, {})
return handler.async_get_subentry_flow(entry)
async def async_finish_flow(
self,
flow: data_entry_flow.FlowHandler[FlowContext, SubentryFlowResult],
result: SubentryFlowResult,
) -> SubentryFlowResult:
"""Finish a subentry flow and add a new subentry to the configuration entry.
The flow.handler and the 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 = self.hass.config_entries.async_get_entry(flow.handler)
if entry is None:
raise UnknownEntry(flow.handler)
unique_id = result.get("unique_id")
if "unique_id" in result and not isinstance(unique_id, str):
raise HomeAssistantError("unique_id must be a string")
if any(
subentry.unique_id == unique_id for subentry in entry.subentries.values()
):
raise data_entry_flow.AbortFlow("already_configured")
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]):
"""Base class for config subentry flows."""
_flow_result = SubentryFlowResult
handler: str
@callback
def async_create_entry( # type: ignore[override]
self,
*,
title: str | None = None,
data: Mapping[str, Any],
description: str | None = None,
description_placeholders: Mapping[str, str] | None = None,
unique_id: str,
) -> 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( async def async_create_flow(
self, self,
handler_key: str, handler_key: str,
@ -3014,7 +3283,7 @@ class OptionsFlowManager(
) -> OptionsFlow: ) -> OptionsFlow:
"""Create an options flow for a config entry. """Create an options flow for a config entry.
Entry_id and flow.handler is the same thing to map entry with flow. The entry_id and the flow.handler is the same thing to map entry with flow.
""" """
entry = self._async_get_config_entry(handler_key) entry = self._async_get_config_entry(handler_key)
handler = await _async_get_flow_handler(self.hass, entry.domain, {}) handler = await _async_get_flow_handler(self.hass, entry.domain, {})
@ -3030,7 +3299,7 @@ class OptionsFlowManager(
This method is called when a flow step returns FlowResultType.ABORT or This method is called when a flow step returns FlowResultType.ABORT or
FlowResultType.CREATE_ENTRY. FlowResultType.CREATE_ENTRY.
Flow.handler and entry_id is the same thing to map flow with entry. The flow.handler and the entry_id is the same thing to map flow with entry.
""" """
flow = cast(OptionsFlow, flow) flow = cast(OptionsFlow, flow)

View file

@ -1000,6 +1000,7 @@ class MockConfigEntry(config_entries.ConfigEntry):
reason=None, reason=None,
source=config_entries.SOURCE_USER, source=config_entries.SOURCE_USER,
state=None, state=None,
subentries_data=None,
title="Mock Title", title="Mock Title",
unique_id=None, unique_id=None,
version=1, version=1,
@ -1016,6 +1017,7 @@ class MockConfigEntry(config_entries.ConfigEntry):
"options": options or {}, "options": options or {},
"pref_disable_new_entities": pref_disable_new_entities, "pref_disable_new_entities": pref_disable_new_entities,
"pref_disable_polling": pref_disable_polling, "pref_disable_polling": pref_disable_polling,
"subentries_data": subentries_data or (),
"title": title, "title": title,
"unique_id": unique_id, "unique_id": unique_id,
"version": version, "version": version,

View file

@ -21,6 +21,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -19,6 +19,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Home', 'title': 'Home',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -35,6 +35,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 2, 'version': 2,

View file

@ -47,6 +47,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 3, 'version': 3,

View file

@ -101,6 +101,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': 'XXXXXXX', 'unique_id': 'XXXXXXX',
'version': 1, 'version': 1,

View file

@ -287,6 +287,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -101,6 +101,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': 'installation1', 'unique_id': 'installation1',
'version': 1, 'version': 1,

View file

@ -17,6 +17,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 2, 'version': 2,

View file

@ -47,6 +47,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 3, 'version': 3,

View file

@ -48,6 +48,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 3, 'version': 3,

View file

@ -19,6 +19,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': 'very_unique_string', 'unique_id': 'very_unique_string',
'version': 1, 'version': 1,

View file

@ -17,6 +17,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -44,6 +44,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -71,6 +71,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,
@ -135,6 +137,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -145,6 +145,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": True, "supports_options": True,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": True, "supports_unload": True,
"title": "Test 1", "title": "Test 1",
}, },
@ -163,6 +164,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 2", "title": "Test 2",
}, },
@ -181,6 +183,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 3", "title": "Test 3",
}, },
@ -199,6 +202,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 4", "title": "Test 4",
}, },
@ -217,6 +221,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 5", "title": "Test 5",
}, },
@ -583,6 +588,7 @@ async def test_create_account(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test Entry", "title": "Test Entry",
}, },
@ -590,6 +596,7 @@ async def test_create_account(hass: HomeAssistant, client: TestClient) -> None:
"description_placeholders": None, "description_placeholders": None,
"options": {}, "options": {},
"minor_version": 1, "minor_version": 1,
"subentries": [],
} }
@ -666,6 +673,7 @@ async def test_two_step_flow(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "user-title", "title": "user-title",
}, },
@ -673,6 +681,7 @@ async def test_two_step_flow(hass: HomeAssistant, client: TestClient) -> None:
"description_placeholders": None, "description_placeholders": None,
"options": {}, "options": {},
"minor_version": 1, "minor_version": 1,
"subentries": [],
} }
@ -1092,6 +1101,253 @@ async def test_options_flow_with_invalid_data(
assert data == {"errors": {"choices": "invalid is not a valid option"}} assert data == {"errors": {"choices": "invalid is not a valid option"}}
async def test_subentry_flow(hass: HomeAssistant, client) -> None:
"""Test we can start a subentry flow."""
class TestFlow(core_ce.ConfigFlow):
@staticmethod
@callback
def async_get_subentry_flow(config_entry):
class SubentryFlowHandler(core_ce.ConfigSubentryFlow):
async def async_step_init(self, user_input=None):
schema = OrderedDict()
schema[vol.Required("enabled")] = bool
return self.async_show_form(
step_id="user",
data_schema=schema,
description_placeholders={"enabled": "Set to true to be true"},
)
async def async_step_user(self, user_input=None):
raise NotImplementedError
return SubentryFlowHandler()
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
MockConfigEntry(
domain="test",
entry_id="test1",
source="bla",
).add_to_hass(hass)
entry = hass.config_entries.async_entries()[0]
with patch.dict(HANDLERS, {"test": TestFlow}):
url = "/api/config/config_entries/subentries/flow"
resp = await client.post(url, json={"handler": entry.entry_id})
assert resp.status == HTTPStatus.OK
data = await resp.json()
data.pop("flow_id")
assert data == {
"type": "form",
"handler": "test1",
"step_id": "user",
"data_schema": [{"name": "enabled", "required": True, "type": "boolean"}],
"description_placeholders": {"enabled": "Set to true to be true"},
"errors": None,
"last_step": None,
"preview": None,
}
@pytest.mark.parametrize(
("endpoint", "method"),
[
("/api/config/config_entries/subentries/flow", "post"),
("/api/config/config_entries/subentries/flow/1", "get"),
("/api/config/config_entries/subentries/flow/1", "post"),
],
)
async def test_subentry_flow_unauth(
hass: HomeAssistant, client, hass_admin_user: MockUser, endpoint: str, method: str
) -> None:
"""Test unauthorized on subentry flow."""
class TestFlow(core_ce.ConfigFlow):
@staticmethod
@callback
def async_get_subentry_flow(config_entry):
class SubentryFlowHandler(core_ce.ConfigSubentryFlow):
async def async_step_init(self, user_input=None):
schema = OrderedDict()
schema[vol.Required("enabled")] = bool
return self.async_show_form(
step_id="user",
data_schema=schema,
description_placeholders={"enabled": "Set to true to be true"},
)
return SubentryFlowHandler()
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
MockConfigEntry(
domain="test",
entry_id="test1",
source="bla",
).add_to_hass(hass)
entry = hass.config_entries.async_entries()[0]
hass_admin_user.groups = []
with patch.dict(HANDLERS, {"test": TestFlow}):
resp = await getattr(client, method)(endpoint, json={"handler": entry.entry_id})
assert resp.status == HTTPStatus.UNAUTHORIZED
async def test_two_step_subentry_flow(hass: HomeAssistant, client) -> None:
"""Test we can finish a two step subentry flow."""
mock_integration(
hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True))
)
mock_platform(hass, "test.config_flow", None)
class TestFlow(core_ce.ConfigFlow):
@staticmethod
@callback
def async_get_subentry_flow(config_entry):
class SubentryFlowHandler(core_ce.ConfigSubentryFlow):
async def async_step_init(self, user_input=None):
return await self.async_step_finish()
async def async_step_finish(self, user_input=None):
if user_input:
return self.async_create_entry(
title="Mock title", data=user_input, unique_id="test"
)
return self.async_show_form(
step_id="finish", data_schema=vol.Schema({"enabled": bool})
)
return SubentryFlowHandler()
MockConfigEntry(
domain="test",
entry_id="test1",
source="bla",
).add_to_hass(hass)
entry = hass.config_entries.async_entries()[0]
with patch.dict(HANDLERS, {"test": TestFlow}):
url = "/api/config/config_entries/subentries/flow"
resp = await client.post(url, json={"handler": entry.entry_id})
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
expected_data = {
"data_schema": [{"name": "enabled", "type": "boolean"}],
"description_placeholders": None,
"errors": None,
"flow_id": flow_id,
"handler": "test1",
"last_step": None,
"preview": None,
"step_id": "finish",
"type": "form",
}
assert data == expected_data
resp = await client.get(f"/api/config/config_entries/subentries/flow/{flow_id}")
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == expected_data
resp = await client.post(
f"/api/config/config_entries/subentries/flow/{flow_id}",
json={"enabled": True},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"description_placeholders": None,
"description": None,
"flow_id": flow_id,
"handler": "test1",
"title": "Mock title",
"type": "create_entry",
"unique_id": "test",
}
async def test_subentry_flow_with_invalid_data(hass: HomeAssistant, client) -> None:
"""Test a subentry flow with invalid_data."""
mock_integration(
hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True))
)
mock_platform(hass, "test.config_flow", None)
class TestFlow(core_ce.ConfigFlow):
@staticmethod
@callback
def async_get_subentry_flow(config_entry):
class SubentryFlowHandler(core_ce.ConfigSubentryFlow):
async def async_step_init(self, user_input=None):
return self.async_show_form(
step_id="finish",
data_schema=vol.Schema(
{
vol.Required(
"choices", default=["invalid", "valid"]
): cv.multi_select({"valid": "Valid"})
}
),
)
async def async_step_finish(self, user_input=None):
return self.async_create_entry(
title="Enable disable", data=user_input
)
return SubentryFlowHandler()
MockConfigEntry(
domain="test",
entry_id="test1",
source="bla",
).add_to_hass(hass)
entry = hass.config_entries.async_entries()[0]
with patch.dict(HANDLERS, {"test": TestFlow}):
url = "/api/config/config_entries/subentries/flow"
resp = await client.post(url, json={"handler": entry.entry_id})
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data.pop("flow_id")
assert data == {
"type": "form",
"handler": "test1",
"step_id": "finish",
"data_schema": [
{
"default": ["invalid", "valid"],
"name": "choices",
"options": {"valid": "Valid"},
"required": True,
"type": "multi_select",
}
],
"description_placeholders": None,
"errors": None,
"last_step": None,
"preview": None,
}
with patch.dict(HANDLERS, {"test": TestFlow}):
resp = await client.post(
f"/api/config/config_entries/subentries/flow/{flow_id}",
json={"choices": ["valid", "invalid"]},
)
assert resp.status == HTTPStatus.BAD_REQUEST
data = await resp.json()
assert data == {"errors": {"choices": "invalid is not a valid option"}}
@pytest.mark.usefixtures("freezer") @pytest.mark.usefixtures("freezer")
async def test_get_single( async def test_get_single(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator hass: HomeAssistant, hass_ws_client: WebSocketGenerator
@ -1132,6 +1388,7 @@ async def test_get_single(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Mock Title", "title": "Mock Title",
} }
@ -1496,6 +1753,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 1", "title": "Test 1",
}, },
@ -1515,6 +1773,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 2", "title": "Test 2",
}, },
@ -1534,6 +1793,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 3", "title": "Test 3",
}, },
@ -1553,6 +1813,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 4", "title": "Test 4",
}, },
@ -1572,6 +1833,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 5", "title": "Test 5",
}, },
@ -1602,6 +1864,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 1", "title": "Test 1",
} }
@ -1631,6 +1894,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 4", "title": "Test 4",
}, },
@ -1650,6 +1914,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 5", "title": "Test 5",
}, },
@ -1679,6 +1944,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 1", "title": "Test 1",
}, },
@ -1698,6 +1964,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 3", "title": "Test 3",
}, },
@ -1733,6 +2000,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 1", "title": "Test 1",
}, },
@ -1752,6 +2020,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 2", "title": "Test 2",
}, },
@ -1771,6 +2040,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 3", "title": "Test 3",
}, },
@ -1790,6 +2060,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 4", "title": "Test 4",
}, },
@ -1809,6 +2080,7 @@ async def test_get_matching_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 5", "title": "Test 5",
}, },
@ -1916,6 +2188,7 @@ async def test_subscribe_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 1", "title": "Test 1",
}, },
@ -1938,6 +2211,7 @@ async def test_subscribe_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 2", "title": "Test 2",
}, },
@ -1960,6 +2234,7 @@ async def test_subscribe_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 3", "title": "Test 3",
}, },
@ -1988,6 +2263,7 @@ async def test_subscribe_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "changed", "title": "changed",
}, },
@ -2017,6 +2293,7 @@ async def test_subscribe_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "changed", "title": "changed",
}, },
@ -2045,6 +2322,7 @@ async def test_subscribe_entries_ws(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "changed", "title": "changed",
}, },
@ -2135,6 +2413,7 @@ async def test_subscribe_entries_ws_filtered(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 1", "title": "Test 1",
}, },
@ -2157,6 +2436,7 @@ async def test_subscribe_entries_ws_filtered(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "Test 3", "title": "Test 3",
}, },
@ -2187,6 +2467,7 @@ async def test_subscribe_entries_ws_filtered(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "changed", "title": "changed",
}, },
@ -2213,6 +2494,7 @@ async def test_subscribe_entries_ws_filtered(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "changed too", "title": "changed too",
}, },
@ -2243,6 +2525,7 @@ async def test_subscribe_entries_ws_filtered(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "changed", "title": "changed",
}, },
@ -2271,6 +2554,7 @@ async def test_subscribe_entries_ws_filtered(
"supports_options": False, "supports_options": False,
"supports_reconfigure": False, "supports_reconfigure": False,
"supports_remove_device": False, "supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False, "supports_unload": False,
"title": "changed", "title": "changed",
}, },
@ -2478,3 +2762,133 @@ async def test_does_not_support_reconfigure(
response response
== '{"message":"Handler ConfigEntriesFlowManager doesn\'t support step reconfigure"}' == '{"message":"Handler ConfigEntriesFlowManager doesn\'t support step reconfigure"}'
) )
async def test_list_subentries(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test that we can list subentries."""
assert await async_setup_component(hass, "config", {})
ws_client = await hass_ws_client(hass)
entry = MockConfigEntry(
domain="test",
state=core_ce.ConfigEntryState.LOADED,
subentries_data=[
core_ce.ConfigSubentryData(
data={"test": "test"},
subentry_id="mock_id",
title="Mock title",
unique_id="test",
)
],
)
entry.add_to_hass(hass)
assert entry.pref_disable_new_entities is False
assert entry.pref_disable_polling is False
await ws_client.send_json_auto_id(
{
"type": "config_entries/subentries/list",
"entry_id": entry.entry_id,
}
)
response = await ws_client.receive_json()
assert response["success"]
assert response["result"] == [
{"subentry_id": "mock_id", "title": "Mock title", "unique_id": "test"},
]
# Try listing subentries for an unknown entry
await ws_client.send_json_auto_id(
{
"type": "config_entries/subentries/list",
"entry_id": "no_such_entry",
}
)
response = await ws_client.receive_json()
assert not response["success"]
assert response["error"] == {
"code": "not_found",
"message": "Config entry not found",
}
async def test_delete_subentry(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test that we can delete a subentry."""
assert await async_setup_component(hass, "config", {})
ws_client = await hass_ws_client(hass)
entry = MockConfigEntry(
domain="test",
state=core_ce.ConfigEntryState.LOADED,
subentries_data=[
core_ce.ConfigSubentryData(
data={"test": "test"}, subentry_id="mock_id", title="Mock title"
)
],
)
entry.add_to_hass(hass)
assert entry.pref_disable_new_entities is False
assert entry.pref_disable_polling is False
await ws_client.send_json_auto_id(
{
"type": "config_entries/subentries/delete",
"entry_id": entry.entry_id,
"subentry_id": "mock_id",
}
)
response = await ws_client.receive_json()
assert response["success"]
assert response["result"] is None
await ws_client.send_json_auto_id(
{
"type": "config_entries/subentries/list",
"entry_id": entry.entry_id,
}
)
response = await ws_client.receive_json()
assert response["success"]
assert response["result"] == []
# Try deleting the subentry again
await ws_client.send_json_auto_id(
{
"type": "config_entries/subentries/delete",
"entry_id": entry.entry_id,
"subentry_id": "mock_id",
}
)
response = await ws_client.receive_json()
assert not response["success"]
assert response["error"] == {
"code": "not_found",
"message": "Config subentry not found",
}
# Try deleting subentry from an unknown entry
await ws_client.send_json_auto_id(
{
"type": "config_entries/subentries/delete",
"entry_id": "no_such_entry",
"subentry_id": "mock_id",
}
)
response = await ws_client.receive_json()
assert not response["success"]
assert response["error"] == {
"code": "not_found",
"message": "Config entry not found",
}

View file

@ -21,6 +21,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -48,6 +48,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': '123456', 'unique_id': '123456',
'version': 1, 'version': 1,

View file

@ -32,6 +32,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -17,6 +17,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'dsmr_reader', 'title': 'dsmr_reader',
'unique_id': 'UNIQUE_TEST_ID', 'unique_id': 'UNIQUE_TEST_ID',
'version': 1, 'version': 1,

View file

@ -17,6 +17,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,
@ -70,6 +72,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -34,10 +34,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'CN11A1A00001', 'title': 'CN11A1A00001',
'unique_id': 'CN11A1A00001', 'unique_id': 'CN11A1A00001',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'CN11A1A00001', 'title': 'CN11A1A00001',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -79,10 +83,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'zeroconf', 'source': 'zeroconf',
'subentries': list([
]),
'title': 'CN11A1A00001', 'title': 'CN11A1A00001',
'unique_id': 'CN11A1A00001', 'unique_id': 'CN11A1A00001',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'CN11A1A00001', 'title': 'CN11A1A00001',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -123,10 +131,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'zeroconf', 'source': 'zeroconf',
'subentries': list([
]),
'title': 'CN11A1A00001', 'title': 'CN11A1A00001',
'unique_id': 'CN11A1A00001', 'unique_id': 'CN11A1A00001',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'CN11A1A00001', 'title': 'CN11A1A00001',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,

View file

@ -28,10 +28,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'EnergyZero', 'title': 'EnergyZero',
'unique_id': 'energyzero', 'unique_id': 'energyzero',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'EnergyZero', 'title': 'EnergyZero',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,

View file

@ -20,6 +20,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,
@ -454,6 +456,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,
@ -928,6 +932,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -20,6 +20,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'ESPHome Device', 'title': 'ESPHome Device',
'unique_id': '11:22:33:44:55:aa', 'unique_id': '11:22:33:44:55:aa',
'version': 1, 'version': 1,

View file

@ -79,6 +79,7 @@ async def test_diagnostics_with_bluetooth(
"pref_disable_new_entities": False, "pref_disable_new_entities": False,
"pref_disable_polling": False, "pref_disable_polling": False,
"source": "user", "source": "user",
"subentries": [],
"title": "Mock Title", "title": "Mock Title",
"unique_id": "11:22:33:44:55:aa", "unique_id": "11:22:33:44:55:aa",
"version": 1, "version": 1,

View file

@ -23,6 +23,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Green House', 'title': 'Green House',
'unique_id': 'unique', 'unique_id': 'unique',
'version': 2, 'version': 2,

View file

@ -61,6 +61,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -17,6 +17,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -19,6 +19,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'fyta_user', 'title': 'fyta_user',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -66,10 +66,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'bluetooth', 'source': 'bluetooth',
'subentries': list([
]),
'title': 'Gardena Water Computer', 'title': 'Gardena Water Computer',
'unique_id': '00000000-0000-0000-0000-000000000001', 'unique_id': '00000000-0000-0000-0000-000000000001',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Gardena Water Computer', 'title': 'Gardena Water Computer',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -223,10 +227,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Gardena Water Computer', 'title': 'Gardena Water Computer',
'unique_id': '00000000-0000-0000-0000-000000000001', 'unique_id': '00000000-0000-0000-0000-000000000001',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Gardena Water Computer', 'title': 'Gardena Water Computer',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,

View file

@ -17,6 +17,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Home', 'title': 'Home',
'unique_id': '123', 'unique_id': '123',
'version': 1, 'version': 1,

View file

@ -17,6 +17,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -15,6 +15,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'import', 'source': 'import',
'subentries': list([
]),
'title': '1234', 'title': '1234',
'unique_id': '1234', 'unique_id': '1234',
'version': 1, 'version': 1,

View file

@ -42,6 +42,7 @@ async def test_entry_diagnostics(
"created_at": ANY, "created_at": ANY,
"modified_at": ANY, "modified_at": ANY,
"discovery_keys": {}, "discovery_keys": {},
"subentries": [],
}, },
"data": { "data": {
"valve_controller": { "valve_controller": {

View file

@ -30,10 +30,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'zeroconf', 'source': 'zeroconf',
'subentries': list([
]),
'title': 'P1 meter', 'title': 'P1 meter',
'unique_id': 'HWE-P1_aabbccddeeff', 'unique_id': 'HWE-P1_aabbccddeeff',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'P1 meter', 'title': 'P1 meter',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -74,10 +78,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'zeroconf', 'source': 'zeroconf',
'subentries': list([
]),
'title': 'P1 meter', 'title': 'P1 meter',
'unique_id': 'HWE-P1_aabbccddeeff', 'unique_id': 'HWE-P1_aabbccddeeff',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'P1 meter', 'title': 'P1 meter',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -118,10 +126,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'zeroconf', 'source': 'zeroconf',
'subentries': list([
]),
'title': 'Energy Socket', 'title': 'Energy Socket',
'unique_id': 'HWE-SKT_aabbccddeeff', 'unique_id': 'HWE-SKT_aabbccddeeff',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Energy Socket', 'title': 'Energy Socket',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -158,10 +170,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'P1 meter', 'title': 'P1 meter',
'unique_id': 'HWE-P1_3c39e7aabbcc', 'unique_id': 'HWE-P1_3c39e7aabbcc',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'P1 meter', 'title': 'P1 meter',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,

View file

@ -190,6 +190,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Husqvarna Automower of Erika Mustermann', 'title': 'Husqvarna Automower of Erika Mustermann',
'unique_id': '123', 'unique_id': '123',
'version': 1, 'version': 1,

View file

@ -15,6 +15,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'River Name (Station Name)', 'title': 'River Name (Station Name)',
'unique_id': '123', 'unique_id': '123',
'version': 1, 'version': 1,

View file

@ -358,6 +358,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -57,6 +57,7 @@ async def test_entry_diagnostics(
"created_at": ANY, "created_at": ANY,
"modified_at": ANY, "modified_at": ANY,
"discovery_keys": {}, "discovery_keys": {},
"subentries": [],
}, },
"client": { "client": {
"version": "api_version='0.2.0' hostname='scb' name='PUCK RESTful API' sw_version='01.16.05025'", "version": "api_version='0.2.0' hostname='scb' name='PUCK RESTful API' sw_version='01.16.05025'",

View file

@ -25,6 +25,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -73,6 +73,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'test-site-name', 'title': 'test-site-name',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -17,6 +17,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'envy', 'title': 'envy',
'unique_id': '00:11:22:33:44:55', 'unique_id': '00:11:22:33:44:55',
'version': 1, 'version': 1,

View file

@ -17,6 +17,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'melcloud', 'title': 'melcloud',
'unique_id': 'UNIQUE_TEST_ID', 'unique_id': 'UNIQUE_TEST_ID',
'version': 1, 'version': 1,

View file

@ -16,6 +16,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -28,6 +28,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -646,6 +646,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': 'netatmo', 'unique_id': 'netatmo',
'version': 1, 'version': 1,

View file

@ -17,6 +17,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Fake Profile', 'title': 'Fake Profile',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -60,6 +60,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -37,6 +37,7 @@ async def test_entry_diagnostics(
"created_at": ANY, "created_at": ANY,
"modified_at": ANY, "modified_at": ANY,
"discovery_keys": {}, "discovery_keys": {},
"subentries": [],
}, },
"data": { "data": {
"bridges": [ "bridges": [

View file

@ -24,6 +24,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': 'aa:bb:cc:dd:ee:ff', 'unique_id': 'aa:bb:cc:dd:ee:ff',
'version': 1, 'version': 1,

View file

@ -39,6 +39,7 @@ async def test_entry_diagnostics(
"created_at": ANY, "created_at": ANY,
"modified_at": ANY, "modified_at": ANY,
"discovery_keys": {}, "discovery_keys": {},
"subentries": [],
}, },
"data": { "data": {
"protection_window": { "protection_window": {

View file

@ -16,6 +16,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': 'unique_thingy', 'unique_id': 'unique_thingy',
'version': 2, 'version': 2,
@ -38,6 +40,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': 'unique_thingy', 'unique_id': 'unique_thingy',
'version': 2, 'version': 2,

View file

@ -31,6 +31,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': '70272185-xxxx-xxxx-xxxx-43bea330dcae', 'unique_id': '70272185-xxxx-xxxx-xxxx-43bea330dcae',
'version': 1, 'version': 1,

View file

@ -94,6 +94,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -155,6 +155,7 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry)
"version": 1, "version": 1,
"options": {}, "options": {},
"minor_version": 1, "minor_version": 1,
"subentries": (),
} }
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -33,6 +33,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -102,6 +102,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'home', 'title': 'home',
'unique_id': 'proximity_home', 'unique_id': 'proximity_home',
'version': 1, 'version': 1,

View file

@ -52,6 +52,7 @@ MOCK_FLOW_RESULT = {
"title": "test_ps4", "title": "test_ps4",
"data": MOCK_DATA, "data": MOCK_DATA,
"options": {}, "options": {},
"subentries": (),
} }
MOCK_ENTRY_ID = "SomeID" MOCK_ENTRY_ID = "SomeID"

View file

@ -38,6 +38,7 @@ async def test_entry_diagnostics(
"created_at": ANY, "created_at": ANY,
"modified_at": ANY, "modified_at": ANY,
"discovery_keys": {}, "discovery_keys": {},
"subentries": [],
}, },
"data": { "data": {
"fields": [ "fields": [

View file

@ -17,6 +17,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,
@ -84,6 +86,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -1144,6 +1144,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 2, 'version': 2,
@ -2275,6 +2277,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 2, 'version': 2,

View file

@ -34,6 +34,7 @@ async def test_entry_diagnostics(
"created_at": ANY, "created_at": ANY,
"modified_at": ANY, "modified_at": ANY,
"discovery_keys": {}, "discovery_keys": {},
"subentries": [],
}, },
"data": [ "data": [
{ {

View file

@ -44,6 +44,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 2, 'version': 2,

View file

@ -51,6 +51,7 @@ async def test_entry_diagnostics(
"pref_disable_new_entities": False, "pref_disable_new_entities": False,
"pref_disable_polling": False, "pref_disable_polling": False,
"source": "user", "source": "user",
"subentries": [],
"title": "Mock Title", "title": "Mock Title",
"unique_id": "any", "unique_id": "any",
"version": 2, "version": 2,
@ -91,6 +92,7 @@ async def test_entry_diagnostics_encrypted(
"pref_disable_new_entities": False, "pref_disable_new_entities": False,
"pref_disable_polling": False, "pref_disable_polling": False,
"source": "user", "source": "user",
"subentries": [],
"title": "Mock Title", "title": "Mock Title",
"unique_id": "any", "unique_id": "any",
"version": 2, "version": 2,
@ -130,6 +132,7 @@ async def test_entry_diagnostics_encrypte_offline(
"pref_disable_new_entities": False, "pref_disable_new_entities": False,
"pref_disable_polling": False, "pref_disable_polling": False,
"source": "user", "source": "user",
"subentries": [],
"title": "Mock Title", "title": "Mock Title",
"unique_id": "any", "unique_id": "any",
"version": 2, "version": 2,

View file

@ -18,6 +18,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Pentair: DD-EE-FF', 'title': 'Pentair: DD-EE-FF',
'unique_id': 'aa:bb:cc:dd:ee:ff', 'unique_id': 'aa:bb:cc:dd:ee:ff',
'version': 1, 'version': 1,

View file

@ -32,6 +32,7 @@ async def test_entry_diagnostics(
"created_at": ANY, "created_at": ANY,
"modified_at": ANY, "modified_at": ANY,
"discovery_keys": {}, "discovery_keys": {},
"subentries": [],
}, },
"subscription_data": { "subscription_data": {
"12345": { "12345": {

View file

@ -19,6 +19,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'solarlog', 'title': 'solarlog',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -136,6 +136,7 @@ async def test_user_form_pin_not_required(
"data": deepcopy(TEST_CONFIG), "data": deepcopy(TEST_CONFIG),
"options": {}, "options": {},
"minor_version": 1, "minor_version": 1,
"subentries": (),
} }
expected["data"][CONF_PIN] = None expected["data"][CONF_PIN] = None
@ -345,6 +346,7 @@ async def test_pin_form_success(hass: HomeAssistant, pin_form) -> None:
"data": TEST_CONFIG, "data": TEST_CONFIG,
"options": {}, "options": {},
"minor_version": 1, "minor_version": 1,
"subentries": (),
} }
result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID
assert result == expected assert result == expected

View file

@ -69,5 +69,6 @@ async def test_diagnostics(
"created_at": ANY, "created_at": ANY,
"modified_at": ANY, "modified_at": ANY,
"discovery_keys": {}, "discovery_keys": {},
"subentries": [],
}, },
} }

View file

@ -56,6 +56,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'System Monitor', 'title': 'System Monitor',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,
@ -111,6 +113,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'System Monitor', 'title': 'System Monitor',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -32,10 +32,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Tailwind iQ3', 'title': 'Tailwind iQ3',
'unique_id': '3c:e9:0e:6d:21:84', 'unique_id': '3c:e9:0e:6d:21:84',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Tailwind iQ3', 'title': 'Tailwind iQ3',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -78,10 +82,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'zeroconf', 'source': 'zeroconf',
'subentries': list([
]),
'title': 'Tailwind iQ3', 'title': 'Tailwind iQ3',
'unique_id': '3c:e9:0e:6d:21:84', 'unique_id': '3c:e9:0e:6d:21:84',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Tailwind iQ3', 'title': 'Tailwind iQ3',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,

View file

@ -37,6 +37,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -17,6 +17,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': 'very_unique_string', 'unique_id': 'very_unique_string',
'version': 1, 'version': 1,

View file

@ -24,6 +24,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '12345', 'title': '12345',
'unique_id': '12345', 'unique_id': '12345',
'version': 1, 'version': 1,
@ -54,6 +56,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Old Tuya configuration entry', 'title': 'Old Tuya configuration entry',
'unique_id': '12345', 'unique_id': '12345',
'version': 1, 'version': 1,
@ -107,10 +111,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'mocked_username', 'title': 'mocked_username',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'mocked_username', 'title': 'mocked_username',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,

View file

@ -36,10 +36,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '12345', 'title': '12345',
'unique_id': '12345', 'unique_id': '12345',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': '12345', 'title': '12345',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -82,10 +86,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '12345', 'title': '12345',
'unique_id': '12345', 'unique_id': '12345',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': '12345', 'title': '12345',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,

View file

@ -37,6 +37,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Twinkly', 'title': 'Twinkly',
'unique_id': '4c8fccf5-e08a-4173-92d5-49bf479252a2', 'unique_id': '4c8fccf5-e08a-4173-92d5-49bf479252a2',
'version': 1, 'version': 1,

View file

@ -42,6 +42,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': '1', 'unique_id': '1',
'version': 1, 'version': 1,

View file

@ -27,10 +27,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Uptime', 'title': 'Uptime',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Uptime', 'title': 'Uptime',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,

View file

@ -25,6 +25,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Energy Bill', 'title': 'Energy Bill',
'unique_id': None, 'unique_id': None,
'version': 2, 'version': 2,

View file

@ -16,6 +16,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': 'ABC123', 'unique_id': 'ABC123',
'version': 1, 'version': 1,

View file

@ -4731,6 +4731,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': 'ViCare', 'unique_id': 'ViCare',
'version': 1, 'version': 1,

View file

@ -35,6 +35,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -27,6 +27,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': '**REDACTED**', 'unique_id': '**REDACTED**',
'version': 1, 'version': 1,

View file

@ -253,6 +253,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': '**REDACTED**', 'title': '**REDACTED**',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -61,5 +61,6 @@ async def test_diagnostics(
"created_at": entry.created_at.isoformat(), "created_at": entry.created_at.isoformat(),
"modified_at": entry.modified_at.isoformat(), "modified_at": entry.modified_at.isoformat(),
"discovery_keys": {}, "discovery_keys": {},
"subentries": [],
}, },
} }

View file

@ -38,6 +38,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -30,10 +30,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Example.com', 'title': 'Example.com',
'unique_id': 'example.com', 'unique_id': 'example.com',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Example.com', 'title': 'Example.com',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -70,10 +74,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Example.com', 'title': 'Example.com',
'unique_id': 'example.com', 'unique_id': 'example.com',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Example.com', 'title': 'Example.com',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -110,10 +118,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Example.com', 'title': 'Example.com',
'unique_id': 'example.com', 'unique_id': 'example.com',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Example.com', 'title': 'Example.com',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -150,10 +162,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Example.com', 'title': 'Example.com',
'unique_id': 'example.com', 'unique_id': 'example.com',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Example.com', 'title': 'Example.com',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -190,10 +206,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Example.com', 'title': 'Example.com',
'unique_id': 'example.com', 'unique_id': 'example.com',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Example.com', 'title': 'Example.com',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,

View file

@ -40,6 +40,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -36,10 +36,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'hassio', 'source': 'hassio',
'subentries': list([
]),
'title': 'Piper', 'title': 'Piper',
'unique_id': '1234', 'unique_id': '1234',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Piper', 'title': 'Piper',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -82,10 +86,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'hassio', 'source': 'hassio',
'subentries': list([
]),
'title': 'Piper', 'title': 'Piper',
'unique_id': '1234', 'unique_id': '1234',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Piper', 'title': 'Piper',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,
@ -127,10 +135,14 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'zeroconf', 'source': 'zeroconf',
'subentries': list([
]),
'title': 'Test Satellite', 'title': 'Test Satellite',
'unique_id': 'test_zeroconf_name._wyoming._tcp.local._Test Satellite', 'unique_id': 'test_zeroconf_name._wyoming._tcp.local._Test Satellite',
'version': 1, 'version': 1,
}), }),
'subentries': tuple(
),
'title': 'Test Satellite', 'title': 'Test Satellite',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>, 'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1, 'version': 1,

View file

@ -113,6 +113,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 4, 'version': 4,

View file

@ -16,6 +16,8 @@
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,
'pref_disable_polling': False, 'pref_disable_polling': False,
'source': 'user', 'source': 'user',
'subentries': list([
]),
'title': 'Mock Title', 'title': 'Mock Title',
'unique_id': None, 'unique_id': None,
'version': 1, 'version': 1,

View file

@ -905,7 +905,7 @@ async def test_entries_excludes_ignore_and_disabled(
async def test_saving_and_loading( async def test_saving_and_loading(
hass: HomeAssistant, freezer: FrozenDateTimeFactory hass: HomeAssistant, freezer: FrozenDateTimeFactory, hass_storage: dict[str, Any]
) -> None: ) -> None:
"""Test that we're saving and loading correctly.""" """Test that we're saving and loading correctly."""
mock_integration( mock_integration(
@ -922,7 +922,17 @@ async def test_saving_and_loading(
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Test user step.""" """Test user step."""
await self.async_set_unique_id("unique") await self.async_set_unique_id("unique")
return self.async_create_entry(title="Test Title", data={"token": "abcd"}) subentries = [
config_entries.ConfigSubentryData(
data={"foo": "bar"}, title="subentry 1"
),
config_entries.ConfigSubentryData(
data={"sun": "moon"}, title="subentry 2", unique_id="very_unique"
),
]
return self.async_create_entry(
title="Test Title", data={"token": "abcd"}, subentries=subentries
)
with mock_config_flow("test", TestFlow): with mock_config_flow("test", TestFlow):
await hass.config_entries.flow.async_init( await hass.config_entries.flow.async_init(
@ -971,6 +981,98 @@ async def test_saving_and_loading(
# To execute the save # To execute the save
await hass.async_block_till_done() await hass.async_block_till_done()
stored_data = hass_storage["core.config_entries"]
assert stored_data == {
"data": {
"entries": [
{
"created_at": ANY,
"data": {
"token": "abcd",
},
"disabled_by": None,
"discovery_keys": {},
"domain": "test",
"entry_id": ANY,
"minor_version": 1,
"modified_at": ANY,
"options": {},
"pref_disable_new_entities": True,
"pref_disable_polling": True,
"source": "user",
"subentries": [
{
"data": {"foo": "bar"},
"subentry_id": ANY,
"title": "subentry 1",
"unique_id": None,
},
{
"data": {"sun": "moon"},
"subentry_id": ANY,
"title": "subentry 2",
"unique_id": "very_unique",
},
],
"title": "Test Title",
"unique_id": "unique",
"version": 5,
},
{
"created_at": ANY,
"data": {
"username": "bla",
},
"disabled_by": None,
"discovery_keys": {
"test": [
{"domain": "test", "key": "blah", "version": 1},
],
},
"domain": "test",
"entry_id": ANY,
"minor_version": 1,
"modified_at": ANY,
"options": {},
"pref_disable_new_entities": False,
"pref_disable_polling": False,
"source": "user",
"subentries": [],
"title": "Test 2 Title",
"unique_id": None,
"version": 3,
},
{
"created_at": ANY,
"data": {
"username": "bla",
},
"disabled_by": None,
"discovery_keys": {
"test": [
{"domain": "test", "key": ["a", "b"], "version": 1},
],
},
"domain": "test",
"entry_id": ANY,
"minor_version": 1,
"modified_at": ANY,
"options": {},
"pref_disable_new_entities": False,
"pref_disable_polling": False,
"source": "user",
"subentries": [],
"title": "Test 2 Title",
"unique_id": None,
"version": 3,
},
],
},
"key": "core.config_entries",
"minor_version": 5,
"version": 1,
}
# Now load written data in new config manager # Now load written data in new config manager
manager = config_entries.ConfigEntries(hass, {}) manager = config_entries.ConfigEntries(hass, {})
await manager.async_initialize() await manager.async_initialize()
@ -983,6 +1085,25 @@ async def test_saving_and_loading(
): ):
assert orig.as_dict() == loaded.as_dict() assert orig.as_dict() == loaded.as_dict()
hass.config_entries.async_update_entry(
entry_1,
pref_disable_polling=False,
pref_disable_new_entities=False,
)
# To trigger the call_later
freezer.tick(1.0)
async_fire_time_changed(hass)
# To execute the save
await hass.async_block_till_done()
# Assert no data is lost when storing again
expected_stored_data = stored_data
expected_stored_data["data"]["entries"][0]["modified_at"] = ANY
expected_stored_data["data"]["entries"][0]["pref_disable_new_entities"] = False
expected_stored_data["data"]["entries"][0]["pref_disable_polling"] = False
assert hass_storage["core.config_entries"] == expected_stored_data | {}
@freeze_time("2024-02-14 12:00:00") @freeze_time("2024-02-14 12:00:00")
async def test_as_dict(snapshot: SnapshotAssertion) -> None: async def test_as_dict(snapshot: SnapshotAssertion) -> None:
@ -1416,6 +1537,42 @@ async def test_update_entry_options_and_trigger_listener(
assert len(update_listener_calls) == 1 assert len(update_listener_calls) == 1
async def test_update_subentry_and_trigger_listener(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test that we can update subentry and trigger listener."""
entry = MockConfigEntry(domain="test", options={"first": True})
entry.add_to_manager(manager)
update_listener_calls = []
subentry = config_entries.ConfigSubentry(
data={"test": "test"}, unique_id="test", title="Mock title"
)
async def update_listener(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> None:
"""Test function."""
assert entry.subentries == expected_subentries
update_listener_calls.append(None)
entry.add_update_listener(update_listener)
expected_subentries = {subentry.subentry_id: subentry}
assert manager.async_add_subentry(entry, subentry) is True
await hass.async_block_till_done(wait_background_tasks=True)
assert entry.subentries == expected_subentries
assert len(update_listener_calls) == 1
expected_subentries = {}
assert manager.async_remove_subentry(entry, subentry.subentry_id) is True
await hass.async_block_till_done(wait_background_tasks=True)
assert entry.subentries == expected_subentries
assert len(update_listener_calls) == 2
async def test_setup_raise_not_ready( async def test_setup_raise_not_ready(
hass: HomeAssistant, hass: HomeAssistant,
manager: config_entries.ConfigEntries, manager: config_entries.ConfigEntries,
@ -1742,20 +1899,335 @@ async def test_entry_options_unknown_config_entry(
mock_integration(hass, MockModule("test")) mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None) mock_platform(hass, "test.config_flow", None)
class TestFlow: with pytest.raises(config_entries.UnknownEntry):
await manager.options.async_create_flow(
"blah", context={"source": "test"}, data=None
)
async def test_create_entry_subentries(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test a config entry being created with subentries."""
subentrydata = config_entries.ConfigSubentryData(
data={"test": "test"},
title="Mock title",
unique_id="test",
)
async def mock_async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Mock setup."""
hass.async_create_task(
hass.config_entries.flow.async_init(
"comp",
context={"source": config_entries.SOURCE_IMPORT},
data={"data": "data", "subentry": subentrydata},
)
)
return True
async_setup_entry = AsyncMock(return_value=True)
mock_integration(
hass,
MockModule(
"comp", async_setup=mock_async_setup, async_setup_entry=async_setup_entry
),
)
mock_platform(hass, "comp.config_flow", None)
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
VERSION = 1
async def async_step_import(self, user_input):
"""Test import step creating entry, with subentry."""
return self.async_create_entry(
title="title",
data={"example": user_input["data"]},
subentries=[user_input["subentry"]],
)
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
assert await async_setup_component(hass, "comp", {})
await hass.async_block_till_done()
assert len(async_setup_entry.mock_calls) == 1
entries = hass.config_entries.async_entries("comp")
assert len(entries) == 1
assert entries[0].supports_subentries is False
assert entries[0].data == {"example": "data"}
assert len(entries[0].subentries) == 1
subentry_id = list(entries[0].subentries)[0]
subentry = config_entries.ConfigSubentry(
data=subentrydata["data"],
subentry_id=subentry_id,
title=subentrydata["title"],
unique_id="test",
)
assert entries[0].subentries == {subentry_id: subentry}
async def test_entry_subentry(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test that we can add a subentry to an entry."""
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
entry = MockConfigEntry(domain="test", data={"first": True})
entry.add_to_manager(manager)
class TestFlow(config_entries.ConfigFlow):
"""Test flow.""" """Test flow."""
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_subentry_flow(config_entry):
"""Test options flow.""" """Test subentry flow."""
class SubentryFlowHandler(data_entry_flow.FlowHandler):
"""Test subentry flow handler."""
return SubentryFlowHandler()
with mock_config_flow("test", TestFlow):
flow = await manager.subentries.async_create_flow(
entry.entry_id, context={"source": "test"}, data=None
)
flow.handler = entry.entry_id # Used to keep reference to config entry
await manager.subentries.async_finish_flow(
flow,
{
"data": {"second": True},
"title": "Mock title",
"type": data_entry_flow.FlowResultType.CREATE_ENTRY,
"unique_id": "test",
},
)
assert entry.data == {"first": True}
assert entry.options == {}
subentry_id = list(entry.subentries)[0]
assert entry.subentries == {
subentry_id: config_entries.ConfigSubentry(
data={"second": True},
subentry_id=subentry_id,
title="Mock title",
unique_id="test",
)
}
assert entry.supports_subentries is True
async def test_entry_subentry_non_string(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test adding an invalid subentry to an entry."""
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
entry = MockConfigEntry(domain="test", data={"first": True})
entry.add_to_manager(manager)
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
@staticmethod
@callback
def async_get_subentry_flow(config_entry):
"""Test subentry flow."""
class SubentryFlowHandler(data_entry_flow.FlowHandler):
"""Test subentry flow handler."""
return SubentryFlowHandler()
with mock_config_flow("test", TestFlow):
flow = await manager.subentries.async_create_flow(
entry.entry_id, context={"source": "test"}, data=None
)
flow.handler = entry.entry_id # Used to keep reference to config entry
with pytest.raises(HomeAssistantError):
await manager.subentries.async_finish_flow(
flow,
{
"data": {"second": True},
"title": "Mock title",
"type": data_entry_flow.FlowResultType.CREATE_ENTRY,
"unique_id": 123,
},
)
async def test_entry_subentry_duplicate(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test adding a duplicated subentry to an entry."""
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
entry = MockConfigEntry(
domain="test",
data={"first": True},
subentries_data=[
config_entries.ConfigSubentryData(
data={},
subentry_id="blabla",
title="Mock title",
unique_id="test",
)
],
)
entry.add_to_manager(manager)
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
@staticmethod
@callback
def async_get_subentry_flow(config_entry):
"""Test subentry flow."""
class SubentryFlowHandler(data_entry_flow.FlowHandler):
"""Test subentry flow handler."""
return SubentryFlowHandler()
with mock_config_flow("test", TestFlow):
flow = await manager.subentries.async_create_flow(
entry.entry_id, context={"source": "test"}, data=None
)
flow.handler = entry.entry_id # Used to keep reference to config entry
with pytest.raises(HomeAssistantError):
await manager.subentries.async_finish_flow(
flow,
{
"data": {"second": True},
"title": "Mock title",
"type": data_entry_flow.FlowResultType.CREATE_ENTRY,
"unique_id": "test",
},
)
async def test_entry_subentry_abort(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test that we can abort subentry flow."""
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
entry = MockConfigEntry(domain="test", data={"first": True})
entry.add_to_manager(manager)
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
@staticmethod
@callback
def async_get_subentry_flow(config_entry):
"""Test subentry flow."""
class SubentryFlowHandler(data_entry_flow.FlowHandler):
"""Test subentry flow handler."""
return SubentryFlowHandler()
with mock_config_flow("test", TestFlow):
flow = await manager.subentries.async_create_flow(
entry.entry_id, context={"source": "test"}, data=None
)
flow.handler = entry.entry_id # Used to keep reference to config entry
assert await manager.subentries.async_finish_flow(
flow, {"type": data_entry_flow.FlowResultType.ABORT, "reason": "test"}
)
async def test_entry_subentry_unknown_config_entry(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test attempting to start a subentry flow for an unknown config entry."""
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
with pytest.raises(config_entries.UnknownEntry): with pytest.raises(config_entries.UnknownEntry):
await manager.options.async_create_flow( await manager.subentries.async_create_flow(
"blah", context={"source": "test"}, data=None "blah", context={"source": "test"}, data=None
) )
async def test_entry_subentry_deleted_config_entry(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test attempting to finish a subentry flow for a deleted config entry."""
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
entry = MockConfigEntry(domain="test", data={"first": True})
entry.add_to_manager(manager)
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
@staticmethod
@callback
def async_get_subentry_flow(config_entry):
"""Test subentry flow."""
class SubentryFlowHandler(data_entry_flow.FlowHandler):
"""Test subentry flow handler."""
return SubentryFlowHandler()
with mock_config_flow("test", TestFlow):
flow = await manager.subentries.async_create_flow(
entry.entry_id, context={"source": "test"}, data=None
)
flow.handler = entry.entry_id # Used to keep reference to config entry
await hass.config_entries.async_remove(entry.entry_id)
with pytest.raises(config_entries.UnknownEntry):
await manager.subentries.async_finish_flow(
flow,
{
"data": {"second": True},
"title": "Mock title",
"type": data_entry_flow.FlowResultType.CREATE_ENTRY,
"unique_id": "test",
},
)
async def test_entry_subentry_unsupported(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test attempting to start a subentry flow for a config entry without support."""
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
entry = MockConfigEntry(domain="test", data={"first": True})
entry.add_to_manager(manager)
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
with (
mock_config_flow("test", TestFlow),
pytest.raises(data_entry_flow.UnknownHandler),
):
await manager.subentries.async_create_flow(
entry.entry_id, context={"source": "test"}, data=None
)
async def test_entry_setup_succeed( async def test_entry_setup_succeed(
hass: HomeAssistant, manager: config_entries.ConfigEntries hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None: ) -> None:
@ -3911,21 +4383,20 @@ async def test_updating_entry_with_and_without_changes(
assert manager.async_update_entry(entry) is False assert manager.async_update_entry(entry) is False
for change in ( for change, expected_value in (
{"data": {"second": True, "third": 456}}, ({"data": {"second": True, "third": 456}}, {"second": True, "third": 456}),
{"data": {"second": True}}, ({"data": {"second": True}}, {"second": True}),
{"minor_version": 2}, ({"minor_version": 2}, 2),
{"options": {"hello": True}}, ({"options": {"hello": True}}, {"hello": True}),
{"pref_disable_new_entities": True}, ({"pref_disable_new_entities": True}, True),
{"pref_disable_polling": True}, ({"pref_disable_polling": True}, True),
{"title": "sometitle"}, ({"title": "sometitle"}, "sometitle"),
{"unique_id": "abcd1234"}, ({"unique_id": "abcd1234"}, "abcd1234"),
{"version": 2}, ({"version": 2}, 2),
): ):
assert manager.async_update_entry(entry, **change) is True assert manager.async_update_entry(entry, **change) is True
key = next(iter(change)) key = next(iter(change))
value = next(iter(change.values())) assert getattr(entry, key) == expected_value
assert getattr(entry, key) == value
assert manager.async_update_entry(entry, **change) is False assert manager.async_update_entry(entry, **change) is False
assert manager.async_entry_for_domain_unique_id("test", "abc123") is None assert manager.async_entry_for_domain_unique_id("test", "abc123") is None
@ -5439,6 +5910,7 @@ async def test_unhashable_unique_id_fails(
minor_version=1, minor_version=1,
options={}, options={},
source="test", source="test",
subentries_data=(),
title="title", title="title",
unique_id=unique_id, unique_id=unique_id,
version=1, version=1,
@ -5474,6 +5946,7 @@ async def test_unhashable_unique_id_fails_on_update(
minor_version=1, minor_version=1,
options={}, options={},
source="test", source="test",
subentries_data=(),
title="title", title="title",
unique_id="123", unique_id="123",
version=1, version=1,
@ -5504,6 +5977,7 @@ async def test_string_unique_id_no_warning(
minor_version=1, minor_version=1,
options={}, options={},
source="test", source="test",
subentries_data=(),
title="title", title="title",
unique_id="123", unique_id="123",
version=1, version=1,
@ -5546,6 +6020,7 @@ async def test_hashable_unique_id(
minor_version=1, minor_version=1,
options={}, options={},
source="test", source="test",
subentries_data=(),
title="title", title="title",
unique_id=unique_id, unique_id=unique_id,
version=1, version=1,
@ -5580,6 +6055,7 @@ async def test_no_unique_id_no_warning(
minor_version=1, minor_version=1,
options={}, options={},
source="test", source="test",
subentries_data=(),
title="title", title="title",
unique_id=None, unique_id=None,
version=1, version=1,
@ -6504,6 +6980,7 @@ async def test_migration_from_1_2(
"pref_disable_new_entities": False, "pref_disable_new_entities": False,
"pref_disable_polling": False, "pref_disable_polling": False,
"source": "import", "source": "import",
"subentries": {},
"title": "Sun", "title": "Sun",
"unique_id": None, "unique_id": None,
"version": 1, "version": 1,