diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index ccb44b1963b..6d38a3cd4a2 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -18,6 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers import selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowMenuStep, HelperFlowStep, ) @@ -77,9 +78,13 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW = {"user": HelperFlowStep(CONFIG_SCHEMA)} +CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "user": HelperFlowStep(CONFIG_SCHEMA) +} -OPTIONS_FLOW = {"init": HelperFlowStep(OPTIONS_SCHEMA)} +OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "init": HelperFlowStep(OPTIONS_SCHEMA) +} class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index dafb43924a7..ca356ee70f3 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Group integration.""" from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Callable, Mapping from typing import Any, cast import voluptuous as vol @@ -11,6 +11,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowMenuStep, HelperFlowStep, ) @@ -61,43 +62,44 @@ LIGHT_CONFIG_SCHEMA = vol.Schema( ).extend(LIGHT_OPTIONS_SCHEMA.schema) -INITIAL_STEP_SCHEMA = vol.Schema( - { - vol.Required("group_type"): selector.selector( - { - "select": { - "options": [ - "binary_sensor", - "cover", - "fan", - "light", - "media_player", - ] - } - } - ) - } -) +GROUP_TYPES = ["binary_sensor", "cover", "fan", "light", "media_player"] @callback -def choose_config_step(options: dict[str, Any]) -> str: - """Return next step_id when group_type is selected.""" +def choose_options_step(options: dict[str, Any]) -> str: + """Return next step_id for options flow according to group_type.""" return cast(str, options["group_type"]) -CONFIG_FLOW = { - "user": HelperFlowStep(INITIAL_STEP_SCHEMA, next_step=choose_config_step), - "binary_sensor": HelperFlowStep(BINARY_SENSOR_CONFIG_SCHEMA), - "cover": HelperFlowStep(basic_group_config_schema("cover")), - "fan": HelperFlowStep(basic_group_config_schema("fan")), - "light": HelperFlowStep(LIGHT_CONFIG_SCHEMA), - "media_player": HelperFlowStep(basic_group_config_schema("media_player")), +def set_group_type(group_type: str) -> Callable[[dict[str, Any]], dict[str, Any]]: + """Set group type.""" + + @callback + def _set_group_type(user_input: dict[str, Any]) -> dict[str, Any]: + """Add group type to user input.""" + return {"group_type": group_type, **user_input} + + return _set_group_type + + +CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "user": HelperFlowMenuStep(GROUP_TYPES), + "binary_sensor": HelperFlowStep( + BINARY_SENSOR_CONFIG_SCHEMA, set_group_type("binary_sensor") + ), + "cover": HelperFlowStep( + basic_group_config_schema("cover"), set_group_type("cover") + ), + "fan": HelperFlowStep(basic_group_config_schema("fan"), set_group_type("fan")), + "light": HelperFlowStep(LIGHT_CONFIG_SCHEMA, set_group_type("light")), + "media_player": HelperFlowStep( + basic_group_config_schema("media_player"), set_group_type("media_player") + ), } -OPTIONS_FLOW = { - "init": HelperFlowStep(None, next_step=choose_config_step), +OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "init": HelperFlowStep(None, next_step=choose_options_step), "binary_sensor": HelperFlowStep(BINARY_SENSOR_OPTIONS_SCHEMA), "cover": HelperFlowStep(basic_group_options_schema("cover")), "fan": HelperFlowStep(basic_group_options_schema("fan")), diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index 0b7388b293b..440ec7740ab 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -4,8 +4,13 @@ "step": { "user": { "title": "New Group", - "data": { - "group_type": "Group type" + "description": "Select group type", + "menu_options": { + "binary_sensor": "Binary sensor group", + "cover": "Cover group", + "fan": "Fan group", + "light": "Light group", + "media_player": "Media player group" } }, "binary_sensor": { diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index eec26861897..57a24b0bdba 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -46,8 +46,13 @@ "title": "New Group" }, "user": { - "data": { - "group_type": "Group type" + "description": "Select group type", + "menu_options": { + "binary_sensor": "Binary sensor group", + "cover": "Cover group", + "fan": "Fan group", + "light": "Light group", + "media_player": "Media player group" }, "title": "New Group" } diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py index 76379a89002..bf9ad853205 100644 --- a/homeassistant/components/integration/config_flow.py +++ b/homeassistant/components/integration/config_flow.py @@ -18,6 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers import selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowMenuStep, HelperFlowStep, ) @@ -87,9 +88,13 @@ CONFIG_SCHEMA = vol.Schema( } ) -CONFIG_FLOW = {"user": HelperFlowStep(CONFIG_SCHEMA)} +CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "user": HelperFlowStep(CONFIG_SCHEMA) +} -OPTIONS_FLOW = {"init": HelperFlowStep(OPTIONS_SCHEMA)} +OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "init": HelperFlowStep(OPTIONS_SCHEMA) +} class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index 800e056cd26..6425d212f10 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -7,16 +7,18 @@ from typing import Any import voluptuous as vol from homeassistant.const import CONF_ENTITY_ID, Platform -from homeassistant.helpers import ( - entity_registry as er, - helper_config_entry_flow, - selector, +from homeassistant.helpers import entity_registry as er, selector +from homeassistant.helpers.helper_config_entry_flow import ( + HelperConfigFlowHandler, + HelperFlowMenuStep, + HelperFlowStep, + wrapped_entity_config_entry_title, ) from .const import CONF_TARGET_DOMAIN, DOMAIN -CONFIG_FLOW = { - "user": helper_config_entry_flow.HelperFlowStep( +CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "user": HelperFlowStep( vol.Schema( { vol.Required(CONF_ENTITY_ID): selector.selector( @@ -41,9 +43,7 @@ CONFIG_FLOW = { } -class SwitchAsXConfigFlowHandler( - helper_config_entry_flow.HelperConfigFlowHandler, domain=DOMAIN -): +class SwitchAsXConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): """Handle a config flow for Switch as X.""" config_flow = CONFIG_FLOW @@ -58,6 +58,4 @@ class SwitchAsXConfigFlowHandler( options[CONF_ENTITY_ID], hidden_by=er.RegistryEntryHider.INTEGRATION ) - return helper_config_entry_flow.wrapped_entity_config_entry_title( - self.hass, options[CONF_ENTITY_ID] - ) + return wrapped_entity_config_entry_title(self.hass, options[CONF_ENTITY_ID]) diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index 01e5364284a..36d4c5e6239 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -11,6 +11,7 @@ from homeassistant.helpers import selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, HelperFlowError, + HelperFlowMenuStep, HelperFlowStep, ) @@ -43,11 +44,11 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW = { +CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { "user": HelperFlowStep(CONFIG_SCHEMA, validate_user_input=_validate_mode) } -OPTIONS_FLOW = { +OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { "init": HelperFlowStep(OPTIONS_SCHEMA, validate_user_input=_validate_mode) } diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 080ef86fac6..628a89dd89b 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -83,7 +83,7 @@ class FlowResult(TypedDict, total=False): result: Any last_step: bool | None options: Mapping[str, Any] - menu_options: list[str] | Mapping[str, Any] + menu_options: list[str] | dict[str, str] @callback diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index b9e35d9d336..f64b2463bdd 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -5,7 +5,8 @@ from abc import abstractmethod from collections.abc import Callable, Mapping import copy from dataclasses import dataclass -from typing import Any +import types +from typing import Any, cast import voluptuous as vol @@ -42,13 +43,21 @@ class HelperFlowStep: next_step: Callable[[dict[str, Any]], str | None] = lambda _: None +@dataclass +class HelperFlowMenuStep: + """Define a helper config or options flow menu step.""" + + # Menu options + options: list[str] | dict[str, str] + + class HelperCommonFlowHandler: """Handle a config or options flow for helper.""" def __init__( self, handler: HelperConfigFlowHandler | HelperOptionsFlowHandler, - flow: dict[str, HelperFlowStep], + flow: dict[str, HelperFlowStep | HelperFlowMenuStep], config_entry: config_entries.ConfigEntry | None, ) -> None: """Initialize a common handler.""" @@ -60,24 +69,31 @@ class HelperCommonFlowHandler: self, step_id: str, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a step.""" - next_step_id: str = step_id + if isinstance(self._flow[step_id], HelperFlowStep): + return await self._async_form_step(step_id, user_input) + return await self._async_menu_step(step_id, user_input) - if user_input is not None and self._flow[next_step_id].schema is not None: + async def _async_form_step( + self, step_id: str, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a form step.""" + form_step: HelperFlowStep = cast(HelperFlowStep, self._flow[step_id]) + + if user_input is not None and form_step.schema is not None: # Do extra validation of user input try: - user_input = self._flow[next_step_id].validate_user_input(user_input) + user_input = form_step.validate_user_input(user_input) except HelperFlowError as exc: - return self._show_next_step(next_step_id, exc, user_input) + return self._show_next_step(step_id, exc, user_input) if user_input is not None: # User input was validated successfully, update options self._options.update(user_input) - if self._flow[next_step_id].next_step and ( - user_input is not None or self._flow[next_step_id].schema is None - ): + next_step_id: str = step_id + if form_step.next_step and (user_input is not None or form_step.schema is None): # Get next step - next_step_id_or_end_flow = self._flow[next_step_id].next_step(self._options) + next_step_id_or_end_flow = form_step.next_step(self._options) if next_step_id_or_end_flow is None: # Flow done, create entry or update config entry options return self._handler.async_create_entry(data=self._options) @@ -92,11 +108,13 @@ class HelperCommonFlowHandler: error: HelperFlowError | None = None, user_input: dict[str, Any] | None = None, ) -> FlowResult: - """Show step for next step.""" + """Show form for next step.""" + form_step: HelperFlowStep = cast(HelperFlowStep, self._flow[next_step_id]) + options = dict(self._options) if user_input: options.update(user_input) - if (data_schema := self._flow[next_step_id].schema) and data_schema.schema: + if (data_schema := form_step.schema) and data_schema.schema: # Make a copy of the schema with suggested values set to saved options schema = {} for key, val in data_schema.schema.items(): @@ -115,12 +133,22 @@ class HelperCommonFlowHandler: step_id=next_step_id, data_schema=data_schema, errors=errors ) + async def _async_menu_step( + self, step_id: str, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a menu step.""" + form_step: HelperFlowMenuStep = cast(HelperFlowMenuStep, self._flow[step_id]) + return self._handler.async_show_menu( + step_id=step_id, + menu_options=form_step.options, + ) + class HelperConfigFlowHandler(config_entries.ConfigFlow): """Handle a config flow for helper integrations.""" - config_flow: dict[str, HelperFlowStep] - options_flow: dict[str, HelperFlowStep] | None = None + config_flow: dict[str, HelperFlowStep | HelperFlowMenuStep] + options_flow: dict[str, HelperFlowStep | HelperFlowMenuStep] | None = None VERSION = 1 @@ -146,7 +174,7 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): # Create flow step methods for each step defined in the flow schema for step in cls.config_flow: - setattr(cls, f"async_step_{step}", cls._async_step) + setattr(cls, f"async_step_{step}", cls._async_step(step)) def __init__(self) -> None: """Initialize config flow.""" @@ -160,12 +188,19 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): """Return options flow support for this handler.""" return cls.options_flow is not None - async def _async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: - """Handle a config flow step.""" - step_id = self.cur_step["step_id"] if self.cur_step else "user" - result = await self._common_handler.async_step(step_id, user_input) + @staticmethod + def _async_step(step_id: str) -> Callable: + """Generate a step handler.""" - return result + async def _async_step( + self: HelperConfigFlowHandler, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a config flow step.""" + # pylint: disable-next=protected-access + result = await self._common_handler.async_step(step_id, user_input) + return result + + return _async_step # pylint: disable-next=no-self-use @abstractmethod @@ -224,13 +259,25 @@ class HelperOptionsFlowHandler(config_entries.OptionsFlow): self._async_options_flow_finished = async_options_flow_finished for step in options_flow: - setattr(self, f"async_step_{step}", self._async_step) + setattr( + self, + f"async_step_{step}", + types.MethodType(self._async_step(step), self), + ) - async def _async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: - """Handle an options flow step.""" - # pylint: disable-next=unsubscriptable-object # self.cur_step is a dict - step_id = self.cur_step["step_id"] if self.cur_step else "init" - return await self._common_handler.async_step(step_id, user_input) + @staticmethod + def _async_step(step_id: str) -> Callable: + """Generate a step handler.""" + + async def _async_step( + self: HelperConfigFlowHandler, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle an options flow step.""" + # pylint: disable-next=protected-access + result = await self._common_handler.async_step(step_id, user_input) + return result + + return _async_step @callback def async_create_entry( # pylint: disable=arguments-differ diff --git a/script/scaffold/templates/config_flow_helper/integration/config_flow.py b/script/scaffold/templates/config_flow_helper/integration/config_flow.py index ad0e8a3eb90..8f4db82c77c 100644 --- a/script/scaffold/templates/config_flow_helper/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_helper/integration/config_flow.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_ENTITY_ID from homeassistant.helpers import selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowMenuStep, HelperFlowStep, ) @@ -29,9 +30,13 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW = {"user": HelperFlowStep(CONFIG_SCHEMA)} +CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "user": HelperFlowStep(CONFIG_SCHEMA) +} -OPTIONS_FLOW = {"init": HelperFlowStep(OPTIONS_SCHEMA)} +OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "init": HelperFlowStep(OPTIONS_SCHEMA) +} class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index f5a9fb22222..9c2657f0001 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -6,7 +6,11 @@ import pytest from homeassistant import config_entries from homeassistant.components.group import DOMAIN, async_setup_entry from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import ( + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, + RESULT_TYPE_MENU, +) from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -42,12 +46,11 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None + assert result["type"] == RESULT_TYPE_MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"group_type": group_type}, + {"next_step_id": group_type}, ) await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM @@ -130,12 +133,11 @@ async def test_config_flow_hides_members( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None + assert result["type"] == RESULT_TYPE_MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"group_type": group_type}, + {"next_step_id": group_type}, ) await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM @@ -251,13 +253,11 @@ async def test_options( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None - assert get_suggested(result["data_schema"].schema, "group_type") is None + assert result["type"] == RESULT_TYPE_MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"group_type": group_type}, + {"next_step_id": group_type}, ) await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM