Add config flow to Scrape (#81193)

* Scrape take 2

* cleanup

* new entity name

* Fix name, add tests

* Use FlowResultType

* Add test abort

* hassfest

* Remove not needed test

* clean

* Remove config entry and implement datacoordinator

* fix codeowners

* fix codeowners

* codeowners reset

* Fix coordinator

* Remove test config_flow

* Fix tests

* hassfest

* reset config flow

* reset strings

* reset sensor

* Reconfig

* Fix tests

* coverage

* Remove coverage

* Remove print

* Add config flow

* Fix config flow

* Add back init

* Add entry to sensor

* float to int

* Fix SelectSelector

* Add tests for config entry to existing

* Test config flow

* Fix test reload

* Fix rebase

* Fix strings

* clean init

* Clean test_sensor

* Align sensor setup entry

* Add error to strings

* review changes

* clean tests

* Add back options flow

* review changes

* update_listener

* Add tests

* Remove duplicate abort

* strings

* sensors to sensor

* review changes

* more review changes

* clarify test payload

* fixture name
This commit is contained in:
G Johansson 2022-11-21 21:39:39 +01:00 committed by GitHub
parent 848821139d
commit b3dd59f202
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 528 additions and 87 deletions

View file

@ -10,6 +10,7 @@ import voluptuous as vol
from homeassistant.components.rest import RESOURCE_SCHEMA, create_rest_data_from_config
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_SCAN_INTERVAL,
@ -22,7 +23,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.template_entity import TEMPLATE_SENSOR_BASE_SCHEMA
from homeassistant.helpers.typing import ConfigType
from .const import CONF_INDEX, CONF_SELECT, DEFAULT_SCAN_INTERVAL, DOMAIN
from .const import CONF_INDEX, CONF_SELECT, DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS
from .coordinator import ScrapeCoordinator
SENSOR_SCHEMA = vol.Schema(
@ -79,3 +80,37 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await asyncio.gather(*load_coroutines)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Scrape from a config entry."""
rest_config: dict[str, Any] = COMBINED_SCHEMA(dict(entry.options))
rest = create_rest_data_from_config(hass, rest_config)
coordinator = ScrapeCoordinator(
hass,
rest,
DEFAULT_SCAN_INTERVAL,
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Scrape config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
del hass.data[DOMAIN][entry.entry_id]
if not hass.data[DOMAIN]:
del hass.data[DOMAIN]
return unload_ok
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View file

@ -6,6 +6,9 @@ from typing import Any
import voluptuous as vol
from homeassistant.components.rest import create_rest_data_from_config
from homeassistant.components.rest.data import DEFAULT_TIMEOUT
from homeassistant.components.rest.schema import DEFAULT_METHOD, METHODS
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
SensorDeviceClass,
@ -16,18 +19,23 @@ from homeassistant.const import (
CONF_AUTHENTICATION,
CONF_DEVICE_CLASS,
CONF_HEADERS,
CONF_METHOD,
CONF_NAME,
CONF_PASSWORD,
CONF_RESOURCE,
CONF_TIMEOUT,
CONF_UNIT_OF_MEASUREMENT,
CONF_USERNAME,
CONF_VALUE_TEMPLATE,
CONF_VERIFY_SSL,
HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION,
UnitOfTemperature,
)
from homeassistant.core import async_get_hass
from homeassistant.helpers.schema_config_entry_flow import (
SchemaConfigFlowHandler,
SchemaFlowError,
SchemaFlowFormStep,
SchemaFlowMenuStep,
SchemaOptionsFlowHandler,
@ -47,20 +55,15 @@ from homeassistant.helpers.selector import (
TextSelectorType,
)
from . import COMBINED_SCHEMA
from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DOMAIN
SCHEMA_SETUP = {
vol.Optional(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
RESOURCE_SETUP = {
vol.Required(CONF_RESOURCE): TextSelector(
TextSelectorConfig(type=TextSelectorType.URL)
),
vol.Required(CONF_SELECT): TextSelector(),
}
SCHEMA_OPT = {
vol.Optional(CONF_ATTRIBUTE): TextSelector(),
vol.Optional(CONF_INDEX, default=0): NumberSelector(
NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX)
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): SelectSelector(
SelectSelectorConfig(options=METHODS, mode=SelectSelectorMode.DROPDOWN)
),
vol.Optional(CONF_AUTHENTICATION): SelectSelector(
SelectSelectorConfig(
@ -73,32 +76,74 @@ SCHEMA_OPT = {
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
vol.Optional(CONF_HEADERS): ObjectSelector(),
vol.Optional(CONF_UNIT_OF_MEASUREMENT): TextSelector(),
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): BooleanSelector(),
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): NumberSelector(
NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX)
),
}
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)
),
vol.Optional(CONF_ATTRIBUTE): TextSelector(),
vol.Optional(CONF_VALUE_TEMPLATE): TemplateSelector(),
vol.Optional(CONF_DEVICE_CLASS): SelectSelector(
SelectSelectorConfig(
options=[e.value for e in SensorDeviceClass],
options=[cls.value for cls in SensorDeviceClass],
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(CONF_STATE_CLASS): SelectSelector(
SelectSelectorConfig(
options=[e.value for e in SensorStateClass],
options=[cls.value for cls in SensorStateClass],
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(CONF_UNIT_OF_MEASUREMENT): SelectSelector(
SelectSelectorConfig(
options=[cls.value for cls in UnitOfTemperature],
custom_value=True,
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(CONF_VALUE_TEMPLATE): TemplateSelector(),
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): BooleanSelector(),
}
DATA_SCHEMA = vol.Schema({**SCHEMA_SETUP, **SCHEMA_OPT})
DATA_SCHEMA_OPT = vol.Schema({**SCHEMA_OPT})
def validate_rest_setup(user_input: dict[str, Any]) -> dict[str, Any]:
"""Validate rest setup."""
hass = async_get_hass()
rest_config: dict[str, Any] = COMBINED_SCHEMA(user_input)
try:
create_rest_data_from_config(hass, rest_config)
except Exception as err:
raise SchemaFlowError("resource_error") from err
return user_input
def validate_sensor_setup(user_input: dict[str, Any]) -> dict[str, Any]:
"""Validate sensor setup."""
return {"sensor": [{**user_input, CONF_INDEX: int(user_input[CONF_INDEX])}]}
DATA_SCHEMA_RESOURCE = vol.Schema(RESOURCE_SETUP)
DATA_SCHEMA_SENSOR = vol.Schema(SENSOR_SETUP)
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
"user": SchemaFlowFormStep(DATA_SCHEMA),
"import": SchemaFlowFormStep(DATA_SCHEMA),
"user": SchemaFlowFormStep(
schema=DATA_SCHEMA_RESOURCE,
next_step=lambda _: "sensor",
validate_user_input=validate_rest_setup,
),
"sensor": SchemaFlowFormStep(
schema=DATA_SCHEMA_SENSOR,
validate_user_input=validate_sensor_setup,
),
}
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
"init": SchemaFlowFormStep(DATA_SCHEMA_OPT),
"init": SchemaFlowFormStep(DATA_SCHEMA_RESOURCE),
}
@ -110,12 +155,7 @@ class ScrapeConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title."""
return options[CONF_NAME]
def async_config_flow_finished(self, options: Mapping[str, Any]) -> None:
"""Check for duplicate records."""
data: dict[str, Any] = dict(options)
self._async_abort_entries_match(data)
return options[CONF_RESOURCE]
class ScrapeOptionsFlowHandler(SchemaOptionsFlowHandler):

View file

@ -5,5 +5,6 @@
"requirements": ["beautifulsoup4==4.11.1", "lxml==4.9.1"],
"after_dependencies": ["rest"],
"codeowners": ["@fabaff", "@gjohansson-ST", "@epenet"],
"iot_class": "cloud_polling"
"iot_class": "cloud_polling",
"config_flow": true
}

View file

@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
STATE_CLASSES_SCHEMA,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_AUTHENTICATION,
@ -144,6 +145,45 @@ async def async_setup_platform(
async_add_entities(entities)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Scrape sensor entry."""
entities: list = []
coordinator: ScrapeCoordinator = hass.data[DOMAIN][entry.entry_id]
config = dict(entry.options)
for sensor in config["sensor"]:
sensor_config: ConfigType = vol.Schema(
TEMPLATE_SENSOR_BASE_SCHEMA.schema, extra=vol.ALLOW_EXTRA
)(sensor)
name: str = sensor_config[CONF_NAME]
select: str = sensor_config[CONF_SELECT]
attr: str | None = sensor_config.get(CONF_ATTRIBUTE)
index: int = int(sensor_config[CONF_INDEX])
value_string: str | None = sensor_config.get(CONF_VALUE_TEMPLATE)
value_template: Template | None = (
Template(value_string, hass) if value_string is not None else None
)
entities.append(
ScrapeSensor(
hass,
coordinator,
sensor_config,
name,
None,
select,
attr,
index,
value_template,
)
)
async_add_entities(entities)
class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], TemplateSensor):
"""Representation of a web scrape sensor."""

View file

@ -3,35 +3,48 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"resource_error": "Could not update rest data. Verify your configuration"
},
"step": {
"user": {
"data": {
"name": "[%key:common::config_flow::data::name%]",
"resource": "Resource",
"select": "Select",
"attribute": "Attribute",
"index": "Index",
"value_template": "Value Template",
"unit_of_measurement": "Unit of Measurement",
"device_class": "Device Class",
"state_class": "State Class",
"authentication": "Authentication",
"authentication": "Select authentication method",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"headers": "Headers"
"headers": "Headers",
"method": "Method",
"timeout": "Timeout"
},
"data_description": {
"resource": "The URL to the website that contains the value",
"authentication": "Type of the HTTP authentication. Either basic or digest",
"verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed",
"headers": "Headers to use for the web request",
"timeout": "Timeout for connection to website"
}
},
"sensor": {
"data": {
"name": "[%key:common::config_flow::data::name%]",
"attribute": "Attribute",
"index": "Index",
"select": "Select",
"value_template": "Value Template",
"device_class": "Device Class",
"state_class": "State Class",
"unit_of_measurement": "Unit of Measurement"
},
"data_description": {
"select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details",
"attribute": "Get value of an attribute on the selected tag",
"index": "Defines which of the elements returned by the CSS selector to use",
"value_template": "Defines a template to get the state of the sensor",
"device_class": "The type/class of the sensor to set the icon in the frontend",
"state_class": "The state_class of the sensor",
"authentication": "Type of the HTTP authentication. Either basic or digest",
"verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed",
"headers": "Headers to use for the web request"
"unit_of_measurement": "Choose temperature measurement or create your own"
}
}
}
@ -40,32 +53,21 @@
"step": {
"init": {
"data": {
"name": "[%key:component::scrape::config::step::user::data::name%]",
"resource": "[%key:component::scrape::config::step::user::data::resource%]",
"select": "[%key:component::scrape::config::step::user::data::select%]",
"attribute": "[%key:component::scrape::config::step::user::data::attribute%]",
"index": "[%key:component::scrape::config::step::user::data::index%]",
"value_template": "[%key:component::scrape::config::step::user::data::value_template%]",
"unit_of_measurement": "[%key:component::scrape::config::step::user::data::unit_of_measurement%]",
"device_class": "[%key:component::scrape::config::step::user::data::device_class%]",
"state_class": "[%key:component::scrape::config::step::user::data::state_class%]",
"method": "[%key:component::scrape::config::step::user::data::method%]",
"authentication": "[%key:component::scrape::config::step::user::data::authentication%]",
"verify_ssl": "[%key:component::scrape::config::step::user::data::verify_ssl%]",
"username": "[%key:component::scrape::config::step::user::data::username%]",
"password": "[%key:component::scrape::config::step::user::data::password%]",
"headers": "[%key:component::scrape::config::step::user::data::headers%]"
"headers": "[%key:component::scrape::config::step::user::data::headers%]",
"verify_ssl": "[%key:component::scrape::config::step::user::data::verify_ssl%]",
"timeout": "[%key:component::scrape::config::step::user::data::timeout%]"
},
"data_description": {
"resource": "[%key:component::scrape::config::step::user::data_description::resource%]",
"select": "[%key:component::scrape::config::step::user::data_description::select%]",
"attribute": "[%key:component::scrape::config::step::user::data_description::attribute%]",
"index": "[%key:component::scrape::config::step::user::data_description::index%]",
"value_template": "[%key:component::scrape::config::step::user::data_description::value_template%]",
"device_class": "[%key:component::scrape::config::step::user::data_description::device_class%]",
"state_class": "[%key:component::scrape::config::step::user::data_description::state_class%]",
"authentication": "[%key:component::scrape::config::step::user::data_description::authentication%]",
"headers": "[%key:component::scrape::config::step::user::data_description::headers%]",
"verify_ssl": "[%key:component::scrape::config::step::user::data_description::verify_ssl%]",
"headers": "[%key:component::scrape::config::step::user::data_description::headers%]"
"timeout": "[%key:component::scrape::config::step::user::data_description::timeout%]"
}
}
}

View file

@ -3,34 +3,47 @@
"abort": {
"already_configured": "Account is already configured"
},
"error": {
"resource_error": "Could not update rest data. Verify your configuration"
},
"step": {
"user": {
"sensor": {
"data": {
"attribute": "Attribute",
"authentication": "Authentication",
"device_class": "Device Class",
"headers": "Headers",
"index": "Index",
"name": "Name",
"password": "Password",
"resource": "Resource",
"select": "Select",
"state_class": "State Class",
"unit_of_measurement": "Unit of Measurement",
"username": "Username",
"value_template": "Value Template",
"verify_ssl": "Verify SSL certificate"
"value_template": "Value Template"
},
"data_description": {
"attribute": "Get value of an attribute on the selected tag",
"authentication": "Type of the HTTP authentication. Either basic or digest",
"device_class": "The type/class of the sensor to set the icon in the frontend",
"headers": "Headers to use for the web request",
"index": "Defines which of the elements returned by the CSS selector to use",
"resource": "The URL to the website that contains the value",
"select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details",
"state_class": "The state_class of the sensor",
"value_template": "Defines a template to get the state 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"
}
},
"user": {
"data": {
"authentication": "Select authentication method",
"headers": "Headers",
"method": "Method",
"password": "Password",
"resource": "Resource",
"timeout": "Timeout",
"username": "Username",
"verify_ssl": "Verify SSL certificate"
},
"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"
}
}
@ -46,31 +59,20 @@
"step": {
"init": {
"data": {
"attribute": "Attribute",
"authentication": "Authentication",
"device_class": "Device Class",
"authentication": "Select authentication method",
"headers": "Headers",
"index": "Index",
"name": "Name",
"method": "Method",
"password": "Password",
"resource": "Resource",
"select": "Select",
"state_class": "State Class",
"unit_of_measurement": "Unit of Measurement",
"timeout": "Timeout",
"username": "Username",
"value_template": "Value Template",
"verify_ssl": "Verify SSL certificate"
},
"data_description": {
"attribute": "Get value of an attribute on the selected tag",
"authentication": "Type of the HTTP authentication. Either basic or digest",
"device_class": "The type/class of the sensor to set the icon in the frontend",
"headers": "Headers to use for the web request",
"index": "Defines which of the elements returned by the CSS selector to use",
"resource": "The URL to the website that contains the value",
"select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details",
"state_class": "The state_class of the sensor",
"value_template": "Defines a template to get the state of the sensor",
"timeout": "Timeout for connection to website",
"verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed"
}
}

View file

@ -347,6 +347,7 @@ FLOWS = {
"ruuvitag_ble",
"sabnzbd",
"samsungtv",
"scrape",
"screenlogic",
"season",
"sense",

View file

@ -4597,7 +4597,7 @@
"scrape": {
"name": "Scrape",
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "cloud_polling"
},
"screenaway": {

View file

@ -98,11 +98,20 @@ class MockRestData:
self.count += 1
if self.payload == "test_scrape_sensor":
self.data = (
# Default
"<div class='current-version material-card text'>"
"<h1>Current Version: 2021.12.10</h1>Released: <span class='release-date'>January 17, 2022</span>"
"<div class='links' style='links'><a href='/latest-release-notes/'>Release notes</a></div></div>"
"<template>Trying to get</template>"
)
if self.payload == "test_scrape_sensor2":
self.data = (
# Hidden version
"<div class='current-version material-card text'>"
"<h1>Hidden Version: 2021.12.10</h1>Released: <span class='release-date'>January 17, 2022</span>"
"<div class='links' style='links'><a href='/latest-release-notes/'>Release notes</a></div></div>"
"<template>Trying to get</template>"
)
if self.payload == "test_scrape_uom_and_classes":
self.data = (
"<div class='current-temp temp-card text'>"

View file

@ -0,0 +1,80 @@
"""Fixtures for the Scrape integration."""
from __future__ import annotations
from typing import Any
from unittest.mock import patch
import pytest
from homeassistant.components.rest.data import DEFAULT_TIMEOUT
from homeassistant.components.rest.schema import DEFAULT_METHOD, DEFAULT_VERIFY_SSL
from homeassistant.components.scrape.const import CONF_INDEX, CONF_SELECT, DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import (
CONF_METHOD,
CONF_NAME,
CONF_RESOURCE,
CONF_TIMEOUT,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from . import MockRestData
from tests.common import MockConfigEntry
@pytest.fixture(name="get_config")
async def get_config_to_integration_load() -> dict[str, Any]:
"""Return default minimal configuration.
To override the config, tests can be marked with:
@pytest.mark.parametrize("get_config", [{...}])
"""
return {
CONF_RESOURCE: "https://www.home-assistant.io",
CONF_METHOD: DEFAULT_METHOD,
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
CONF_TIMEOUT: DEFAULT_TIMEOUT,
"sensor": [
{
CONF_NAME: "Current version",
CONF_SELECT: ".current-version h1",
CONF_INDEX: 0,
}
],
}
@pytest.fixture(name="get_data")
async def get_data_to_integration_load() -> MockRestData:
"""Return RestData.
To override the config, tests can be marked with:
@pytest.mark.parametrize("get_data", [{...}])
"""
return MockRestData("test_scrape_sensor")
@pytest.fixture(name="loaded_entry")
async def load_integration(
hass: HomeAssistant, get_config: dict[str, Any], get_data: MockRestData
) -> MockConfigEntry:
"""Set up the Scrape integration in Home Assistant."""
config_entry = MockConfigEntry(
domain=DOMAIN,
source=SOURCE_USER,
options=get_config,
entry_id="1",
)
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.rest.RestData",
return_value=get_data,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry

View file

@ -0,0 +1,206 @@
"""Test the Scrape config flow."""
from __future__ import annotations
from unittest.mock import patch
from homeassistant import config_entries
from homeassistant.components.rest.data import DEFAULT_TIMEOUT
from homeassistant.components.rest.schema import DEFAULT_METHOD
from homeassistant.components.scrape import DOMAIN
from homeassistant.components.scrape.const import (
CONF_INDEX,
CONF_SELECT,
DEFAULT_VERIFY_SSL,
)
from homeassistant.const import (
CONF_METHOD,
CONF_NAME,
CONF_PASSWORD,
CONF_RESOURCE,
CONF_TIMEOUT,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.exceptions import HomeAssistantError
from . import MockRestData
from tests.common import MockConfigEntry
async def test_form(hass: HomeAssistant, get_data: MockRestData) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["step_id"] == "user"
assert result["type"] == FlowResultType.FORM
with patch(
"homeassistant.components.rest.RestData",
return_value=get_data,
) as mock_data, patch(
"homeassistant.components.scrape.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_RESOURCE: "https://www.home-assistant.io",
CONF_METHOD: "GET",
CONF_VERIFY_SSL: True,
CONF_TIMEOUT: 10.0,
},
)
await hass.async_block_till_done()
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_NAME: "Current version",
CONF_SELECT: ".current-version h1",
CONF_INDEX: 0.0,
},
)
await hass.async_block_till_done()
assert result3["type"] == FlowResultType.CREATE_ENTRY
assert result3["version"] == 1
assert result3["options"] == {
CONF_RESOURCE: "https://www.home-assistant.io",
CONF_METHOD: "GET",
CONF_VERIFY_SSL: True,
CONF_TIMEOUT: 10.0,
"sensor": [
{
CONF_NAME: "Current version",
CONF_SELECT: ".current-version h1",
CONF_INDEX: 0.0,
}
],
}
assert len(mock_data.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_flow_fails(hass: HomeAssistant, get_data: MockRestData) -> None:
"""Test config flow error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == config_entries.SOURCE_USER
with patch(
"homeassistant.components.rest.RestData",
side_effect=HomeAssistantError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_RESOURCE: "https://www.home-assistant.io",
CONF_METHOD: "GET",
CONF_VERIFY_SSL: True,
CONF_TIMEOUT: 10.0,
},
)
assert result2["errors"] == {"base": "resource_error"}
with patch("homeassistant.components.rest.RestData", return_value=get_data,), patch(
"homeassistant.components.scrape.async_setup_entry",
return_value=True,
):
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_RESOURCE: "https://www.home-assistant.io",
CONF_METHOD: "GET",
CONF_VERIFY_SSL: True,
CONF_TIMEOUT: 10.0,
},
)
await hass.async_block_till_done()
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
{
CONF_NAME: "Current version",
CONF_SELECT: ".current-version h1",
CONF_INDEX: 0.0,
},
)
await hass.async_block_till_done()
assert result4["type"] == FlowResultType.CREATE_ENTRY
assert result4["title"] == "https://www.home-assistant.io"
assert result4["options"] == {
CONF_RESOURCE: "https://www.home-assistant.io",
CONF_METHOD: "GET",
CONF_VERIFY_SSL: True,
CONF_TIMEOUT: 10.0,
"sensor": [
{
CONF_NAME: "Current version",
CONF_SELECT: ".current-version h1",
CONF_INDEX: 0.0,
}
],
}
async def test_options_flow(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None:
"""Test options config flow."""
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["step_id"] == "init"
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_RESOURCE: "https://www.home-assistant.io",
CONF_METHOD: DEFAULT_METHOD,
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
CONF_TIMEOUT: DEFAULT_TIMEOUT,
CONF_USERNAME: "secret_username",
CONF_PASSWORD: "secret_password",
},
)
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,
}
],
}
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"

View file

@ -6,6 +6,7 @@ from unittest.mock import patch
import pytest
from homeassistant import config_entries
from homeassistant.components.scrape.const import DEFAULT_SCAN_INTERVAL, DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -13,7 +14,7 @@ from homeassistant.setup import async_setup_component
from . import MockRestData, return_integration_config
from tests.common import async_fire_time_changed
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_setup_config(hass: HomeAssistant) -> None:
@ -109,3 +110,18 @@ async def test_setup_config_no_sensors(
):
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
async def test_setup_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None:
"""Test setup entry."""
assert loaded_entry.state == config_entries.ConfigEntryState.LOADED
async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None:
"""Test unload an entry."""
assert loaded_entry.state == config_entries.ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(loaded_entry.entry_id)
await hass.async_block_till_done()
assert loaded_entry.state is config_entries.ConfigEntryState.NOT_LOADED

View file

@ -26,7 +26,7 @@ from homeassistant.setup import async_setup_component
from . import MockRestData, return_config, return_integration_config
from tests.common import async_fire_time_changed
from tests.common import MockConfigEntry, async_fire_time_changed
DOMAIN = "scrape"
@ -405,3 +405,12 @@ async def test_scrape_sensor_unique_id(hass: HomeAssistant) -> None:
entity = entity_reg.async_get("sensor.ha_version")
assert entity.unique_id == "ha_version_unique_id"
async def test_setup_config_entry(
hass: HomeAssistant, loaded_entry: MockConfigEntry
) -> None:
"""Test setup from config entry."""
state = hass.states.get("sensor.current_version")
assert state.state == "Current Version: 2021.12.10"