Make initial group config flow step a menu (#68565)

This commit is contained in:
Erik Montnemery 2022-03-23 16:34:44 +01:00 committed by GitHub
parent d3809e4a09
commit a50bac5cc2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 164 additions and 91 deletions

View file

@ -18,6 +18,7 @@ from homeassistant.const import (
from homeassistant.helpers import selector from homeassistant.helpers import selector
from homeassistant.helpers.helper_config_entry_flow import ( from homeassistant.helpers.helper_config_entry_flow import (
HelperConfigFlowHandler, HelperConfigFlowHandler,
HelperFlowMenuStep,
HelperFlowStep, HelperFlowStep,
) )
@ -77,9 +78,13 @@ CONFIG_SCHEMA = vol.Schema(
} }
).extend(OPTIONS_SCHEMA.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): class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):

View file

@ -1,7 +1,7 @@
"""Config flow for Group integration.""" """Config flow for Group integration."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Callable, Mapping
from typing import Any, cast from typing import Any, cast
import voluptuous as vol 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 import entity_registry as er, selector
from homeassistant.helpers.helper_config_entry_flow import ( from homeassistant.helpers.helper_config_entry_flow import (
HelperConfigFlowHandler, HelperConfigFlowHandler,
HelperFlowMenuStep,
HelperFlowStep, HelperFlowStep,
) )
@ -61,43 +62,44 @@ LIGHT_CONFIG_SCHEMA = vol.Schema(
).extend(LIGHT_OPTIONS_SCHEMA.schema) ).extend(LIGHT_OPTIONS_SCHEMA.schema)
INITIAL_STEP_SCHEMA = vol.Schema( GROUP_TYPES = ["binary_sensor", "cover", "fan", "light", "media_player"]
{
vol.Required("group_type"): selector.selector(
{
"select": {
"options": [
"binary_sensor",
"cover",
"fan",
"light",
"media_player",
]
}
}
)
}
)
@callback @callback
def choose_config_step(options: dict[str, Any]) -> str: def choose_options_step(options: dict[str, Any]) -> str:
"""Return next step_id when group_type is selected.""" """Return next step_id for options flow according to group_type."""
return cast(str, options["group_type"]) return cast(str, options["group_type"])
CONFIG_FLOW = { def set_group_type(group_type: str) -> Callable[[dict[str, Any]], dict[str, Any]]:
"user": HelperFlowStep(INITIAL_STEP_SCHEMA, next_step=choose_config_step), """Set group type."""
"binary_sensor": HelperFlowStep(BINARY_SENSOR_CONFIG_SCHEMA),
"cover": HelperFlowStep(basic_group_config_schema("cover")), @callback
"fan": HelperFlowStep(basic_group_config_schema("fan")), def _set_group_type(user_input: dict[str, Any]) -> dict[str, Any]:
"light": HelperFlowStep(LIGHT_CONFIG_SCHEMA), """Add group type to user input."""
"media_player": HelperFlowStep(basic_group_config_schema("media_player")), 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 = { OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = {
"init": HelperFlowStep(None, next_step=choose_config_step), "init": HelperFlowStep(None, next_step=choose_options_step),
"binary_sensor": HelperFlowStep(BINARY_SENSOR_OPTIONS_SCHEMA), "binary_sensor": HelperFlowStep(BINARY_SENSOR_OPTIONS_SCHEMA),
"cover": HelperFlowStep(basic_group_options_schema("cover")), "cover": HelperFlowStep(basic_group_options_schema("cover")),
"fan": HelperFlowStep(basic_group_options_schema("fan")), "fan": HelperFlowStep(basic_group_options_schema("fan")),

View file

@ -4,8 +4,13 @@
"step": { "step": {
"user": { "user": {
"title": "New Group", "title": "New Group",
"data": { "description": "Select group type",
"group_type": "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": { "binary_sensor": {

View file

@ -46,8 +46,13 @@
"title": "New Group" "title": "New Group"
}, },
"user": { "user": {
"data": { "description": "Select group type",
"group_type": "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" "title": "New Group"
} }

View file

@ -18,6 +18,7 @@ from homeassistant.const import (
from homeassistant.helpers import selector from homeassistant.helpers import selector
from homeassistant.helpers.helper_config_entry_flow import ( from homeassistant.helpers.helper_config_entry_flow import (
HelperConfigFlowHandler, HelperConfigFlowHandler,
HelperFlowMenuStep,
HelperFlowStep, 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): class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):

View file

@ -7,16 +7,18 @@ from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.const import CONF_ENTITY_ID, Platform
from homeassistant.helpers import ( from homeassistant.helpers import entity_registry as er, selector
entity_registry as er, from homeassistant.helpers.helper_config_entry_flow import (
helper_config_entry_flow, HelperConfigFlowHandler,
selector, HelperFlowMenuStep,
HelperFlowStep,
wrapped_entity_config_entry_title,
) )
from .const import CONF_TARGET_DOMAIN, DOMAIN from .const import CONF_TARGET_DOMAIN, DOMAIN
CONFIG_FLOW = { CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = {
"user": helper_config_entry_flow.HelperFlowStep( "user": HelperFlowStep(
vol.Schema( vol.Schema(
{ {
vol.Required(CONF_ENTITY_ID): selector.selector( vol.Required(CONF_ENTITY_ID): selector.selector(
@ -41,9 +43,7 @@ CONFIG_FLOW = {
} }
class SwitchAsXConfigFlowHandler( class SwitchAsXConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
helper_config_entry_flow.HelperConfigFlowHandler, domain=DOMAIN
):
"""Handle a config flow for Switch as X.""" """Handle a config flow for Switch as X."""
config_flow = CONFIG_FLOW config_flow = CONFIG_FLOW
@ -58,6 +58,4 @@ class SwitchAsXConfigFlowHandler(
options[CONF_ENTITY_ID], hidden_by=er.RegistryEntryHider.INTEGRATION options[CONF_ENTITY_ID], hidden_by=er.RegistryEntryHider.INTEGRATION
) )
return helper_config_entry_flow.wrapped_entity_config_entry_title( return wrapped_entity_config_entry_title(self.hass, options[CONF_ENTITY_ID])
self.hass, options[CONF_ENTITY_ID]
)

View file

@ -11,6 +11,7 @@ from homeassistant.helpers import selector
from homeassistant.helpers.helper_config_entry_flow import ( from homeassistant.helpers.helper_config_entry_flow import (
HelperConfigFlowHandler, HelperConfigFlowHandler,
HelperFlowError, HelperFlowError,
HelperFlowMenuStep,
HelperFlowStep, HelperFlowStep,
) )
@ -43,11 +44,11 @@ CONFIG_SCHEMA = vol.Schema(
} }
).extend(OPTIONS_SCHEMA.schema) ).extend(OPTIONS_SCHEMA.schema)
CONFIG_FLOW = { CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = {
"user": HelperFlowStep(CONFIG_SCHEMA, validate_user_input=_validate_mode) "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) "init": HelperFlowStep(OPTIONS_SCHEMA, validate_user_input=_validate_mode)
} }

View file

@ -83,7 +83,7 @@ class FlowResult(TypedDict, total=False):
result: Any result: Any
last_step: bool | None last_step: bool | None
options: Mapping[str, Any] options: Mapping[str, Any]
menu_options: list[str] | Mapping[str, Any] menu_options: list[str] | dict[str, str]
@callback @callback

View file

@ -5,7 +5,8 @@ from abc import abstractmethod
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
import copy import copy
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any import types
from typing import Any, cast
import voluptuous as vol import voluptuous as vol
@ -42,13 +43,21 @@ class HelperFlowStep:
next_step: Callable[[dict[str, Any]], str | None] = lambda _: None 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: class HelperCommonFlowHandler:
"""Handle a config or options flow for helper.""" """Handle a config or options flow for helper."""
def __init__( def __init__(
self, self,
handler: HelperConfigFlowHandler | HelperOptionsFlowHandler, handler: HelperConfigFlowHandler | HelperOptionsFlowHandler,
flow: dict[str, HelperFlowStep], flow: dict[str, HelperFlowStep | HelperFlowMenuStep],
config_entry: config_entries.ConfigEntry | None, config_entry: config_entries.ConfigEntry | None,
) -> None: ) -> None:
"""Initialize a common handler.""" """Initialize a common handler."""
@ -60,24 +69,31 @@ class HelperCommonFlowHandler:
self, step_id: str, user_input: dict[str, Any] | None = None self, step_id: str, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Handle a step.""" """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 # Do extra validation of user input
try: 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: 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: if user_input is not None:
# User input was validated successfully, update options # User input was validated successfully, update options
self._options.update(user_input) self._options.update(user_input)
if self._flow[next_step_id].next_step and ( next_step_id: str = step_id
user_input is not None or self._flow[next_step_id].schema is None if form_step.next_step and (user_input is not None or form_step.schema is None):
):
# Get next step # 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: if next_step_id_or_end_flow is None:
# Flow done, create entry or update config entry options # Flow done, create entry or update config entry options
return self._handler.async_create_entry(data=self._options) return self._handler.async_create_entry(data=self._options)
@ -92,11 +108,13 @@ class HelperCommonFlowHandler:
error: HelperFlowError | None = None, error: HelperFlowError | None = None,
user_input: dict[str, Any] | None = None, user_input: dict[str, Any] | None = None,
) -> FlowResult: ) -> 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) options = dict(self._options)
if user_input: if user_input:
options.update(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 # Make a copy of the schema with suggested values set to saved options
schema = {} schema = {}
for key, val in data_schema.schema.items(): 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 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): class HelperConfigFlowHandler(config_entries.ConfigFlow):
"""Handle a config flow for helper integrations.""" """Handle a config flow for helper integrations."""
config_flow: dict[str, HelperFlowStep] config_flow: dict[str, HelperFlowStep | HelperFlowMenuStep]
options_flow: dict[str, HelperFlowStep] | None = None options_flow: dict[str, HelperFlowStep | HelperFlowMenuStep] | None = None
VERSION = 1 VERSION = 1
@ -146,7 +174,7 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow):
# Create flow step methods for each step defined in the flow schema # Create flow step methods for each step defined in the flow schema
for step in cls.config_flow: 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: def __init__(self) -> None:
"""Initialize config flow.""" """Initialize config flow."""
@ -160,12 +188,19 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow):
"""Return options flow support for this handler.""" """Return options flow support for this handler."""
return cls.options_flow is not None return cls.options_flow is not None
async def _async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: @staticmethod
"""Handle a config flow step.""" def _async_step(step_id: str) -> Callable:
step_id = self.cur_step["step_id"] if self.cur_step else "user" """Generate a step handler."""
result = await self._common_handler.async_step(step_id, user_input)
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 # pylint: disable-next=no-self-use
@abstractmethod @abstractmethod
@ -224,13 +259,25 @@ class HelperOptionsFlowHandler(config_entries.OptionsFlow):
self._async_options_flow_finished = async_options_flow_finished self._async_options_flow_finished = async_options_flow_finished
for step in options_flow: 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: @staticmethod
"""Handle an options flow step.""" def _async_step(step_id: str) -> Callable:
# pylint: disable-next=unsubscriptable-object # self.cur_step is a dict """Generate a step handler."""
step_id = self.cur_step["step_id"] if self.cur_step else "init"
return await self._common_handler.async_step(step_id, user_input) 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 @callback
def async_create_entry( # pylint: disable=arguments-differ def async_create_entry( # pylint: disable=arguments-differ

View file

@ -10,6 +10,7 @@ from homeassistant.const import CONF_ENTITY_ID
from homeassistant.helpers import selector from homeassistant.helpers import selector
from homeassistant.helpers.helper_config_entry_flow import ( from homeassistant.helpers.helper_config_entry_flow import (
HelperConfigFlowHandler, HelperConfigFlowHandler,
HelperFlowMenuStep,
HelperFlowStep, HelperFlowStep,
) )
@ -29,9 +30,13 @@ CONFIG_SCHEMA = vol.Schema(
} }
).extend(OPTIONS_SCHEMA.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): class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):

View file

@ -6,7 +6,11 @@ import pytest
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.group import DOMAIN, async_setup_entry from homeassistant.components.group import DOMAIN, async_setup_entry
from homeassistant.core import HomeAssistant 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 homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -42,12 +46,11 @@ async def test_config_flow(
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_MENU
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"group_type": group_type}, {"next_step_id": group_type},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_FORM 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_MENU
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"group_type": group_type}, {"next_step_id": group_type},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
@ -251,13 +253,11 @@ async def test_options(
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_MENU
assert result["errors"] is None
assert get_suggested(result["data_schema"].schema, "group_type") is None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"group_type": group_type}, {"next_step_id": group_type},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM