From cbf8a41eede0f66e12dde5c535a7b4c862c97f03 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:16:28 +0100 Subject: [PATCH] Add ability to edit sensors in scrape config flow (#82926) * Add ability to edit sensors in scrape config flow * Fix docstring * Update homeassistant/components/scrape/config_flow.py Co-authored-by: Erik Montnemery * docstring Co-authored-by: Erik Montnemery --- .../components/scrape/config_flow.py | 67 ++++++++++++++++++- homeassistant/components/scrape/strings.json | 22 ++++++ .../components/scrape/translations/en.json | 35 +++++----- tests/components/scrape/test_config_flow.py | 66 ++++++++++++++++++ 4 files changed, 172 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/scrape/config_flow.py b/homeassistant/components/scrape/config_flow.py index f4b6420df66..cbd0ed7d525 100644 --- a/homeassistant/components/scrape/config_flow.py +++ b/homeassistant/components/scrape/config_flow.py @@ -87,7 +87,6 @@ RESOURCE_SETUP = { } SENSOR_SETUP = { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): TextSelector(), vol.Required(CONF_SELECT): TextSelector(), vol.Optional(CONF_INDEX, default=0): NumberSelector( NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) @@ -146,6 +145,49 @@ async def validate_sensor_setup( return {} +async def validate_select_sensor( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Store sensor index in flow state.""" + handler.flow_state["_idx"] = int(user_input[CONF_INDEX]) + return {} + + +async def get_select_sensor_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema for selecting a sensor.""" + return vol.Schema( + { + vol.Required(CONF_INDEX): vol.In( + { + str(index): config[CONF_NAME] + for index, config in enumerate(handler.options[SENSOR_DOMAIN]) + }, + ) + } + ) + + +async def get_edit_sensor_suggested_values( + handler: SchemaCommonFlowHandler, +) -> dict[str, Any]: + """Return suggested values for sensor editing.""" + idx: int = handler.flow_state["_idx"] + return handler.options[SENSOR_DOMAIN][idx] + + +async def validate_sensor_edit( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Update edited sensor.""" + user_input[CONF_INDEX] = int(user_input[CONF_INDEX]) + + # 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. + idx: int = handler.flow_state["_idx"] + handler.options[SENSOR_DOMAIN][idx].update(user_input) + return {} + + async def get_remove_sensor_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: """Return schema for sensor removal.""" return vol.Schema( @@ -183,7 +225,13 @@ async def validate_remove_sensor( DATA_SCHEMA_RESOURCE = vol.Schema(RESOURCE_SETUP) -DATA_SCHEMA_SENSOR = vol.Schema(SENSOR_SETUP) +DATA_SCHEMA_EDIT_SENSOR = vol.Schema(SENSOR_SETUP) +DATA_SCHEMA_SENSOR = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): TextSelector(), + **SENSOR_SETUP, + } +) CONFIG_FLOW = { "user": SchemaFlowFormStep( @@ -197,7 +245,9 @@ CONFIG_FLOW = { ), } OPTIONS_FLOW = { - "init": SchemaFlowMenuStep(["resource", "add_sensor", "remove_sensor"]), + "init": SchemaFlowMenuStep( + ["resource", "add_sensor", "select_edit_sensor", "remove_sensor"] + ), "resource": SchemaFlowFormStep( DATA_SCHEMA_RESOURCE, validate_user_input=validate_rest_setup, @@ -207,6 +257,17 @@ OPTIONS_FLOW = { suggested_values=None, validate_user_input=validate_sensor_setup, ), + "select_edit_sensor": SchemaFlowFormStep( + get_select_sensor_schema, + suggested_values=None, + validate_user_input=validate_select_sensor, + next_step="edit_sensor", + ), + "edit_sensor": SchemaFlowFormStep( + DATA_SCHEMA_EDIT_SENSOR, + suggested_values=get_edit_sensor_suggested_values, + validate_user_input=validate_sensor_edit, + ), "remove_sensor": SchemaFlowFormStep( get_remove_sensor_schema, suggested_values=None, diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index 1ac50c695c6..907aa2a9dfd 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -54,6 +54,7 @@ "init": { "menu_options": { "add_sensor": "Add sensor", + "select_edit_sensor": "Configure sensor", "remove_sensor": "Remove sensor", "resource": "Configure resource" } @@ -79,6 +80,27 @@ "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data_description::unit_of_measurement%]" } }, + "edit_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%]", diff --git a/homeassistant/components/scrape/translations/en.json b/homeassistant/components/scrape/translations/en.json index ce66908a75d..4b0e96da680 100644 --- a/homeassistant/components/scrape/translations/en.json +++ b/homeassistant/components/scrape/translations/en.json @@ -78,26 +78,31 @@ "value_template": "Defines a template to get the state of the sensor" } }, - "init": { + "edit_sensor": { "data": { - "authentication": "Select authentication method", - "headers": "Headers", - "method": "Method", - "password": "Password", - "resource": "Resource", - "timeout": "Timeout", - "username": "Username", - "verify_ssl": "Verify SSL certificate" + "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": { - "authentication": "Type of the HTTP authentication. Either basic or digest", - "headers": "Headers to use for the web request", - "resource": "The URL to the website that contains the value", - "timeout": "Timeout for connection to website", - "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed" - }, + "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" + } + }, + "init": { "menu_options": { "add_sensor": "Add sensor", + "select_edit_sensor": "Configure sensor", "remove_sensor": "Remove sensor", "resource": "Configure resource" } diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py index 883d9ec4ab9..f6df8a80f21 100644 --- a/tests/components/scrape/test_config_flow.py +++ b/tests/components/scrape/test_config_flow.py @@ -356,3 +356,69 @@ async def test_options_add_remove_sensor_flow( # Check the state of the new entity state = hass.states.get("sensor.template") assert state.state == "Trying to get" + + +async def test_options_edit_sensor_flow( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test options flow to edit 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": "select_edit_sensor"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_sensor" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "edit_sensor" + + mocker = MockRestData("test_scrape_sensor2") + with patch("homeassistant.components.rest.RestData", return_value=mocker): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_SELECT: "template", + CONF_INDEX: 0.0, + }, + ) + 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, + "sensor": [ + { + CONF_NAME: "Current version", + CONF_SELECT: "template", + CONF_INDEX: 0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", + }, + ], + } + + await hass.async_block_till_done() + + # Check the entity was updated + 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 == "Trying to get"