Config flow for scrape integration (#70476)

This commit is contained in:
G Johansson 2022-06-03 21:24:04 +02:00 committed by GitHub
parent 5ee2f4f438
commit 8d0dd1fe8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 853 additions and 142 deletions

View file

@ -887,8 +887,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/scene/ @home-assistant/core
/tests/components/scene/ @home-assistant/core
/homeassistant/components/schluter/ @prairieapps
/homeassistant/components/scrape/ @fabaff
/tests/components/scrape/ @fabaff
/homeassistant/components/scrape/ @fabaff @gjohansson-ST
/tests/components/scrape/ @fabaff @gjohansson-ST
/homeassistant/components/screenlogic/ @dieselrabbit @bdraco
/tests/components/screenlogic/ @dieselrabbit @bdraco
/homeassistant/components/script/ @home-assistant/core

View file

@ -1 +1,64 @@
"""The scrape component."""
from __future__ import annotations
import httpx
from homeassistant.components.rest.data import RestData
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_AUTHENTICATION,
CONF_HEADERS,
CONF_PASSWORD,
CONF_RESOURCE,
CONF_USERNAME,
CONF_VERIFY_SSL,
HTTP_DIGEST_AUTHENTICATION,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN, PLATFORMS
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Scrape from a config entry."""
resource: str = entry.options[CONF_RESOURCE]
method: str = "GET"
payload: str | None = None
headers: str | None = entry.options.get(CONF_HEADERS)
verify_ssl: bool = entry.options[CONF_VERIFY_SSL]
username: str | None = entry.options.get(CONF_USERNAME)
password: str | None = entry.options.get(CONF_PASSWORD)
auth: httpx.DigestAuth | tuple[str, str] | None = None
if username and password:
if entry.options.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
auth = httpx.DigestAuth(username, password)
else:
auth = (username, password)
rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl)
await rest.async_update()
if rest.data is None:
raise ConfigEntryNotReady
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = rest
entry.async_on_unload(entry.add_update_listener(async_update_listener))
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener for options."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Scrape config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View file

@ -0,0 +1,122 @@
"""Adds config flow for Scrape integration."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
import voluptuous as vol
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_AUTHENTICATION,
CONF_DEVICE_CLASS,
CONF_HEADERS,
CONF_NAME,
CONF_PASSWORD,
CONF_RESOURCE,
CONF_UNIT_OF_MEASUREMENT,
CONF_USERNAME,
CONF_VALUE_TEMPLATE,
CONF_VERIFY_SSL,
HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION,
)
from homeassistant.helpers.schema_config_entry_flow import (
SchemaConfigFlowHandler,
SchemaFlowFormStep,
SchemaFlowMenuStep,
SchemaOptionsFlowHandler,
)
from homeassistant.helpers.selector import (
BooleanSelector,
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
ObjectSelector,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TemplateSelector,
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DOMAIN
SCHEMA_SETUP = {
vol.Optional(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
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_AUTHENTICATION): SelectSelector(
SelectSelectorConfig(
options=[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION],
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(CONF_USERNAME): TextSelector(),
vol.Optional(CONF_PASSWORD): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
vol.Optional(CONF_HEADERS): ObjectSelector(),
vol.Optional(CONF_UNIT_OF_MEASUREMENT): TextSelector(),
vol.Optional(CONF_DEVICE_CLASS): SelectSelector(
SelectSelectorConfig(
options=[e.value for e in SensorDeviceClass],
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(CONF_STATE_CLASS): SelectSelector(
SelectSelectorConfig(
options=[e.value for e in SensorStateClass],
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})
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
"user": SchemaFlowFormStep(DATA_SCHEMA),
"import": SchemaFlowFormStep(DATA_SCHEMA),
}
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
"init": SchemaFlowFormStep(DATA_SCHEMA_OPT),
}
class ScrapeConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config flow for Scrape."""
config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW
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)
class ScrapeOptionsFlowHandler(SchemaOptionsFlowHandler):
"""Handle a config flow for Scrape."""

View file

@ -0,0 +1,13 @@
"""Constants for Scrape integration."""
from __future__ import annotations
from homeassistant.const import Platform
DOMAIN = "scrape"
DEFAULT_NAME = "Web scrape"
DEFAULT_VERIFY_SSL = True
PLATFORMS = [Platform.SENSOR]
CONF_SELECT = "select"
CONF_INDEX = "index"

View file

@ -4,6 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/scrape",
"requirements": ["beautifulsoup4==4.11.1", "lxml==4.8.0"],
"after_dependencies": ["rest"],
"codeowners": ["@fabaff"],
"codeowners": ["@fabaff", "@gjohansson-ST"],
"config_flow": true,
"iot_class": "cloud_polling"
}

View file

@ -5,7 +5,6 @@ import logging
from typing import Any
from bs4 import BeautifulSoup
import httpx
import voluptuous as vol
from homeassistant.components.rest.data import RestData
@ -16,7 +15,9 @@ from homeassistant.components.sensor import (
STATE_CLASSES_SCHEMA,
SensorEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_AUTHENTICATION,
CONF_DEVICE_CLASS,
CONF_HEADERS,
@ -31,26 +32,24 @@ from homeassistant.const import (
HTTP_DIGEST_AUTHENTICATION,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_ATTR = "attribute"
CONF_SELECT = "select"
CONF_INDEX = "index"
DEFAULT_NAME = "Web scrape"
DEFAULT_VERIFY_SSL = True
ICON = "mdi:web"
PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_RESOURCE): cv.string,
vol.Required(CONF_SELECT): cv.string,
vol.Optional(CONF_ATTR): cv.string,
vol.Optional(CONF_ATTRIBUTE): cv.string,
vol.Optional(CONF_INDEX, default=0): cv.positive_int,
vol.Optional(CONF_AUTHENTICATION): vol.In(
[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
@ -62,7 +61,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VALUE_TEMPLATE): cv.string,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
}
)
@ -75,37 +74,47 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Web scrape sensor."""
name: str = config[CONF_NAME]
resource: str = config[CONF_RESOURCE]
method: str = "GET"
payload: str | None = None
headers: str | None = config.get(CONF_HEADERS)
verify_ssl: bool = config[CONF_VERIFY_SSL]
select: str | None = config.get(CONF_SELECT)
attr: str | None = config.get(CONF_ATTR)
index: int = config[CONF_INDEX]
unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT)
device_class: str | None = config.get(CONF_DEVICE_CLASS)
state_class: str | None = config.get(CONF_STATE_CLASS)
username: str | None = config.get(CONF_USERNAME)
password: str | None = config.get(CONF_PASSWORD)
value_template: Template | None = config.get(CONF_VALUE_TEMPLATE)
_LOGGER.warning(
# Config flow added in Home Assistant Core 2022.7, remove import flow in 2022.9
"Loading Scrape via platform setup has been deprecated in Home Assistant 2022.7 "
"Your configuration has been automatically imported and you can "
"remove it from your configuration.yaml"
)
if config.get(CONF_VALUE_TEMPLATE):
template: Template = Template(config[CONF_VALUE_TEMPLATE])
template.ensure_valid()
config[CONF_VALUE_TEMPLATE] = template.template
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Scrape sensor entry."""
name: str = entry.options[CONF_NAME]
resource: str = entry.options[CONF_RESOURCE]
select: str | None = entry.options.get(CONF_SELECT)
attr: str | None = entry.options.get(CONF_ATTRIBUTE)
index: int = int(entry.options[CONF_INDEX])
unit: str | None = entry.options.get(CONF_UNIT_OF_MEASUREMENT)
device_class: str | None = entry.options.get(CONF_DEVICE_CLASS)
state_class: str | None = entry.options.get(CONF_STATE_CLASS)
value_template: str | None = entry.options.get(CONF_VALUE_TEMPLATE)
entry_id: str = entry.entry_id
val_template: Template | None = None
if value_template is not None:
value_template.hass = hass
val_template = Template(value_template, hass)
auth: httpx.DigestAuth | tuple[str, str] | None = None
if username and password:
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
auth = httpx.DigestAuth(username, password)
else:
auth = (username, password)
rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl)
await rest.async_update()
if rest.data is None:
raise PlatformNotReady
rest = hass.data.setdefault(DOMAIN, {})[entry.entry_id]
async_add_entities(
[
@ -115,10 +124,12 @@ async def async_setup_platform(
select,
attr,
index,
value_template,
val_template,
unit,
device_class,
state_class,
entry_id,
resource,
)
],
True,
@ -128,6 +139,8 @@ async def async_setup_platform(
class ScrapeSensor(SensorEntity):
"""Representation of a web scrape sensor."""
_attr_icon = ICON
def __init__(
self,
rest: RestData,
@ -139,6 +152,8 @@ class ScrapeSensor(SensorEntity):
unit: str | None,
device_class: str | None,
state_class: str | None,
entry_id: str,
resource: str,
) -> None:
"""Initialize a web scrape sensor."""
self.rest = rest
@ -151,6 +166,14 @@ class ScrapeSensor(SensorEntity):
self._attr_native_unit_of_measurement = unit
self._attr_device_class = device_class
self._attr_state_class = state_class
self._attr_unique_id = entry_id
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, entry_id)},
manufacturer="Scrape",
name=name,
configuration_url=resource,
)
def _extract_value(self) -> Any:
"""Parse the html extraction in the executor."""

View file

@ -0,0 +1,73 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"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",
"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"
},
"data_description": {
"resource": "The URL to the website that contains the value",
"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"
}
}
}
},
"options": {
"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%]",
"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%]"
},
"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%]",
"verify_ssl": "[%key:component::scrape::config::step::user::data_description::verify_ssl%]",
"headers": "[%key:component::scrape::config::step::user::data_description::headers%]"
}
}
}
}
}

View file

@ -0,0 +1,73 @@
{
"config": {
"abort": {
"already_configured": "Account is already configured"
},
"step": {
"user": {
"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"
},
"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",
"verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed"
}
}
}
},
"options": {
"step": {
"init": {
"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"
},
"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",
"verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed"
}
}
}
}
}

View file

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

View file

@ -2,6 +2,42 @@
from __future__ import annotations
from typing import Any
from unittest.mock import patch
from homeassistant.components.scrape.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def init_integration(
hass: HomeAssistant,
config: dict[str, Any],
data: str,
entry_id: str = "1",
source: str = SOURCE_USER,
) -> MockConfigEntry:
"""Set up the Scrape integration in Home Assistant."""
config_entry = MockConfigEntry(
domain=DOMAIN,
source=source,
data={},
options=config,
entry_id=entry_id,
)
config_entry.add_to_hass(hass)
mocker = MockRestData(data)
with patch(
"homeassistant.components.scrape.RestData",
return_value=mocker,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry
def return_config(
@ -25,6 +61,8 @@ def return_config(
"resource": "https://www.home-assistant.io",
"select": select,
"name": name,
"index": 0,
"verify_ssl": True,
}
if attribute:
config["attribute"] = attribute
@ -38,7 +76,7 @@ def return_config(
config["device_class"] = device_class
if state_class:
config["state_class"] = state_class
if authentication:
if username:
config["authentication"] = authentication
config["username"] = username
config["password"] = password

View file

@ -0,0 +1,194 @@
"""Test the Scrape config flow."""
from __future__ import annotations
from unittest.mock import patch
from homeassistant import config_entries
from homeassistant.components.scrape.const import CONF_INDEX, CONF_SELECT, DOMAIN
from homeassistant.const import (
CONF_NAME,
CONF_RESOURCE,
CONF_VALUE_TEMPLATE,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from . import MockRestData
from tests.common import MockConfigEntry
async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
with patch(
"homeassistant.components.scrape.sensor.RestData",
return_value=MockRestData("test_scrape_sensor"),
), 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_NAME: "Release",
CONF_SELECT: ".current-version h1",
CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}",
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "Release"
assert result2["options"] == {
"resource": "https://www.home-assistant.io",
"name": "Release",
"select": ".current-version h1",
"value_template": "{{ value.split(':')[1] }}",
"index": 0.0,
"verify_ssl": True,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_import_flow_success(hass: HomeAssistant) -> None:
"""Test a successful import of yaml."""
with patch(
"homeassistant.components.scrape.sensor.RestData",
return_value=MockRestData("test_scrape_sensor"),
), patch(
"homeassistant.components.scrape.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
CONF_RESOURCE: "https://www.home-assistant.io",
CONF_NAME: "Release",
CONF_SELECT: ".current-version h1",
CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}",
CONF_INDEX: 0,
CONF_VERIFY_SSL: True,
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "Release"
assert result2["options"] == {
"resource": "https://www.home-assistant.io",
"name": "Release",
"select": ".current-version h1",
"value_template": "{{ value.split(':')[1] }}",
"index": 0,
"verify_ssl": True,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_import_flow_already_exist(hass: HomeAssistant) -> None:
"""Test import of yaml already exist."""
MockConfigEntry(
domain=DOMAIN,
data={},
options={
"resource": "https://www.home-assistant.io",
"name": "Release",
"select": ".current-version h1",
"value_template": "{{ value.split(':')[1] }}",
"index": 0,
"verify_ssl": True,
},
).add_to_hass(hass)
with patch(
"homeassistant.components.scrape.sensor.RestData",
return_value=MockRestData("test_scrape_sensor"),
), patch(
"homeassistant.components.scrape.async_setup_entry",
return_value=True,
):
result3 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
CONF_RESOURCE: "https://www.home-assistant.io",
CONF_NAME: "Release",
CONF_SELECT: ".current-version h1",
CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}",
CONF_INDEX: 0,
CONF_VERIFY_SSL: True,
},
)
await hass.async_block_till_done()
assert result3["type"] == RESULT_TYPE_ABORT
assert result3["reason"] == "already_configured"
async def test_options_form(hass: HomeAssistant) -> None:
"""Test we get the form in options."""
entry = MockConfigEntry(
domain=DOMAIN,
data={},
options={
"resource": "https://www.home-assistant.io",
"name": "Release",
"select": ".current-version h1",
"value_template": "{{ value.split(':')[1] }}",
"index": 0,
"verify_ssl": True,
},
entry_id="1",
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.scrape.RestData",
return_value=MockRestData("test_scrape_sensor"),
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(entry.entry_id)
with patch(
"homeassistant.components.scrape.RestData",
return_value=MockRestData("test_scrape_sensor"),
):
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
"value_template": "{{ value.split(':')[1] }}",
"index": 1.0,
"verify_ssl": True,
},
)
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["data"] == {
"resource": "https://www.home-assistant.io",
"name": "Release",
"select": ".current-version h1",
"value_template": "{{ value.split(':')[1] }}",
"index": 1.0,
"verify_ssl": True,
}
entry_check = hass.config_entries.async_get_entry("1")
assert entry_check.state == config_entries.ConfigEntryState.LOADED
assert entry_check.update_listeners is not None

View file

@ -0,0 +1,89 @@
"""Test Scrape component setup process."""
from __future__ import annotations
from unittest.mock import patch
from homeassistant.components.scrape.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from . import MockRestData
from tests.common import MockConfigEntry
TEST_CONFIG = {
"resource": "https://www.home-assistant.io",
"name": "Release",
"select": ".current-version h1",
"value_template": "{{ value.split(':')[1] }}",
"index": 0,
"verify_ssl": True,
}
async def test_setup_entry(hass: HomeAssistant) -> None:
"""Test setup entry."""
entry = MockConfigEntry(
domain=DOMAIN,
data={},
options=TEST_CONFIG,
title="Release",
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.scrape.RestData",
return_value=MockRestData("test_scrape_sensor"),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.release")
assert state
async def test_setup_entry_no_data_fails(hass: HomeAssistant) -> None:
"""Test setup entry no data fails."""
entry = MockConfigEntry(
domain=DOMAIN, data={}, options=TEST_CONFIG, title="Release", entry_id="1"
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.scrape.RestData",
return_value=MockRestData("test_scrape_sensor_no_data"),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.ha_version")
assert state is None
entry = hass.config_entries.async_get_entry("1")
assert entry.state == ConfigEntryState.SETUP_RETRY
async def test_remove_entry(hass: HomeAssistant) -> None:
"""Test remove entry."""
entry = MockConfigEntry(
domain=DOMAIN,
data={},
options=TEST_CONFIG,
title="Release",
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.scrape.RestData",
return_value=MockRestData("test_scrape_sensor"),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.release")
assert state
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.release")
assert not state

View file

@ -3,10 +3,15 @@ from __future__ import annotations
from unittest.mock import patch
import pytest
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.components.sensor.const import CONF_STATE_CLASS
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_RESOURCE,
CONF_UNIT_OF_MEASUREMENT,
STATE_UNKNOWN,
TEMP_CELSIUS,
@ -15,22 +20,20 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.setup import async_setup_component
from . import MockRestData, return_config
from . import MockRestData, init_integration, return_config
from tests.common import MockConfigEntry
DOMAIN = "scrape"
async def test_scrape_sensor(hass: HomeAssistant) -> None:
"""Test Scrape sensor minimal."""
config = {"sensor": return_config(select=".current-version h1", name="HA version")}
mocker = MockRestData("test_scrape_sensor")
with patch(
"homeassistant.components.scrape.sensor.RestData",
return_value=mocker,
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
await init_integration(
hass,
return_config(select=".current-version h1", name="HA version"),
"test_scrape_sensor",
)
state = hass.states.get("sensor.ha_version")
assert state.state == "Current Version: 2021.12.10"
@ -38,21 +41,15 @@ async def test_scrape_sensor(hass: HomeAssistant) -> None:
async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None:
"""Test Scrape sensor with value template."""
config = {
"sensor": return_config(
await init_integration(
hass,
return_config(
select=".current-version h1",
name="HA version",
template="{{ value.split(':')[1] }}",
)
}
mocker = MockRestData("test_scrape_sensor")
with patch(
"homeassistant.components.scrape.sensor.RestData",
return_value=mocker,
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
),
"test_scrape_sensor",
)
state = hass.states.get("sensor.ha_version")
assert state.state == "2021.12.10"
@ -60,24 +57,18 @@ async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None:
async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None:
"""Test Scrape sensor for unit of measurement, device class and state class."""
config = {
"sensor": return_config(
await init_integration(
hass,
return_config(
select=".current-temp h3",
name="Current Temp",
template="{{ value.split(':')[1] }}",
uom="°C",
device_class="temperature",
state_class="measurement",
)
}
mocker = MockRestData("test_scrape_uom_and_classes")
with patch(
"homeassistant.components.scrape.sensor.RestData",
return_value=mocker,
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
),
"test_scrape_uom_and_classes",
)
state = hass.states.get("sensor.current_temp")
assert state.state == "22.1"
@ -88,31 +79,28 @@ async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None:
async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None:
"""Test Scrape sensor with authentication."""
config = {
"sensor": [
return_config(
select=".return",
name="Auth page",
username="user@secret.com",
password="12345678",
authentication="digest",
),
return_config(
select=".return",
name="Auth page2",
username="user@secret.com",
password="12345678",
),
]
}
mocker = MockRestData("test_scrape_sensor_authentication")
with patch(
"homeassistant.components.scrape.sensor.RestData",
return_value=mocker,
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
await init_integration(
hass,
return_config(
select=".return",
name="Auth page",
username="user@secret.com",
password="12345678",
authentication="digest",
),
"test_scrape_sensor_authentication",
)
await init_integration(
hass,
return_config(
select=".return",
name="Auth page2",
username="user@secret.com",
password="12345678",
),
"test_scrape_sensor_authentication",
entry_id="2",
)
state = hass.states.get("sensor.auth_page")
assert state.state == "secret text"
@ -122,15 +110,11 @@ async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None:
async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None:
"""Test Scrape sensor fails on no data."""
config = {"sensor": return_config(select=".current-version h1", name="HA version")}
mocker = MockRestData("test_scrape_sensor_no_data")
with patch(
"homeassistant.components.scrape.sensor.RestData",
return_value=mocker,
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
await init_integration(
hass,
return_config(select=".current-version h1", name="HA version"),
"test_scrape_sensor_no_data",
)
state = hass.states.get("sensor.ha_version")
assert state is None
@ -138,14 +122,21 @@ async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None:
async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None:
"""Test Scrape sensor no data on refresh."""
config = {"sensor": return_config(select=".current-version h1", name="HA version")}
config_entry = MockConfigEntry(
domain=DOMAIN,
source=SOURCE_USER,
data={},
options=return_config(select=".current-version h1", name="HA version"),
entry_id="1",
)
config_entry.add_to_hass(hass)
mocker = MockRestData("test_scrape_sensor")
with patch(
"homeassistant.components.scrape.sensor.RestData",
"homeassistant.components.scrape.RestData",
return_value=mocker,
):
assert await async_setup_component(hass, "sensor", config)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.ha_version")
@ -162,20 +153,17 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None:
async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None:
"""Test Scrape sensor with attribute and tag."""
config = {
"sensor": [
return_config(select="div", name="HA class", index=1, attribute="class"),
return_config(select="template", name="HA template"),
]
}
mocker = MockRestData("test_scrape_sensor")
with patch(
"homeassistant.components.scrape.sensor.RestData",
return_value=mocker,
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
await init_integration(
hass,
return_config(select="div", name="HA class", index=1, attribute="class"),
"test_scrape_sensor",
)
await init_integration(
hass,
return_config(select="template", name="HA template"),
"test_scrape_sensor",
entry_id="2",
)
state = hass.states.get("sensor.ha_class")
assert state.state == "['links']"
@ -185,22 +173,55 @@ async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None:
async def test_scrape_sensor_errors(hass: HomeAssistant) -> None:
"""Test Scrape sensor handle errors."""
config = {
"sensor": [
return_config(select="div", name="HA class", index=5, attribute="class"),
return_config(select="div", name="HA class2", attribute="classes"),
]
}
mocker = MockRestData("test_scrape_sensor")
with patch(
"homeassistant.components.scrape.sensor.RestData",
return_value=mocker,
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
await init_integration(
hass,
return_config(select="div", name="HA class", index=5, attribute="class"),
"test_scrape_sensor",
)
await init_integration(
hass,
return_config(select="div", name="HA class2", attribute="classes"),
"test_scrape_sensor",
entry_id="2",
)
state = hass.states.get("sensor.ha_class")
assert state.state == STATE_UNKNOWN
state2 = hass.states.get("sensor.ha_class2")
assert state2.state == STATE_UNKNOWN
async def test_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None:
"""Test the Scrape sensor import."""
config = {
"sensor": {
"platform": "scrape",
"resource": "https://www.home-assistant.io",
"select": ".current-version h1",
"name": "HA Version",
"index": 0,
"verify_ssl": True,
"value_template": "{{ value.split(':')[1] }}",
}
}
mocker = MockRestData("test_scrape_sensor")
with patch(
"homeassistant.components.scrape.RestData",
return_value=mocker,
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
assert (
"Loading Scrape via platform setup has been deprecated in Home Assistant"
in caplog.text
)
assert hass.config_entries.async_entries(DOMAIN)
options = hass.config_entries.async_entries(DOMAIN)[0].options
assert options[CONF_NAME] == "HA Version"
assert options[CONF_RESOURCE] == "https://www.home-assistant.io"
state = hass.states.get("sensor.ha_version")
assert state.state == "2021.12.10"