diff --git a/homeassistant/components/scrape/config_flow.py b/homeassistant/components/scrape/config_flow.py index b53f2f12bd2..8b598ca90be 100644 --- a/homeassistant/components/scrape/config_flow.py +++ b/homeassistant/components/scrape/config_flow.py @@ -40,7 +40,7 @@ from homeassistant.helpers.schema_config_entry_flow import ( SchemaConfigFlowHandler, SchemaFlowError, SchemaFlowFormStep, - SchemaOptionsFlowHandler, + SchemaFlowMenuStep, ) from homeassistant.helpers.selector import ( BooleanSelector, @@ -130,16 +130,15 @@ def validate_rest_setup( def validate_sensor_setup( handler: SchemaCommonFlowHandler, user_input: dict[str, Any] ) -> dict[str, Any]: - """Validate sensor setup.""" - return { - "sensor": [ - { - **user_input, - CONF_INDEX: int(user_input[CONF_INDEX]), - CONF_UNIQUE_ID: str(uuid.uuid1()), - } - ] - } + """Validate sensor input.""" + user_input[CONF_INDEX] = int(user_input[CONF_INDEX]) + user_input[CONF_UNIQUE_ID] = str(uuid.uuid1()) + + # Standard behavior is to merge the result with the options. + # In this case, we want to add a sub-item so we update the options directly. + sensors: list[dict[str, Any]] = handler.options.setdefault("sensor", []) + sensors.append(user_input) + return {} DATA_SCHEMA_RESOURCE = vol.Schema(RESOURCE_SETUP) @@ -157,7 +156,16 @@ CONFIG_FLOW = { ), } OPTIONS_FLOW = { - "init": SchemaFlowFormStep(DATA_SCHEMA_RESOURCE), + "init": SchemaFlowMenuStep(["resource", "add_sensor"]), + "resource": SchemaFlowFormStep( + DATA_SCHEMA_RESOURCE, + validate_user_input=validate_rest_setup, + ), + "add_sensor": SchemaFlowFormStep( + DATA_SCHEMA_SENSOR, + suggested_values=None, + validate_user_input=validate_sensor_setup, + ), } @@ -170,7 +178,3 @@ class ScrapeConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" return options[CONF_RESOURCE] - - -class ScrapeOptionsFlowHandler(SchemaOptionsFlowHandler): - """Handle a config flow for Scrape.""" diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index 391bfe5ad9f..d14b0916f6b 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -52,6 +52,33 @@ "options": { "step": { "init": { + "menu_options": { + "add_sensor": "Add sensor", + "resource": "Configure resource" + } + }, + "add_sensor": { + "data": { + "name": "[%key:component::scrape::config::step::sensor::data::name%]", + "attribute": "[%key:component::scrape::config::step::sensor::data::attribute%]", + "index": "[%key:component::scrape::config::step::sensor::data::index%]", + "select": "[%key:component::scrape::config::step::sensor::data::select%]", + "value_template": "[%key:component::scrape::config::step::sensor::data::value_template%]", + "device_class": "[%key:component::scrape::config::step::sensor::data::device_class%]", + "state_class": "[%key:component::scrape::config::step::sensor::data::state_class%]", + "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data::unit_of_measurement%]" + }, + "data_description": { + "select": "[%key:component::scrape::config::step::sensor::data_description::select%]", + "attribute": "[%key:component::scrape::config::step::sensor::data_description::attribute%]", + "index": "[%key:component::scrape::config::step::sensor::data_description::index%]", + "value_template": "[%key:component::scrape::config::step::sensor::data_description::value_template%]", + "device_class": "[%key:component::scrape::config::step::sensor::data_description::device_class%]", + "state_class": "[%key:component::scrape::config::step::sensor::data_description::state_class%]", + "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data_description::unit_of_measurement%]" + } + }, + "resource": { "data": { "resource": "[%key:component::scrape::config::step::user::data::resource%]", "method": "[%key:component::scrape::config::step::user::data::method%]", diff --git a/homeassistant/components/scrape/translations/en.json b/homeassistant/components/scrape/translations/en.json index 8cc642c12a2..868ed3abaa6 100644 --- a/homeassistant/components/scrape/translations/en.json +++ b/homeassistant/components/scrape/translations/en.json @@ -58,6 +58,33 @@ "options": { "step": { "init": { + "menu_options": { + "add_sensor": "Add sensor", + "resource": "Configure resource" + } + }, + "add_sensor": { + "data": { + "attribute": "Attribute", + "device_class": "Device Class", + "index": "Index", + "name": "Name", + "select": "Select", + "state_class": "State Class", + "unit_of_measurement": "Unit of Measurement", + "value_template": "Value Template" + }, + "data_description": { + "attribute": "Get value of an attribute on the selected tag", + "device_class": "The type/class of the sensor to set the icon in the frontend", + "index": "Defines which of the elements returned by the CSS selector to use", + "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details", + "state_class": "The state_class of the sensor", + "unit_of_measurement": "Choose temperature measurement or create your own", + "value_template": "Defines a template to get the state of the sensor" + } + }, + "resource": { "data": { "authentication": "Select authentication method", "headers": "Headers", diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 64d3adc5d0c..747915c71e2 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.data_entry_flow import FlowResult, UnknownHandler from . import entity_registry as er, selector +from .typing import UNDEFINED, UndefinedType class SchemaFlowError(Exception): @@ -63,10 +64,12 @@ class SchemaFlowFormStep(SchemaFlowStep): - If `next_step` is None, the flow is ended with `FlowResultType.CREATE_ENTRY`. """ - suggested_values: Callable[[SchemaCommonFlowHandler], dict[str, Any]] | None = None + suggested_values: Callable[ + [SchemaCommonFlowHandler], dict[str, Any] + ] | None | UndefinedType = UNDEFINED """Optional property to populate suggested values. - - If `suggested_values` is None, each key in the schema will get a suggested value + - If `suggested_values` is UNDEFINED, each key in the schema will get a suggested value from an option with the same key. Note: if a step is retried due to a validation failure, then the user input will have @@ -101,6 +104,11 @@ class SchemaCommonFlowHandler: """Return parent handler.""" return self._handler + @property + def options(self) -> dict[str, Any]: + """Return the options linked to the current flow handler.""" + return self._options + async def async_step( self, step_id: str, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -189,10 +197,12 @@ class SchemaCommonFlowHandler: if (data_schema := self._get_schema(form_step)) is None: return self._show_next_step_or_create_entry(form_step) - if form_step.suggested_values: - suggested_values = form_step.suggested_values(self) - else: + suggested_values: dict[str, Any] = {} + if form_step.suggested_values is UNDEFINED: suggested_values = self._options + elif form_step.suggested_values: + suggested_values = form_step.suggested_values(self) + if user_input: # We don't want to mutate the existing options suggested_values = copy.deepcopy(suggested_values) diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py index b097dc7a5a0..9e7a895eadd 100644 --- a/tests/components/scrape/test_config_flow.py +++ b/tests/components/scrape/test_config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations from unittest.mock import patch +import uuid from homeassistant import config_entries from homeassistant.components.rest.data import DEFAULT_TIMEOUT @@ -156,17 +157,27 @@ async def test_flow_fails(hass: HomeAssistant, get_data: MockRestData) -> None: } -async def test_options_flow(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: - """Test options config flow.""" +async def test_options_resource_flow( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test options flow for a resource.""" state = hass.states.get("sensor.current_version") assert state.state == "Current Version: 2021.12.10" result = await hass.config_entries.options.async_init(loaded_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] == FlowResultType.MENU assert result["step_id"] == "init" + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "resource"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "resource" + mocker = MockRestData("test_scrape_sensor2") with patch("homeassistant.components.rest.RestData", return_value=mocker): result = await hass.config_entries.options.async_configure( @@ -182,29 +193,100 @@ async def test_options_flow(hass: HomeAssistant, loaded_entry: MockConfigEntry) ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"] == { - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_METHOD: "GET", - CONF_VERIFY_SSL: True, - CONF_TIMEOUT: 10.0, - CONF_USERNAME: "secret_username", - CONF_PASSWORD: "secret_password", - "sensor": [ - { - CONF_NAME: "Current version", - CONF_SELECT: ".current-version h1", - CONF_INDEX: 0.0, - CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", - } - ], - } + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10.0, + CONF_USERNAME: "secret_username", + CONF_PASSWORD: "secret_password", + "sensor": [ + { + CONF_NAME: "Current version", + CONF_SELECT: ".current-version h1", + CONF_INDEX: 0.0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", + } + ], + } + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 1 + + # Check the state of the entity has changed as expected + state = hass.states.get("sensor.current_version") + assert state.state == "Hidden Version: 2021.12.10" + + +async def test_options_add_sensor_flow( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test options flow to add a sensor.""" + + state = hass.states.get("sensor.current_version") + assert state.state == "Current Version: 2021.12.10" + + result = await hass.config_entries.options.async_init(loaded_entry.entry_id) + + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "add_sensor"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_sensor" + + mocker = MockRestData("test_scrape_sensor2") + with patch("homeassistant.components.rest.RestData", return_value=mocker), patch( + "homeassistant.components.scrape.config_flow.uuid.uuid1", + return_value=uuid.UUID("3699ef88-69e6-11ed-a1eb-0242ac120003"), + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Template", + CONF_SELECT: "template", + CONF_INDEX: 0.0, + }, + ) await hass.async_block_till_done() - # Check the entity was updated, no new entity was created - assert len(hass.states.async_all()) == 1 + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10, + "sensor": [ + { + CONF_NAME: "Current version", + CONF_SELECT: ".current-version h1", + CONF_INDEX: 0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", + }, + { + CONF_NAME: "Template", + CONF_SELECT: "template", + CONF_INDEX: 0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120003", + }, + ], + } - # Check the state of the entity has changed as expected - state = hass.states.get("sensor.current_version") - assert state.state == "Hidden Version: 2021.12.10" + await hass.async_block_till_done() + + # Check the entity was updated, with the new entity + assert len(hass.states.async_all()) == 2 + + # Check the state of the entity has changed as expected + state = hass.states.get("sensor.current_version") + assert state.state == "Hidden Version: 2021.12.10" + + state = hass.states.get("sensor.template") + assert state.state == "Trying to get"