Add ability to add sensors in scrape config flow (#82802)

* Add ability to add sensors in scrape config flow

* Fix menu

* Adjust comment

* Use sentinel

* Adjust docstring
This commit is contained in:
epenet 2022-11-29 10:13:38 +01:00 committed by GitHub
parent 258b9fe663
commit 53e05dec02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 196 additions and 46 deletions

View File

@ -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."""

View File

@ -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%]",

View File

@ -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",

View File

@ -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)

View File

@ -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(
@ -208,3 +219,74 @@ async def test_options_flow(hass: HomeAssistant, loaded_entry: MockConfigEntry)
# 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()
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",
},
],
}
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"