Add reconfigure support to config subentries

This commit is contained in:
Erik 2024-12-16 12:25:49 +01:00
parent a12a42710f
commit fa6cc8edfe
5 changed files with 383 additions and 221 deletions

View File

@ -326,6 +326,9 @@ class SubentryManagerFlowIndexView(
"""Return context."""
context = super().get_context(data)
context["source"] = config_entries.SOURCE_USER
if subentry_id := data.get("subentry_id"):
context["source"] = config_entries.SOURCE_RECONFIGURE
context["subentry_id"] = subentry_id
return context

View File

@ -339,7 +339,14 @@ class ConfigSubentryDataWithId(ConfigSubentryData):
subentry_id: str
class SubentryFlowResult(FlowResult[FlowContext, tuple[str, str]], total=False):
class SubentryFlowContext(FlowContext, total=False):
"""Typed context dict for config flow."""
entry_id: str
subentry_id: str
class SubentryFlowResult(FlowResult[SubentryFlowContext, tuple[str, str]], total=False):
"""Typed result dict for subentry flow."""
unique_id: str | None
@ -389,7 +396,7 @@ class ConfigEntry(Generic[_DataT]):
supports_remove_device: bool | None
_supports_options: bool | None
_supports_reconfigure: bool | None
_supported_subentries: tuple[str, ...] | None
_supported_subentry_flows: dict[str, dict[str, bool]] | None
update_listeners: list[UpdateListenerType]
_async_cancel_retry_setup: Callable[[], Any] | None
_on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None
@ -500,7 +507,7 @@ class ConfigEntry(Generic[_DataT]):
_setter(self, "_supports_reconfigure", None)
# Supports subentries
_setter(self, "_supported_subentries", None)
_setter(self, "_supported_subentry_flows", None)
# Listeners to call on update
_setter(self, "update_listeners", [])
@ -575,16 +582,26 @@ class ConfigEntry(Generic[_DataT]):
return self._supports_reconfigure or False
@property
def supported_subentries(self) -> tuple[str, ...]:
def supported_subentry_flows(self) -> dict[str, dict[str, bool]]:
"""Return supported subentries."""
if self._supported_subentries is None and (
if self._supported_subentry_flows is None and (
handler := HANDLERS.get(self.domain)
):
# work out sub entries supported by the handler
supported_flows = handler.async_get_supported_subentry_flows(self)
object.__setattr__(
self, "_supported_subentries", handler.async_supported_subentries(self)
self,
"_supported_subentry_flows",
{
subentry_flow_type: {
"supports_reconfigure": hasattr(
subentry_flow_handler, "async_step_reconfigure"
)
return self._supported_subentries or ()
}
for subentry_flow_type, subentry_flow_handler in supported_flows.items()
},
)
return self._supported_subentry_flows or {}
def clear_state_cache(self) -> None:
"""Clear cached properties that are included in as_json_fragment."""
@ -605,7 +622,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,
"supported_subentries": self.supported_subentries,
"supported_subentry_flows": self.supported_subentry_flows,
"pref_disable_new_entities": self.pref_disable_new_entities,
"pref_disable_polling": self.pref_disable_polling,
"disabled_by": self.disabled_by,
@ -2378,6 +2395,11 @@ class ConfigEntries:
_setter(entry, "modified_at", utcnow())
self._async_save_and_notify(entry)
return True
@callback
def _async_save_and_notify(self, entry: ConfigEntry) -> None:
for listener in entry.update_listeners:
self.hass.async_create_task(
listener(self.hass, entry),
@ -2388,7 +2410,6 @@ class ConfigEntries:
entry.clear_state_cache()
entry.clear_storage_cache()
self._async_dispatch(ConfigEntryChange.UPDATED, entry)
return True
@callback
def async_add_subentry(self, entry: ConfigEntry, subentry: ConfigSubentry) -> bool:
@ -2417,6 +2438,54 @@ class ConfigEntries:
ent_reg.async_clear_config_subentry(entry.entry_id, subentry_id)
return result
@callback
def async_update_subentry(
self,
entry: ConfigEntry,
subentry: ConfigSubentry,
*,
data: Mapping[str, Any] | UndefinedType = UNDEFINED,
title: str | UndefinedType = UNDEFINED,
unique_id: str | None | 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
"""
if entry.entry_id not in self._entries:
raise UnknownEntry(entry.entry_id)
if subentry.subentry_id not in entry.subentries:
raise UnknownSubEntry(subentry.subentry_id)
self.hass.verify_event_loop_thread("hass.config_entries.async_update_subentry")
changed = False
_setter = object.__setattr__
if unique_id is not UNDEFINED and subentry.unique_id != unique_id:
self._raise_if_subentry_unique_id_exists(entry, unique_id)
changed = True
_setter(subentry, "unique_id", unique_id)
if title is not UNDEFINED and subentry.title != title:
changed = True
_setter(subentry, "title", title)
if data is not UNDEFINED and subentry.data != data:
changed = True
_setter(subentry, "data", MappingProxyType(data))
if not changed:
return False
_setter(entry, "modified_at", utcnow())
self._async_save_and_notify(entry)
return True
def _raise_if_subentry_unique_id_exists(
self, entry: ConfigEntry, unique_id: str | None
) -> None:
@ -2763,19 +2832,13 @@ class ConfigFlow(ConfigEntryBaseFlow):
"""Return options flow support for this handler."""
return cls.async_get_options_flow is not ConfigFlow.async_get_options_flow
@staticmethod
@callback
def async_get_subentry_flow(
config_entry: ConfigEntry, subentry_type: str
) -> ConfigSubentryFlow:
"""Get the subentry flow for this handler."""
raise NotImplementedError
@classmethod
@callback
def async_supported_subentries(cls, config_entry: ConfigEntry) -> tuple[str, ...]:
def async_get_supported_subentry_flows(
cls, config_entry: ConfigEntry
) -> dict[str, type[ConfigSubentryFlow]]:
"""Return subentries supported by this handler."""
return ()
return {}
@callback
def _async_abort_entries_match(
@ -3231,7 +3294,9 @@ class _ConfigSubFlowManager:
class ConfigSubentryFlowManager(
data_entry_flow.FlowManager[FlowContext, SubentryFlowResult, tuple[str, str]],
data_entry_flow.FlowManager[
SubentryFlowContext, SubentryFlowResult, tuple[str, str]
],
_ConfigSubFlowManager,
):
"""Manage all the config subentry flows that are in progress."""
@ -3255,18 +3320,19 @@ class ConfigSubentryFlowManager(
entry_id, subentry_type = handler_key
entry = self._async_get_config_entry(entry_id)
handler = await _async_get_flow_handler(self.hass, entry.domain, {})
if subentry_type not in handler.async_supported_subentries(entry):
subentry_flows = handler.async_get_supported_subentry_flows(entry)
if subentry_type not in subentry_flows:
raise data_entry_flow.UnknownHandler(
f"Config entry '{entry.domain}' does not support subentry '{subentry_type}'"
)
subentry_flow = handler.async_get_subentry_flow(entry, subentry_type)
subentry_flow = subentry_flows[subentry_type]()
subentry_flow.init_step = context["source"]
return subentry_flow
async def async_finish_flow(
self,
flow: data_entry_flow.FlowHandler[
FlowContext, SubentryFlowResult, tuple[str, str]
SubentryFlowContext, SubentryFlowResult, tuple[str, str]
],
result: SubentryFlowResult,
) -> SubentryFlowResult:
@ -3302,7 +3368,9 @@ class ConfigSubentryFlowManager(
class ConfigSubentryFlow(
data_entry_flow.FlowHandler[FlowContext, SubentryFlowResult, tuple[str, str]]
data_entry_flow.FlowHandler[
SubentryFlowContext, SubentryFlowResult, tuple[str, str]
]
):
"""Base class for config subentry flows."""
@ -3320,6 +3388,9 @@ class ConfigSubentryFlow(
unique_id: str | None = None,
) -> SubentryFlowResult:
"""Finish config flow and create a config entry."""
if self.source != SOURCE_USER:
raise ValueError(f"Source is {self.source}, expected {SOURCE_USER}")
result = super().async_create_entry(
title=title,
data=data,
@ -3331,6 +3402,70 @@ class ConfigSubentryFlow(
return result
@callback
def async_update_and_abort(
self,
entry: ConfigEntry,
subentry: ConfigSubentry,
*,
unique_id: str | None | UndefinedType = UNDEFINED,
title: str | UndefinedType = UNDEFINED,
data: Mapping[str, Any] | UndefinedType = UNDEFINED,
data_updates: Mapping[str, Any] | UndefinedType = UNDEFINED,
) -> SubentryFlowResult:
"""Update config subentry and finish subentry flow.
:param data: replace the subentry data with new data
:param data_updates: add items from data_updates to subentry data - existing
keys are overridden
:param title: replace the title of the subentry
:param unique_id: replace the unique_id of the subentry
"""
if data_updates is not UNDEFINED:
if data is not UNDEFINED:
raise ValueError("Cannot set both data and data_updates")
data = entry.data | data_updates
self.hass.config_entries.async_update_subentry(
entry=entry,
subentry=subentry,
unique_id=unique_id,
title=title,
data=data,
)
return self.async_abort(reason="reconfigure_successful")
@property
def _reconfigure_entry_id(self) -> str:
"""Return reconfigure entry id."""
if self.source != SOURCE_RECONFIGURE:
raise ValueError(f"Source is {self.source}, expected {SOURCE_RECONFIGURE}")
return self.handler[0]
@callback
def _get_reconfigure_entry(self) -> ConfigEntry:
"""Return the reconfigure config entry linked to the current context."""
return self.hass.config_entries.async_get_known_entry(
self._reconfigure_entry_id
)
@property
def _reconfigure_subentry_id(self) -> str:
"""Return reconfigure subentry id."""
if self.source != SOURCE_RECONFIGURE:
raise ValueError(f"Source is {self.source}, expected {SOURCE_RECONFIGURE}")
return self.context["subentry_id"]
@callback
def _get_reconfigure_subentry(self) -> ConfigSubentry:
"""Return the reconfigure config subentry linked to the current context."""
entry = self.hass.config_entries.async_get_known_entry(
self._reconfigure_entry_id
)
subentry_id = self._reconfigure_subentry_id
if subentry_id not in entry.subentries:
raise UnknownEntry
return entry.subentries[subentry_id]
class OptionsFlowManager(
data_entry_flow.FlowManager[ConfigFlowContext, ConfigFlowResult],

View File

@ -1090,6 +1090,28 @@ class MockConfigEntry(config_entries.ConfigEntry):
},
)
async def start_subentry_reconfigure_flow(
self,
hass: HomeAssistant,
subentry_flow_type: str,
subentry_id: str,
*,
show_advanced_options: bool = False,
) -> ConfigFlowResult:
"""Start a subnetry reconfiguration flow."""
if self.entry_id not in hass.config_entries._entries:
raise ValueError(
"Config entry must be added to hass to start reconfiguration flow"
)
return await hass.config_entries.subentries.async_init(
(self.entry_id, subentry_flow_type),
context={
"source": config_entries.SOURCE_RECONFIGURE,
"subentry_id": subentry_id,
"show_advanced_options": show_advanced_options,
},
)
async def start_reauth_flow(
hass: HomeAssistant,

View File

@ -143,7 +143,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"reason": None,
"source": "bla",
"state": core_ce.ConfigEntryState.NOT_LOADED.value,
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": True,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -163,7 +163,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"reason": "Unsupported API",
"source": "bla2",
"state": core_ce.ConfigEntryState.SETUP_ERROR.value,
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -183,7 +183,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"reason": None,
"source": "bla3",
"state": core_ce.ConfigEntryState.NOT_LOADED.value,
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -203,7 +203,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"reason": None,
"source": "bla4",
"state": core_ce.ConfigEntryState.NOT_LOADED.value,
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -223,7 +223,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None:
"reason": None,
"source": "bla5",
"state": core_ce.ConfigEntryState.NOT_LOADED.value,
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -587,7 +587,7 @@ async def test_create_account(hass: HomeAssistant, client: TestClient) -> None:
"reason": None,
"source": core_ce.SOURCE_USER,
"state": core_ce.ConfigEntryState.LOADED.value,
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -673,7 +673,7 @@ async def test_two_step_flow(hass: HomeAssistant, client: TestClient) -> None:
"reason": None,
"source": core_ce.SOURCE_USER,
"state": core_ce.ConfigEntryState.LOADED.value,
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -1108,9 +1108,6 @@ 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, subentry_type: str):
class SubentryFlowHandler(core_ce.ConfigSubentryFlow):
async def async_step_init(self, user_input=None):
raise NotImplementedError
@ -1124,12 +1121,12 @@ async def test_subentry_flow(hass: HomeAssistant, client) -> None:
description_placeholders={"enabled": "Set to true to be true"},
)
return SubentryFlowHandler()
@classmethod
@callback
def async_supported_subentries(cls, config_entry):
return ("test",)
def async_get_supported_subentry_flows(
cls, config_entry: core_ce.ConfigEntry
) -> dict[str, type[core_ce.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
@ -1160,6 +1157,69 @@ async def test_subentry_flow(hass: HomeAssistant, client) -> None:
}
async def test_subentry_reconfigure_flow(hass: HomeAssistant, client) -> None:
"""Test we can start a subentry reconfigure flow."""
class TestFlow(core_ce.ConfigFlow):
class SubentryFlowHandler(core_ce.ConfigSubentryFlow):
async def async_step_init(self, user_input=None):
raise NotImplementedError
async def async_step_user(self, user_input=None):
raise NotImplementedError
async def async_step_reconfigure(self, user_input=None):
schema = OrderedDict()
schema[vol.Required("enabled")] = bool
return self.async_show_form(
step_id="reconfigure",
data_schema=schema,
description_placeholders={"enabled": "Set to true to be true"},
)
@classmethod
@callback
def async_get_supported_subentry_flows(
cls, config_entry: core_ce.ConfigEntry
) -> dict[str, type[core_ce.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
MockConfigEntry(
domain="test",
entry_id="test1",
source="bla",
subentries_data=[
core_ce.ConfigSubentryData(
data={}, subentry_id="mock_id", title="Title", unique_id=None
)
],
).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, "test"], "subentry_id": "mock_id"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
data.pop("flow_id")
assert data == {
"type": "form",
"handler": ["test1", "test"],
"step_id": "reconfigure",
"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"),
[
@ -1174,9 +1234,6 @@ async def test_subentry_flow_unauth(
"""Test unauthorized on subentry flow."""
class TestFlow(core_ce.ConfigFlow):
@staticmethod
@callback
def async_get_subentry_flow(config_entry, subentry_type: str):
class SubentryFlowHandler(core_ce.ConfigSubentryFlow):
async def async_step_init(self, user_input=None):
schema = OrderedDict()
@ -1187,12 +1244,12 @@ async def test_subentry_flow_unauth(
description_placeholders={"enabled": "Set to true to be true"},
)
return SubentryFlowHandler()
@classmethod
@callback
def async_supported_subentries(cls, config_entry):
return ("test",)
def async_get_supported_subentry_flows(
cls, config_entry: core_ce.ConfigEntry
) -> dict[str, type[core_ce.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
@ -1219,9 +1276,6 @@ async def test_two_step_subentry_flow(hass: HomeAssistant, client) -> None:
mock_platform(hass, "test.config_flow", None)
class TestFlow(core_ce.ConfigFlow):
@staticmethod
@callback
def async_get_subentry_flow(config_entry, subentry_type: str):
class SubentryFlowHandler(core_ce.ConfigSubentryFlow):
async def async_step_user(self, user_input=None):
return await self.async_step_finish()
@ -1236,12 +1290,12 @@ async def test_two_step_subentry_flow(hass: HomeAssistant, client) -> None:
step_id="finish", data_schema=vol.Schema({"enabled": bool})
)
return SubentryFlowHandler()
@classmethod
@callback
def async_supported_subentries(cls, config_entry):
return ("test",)
def async_get_supported_subentry_flows(
cls, config_entry: core_ce.ConfigEntry
) -> dict[str, type[core_ce.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
MockConfigEntry(
domain="test",
@ -1300,9 +1354,6 @@ async def test_subentry_flow_with_invalid_data(hass: HomeAssistant, client) -> N
mock_platform(hass, "test.config_flow", None)
class TestFlow(core_ce.ConfigFlow):
@staticmethod
@callback
def async_get_subentry_flow(config_entry, subentry_type: str):
class SubentryFlowHandler(core_ce.ConfigSubentryFlow):
async def async_step_user(self, user_input=None):
return self.async_show_form(
@ -1317,16 +1368,14 @@ async def test_subentry_flow_with_invalid_data(hass: HomeAssistant, client) -> N
)
async def async_step_finish(self, user_input=None):
return self.async_create_entry(
title="Enable disable", data=user_input
)
return SubentryFlowHandler()
return self.async_create_entry(title="Enable disable", data=user_input)
@classmethod
@callback
def async_supported_subentries(cls, config_entry):
return ("test",)
def async_get_supported_subentry_flows(
cls, config_entry: core_ce.ConfigEntry
) -> dict[str, type[core_ce.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
MockConfigEntry(
domain="test",
@ -1409,7 +1458,7 @@ async def test_get_single(
"reason": None,
"source": "user",
"state": "loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -1771,7 +1820,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -1792,7 +1841,7 @@ async def test_get_matching_entries_ws(
"reason": "Unsupported API",
"source": "bla2",
"state": "setup_error",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -1813,7 +1862,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -1834,7 +1883,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla4",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -1855,7 +1904,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla5",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -1887,7 +1936,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -1918,7 +1967,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla4",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -1939,7 +1988,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla5",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -1970,7 +2019,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -1991,7 +2040,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2028,7 +2077,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2049,7 +2098,7 @@ async def test_get_matching_entries_ws(
"reason": "Unsupported API",
"source": "bla2",
"state": "setup_error",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2070,7 +2119,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2091,7 +2140,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla4",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2112,7 +2161,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla5",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2221,7 +2270,7 @@ async def test_subscribe_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2245,7 +2294,7 @@ async def test_subscribe_entries_ws(
"reason": "Unsupported API",
"source": "bla2",
"state": "setup_error",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2269,7 +2318,7 @@ async def test_subscribe_entries_ws(
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2299,7 +2348,7 @@ async def test_subscribe_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2330,7 +2379,7 @@ async def test_subscribe_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2360,7 +2409,7 @@ async def test_subscribe_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2452,7 +2501,7 @@ async def test_subscribe_entries_ws_filtered(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2476,7 +2525,7 @@ async def test_subscribe_entries_ws_filtered(
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2508,7 +2557,7 @@ async def test_subscribe_entries_ws_filtered(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2536,7 +2585,7 @@ async def test_subscribe_entries_ws_filtered(
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2568,7 +2617,7 @@ async def test_subscribe_entries_ws_filtered(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,
@ -2598,7 +2647,7 @@ async def test_subscribe_entries_ws_filtered(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supported_subentries": [],
"supported_subentry_flows": {},
"supports_options": False,
"supports_reconfigure": False,
"supports_remove_device": False,

View File

@ -1959,7 +1959,7 @@ async def test_create_entry_subentries(
entries = hass.config_entries.async_entries("comp")
assert len(entries) == 1
assert entries[0].supported_subentries == ()
assert entries[0].supported_subentry_flows == {}
assert entries[0].data == {"example": "data"}
assert len(entries[0].subentries) == 1
subentry_id = list(entries[0].subentries)[0]
@ -1984,22 +1984,15 @@ async def test_entry_subentry(
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
@staticmethod
@callback
def async_get_subentry_flow(config_entry, subentry_type: str):
"""Test subentry flow."""
class SubentryFlowHandler(data_entry_flow.FlowHandler):
"""Test subentry flow handler."""
return SubentryFlowHandler()
@classmethod
@callback
def async_supported_subentries(
def async_get_supported_subentry_flows(
cls, config_entry: ConfigEntry
) -> tuple[str, ...]:
return ("test",)
) -> dict[str, type[config_entries.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
with mock_config_flow("test", TestFlow):
flow = await manager.subentries.async_create_flow(
@ -2029,7 +2022,9 @@ async def test_entry_subentry(
unique_id="test",
)
}
assert entry.supported_subentries == ("test",)
assert entry.supported_subentry_flows == {
"test": {"supports_reconfigure": False}
}
async def test_entry_subentry_non_string(
@ -2044,22 +2039,15 @@ async def test_entry_subentry_non_string(
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
@staticmethod
@callback
def async_get_subentry_flow(config_entry, subentry_type: str):
"""Test subentry flow."""
class SubentryFlowHandler(data_entry_flow.FlowHandler):
"""Test subentry flow handler."""
return SubentryFlowHandler()
@classmethod
@callback
def async_supported_subentries(
def async_get_supported_subentry_flows(
cls, config_entry: ConfigEntry
) -> tuple[str, ...]:
return ("test",)
) -> dict[str, type[config_entries.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
with mock_config_flow("test", TestFlow):
flow = await manager.subentries.async_create_flow(
@ -2093,22 +2081,15 @@ async def test_entry_subentry_no_context(
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
@staticmethod
@callback
def async_get_subentry_flow(config_entry, subentry_type: str):
"""Test subentry flow."""
class SubentryFlowHandler(data_entry_flow.FlowHandler):
"""Test subentry flow handler."""
return SubentryFlowHandler()
@classmethod
@callback
def async_supported_subentries(
def async_get_supported_subentry_flows(
cls, config_entry: ConfigEntry
) -> tuple[str, ...]:
return ("test",)
) -> dict[str, type[config_entries.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
with mock_config_flow("test", TestFlow), pytest.raises(KeyError):
await manager.subentries.async_create_flow(
@ -2146,22 +2127,15 @@ async def test_entry_subentry_duplicate(
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
@staticmethod
@callback
def async_get_subentry_flow(config_entry, subentry_type: str):
"""Test subentry flow."""
class SubentryFlowHandler(data_entry_flow.FlowHandler):
"""Test subentry flow handler."""
return SubentryFlowHandler()
@classmethod
@callback
def async_supported_subentries(
def async_get_supported_subentry_flows(
cls, config_entry: ConfigEntry
) -> tuple[str, ...]:
return ("test",)
) -> dict[str, type[config_entries.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
with mock_config_flow("test", TestFlow):
flow = await manager.subentries.async_create_flow(
@ -2194,22 +2168,15 @@ async def test_entry_subentry_abort(
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
@staticmethod
@callback
def async_get_subentry_flow(config_entry, subentry_type: str):
"""Test subentry flow."""
class SubentryFlowHandler(data_entry_flow.FlowHandler):
"""Test subentry flow handler."""
return SubentryFlowHandler()
@classmethod
@callback
def async_supported_subentries(
def async_get_supported_subentry_flows(
cls, config_entry: ConfigEntry
) -> tuple[str, ...]:
return ("test",)
) -> dict[str, type[config_entries.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
with mock_config_flow("test", TestFlow):
flow = await manager.subentries.async_create_flow(
@ -2248,22 +2215,15 @@ async def test_entry_subentry_deleted_config_entry(
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
@staticmethod
@callback
def async_get_subentry_flow(config_entry, subentry_type: str):
"""Test subentry flow."""
class SubentryFlowHandler(data_entry_flow.FlowHandler):
"""Test subentry flow handler."""
return SubentryFlowHandler()
@classmethod
@callback
def async_supported_subentries(
def async_get_supported_subentry_flows(
cls, config_entry: ConfigEntry
) -> tuple[str, ...]:
return ("test",)
) -> dict[str, type[config_entries.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
with mock_config_flow("test", TestFlow):
flow = await manager.subentries.async_create_flow(
@ -2286,7 +2246,7 @@ async def test_entry_subentry_deleted_config_entry(
)
async def test_entry_subentry_unsupported(
async def test_entry_subentry_unsupported_subentry_type(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test attempting to start a subentry flow for a config entry without support."""
@ -2298,22 +2258,15 @@ async def test_entry_subentry_unsupported(
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
@staticmethod
@callback
def async_get_subentry_flow(config_entry, subentry_type: str):
"""Test subentry flow."""
class SubentryFlowHandler(data_entry_flow.FlowHandler):
"""Test subentry flow handler."""
return SubentryFlowHandler()
@classmethod
@callback
def async_supported_subentries(
def async_get_supported_subentry_flows(
cls, config_entry: ConfigEntry
) -> tuple[str, ...]:
return ("test",)
) -> dict[str, type[config_entries.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
with (
mock_config_flow("test", TestFlow),
@ -2329,7 +2282,7 @@ async def test_entry_subentry_unsupported(
)
async def test_entry_subentry_unsupported_subentry_type(
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."""