Add encoding configuration setting to REST and Scape (#90254)

* Create new config parameter for default character encoding if no character encoding is declared

* Changes suggested by gjohansson-ST

* Added config flow for scape

* Removed "character"

* Change to create_async_httpx_client

* Remove CONF_ENCODING from Scrape SENSOR_SCHEMA

* Debug scrape test
This commit is contained in:
Olivier Ouellet 2023-03-28 06:42:31 -04:00 committed by GitHub
parent 3c3860c923
commit 1c465b5ad0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 67 additions and 12 deletions

View File

@ -41,7 +41,15 @@ from homeassistant.helpers.reload import (
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX from .const import (
CONF_ENCODING,
COORDINATOR,
DOMAIN,
PLATFORM_IDX,
REST,
REST_DATA,
REST_IDX,
)
from .data import RestData from .data import RestData
from .schema import CONFIG_SCHEMA, RESOURCE_SCHEMA # noqa: F401 from .schema import CONFIG_SCHEMA, RESOURCE_SCHEMA # noqa: F401
@ -182,7 +190,7 @@ def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> Res
headers: dict[str, str] | None = config.get(CONF_HEADERS) headers: dict[str, str] | None = config.get(CONF_HEADERS)
params: dict[str, str] | None = config.get(CONF_PARAMS) params: dict[str, str] | None = config.get(CONF_PARAMS)
timeout: int = config[CONF_TIMEOUT] timeout: int = config[CONF_TIMEOUT]
encoding: str = config[CONF_ENCODING]
if resource_template is not None: if resource_template is not None:
resource_template.hass = hass resource_template.hass = hass
resource = resource_template.async_render(parse_result=False) resource = resource_template.async_render(parse_result=False)
@ -201,5 +209,14 @@ def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> Res
auth = (username, password) auth = (username, password)
return RestData( return RestData(
hass, method, resource, auth, headers, params, payload, verify_ssl, timeout hass,
method,
resource,
encoding,
auth,
headers,
params,
payload,
verify_ssl,
timeout,
) )

View File

@ -5,6 +5,8 @@ DOMAIN = "rest"
DEFAULT_METHOD = "GET" DEFAULT_METHOD = "GET"
DEFAULT_VERIFY_SSL = True DEFAULT_VERIFY_SSL = True
DEFAULT_FORCE_UPDATE = False DEFAULT_FORCE_UPDATE = False
DEFAULT_ENCODING = "UTF-8"
CONF_ENCODING = "encoding"
DEFAULT_BINARY_SENSOR_NAME = "REST Binary Sensor" DEFAULT_BINARY_SENSOR_NAME = "REST Binary Sensor"
DEFAULT_SENSOR_NAME = "REST Sensor" DEFAULT_SENSOR_NAME = "REST Sensor"

View File

@ -7,7 +7,7 @@ import httpx
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.httpx_client import create_async_httpx_client
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
@ -22,6 +22,7 @@ class RestData:
hass: HomeAssistant, hass: HomeAssistant,
method: str, method: str,
resource: str, resource: str,
encoding: str,
auth: httpx.DigestAuth | tuple[str, str] | None, auth: httpx.DigestAuth | tuple[str, str] | None,
headers: dict[str, str] | None, headers: dict[str, str] | None,
params: dict[str, str] | None, params: dict[str, str] | None,
@ -33,6 +34,7 @@ class RestData:
self._hass = hass self._hass = hass
self._method = method self._method = method
self._resource = resource self._resource = resource
self._encoding = encoding
self._auth = auth self._auth = auth
self._headers = headers self._headers = headers
self._params = params self._params = params
@ -51,8 +53,8 @@ class RestData:
async def async_update(self, log_errors: bool = True) -> None: async def async_update(self, log_errors: bool = True) -> None:
"""Get the latest data from REST service with provided method.""" """Get the latest data from REST service with provided method."""
if not self._async_client: if not self._async_client:
self._async_client = get_async_client( self._async_client = create_async_httpx_client(
self._hass, verify_ssl=self._verify_ssl self._hass, verify_ssl=self._verify_ssl, default_encoding=self._encoding
) )
rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_headers = template.render_complex(self._headers, parse_result=False)

View File

@ -33,8 +33,10 @@ from homeassistant.helpers.template_entity import (
) )
from .const import ( from .const import (
CONF_ENCODING,
CONF_JSON_ATTRS, CONF_JSON_ATTRS,
CONF_JSON_ATTRS_PATH, CONF_JSON_ATTRS_PATH,
DEFAULT_ENCODING,
DEFAULT_FORCE_UPDATE, DEFAULT_FORCE_UPDATE,
DEFAULT_METHOD, DEFAULT_METHOD,
DEFAULT_VERIFY_SSL, DEFAULT_VERIFY_SSL,
@ -57,6 +59,7 @@ RESOURCE_SCHEMA = {
vol.Optional(CONF_PAYLOAD): cv.string, vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
} }
SENSOR_SCHEMA = { SENSOR_SCHEMA = {

View File

@ -60,7 +60,15 @@ from homeassistant.helpers.selector import (
) )
from . import COMBINED_SCHEMA from . import COMBINED_SCHEMA
from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DOMAIN from .const import (
CONF_ENCODING,
CONF_INDEX,
CONF_SELECT,
DEFAULT_ENCODING,
DEFAULT_NAME,
DEFAULT_VERIFY_SSL,
DOMAIN,
)
RESOURCE_SETUP = { RESOURCE_SETUP = {
vol.Required(CONF_RESOURCE): TextSelector( vol.Required(CONF_RESOURCE): TextSelector(
@ -84,6 +92,7 @@ RESOURCE_SETUP = {
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): NumberSelector( vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): NumberSelector(
NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX)
), ),
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): TextSelector(),
} }
SENSOR_SETUP = { SENSOR_SETUP = {

View File

@ -6,11 +6,13 @@ from datetime import timedelta
from homeassistant.const import Platform from homeassistant.const import Platform
DOMAIN = "scrape" DOMAIN = "scrape"
DEFAULT_ENCODING = "UTF-8"
DEFAULT_NAME = "Web scrape" DEFAULT_NAME = "Web scrape"
DEFAULT_VERIFY_SSL = True DEFAULT_VERIFY_SSL = True
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
PLATFORMS = [Platform.SENSOR] PLATFORMS = [Platform.SENSOR]
CONF_ENCODING = "encoding"
CONF_SELECT = "select" CONF_SELECT = "select"
CONF_INDEX = "index" CONF_INDEX = "index"

View File

@ -16,14 +16,16 @@
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"headers": "Headers", "headers": "Headers",
"method": "Method", "method": "Method",
"timeout": "Timeout" "timeout": "Timeout",
"encoding": "Character encoding"
}, },
"data_description": { "data_description": {
"resource": "The URL to the website that contains the value", "resource": "The URL to the website that contains the value",
"authentication": "Type of the HTTP authentication. Either basic or digest", "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", "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed",
"headers": "Headers to use for the web request", "headers": "Headers to use for the web request",
"timeout": "Timeout for connection to website" "timeout": "Timeout for connection to website",
"encoding": "Character encoding to use. Defaults to UTF-8"
} }
}, },
"sensor": { "sensor": {
@ -110,14 +112,16 @@
"password": "[%key:component::scrape::config::step::user::data::password%]", "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%]", "verify_ssl": "[%key:component::scrape::config::step::user::data::verify_ssl%]",
"timeout": "[%key:component::scrape::config::step::user::data::timeout%]" "timeout": "[%key:component::scrape::config::step::user::data::timeout%]",
"encoding": "[%key:component::scrape::config::step::user::data::encoding%]"
}, },
"data_description": { "data_description": {
"resource": "[%key:component::scrape::config::step::user::data_description::resource%]", "resource": "[%key:component::scrape::config::step::user::data_description::resource%]",
"authentication": "[%key:component::scrape::config::step::user::data_description::authentication%]", "authentication": "[%key:component::scrape::config::step::user::data_description::authentication%]",
"headers": "[%key:component::scrape::config::step::user::data_description::headers%]", "headers": "[%key:component::scrape::config::step::user::data_description::headers%]",
"verify_ssl": "[%key:component::scrape::config::step::user::data_description::verify_ssl%]", "verify_ssl": "[%key:component::scrape::config::step::user::data_description::verify_ssl%]",
"timeout": "[%key:component::scrape::config::step::user::data_description::timeout%]" "timeout": "[%key:component::scrape::config::step::user::data_description::timeout%]",
"encoding": "[%key:component::scrape::config::step::user::data_description::encoding%]"
} }
} }
} }

View File

@ -9,7 +9,13 @@ import pytest
from homeassistant.components.rest.data import DEFAULT_TIMEOUT from homeassistant.components.rest.data import DEFAULT_TIMEOUT
from homeassistant.components.rest.schema import DEFAULT_METHOD, DEFAULT_VERIFY_SSL from homeassistant.components.rest.schema import DEFAULT_METHOD, DEFAULT_VERIFY_SSL
from homeassistant.components.scrape.const import CONF_INDEX, CONF_SELECT, DOMAIN from homeassistant.components.scrape.const import (
CONF_ENCODING,
CONF_INDEX,
CONF_SELECT,
DEFAULT_ENCODING,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import ( from homeassistant.const import (
CONF_METHOD, CONF_METHOD,
@ -38,6 +44,7 @@ async def get_config_to_integration_load() -> dict[str, Any]:
CONF_METHOD: DEFAULT_METHOD, CONF_METHOD: DEFAULT_METHOD,
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
CONF_TIMEOUT: DEFAULT_TIMEOUT, CONF_TIMEOUT: DEFAULT_TIMEOUT,
CONF_ENCODING: DEFAULT_ENCODING,
"sensor": [ "sensor": [
{ {
CONF_NAME: "Current version", CONF_NAME: "Current version",

View File

@ -9,8 +9,10 @@ from homeassistant.components.rest.data import DEFAULT_TIMEOUT
from homeassistant.components.rest.schema import DEFAULT_METHOD from homeassistant.components.rest.schema import DEFAULT_METHOD
from homeassistant.components.scrape import DOMAIN from homeassistant.components.scrape import DOMAIN
from homeassistant.components.scrape.const import ( from homeassistant.components.scrape.const import (
CONF_ENCODING,
CONF_INDEX, CONF_INDEX,
CONF_SELECT, CONF_SELECT,
DEFAULT_ENCODING,
DEFAULT_VERIFY_SSL, DEFAULT_VERIFY_SSL,
) )
from homeassistant.const import ( from homeassistant.const import (
@ -75,6 +77,7 @@ async def test_form(hass: HomeAssistant, get_data: MockRestData) -> None:
CONF_METHOD: "GET", CONF_METHOD: "GET",
CONF_VERIFY_SSL: True, CONF_VERIFY_SSL: True,
CONF_TIMEOUT: 10.0, CONF_TIMEOUT: 10.0,
CONF_ENCODING: "UTF-8",
"sensor": [ "sensor": [
{ {
CONF_NAME: "Current version", CONF_NAME: "Current version",
@ -165,6 +168,7 @@ async def test_flow_fails(hass: HomeAssistant, get_data: MockRestData) -> None:
CONF_METHOD: "GET", CONF_METHOD: "GET",
CONF_VERIFY_SSL: True, CONF_VERIFY_SSL: True,
CONF_TIMEOUT: 10.0, CONF_TIMEOUT: 10.0,
CONF_ENCODING: "UTF-8",
"sensor": [ "sensor": [
{ {
CONF_NAME: "Current version", CONF_NAME: "Current version",
@ -206,6 +210,7 @@ async def test_options_resource_flow(
CONF_METHOD: DEFAULT_METHOD, CONF_METHOD: DEFAULT_METHOD,
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
CONF_TIMEOUT: DEFAULT_TIMEOUT, CONF_TIMEOUT: DEFAULT_TIMEOUT,
CONF_ENCODING: DEFAULT_ENCODING,
CONF_USERNAME: "secret_username", CONF_USERNAME: "secret_username",
CONF_PASSWORD: "secret_password", CONF_PASSWORD: "secret_password",
}, },
@ -218,6 +223,7 @@ async def test_options_resource_flow(
CONF_METHOD: "GET", CONF_METHOD: "GET",
CONF_VERIFY_SSL: True, CONF_VERIFY_SSL: True,
CONF_TIMEOUT: 10.0, CONF_TIMEOUT: 10.0,
CONF_ENCODING: "UTF-8",
CONF_USERNAME: "secret_username", CONF_USERNAME: "secret_username",
CONF_PASSWORD: "secret_password", CONF_PASSWORD: "secret_password",
"sensor": [ "sensor": [
@ -282,6 +288,7 @@ async def test_options_add_remove_sensor_flow(
CONF_METHOD: "GET", CONF_METHOD: "GET",
CONF_VERIFY_SSL: True, CONF_VERIFY_SSL: True,
CONF_TIMEOUT: 10, CONF_TIMEOUT: 10,
CONF_ENCODING: "UTF-8",
"sensor": [ "sensor": [
{ {
CONF_NAME: "Current version", CONF_NAME: "Current version",
@ -341,6 +348,7 @@ async def test_options_add_remove_sensor_flow(
CONF_METHOD: "GET", CONF_METHOD: "GET",
CONF_VERIFY_SSL: True, CONF_VERIFY_SSL: True,
CONF_TIMEOUT: 10, CONF_TIMEOUT: 10,
CONF_ENCODING: "UTF-8",
"sensor": [ "sensor": [
{ {
CONF_NAME: "Template", CONF_NAME: "Template",
@ -407,6 +415,7 @@ async def test_options_edit_sensor_flow(
CONF_METHOD: "GET", CONF_METHOD: "GET",
CONF_VERIFY_SSL: True, CONF_VERIFY_SSL: True,
CONF_TIMEOUT: 10, CONF_TIMEOUT: 10,
CONF_ENCODING: "UTF-8",
"sensor": [ "sensor": [
{ {
CONF_NAME: "Current version", CONF_NAME: "Current version",