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(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_entry_disable)
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, ignore_config_flow)
websocket_api.async_register_command(hass, config_subentry_delete)
websocket_api.async_register_command(hass, config_subentry_list)
return True
@ -285,6 +295,48 @@ class OptionManagerFlowResourceView(
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.websocket_command({"type": "config_entries/flow/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)
]
@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 copy import deepcopy
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum, StrEnum
import functools
@ -22,7 +23,7 @@ from functools import cache
import logging
from random import randint
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 propcache import cached_property
@ -123,7 +124,7 @@ HANDLERS: Registry[str, type[ConfigFlow]] = Registry()
STORAGE_KEY = "core.config_entries"
STORAGE_VERSION = 1
STORAGE_VERSION_MINOR = 4
STORAGE_VERSION_MINOR = 5
SAVE_DELAY = 1
@ -251,6 +252,10 @@ class UnknownEntry(ConfigError):
"""Unknown entry specified."""
class UnknownSubEntry(ConfigError):
"""Unknown subentry specified."""
class OperationNotAllowed(ConfigError):
"""Raised when a config entry operation is not allowed."""
@ -295,6 +300,7 @@ class ConfigFlowResult(FlowResult[ConfigFlowContext, str], total=False):
minor_version: int
options: Mapping[str, Any]
subentries: Iterable[ConfigSubentryData]
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]):
"""Hold a configuration entry."""
@ -317,6 +368,7 @@ class ConfigEntry(Generic[_DataT]):
data: MappingProxyType[str, Any]
runtime_data: _DataT
options: MappingProxyType[str, Any]
subentries: MappingProxyType[str, ConfigSubentry]
unique_id: str | None
state: ConfigEntryState
reason: str | None
@ -332,6 +384,7 @@ class ConfigEntry(Generic[_DataT]):
supports_remove_device: bool | None
_supports_options: bool | None
_supports_reconfigure: bool | None
_supports_subentries: bool | None
update_listeners: list[UpdateListenerType]
_async_cancel_retry_setup: Callable[[], Any] | 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,
source: str,
state: ConfigEntryState = ConfigEntryState.NOT_LOADED,
subentries_data: Iterable[ConfigSubentryData | ConfigSubentryDataWithId] | None,
title: str,
unique_id: str | None,
version: int,
@ -386,6 +440,24 @@ class ConfigEntry(Generic[_DataT]):
# Entry options
_setter(self, "options", MappingProxyType(options or {}))
# Subentries
subentries_data = subentries_data or ()
subentries = {}
for subentry_data in subentries_data:
subentry_kwargs = {}
if "subentry_id" in subentry_data:
# If subentry_data has key "subentry_id", we're loading from storage
subentry_kwargs["subentry_id"] = subentry_data["subentry_id"] # type: ignore[typeddict-item]
subentry = ConfigSubentry(
data=MappingProxyType(subentry_data["data"]),
title=subentry_data["title"],
unique_id=subentry_data.get("unique_id"),
**subentry_kwargs,
)
subentries[subentry.subentry_id] = subentry
_setter(self, "subentries", MappingProxyType(subentries))
# Entry system options
if pref_disable_new_entities is None:
pref_disable_new_entities = False
@ -422,6 +494,9 @@ class ConfigEntry(Generic[_DataT]):
# Supports reconfigure
_setter(self, "_supports_reconfigure", None)
# Supports subentries
_setter(self, "_supports_subentries", None)
# Listeners to call on update
_setter(self, "update_listeners", [])
@ -494,6 +569,16 @@ class ConfigEntry(Generic[_DataT]):
)
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:
"""Clear cached properties that are included in as_json_fragment."""
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_unload": self.supports_unload or False,
"supports_reconfigure": self.supports_reconfigure,
"supports_subentries": self.supports_subentries,
"pref_disable_new_entities": self.pref_disable_new_entities,
"pref_disable_polling": self.pref_disable_polling,
"disabled_by": self.disabled_by,
@ -1013,6 +1099,7 @@ class ConfigEntry(Generic[_DataT]):
"pref_disable_new_entities": self.pref_disable_new_entities,
"pref_disable_polling": self.pref_disable_polling,
"source": self.source,
"subentries": [subentry.as_dict() for subentry in self.subentries.values()],
"title": self.title,
"unique_id": self.unique_id,
"version": self.version,
@ -1502,6 +1589,7 @@ class ConfigEntriesFlowManager(
minor_version=result["minor_version"],
options=result["options"],
source=flow.context["source"],
subentries_data=result["subentries"],
title=result["title"],
unique_id=flow.unique_id,
version=result["version"],
@ -1786,6 +1874,11 @@ class ConfigEntryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
for entry in data["entries"]:
entry["discovery_keys"] = {}
if old_minor_version < 5:
# Version 1.4 adds config subentries
for entry in data["entries"]:
entry.setdefault("subentries", entry.get("subentries", {}))
if old_major_version > 1:
raise NotImplementedError
return data
@ -1802,6 +1895,7 @@ class ConfigEntries:
self.hass = hass
self.flow = ConfigEntriesFlowManager(hass, self, hass_config)
self.options = OptionsFlowManager(hass)
self.subentries = ConfigSubentryFlowManager(hass)
self._hass_config = hass_config
self._entries = ConfigEntryItems(hass)
self._store = ConfigEntryStore(hass)
@ -1981,6 +2075,7 @@ class ConfigEntries:
pref_disable_new_entities=entry["pref_disable_new_entities"],
pref_disable_polling=entry["pref_disable_polling"],
source=entry["source"],
subentries_data=entry["subentries"],
title=entry["title"],
unique_id=entry["unique_id"],
version=entry["version"],
@ -2145,6 +2240,44 @@ class ConfigEntries:
If the entry was changed, the update_listeners are
fired and this function returns True
If the entry was not changed, the update_listeners are
not fired and this function returns False
"""
return self._async_update_entry(
entry,
data=data,
discovery_keys=discovery_keys,
minor_version=minor_version,
options=options,
pref_disable_new_entities=pref_disable_new_entities,
pref_disable_polling=pref_disable_polling,
title=title,
unique_id=unique_id,
version=version,
)
@callback
def _async_update_entry(
self,
entry: ConfigEntry,
*,
data: Mapping[str, Any] | UndefinedType = UNDEFINED,
discovery_keys: MappingProxyType[str, tuple[DiscoveryKey, ...]]
| UndefinedType = UNDEFINED,
minor_version: int | UndefinedType = UNDEFINED,
options: Mapping[str, Any] | UndefinedType = UNDEFINED,
pref_disable_new_entities: bool | UndefinedType = UNDEFINED,
pref_disable_polling: bool | UndefinedType = UNDEFINED,
subentries: dict[str, ConfigSubentry] | UndefinedType = UNDEFINED,
title: str | UndefinedType = UNDEFINED,
unique_id: str | None | UndefinedType = UNDEFINED,
version: int | UndefinedType = UNDEFINED,
) -> bool:
"""Update a config entry.
If the entry was changed, the update_listeners are
fired and this function returns True
If the entry was not changed, the update_listeners are
not fired and this function returns False
"""
@ -2202,6 +2335,11 @@ class ConfigEntries:
changed = True
_setter(entry, "options", MappingProxyType(options))
if subentries is not UNDEFINED:
if entry.subentries != subentries:
changed = True
_setter(entry, "subentries", MappingProxyType(subentries))
if not changed:
return False
@ -2219,6 +2357,25 @@ class ConfigEntries:
self._async_dispatch(ConfigEntryChange.UPDATED, entry)
return True
@callback
def async_add_subentry(self, entry: ConfigEntry, subentry: ConfigSubentry) -> bool:
"""Add a subentry to a config entry."""
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
def _async_dispatch(
self, change_type: ConfigEntryChange, entry: ConfigEntry
@ -2539,6 +2696,18 @@ class ConfigFlow(ConfigEntryBaseFlow):
"""Return options flow support for this handler."""
return cls.async_get_options_flow is not ConfigFlow.async_get_options_flow
@staticmethod
@callback
def async_get_subentry_flow(config_entry: ConfigEntry) -> 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
def _async_abort_entries_match(
self, match_dict: dict[str, Any] | None = None
@ -2847,6 +3016,7 @@ class ConfigFlow(ConfigEntryBaseFlow):
description: str | None = None,
description_placeholders: Mapping[str, str] | None = None,
options: Mapping[str, Any] | None = None,
subentries: Iterable[ConfigSubentryData] | None = None,
) -> ConfigFlowResult:
"""Finish config flow and create a config entry."""
if self.source in {SOURCE_REAUTH, SOURCE_RECONFIGURE}:
@ -2872,6 +3042,7 @@ class ConfigFlow(ConfigEntryBaseFlow):
result["minor_version"] = self.MINOR_VERSION
result["options"] = options or {}
result["subentries"] = subentries or ()
result["version"] = self.VERSION
return result
@ -2990,12 +3161,10 @@ class ConfigFlow(ConfigEntryBaseFlow):
raise UnknownEntry
class OptionsFlowManager(
data_entry_flow.FlowManager[ConfigFlowContext, ConfigFlowResult]
):
"""Flow to set options for a configuration entry."""
class _ConfigSubFlowManager:
"""Mixin class for flow managers which manage flows tied to a config entry."""
_flow_result = ConfigFlowResult
hass: HomeAssistant
def _async_get_config_entry(self, config_entry_id: str) -> ConfigEntry:
"""Return config entry or raise if not found."""
@ -3005,6 +3174,106 @@ class OptionsFlowManager(
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(
self,
handler_key: str,
@ -3014,7 +3283,7 @@ class OptionsFlowManager(
) -> OptionsFlow:
"""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)
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
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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -145,6 +145,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": True,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": True,
"title": "Test 1",
},
@ -163,6 +164,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 2",
},
@ -181,6 +183,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 3",
},
@ -199,6 +202,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 4",
},
@ -217,6 +221,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 5",
},
@ -583,6 +588,7 @@ async def test_create_account(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test Entry",
},
@ -590,6 +596,7 @@ async def test_create_account(hass: HomeAssistant, client: TestClient) -> None:
"description_placeholders": None,
"options": {},
"minor_version": 1,
"subentries": [],
}
@ -666,6 +673,7 @@ async def test_two_step_flow(hass: HomeAssistant, client: TestClient) -> None:
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "user-title",
},
@ -673,6 +681,7 @@ async def test_two_step_flow(hass: HomeAssistant, client: TestClient) -> None:
"description_placeholders": None,
"options": {},
"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"}}
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")
async def test_get_single(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
@ -1132,6 +1388,7 @@ async def test_get_single(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Mock Title",
}
@ -1496,6 +1753,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 1",
},
@ -1515,6 +1773,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 2",
},
@ -1534,6 +1793,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 3",
},
@ -1553,6 +1813,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 4",
},
@ -1572,6 +1833,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 5",
},
@ -1602,6 +1864,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 1",
}
@ -1631,6 +1894,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 4",
},
@ -1650,6 +1914,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 5",
},
@ -1679,6 +1944,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 1",
},
@ -1698,6 +1964,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 3",
},
@ -1733,6 +2000,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 1",
},
@ -1752,6 +2020,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 2",
},
@ -1771,6 +2040,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 3",
},
@ -1790,6 +2060,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 4",
},
@ -1809,6 +2080,7 @@ async def test_get_matching_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 5",
},
@ -1916,6 +2188,7 @@ async def test_subscribe_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 1",
},
@ -1938,6 +2211,7 @@ async def test_subscribe_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 2",
},
@ -1960,6 +2234,7 @@ async def test_subscribe_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 3",
},
@ -1988,6 +2263,7 @@ async def test_subscribe_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "changed",
},
@ -2017,6 +2293,7 @@ async def test_subscribe_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "changed",
},
@ -2045,6 +2322,7 @@ async def test_subscribe_entries_ws(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "changed",
},
@ -2135,6 +2413,7 @@ async def test_subscribe_entries_ws_filtered(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 1",
},
@ -2157,6 +2436,7 @@ async def test_subscribe_entries_ws_filtered(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "Test 3",
},
@ -2187,6 +2467,7 @@ async def test_subscribe_entries_ws_filtered(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "changed",
},
@ -2213,6 +2494,7 @@ async def test_subscribe_entries_ws_filtered(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "changed too",
},
@ -2243,6 +2525,7 @@ async def test_subscribe_entries_ws_filtered(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "changed",
},
@ -2271,6 +2554,7 @@ async def test_subscribe_entries_ws_filtered(
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
"supports_subentries": False,
"supports_unload": False,
"title": "changed",
},
@ -2478,3 +2762,133 @@ async def test_does_not_support_reconfigure(
response
== '{"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_polling': False,
'source': 'user',
'subentries': list([
]),
'title': 'Mock Title',
'unique_id': '**REDACTED**',
'version': 1,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -57,6 +57,7 @@ async def test_entry_diagnostics(
"created_at": ANY,
"modified_at": ANY,
"discovery_keys": {},
"subentries": [],
},
"client": {
"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_polling': False,
'source': 'user',
'subentries': list([
]),
'title': 'Mock Title',
'unique_id': None,
'version': 1,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -905,7 +905,7 @@ async def test_entries_excludes_ignore_and_disabled(
async def test_saving_and_loading(
hass: HomeAssistant, freezer: FrozenDateTimeFactory
hass: HomeAssistant, freezer: FrozenDateTimeFactory, hass_storage: dict[str, Any]
) -> None:
"""Test that we're saving and loading correctly."""
mock_integration(
@ -922,7 +922,17 @@ async def test_saving_and_loading(
async def async_step_user(self, user_input=None):
"""Test user step."""
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):
await hass.config_entries.flow.async_init(
@ -971,6 +981,98 @@ async def test_saving_and_loading(
# To execute the save
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
manager = config_entries.ConfigEntries(hass, {})
await manager.async_initialize()
@ -983,6 +1085,25 @@ async def test_saving_and_loading(
):
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")
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
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(
hass: HomeAssistant,
manager: config_entries.ConfigEntries,
@ -1742,20 +1899,335 @@ async def test_entry_options_unknown_config_entry(
mock_integration(hass, MockModule("test"))
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."""
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Test options flow."""
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 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):
await manager.options.async_create_flow(
await manager.subentries.async_create_flow(
"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(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
@ -3911,21 +4383,20 @@ async def test_updating_entry_with_and_without_changes(
assert manager.async_update_entry(entry) is False
for change in (
{"data": {"second": True, "third": 456}},
{"data": {"second": True}},
{"minor_version": 2},
{"options": {"hello": True}},
{"pref_disable_new_entities": True},
{"pref_disable_polling": True},
{"title": "sometitle"},
{"unique_id": "abcd1234"},
{"version": 2},
for change, expected_value in (
({"data": {"second": True, "third": 456}}, {"second": True, "third": 456}),
({"data": {"second": True}}, {"second": True}),
({"minor_version": 2}, 2),
({"options": {"hello": True}}, {"hello": True}),
({"pref_disable_new_entities": True}, True),
({"pref_disable_polling": True}, True),
({"title": "sometitle"}, "sometitle"),
({"unique_id": "abcd1234"}, "abcd1234"),
({"version": 2}, 2),
):
assert manager.async_update_entry(entry, **change) is True
key = next(iter(change))
value = next(iter(change.values()))
assert getattr(entry, key) == value
assert getattr(entry, key) == expected_value
assert manager.async_update_entry(entry, **change) is False
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,
options={},
source="test",
subentries_data=(),
title="title",
unique_id=unique_id,
version=1,
@ -5474,6 +5946,7 @@ async def test_unhashable_unique_id_fails_on_update(
minor_version=1,
options={},
source="test",
subentries_data=(),
title="title",
unique_id="123",
version=1,
@ -5504,6 +5977,7 @@ async def test_string_unique_id_no_warning(
minor_version=1,
options={},
source="test",
subentries_data=(),
title="title",
unique_id="123",
version=1,
@ -5546,6 +6020,7 @@ async def test_hashable_unique_id(
minor_version=1,
options={},
source="test",
subentries_data=(),
title="title",
unique_id=unique_id,
version=1,
@ -5580,6 +6055,7 @@ async def test_no_unique_id_no_warning(
minor_version=1,
options={},
source="test",
subentries_data=(),
title="title",
unique_id=None,
version=1,
@ -6504,6 +6980,7 @@ async def test_migration_from_1_2(
"pref_disable_new_entities": False,
"pref_disable_polling": False,
"source": "import",
"subentries": {},
"title": "Sun",
"unique_id": None,
"version": 1,