mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
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:
parent
258b9fe663
commit
53e05dec02
@ -40,7 +40,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
|||||||
SchemaConfigFlowHandler,
|
SchemaConfigFlowHandler,
|
||||||
SchemaFlowError,
|
SchemaFlowError,
|
||||||
SchemaFlowFormStep,
|
SchemaFlowFormStep,
|
||||||
SchemaOptionsFlowHandler,
|
SchemaFlowMenuStep,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.selector import (
|
from homeassistant.helpers.selector import (
|
||||||
BooleanSelector,
|
BooleanSelector,
|
||||||
@ -130,16 +130,15 @@ def validate_rest_setup(
|
|||||||
def validate_sensor_setup(
|
def validate_sensor_setup(
|
||||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Validate sensor setup."""
|
"""Validate sensor input."""
|
||||||
return {
|
user_input[CONF_INDEX] = int(user_input[CONF_INDEX])
|
||||||
"sensor": [
|
user_input[CONF_UNIQUE_ID] = str(uuid.uuid1())
|
||||||
{
|
|
||||||
**user_input,
|
# Standard behavior is to merge the result with the options.
|
||||||
CONF_INDEX: int(user_input[CONF_INDEX]),
|
# In this case, we want to add a sub-item so we update the options directly.
|
||||||
CONF_UNIQUE_ID: str(uuid.uuid1()),
|
sensors: list[dict[str, Any]] = handler.options.setdefault("sensor", [])
|
||||||
}
|
sensors.append(user_input)
|
||||||
]
|
return {}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
DATA_SCHEMA_RESOURCE = vol.Schema(RESOURCE_SETUP)
|
DATA_SCHEMA_RESOURCE = vol.Schema(RESOURCE_SETUP)
|
||||||
@ -157,7 +156,16 @@ CONFIG_FLOW = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
OPTIONS_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:
|
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||||
"""Return config entry title."""
|
"""Return config entry title."""
|
||||||
return options[CONF_RESOURCE]
|
return options[CONF_RESOURCE]
|
||||||
|
|
||||||
|
|
||||||
class ScrapeOptionsFlowHandler(SchemaOptionsFlowHandler):
|
|
||||||
"""Handle a config flow for Scrape."""
|
|
||||||
|
@ -52,6 +52,33 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"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": {
|
"data": {
|
||||||
"resource": "[%key:component::scrape::config::step::user::data::resource%]",
|
"resource": "[%key:component::scrape::config::step::user::data::resource%]",
|
||||||
"method": "[%key:component::scrape::config::step::user::data::method%]",
|
"method": "[%key:component::scrape::config::step::user::data::method%]",
|
||||||
|
@ -58,6 +58,33 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"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": {
|
"data": {
|
||||||
"authentication": "Select authentication method",
|
"authentication": "Select authentication method",
|
||||||
"headers": "Headers",
|
"headers": "Headers",
|
||||||
|
@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant, callback, split_entity_id
|
|||||||
from homeassistant.data_entry_flow import FlowResult, UnknownHandler
|
from homeassistant.data_entry_flow import FlowResult, UnknownHandler
|
||||||
|
|
||||||
from . import entity_registry as er, selector
|
from . import entity_registry as er, selector
|
||||||
|
from .typing import UNDEFINED, UndefinedType
|
||||||
|
|
||||||
|
|
||||||
class SchemaFlowError(Exception):
|
class SchemaFlowError(Exception):
|
||||||
@ -63,10 +64,12 @@ class SchemaFlowFormStep(SchemaFlowStep):
|
|||||||
- If `next_step` is None, the flow is ended with `FlowResultType.CREATE_ENTRY`.
|
- 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.
|
"""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.
|
from an option with the same key.
|
||||||
|
|
||||||
Note: if a step is retried due to a validation failure, then the user input will have
|
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 parent handler."""
|
||||||
return self._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(
|
async def async_step(
|
||||||
self, step_id: str, user_input: dict[str, Any] | None = None
|
self, step_id: str, user_input: dict[str, Any] | None = None
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
@ -189,10 +197,12 @@ class SchemaCommonFlowHandler:
|
|||||||
if (data_schema := self._get_schema(form_step)) is None:
|
if (data_schema := self._get_schema(form_step)) is None:
|
||||||
return self._show_next_step_or_create_entry(form_step)
|
return self._show_next_step_or_create_entry(form_step)
|
||||||
|
|
||||||
if form_step.suggested_values:
|
suggested_values: dict[str, Any] = {}
|
||||||
suggested_values = form_step.suggested_values(self)
|
if form_step.suggested_values is UNDEFINED:
|
||||||
else:
|
|
||||||
suggested_values = self._options
|
suggested_values = self._options
|
||||||
|
elif form_step.suggested_values:
|
||||||
|
suggested_values = form_step.suggested_values(self)
|
||||||
|
|
||||||
if user_input:
|
if user_input:
|
||||||
# We don't want to mutate the existing options
|
# We don't want to mutate the existing options
|
||||||
suggested_values = copy.deepcopy(suggested_values)
|
suggested_values = copy.deepcopy(suggested_values)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
import uuid
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.rest.data import DEFAULT_TIMEOUT
|
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:
|
async def test_options_resource_flow(
|
||||||
"""Test options config flow."""
|
hass: HomeAssistant, loaded_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test options flow for a resource."""
|
||||||
|
|
||||||
state = hass.states.get("sensor.current_version")
|
state = hass.states.get("sensor.current_version")
|
||||||
assert state.state == "Current Version: 2021.12.10"
|
assert state.state == "Current Version: 2021.12.10"
|
||||||
|
|
||||||
result = await hass.config_entries.options.async_init(loaded_entry.entry_id)
|
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"
|
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")
|
mocker = MockRestData("test_scrape_sensor2")
|
||||||
with patch("homeassistant.components.rest.RestData", return_value=mocker):
|
with patch("homeassistant.components.rest.RestData", return_value=mocker):
|
||||||
result = await hass.config_entries.options.async_configure(
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result["data"] == {
|
assert result["data"] == {
|
||||||
CONF_RESOURCE: "https://www.home-assistant.io",
|
CONF_RESOURCE: "https://www.home-assistant.io",
|
||||||
CONF_METHOD: "GET",
|
CONF_METHOD: "GET",
|
||||||
CONF_VERIFY_SSL: True,
|
CONF_VERIFY_SSL: True,
|
||||||
CONF_TIMEOUT: 10.0,
|
CONF_TIMEOUT: 10.0,
|
||||||
CONF_USERNAME: "secret_username",
|
CONF_USERNAME: "secret_username",
|
||||||
CONF_PASSWORD: "secret_password",
|
CONF_PASSWORD: "secret_password",
|
||||||
"sensor": [
|
"sensor": [
|
||||||
{
|
{
|
||||||
CONF_NAME: "Current version",
|
CONF_NAME: "Current version",
|
||||||
CONF_SELECT: ".current-version h1",
|
CONF_SELECT: ".current-version h1",
|
||||||
CONF_INDEX: 0.0,
|
CONF_INDEX: 0.0,
|
||||||
CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002",
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# Check the entity was updated, no new entity was created
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert len(hass.states.async_all()) == 1
|
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
|
await hass.async_block_till_done()
|
||||||
state = hass.states.get("sensor.current_version")
|
|
||||||
assert state.state == "Hidden Version: 2021.12.10"
|
# 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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user