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 /homeassistant/components/scene/ @home-assistant/core
/tests/components/scene/ @home-assistant/core /tests/components/scene/ @home-assistant/core
/homeassistant/components/schluter/ @prairieapps /homeassistant/components/schluter/ @prairieapps
/homeassistant/components/scrape/ @fabaff /homeassistant/components/scrape/ @fabaff @gjohansson-ST
/tests/components/scrape/ @fabaff /tests/components/scrape/ @fabaff @gjohansson-ST
/homeassistant/components/screenlogic/ @dieselrabbit @bdraco /homeassistant/components/screenlogic/ @dieselrabbit @bdraco
/tests/components/screenlogic/ @dieselrabbit @bdraco /tests/components/screenlogic/ @dieselrabbit @bdraco
/homeassistant/components/script/ @home-assistant/core /homeassistant/components/script/ @home-assistant/core

View file

@ -1 +1,64 @@
"""The scrape component.""" """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", "documentation": "https://www.home-assistant.io/integrations/scrape",
"requirements": ["beautifulsoup4==4.11.1", "lxml==4.8.0"], "requirements": ["beautifulsoup4==4.11.1", "lxml==4.8.0"],
"after_dependencies": ["rest"], "after_dependencies": ["rest"],
"codeowners": ["@fabaff"], "codeowners": ["@fabaff", "@gjohansson-ST"],
"config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View file

@ -5,7 +5,6 @@ import logging
from typing import Any from typing import Any
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import httpx
import voluptuous as vol import voluptuous as vol
from homeassistant.components.rest.data import RestData from homeassistant.components.rest.data import RestData
@ -16,7 +15,9 @@ from homeassistant.components.sensor import (
STATE_CLASSES_SCHEMA, STATE_CLASSES_SCHEMA,
SensorEntity, SensorEntity,
) )
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_AUTHENTICATION, CONF_AUTHENTICATION,
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_HEADERS, CONF_HEADERS,
@ -31,26 +32,24 @@ from homeassistant.const import (
HTTP_DIGEST_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ATTR = "attribute" ICON = "mdi:web"
CONF_SELECT = "select"
CONF_INDEX = "index"
DEFAULT_NAME = "Web scrape"
DEFAULT_VERIFY_SSL = True
PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_RESOURCE): cv.string, vol.Required(CONF_RESOURCE): cv.string,
vol.Required(CONF_SELECT): 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_INDEX, default=0): cv.positive_int,
vol.Optional(CONF_AUTHENTICATION): vol.In( vol.Optional(CONF_AUTHENTICATION): vol.In(
[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] [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_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
vol.Optional(CONF_USERNAME): cv.string, 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, 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, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Web scrape sensor.""" """Set up the Web scrape sensor."""
name: str = config[CONF_NAME] _LOGGER.warning(
resource: str = config[CONF_RESOURCE] # Config flow added in Home Assistant Core 2022.7, remove import flow in 2022.9
method: str = "GET" "Loading Scrape via platform setup has been deprecated in Home Assistant 2022.7 "
payload: str | None = None "Your configuration has been automatically imported and you can "
headers: str | None = config.get(CONF_HEADERS) "remove it from your configuration.yaml"
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)
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: if value_template is not None:
value_template.hass = hass val_template = Template(value_template, hass)
auth: httpx.DigestAuth | tuple[str, str] | None = None rest = hass.data.setdefault(DOMAIN, {})[entry.entry_id]
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
async_add_entities( async_add_entities(
[ [
@ -115,10 +124,12 @@ async def async_setup_platform(
select, select,
attr, attr,
index, index,
value_template, val_template,
unit, unit,
device_class, device_class,
state_class, state_class,
entry_id,
resource,
) )
], ],
True, True,
@ -128,6 +139,8 @@ async def async_setup_platform(
class ScrapeSensor(SensorEntity): class ScrapeSensor(SensorEntity):
"""Representation of a web scrape sensor.""" """Representation of a web scrape sensor."""
_attr_icon = ICON
def __init__( def __init__(
self, self,
rest: RestData, rest: RestData,
@ -139,6 +152,8 @@ class ScrapeSensor(SensorEntity):
unit: str | None, unit: str | None,
device_class: str | None, device_class: str | None,
state_class: str | None, state_class: str | None,
entry_id: str,
resource: str,
) -> None: ) -> None:
"""Initialize a web scrape sensor.""" """Initialize a web scrape sensor."""
self.rest = rest self.rest = rest
@ -151,6 +166,14 @@ class ScrapeSensor(SensorEntity):
self._attr_native_unit_of_measurement = unit self._attr_native_unit_of_measurement = unit
self._attr_device_class = device_class self._attr_device_class = device_class
self._attr_state_class = state_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: def _extract_value(self) -> Any:
"""Parse the html extraction in the executor.""" """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", "ruckus_unleashed",
"sabnzbd", "sabnzbd",
"samsungtv", "samsungtv",
"scrape",
"screenlogic", "screenlogic",
"season", "season",
"sense", "sense",

View file

@ -2,6 +2,42 @@
from __future__ import annotations from __future__ import annotations
from typing import Any 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( def return_config(
@ -25,6 +61,8 @@ def return_config(
"resource": "https://www.home-assistant.io", "resource": "https://www.home-assistant.io",
"select": select, "select": select,
"name": name, "name": name,
"index": 0,
"verify_ssl": True,
} }
if attribute: if attribute:
config["attribute"] = attribute config["attribute"] = attribute
@ -38,7 +76,7 @@ def return_config(
config["device_class"] = device_class config["device_class"] = device_class
if state_class: if state_class:
config["state_class"] = state_class config["state_class"] = state_class
if authentication: if username:
config["authentication"] = authentication config["authentication"] = authentication
config["username"] = username config["username"] = username
config["password"] = password 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 from unittest.mock import patch
import pytest
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.components.sensor.const import CONF_STATE_CLASS from homeassistant.components.sensor.const import CONF_STATE_CLASS
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_NAME,
CONF_RESOURCE,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
STATE_UNKNOWN, STATE_UNKNOWN,
TEMP_CELSIUS, TEMP_CELSIUS,
@ -15,22 +20,20 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.setup import async_setup_component 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" DOMAIN = "scrape"
async def test_scrape_sensor(hass: HomeAssistant) -> None: async def test_scrape_sensor(hass: HomeAssistant) -> None:
"""Test Scrape sensor minimal.""" """Test Scrape sensor minimal."""
config = {"sensor": return_config(select=".current-version h1", name="HA version")} await init_integration(
hass,
mocker = MockRestData("test_scrape_sensor") return_config(select=".current-version h1", name="HA version"),
with patch( "test_scrape_sensor",
"homeassistant.components.scrape.sensor.RestData", )
return_value=mocker,
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
state = hass.states.get("sensor.ha_version") state = hass.states.get("sensor.ha_version")
assert state.state == "Current Version: 2021.12.10" 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: async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None:
"""Test Scrape sensor with value template.""" """Test Scrape sensor with value template."""
config = { await init_integration(
"sensor": return_config( hass,
return_config(
select=".current-version h1", select=".current-version h1",
name="HA version", name="HA version",
template="{{ value.split(':')[1] }}", template="{{ value.split(':')[1] }}",
) ),
} "test_scrape_sensor",
)
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()
state = hass.states.get("sensor.ha_version") state = hass.states.get("sensor.ha_version")
assert state.state == "2021.12.10" 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: async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None:
"""Test Scrape sensor for unit of measurement, device class and state class.""" """Test Scrape sensor for unit of measurement, device class and state class."""
config = { await init_integration(
"sensor": return_config( hass,
return_config(
select=".current-temp h3", select=".current-temp h3",
name="Current Temp", name="Current Temp",
template="{{ value.split(':')[1] }}", template="{{ value.split(':')[1] }}",
uom="°C", uom="°C",
device_class="temperature", device_class="temperature",
state_class="measurement", state_class="measurement",
) ),
} "test_scrape_uom_and_classes",
)
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()
state = hass.states.get("sensor.current_temp") state = hass.states.get("sensor.current_temp")
assert state.state == "22.1" 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: async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None:
"""Test Scrape sensor with authentication.""" """Test Scrape sensor with authentication."""
config = { await init_integration(
"sensor": [ hass,
return_config( return_config(
select=".return", select=".return",
name="Auth page", name="Auth page",
username="user@secret.com", username="user@secret.com",
password="12345678", password="12345678",
authentication="digest", authentication="digest",
), ),
return_config( "test_scrape_sensor_authentication",
select=".return", )
name="Auth page2", await init_integration(
username="user@secret.com", hass,
password="12345678", return_config(
), select=".return",
] name="Auth page2",
} username="user@secret.com",
password="12345678",
mocker = MockRestData("test_scrape_sensor_authentication") ),
with patch( "test_scrape_sensor_authentication",
"homeassistant.components.scrape.sensor.RestData", entry_id="2",
return_value=mocker, )
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
state = hass.states.get("sensor.auth_page") state = hass.states.get("sensor.auth_page")
assert state.state == "secret text" 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: async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None:
"""Test Scrape sensor fails on no data.""" """Test Scrape sensor fails on no data."""
config = {"sensor": return_config(select=".current-version h1", name="HA version")} await init_integration(
hass,
mocker = MockRestData("test_scrape_sensor_no_data") return_config(select=".current-version h1", name="HA version"),
with patch( "test_scrape_sensor_no_data",
"homeassistant.components.scrape.sensor.RestData", )
return_value=mocker,
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
state = hass.states.get("sensor.ha_version") state = hass.states.get("sensor.ha_version")
assert state is None 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: async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None:
"""Test Scrape sensor no data on refresh.""" """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") mocker = MockRestData("test_scrape_sensor")
with patch( with patch(
"homeassistant.components.scrape.sensor.RestData", "homeassistant.components.scrape.RestData",
return_value=mocker, 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() await hass.async_block_till_done()
state = hass.states.get("sensor.ha_version") 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: async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None:
"""Test Scrape sensor with attribute and tag.""" """Test Scrape sensor with attribute and tag."""
config = { await init_integration(
"sensor": [ hass,
return_config(select="div", name="HA class", index=1, attribute="class"), return_config(select="div", name="HA class", index=1, attribute="class"),
return_config(select="template", name="HA template"), "test_scrape_sensor",
] )
} await init_integration(
hass,
mocker = MockRestData("test_scrape_sensor") return_config(select="template", name="HA template"),
with patch( "test_scrape_sensor",
"homeassistant.components.scrape.sensor.RestData", entry_id="2",
return_value=mocker, )
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
state = hass.states.get("sensor.ha_class") state = hass.states.get("sensor.ha_class")
assert state.state == "['links']" 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: async def test_scrape_sensor_errors(hass: HomeAssistant) -> None:
"""Test Scrape sensor handle errors.""" """Test Scrape sensor handle errors."""
config = { await init_integration(
"sensor": [ hass,
return_config(select="div", name="HA class", index=5, attribute="class"), return_config(select="div", name="HA class", index=5, attribute="class"),
return_config(select="div", name="HA class2", attribute="classes"), "test_scrape_sensor",
] )
} await init_integration(
hass,
mocker = MockRestData("test_scrape_sensor") return_config(select="div", name="HA class2", attribute="classes"),
with patch( "test_scrape_sensor",
"homeassistant.components.scrape.sensor.RestData", entry_id="2",
return_value=mocker, )
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
state = hass.states.get("sensor.ha_class") state = hass.states.get("sensor.ha_class")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
state2 = hass.states.get("sensor.ha_class2") state2 = hass.states.get("sensor.ha_class2")
assert state2.state == STATE_UNKNOWN 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"