diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 83485bd16bf..23394c0ee59 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -49,7 +49,9 @@ BINARY_SENSOR_OPTIONS_SCHEMA = basic_group_options_schema("binary_sensor").exten LIGHT_OPTIONS_SCHEMA = basic_group_options_schema("light").extend( { - vol.Required(CONF_ALL, default=False): selector.selector({"boolean": {}}), + vol.Required( + CONF_ALL, default=False, description={"advanced": True} + ): selector.selector({"boolean": {}}), } ) diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index 87716dcbccf..ba62d5d59e5 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -79,6 +79,23 @@ class HelperCommonFlowHandler: """Handle a form step.""" form_step: HelperFlowFormStep = cast(HelperFlowFormStep, self._flow[step_id]) + if ( + user_input is not None + and (data_schema := form_step.schema) + and data_schema.schema + and not self._handler.show_advanced_options + ): + # Add advanced field default if not set + for key in data_schema.schema.keys(): + if isinstance(key, (vol.Optional, vol.Required)): + if ( + key.description + and key.description.get("advanced") + and key.default is not vol.UNDEFINED + and key not in self._options + ): + user_input[str(key.schema)] = key.default() + if user_input is not None and form_step.schema is not None: # Do extra validation of user input try: @@ -120,6 +137,16 @@ class HelperCommonFlowHandler: # Make a copy of the schema with suggested values set to saved options schema = {} for key, val in data_schema.schema.items(): + + if isinstance(key, vol.Marker): + # Exclude advanced field + if ( + key.description + and key.description.get("advanced") + and not self._handler.show_advanced_options + ): + continue + new_key = key if key in options and isinstance(key, vol.Marker): # Copy the marker to not modify the flow schema diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index c9f5768fc8f..67fdec1820f 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -266,6 +266,73 @@ async def test_options( assert get_suggested(result["data_schema"].schema, "name") is None +@pytest.mark.parametrize( + "group_type,extra_options,extra_options_after,advanced", + ( + ("light", {"all": False}, {"all": False}, False), + ("light", {"all": True}, {"all": True}, False), + ("light", {"all": False}, {"all": False}, True), + ("light", {"all": True}, {"all": False}, True), + ), +) +async def test_light_all_options( + hass: HomeAssistant, group_type, extra_options, extra_options_after, advanced +) -> None: + """Test reconfiguring.""" + members1 = [f"{group_type}.one", f"{group_type}.two"] + members2 = [f"{group_type}.four", f"{group_type}.five"] + + group_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entities": members1, + "group_type": group_type, + "name": "Bed Room", + **extra_options, + }, + title="Bed Room", + ) + group_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(group_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{group_type}.bed_room") + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": advanced} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == group_type + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": members2, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "entities": members2, + "group_type": group_type, + "hide_members": False, + "name": "Bed Room", + **extra_options_after, + } + assert config_entry.data == {} + assert config_entry.options == { + "entities": members2, + "group_type": group_type, + "hide_members": False, + "name": "Bed Room", + **extra_options_after, + } + assert config_entry.title == "Bed Room" + + @pytest.mark.parametrize( "hide_members,hidden_by_initial,hidden_by", ((False, "integration", None), (True, None, "integration")), diff --git a/tests/helpers/test_helper_config_entry_flow.py b/tests/helpers/test_helper_config_entry_flow.py index a92a0cd3b36..dece7ace37c 100644 --- a/tests/helpers/test_helper_config_entry_flow.py +++ b/tests/helpers/test_helper_config_entry_flow.py @@ -1,9 +1,52 @@ """Test helper_config_entry_flow.""" +import pytest +import voluptuous as vol + +from homeassistant import data_entry_flow from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.helper_config_entry_flow import ( + HelperConfigFlowHandler, + HelperFlowFormStep, + HelperFlowMenuStep, wrapped_entity_config_entry_title, ) +from homeassistant.util.decorator import Registry + +from tests.common import MockConfigEntry + + +@pytest.fixture +def manager(): + """Return a flow manager.""" + handlers = Registry() + entries = [] + + class FlowManager(data_entry_flow.FlowManager): + """Test flow manager.""" + + async def async_create_flow(self, handler_key, *, context, data): + """Test create flow.""" + handler = handlers.get(handler_key) + + if handler is None: + raise data_entry_flow.UnknownHandler + + flow = handler() + flow.init_step = context.get("init_step", "init") + return flow + + async def async_finish_flow(self, flow, result): + """Test finish flow.""" + if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + result["source"] = flow.context.get("source") + entries.append(result) + return result + + mgr = FlowManager(None) + mgr.mock_created_entries = entries + mgr.mock_reg_handler = handlers.register + return mgr async def test_name(hass: HomeAssistant) -> None: @@ -36,3 +79,208 @@ async def test_name(hass: HomeAssistant) -> None: registry.async_update_entity("switch.ceiling", name="Custom Name") assert wrapped_entity_config_entry_title(hass, entity_id) == "Custom Name" assert wrapped_entity_config_entry_title(hass, entry.id) == "Custom Name" + + +@pytest.mark.parametrize("marker", (vol.Required, vol.Optional)) +async def test_config_flow_advanced_option( + hass: HomeAssistant, manager: data_entry_flow.FlowManager, marker +): + """Test handling of advanced options in config flow.""" + manager.hass = hass + + CONFIG_SCHEMA = vol.Schema( + { + marker("option1"): str, + marker("advanced_no_default", description={"advanced": True}): str, + marker( + "advanced_default", + default="a very reasonable default", + description={"advanced": True}, + ): str, + } + ) + + CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "init": HelperFlowFormStep(CONFIG_SCHEMA) + } + + @manager.mock_reg_handler("test") + class TestFlow(HelperConfigFlowHandler): + config_flow = CONFIG_FLOW + + # Start flow in basic mode + result = await manager.async_init("test") + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert list(result["data_schema"].schema.keys()) == ["option1"] + + result = await manager.async_configure(result["flow_id"], {"option1": "blabla"}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {} + assert result["options"] == { + "advanced_default": "a very reasonable default", + "option1": "blabla", + } + for option in result["options"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + # Start flow in advanced mode + result = await manager.async_init("test", context={"show_advanced_options": True}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert list(result["data_schema"].schema.keys()) == [ + "option1", + "advanced_no_default", + "advanced_default", + ] + + result = await manager.async_configure( + result["flow_id"], {"advanced_no_default": "abc123", "option1": "blabla"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {} + assert result["options"] == { + "advanced_default": "a very reasonable default", + "advanced_no_default": "abc123", + "option1": "blabla", + } + for option in result["options"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + # Start flow in advanced mode + result = await manager.async_init("test", context={"show_advanced_options": True}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert list(result["data_schema"].schema.keys()) == [ + "option1", + "advanced_no_default", + "advanced_default", + ] + + result = await manager.async_configure( + result["flow_id"], + { + "advanced_default": "not default", + "advanced_no_default": "abc123", + "option1": "blabla", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {} + assert result["options"] == { + "advanced_default": "not default", + "advanced_no_default": "abc123", + "option1": "blabla", + } + for option in result["options"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + +@pytest.mark.parametrize("marker", (vol.Required, vol.Optional)) +async def test_options_flow_advanced_option( + hass: HomeAssistant, manager: data_entry_flow.FlowManager, marker +): + """Test handling of advanced options in options flow.""" + manager.hass = hass + + OPTIONS_SCHEMA = vol.Schema( + { + marker("option1"): str, + marker("advanced_no_default", description={"advanced": True}): str, + marker( + "advanced_default", + default="a very reasonable default", + description={"advanced": True}, + ): str, + } + ) + + OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "init": HelperFlowFormStep(OPTIONS_SCHEMA) + } + + class TestFlow(HelperConfigFlowHandler, domain="test"): + config_flow = {} + options_flow = OPTIONS_FLOW + + config_entry = MockConfigEntry( + data={}, + domain="test", + options={ + "option1": "blabla", + "advanced_no_default": "abc123", + "advanced_default": "not default", + }, + ) + config_entry.add_to_hass(hass) + + # Start flow in basic mode + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert list(result["data_schema"].schema.keys()) == ["option1"] + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"option1": "blublu"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "advanced_default": "not default", + "advanced_no_default": "abc123", + "option1": "blublu", + } + for option in result["data"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + # Start flow in advanced mode + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": True} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert list(result["data_schema"].schema.keys()) == [ + "option1", + "advanced_no_default", + "advanced_default", + ] + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"advanced_no_default": "def456", "option1": "blabla"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "advanced_default": "a very reasonable default", + "advanced_no_default": "def456", + "option1": "blabla", + } + for option in result["data"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + # Start flow in advanced mode + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": True} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert list(result["data_schema"].schema.keys()) == [ + "option1", + "advanced_no_default", + "advanced_default", + ] + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + { + "advanced_default": "also not default", + "advanced_no_default": "abc123", + "option1": "blabla", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "advanced_default": "also not default", + "advanced_no_default": "abc123", + "option1": "blabla", + } + for option in result["data"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str)