From 1c465b5ad07e5b9ab589d50297899d2191af3308 Mon Sep 17 00:00:00 2001
From: Olivier Ouellet <85790609+olivierouellet@users.noreply.github.com>
Date: Tue, 28 Mar 2023 06:42:31 -0400
Subject: [PATCH] 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
---
 homeassistant/components/rest/__init__.py     | 23 ++++++++++++++++---
 homeassistant/components/rest/const.py        |  2 ++
 homeassistant/components/rest/data.py         |  8 ++++---
 homeassistant/components/rest/schema.py       |  3 +++
 .../components/scrape/config_flow.py          | 11 ++++++++-
 homeassistant/components/scrape/const.py      |  2 ++
 homeassistant/components/scrape/strings.json  | 12 ++++++----
 tests/components/scrape/conftest.py           |  9 +++++++-
 tests/components/scrape/test_config_flow.py   |  9 ++++++++
 9 files changed, 67 insertions(+), 12 deletions(-)

diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py
index 37c483505b8..637e9da6f9c 100644
--- a/homeassistant/components/rest/__init__.py
+++ b/homeassistant/components/rest/__init__.py
@@ -41,7 +41,15 @@ from homeassistant.helpers.reload import (
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 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 .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)
     params: dict[str, str] | None = config.get(CONF_PARAMS)
     timeout: int = config[CONF_TIMEOUT]
-
+    encoding: str = config[CONF_ENCODING]
     if resource_template is not None:
         resource_template.hass = hass
         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)
 
     return RestData(
-        hass, method, resource, auth, headers, params, payload, verify_ssl, timeout
+        hass,
+        method,
+        resource,
+        encoding,
+        auth,
+        headers,
+        params,
+        payload,
+        verify_ssl,
+        timeout,
     )
diff --git a/homeassistant/components/rest/const.py b/homeassistant/components/rest/const.py
index 5fd32d8fba7..bdc0c5af492 100644
--- a/homeassistant/components/rest/const.py
+++ b/homeassistant/components/rest/const.py
@@ -5,6 +5,8 @@ DOMAIN = "rest"
 DEFAULT_METHOD = "GET"
 DEFAULT_VERIFY_SSL = True
 DEFAULT_FORCE_UPDATE = False
+DEFAULT_ENCODING = "UTF-8"
+CONF_ENCODING = "encoding"
 
 DEFAULT_BINARY_SENSOR_NAME = "REST Binary Sensor"
 DEFAULT_SENSOR_NAME = "REST Sensor"
diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py
index c1990b28336..7a5d62694b9 100644
--- a/homeassistant/components/rest/data.py
+++ b/homeassistant/components/rest/data.py
@@ -7,7 +7,7 @@ import httpx
 
 from homeassistant.core import HomeAssistant
 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
 
@@ -22,6 +22,7 @@ class RestData:
         hass: HomeAssistant,
         method: str,
         resource: str,
+        encoding: str,
         auth: httpx.DigestAuth | tuple[str, str] | None,
         headers: dict[str, str] | None,
         params: dict[str, str] | None,
@@ -33,6 +34,7 @@ class RestData:
         self._hass = hass
         self._method = method
         self._resource = resource
+        self._encoding = encoding
         self._auth = auth
         self._headers = headers
         self._params = params
@@ -51,8 +53,8 @@ class RestData:
     async def async_update(self, log_errors: bool = True) -> None:
         """Get the latest data from REST service with provided method."""
         if not self._async_client:
-            self._async_client = get_async_client(
-                self._hass, verify_ssl=self._verify_ssl
+            self._async_client = create_async_httpx_client(
+                self._hass, verify_ssl=self._verify_ssl, default_encoding=self._encoding
             )
 
         rendered_headers = template.render_complex(self._headers, parse_result=False)
diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py
index cfd8f8a3852..8e0fa9de00e 100644
--- a/homeassistant/components/rest/schema.py
+++ b/homeassistant/components/rest/schema.py
@@ -33,8 +33,10 @@ from homeassistant.helpers.template_entity import (
 )
 
 from .const import (
+    CONF_ENCODING,
     CONF_JSON_ATTRS,
     CONF_JSON_ATTRS_PATH,
+    DEFAULT_ENCODING,
     DEFAULT_FORCE_UPDATE,
     DEFAULT_METHOD,
     DEFAULT_VERIFY_SSL,
@@ -57,6 +59,7 @@ RESOURCE_SCHEMA = {
     vol.Optional(CONF_PAYLOAD): cv.string,
     vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
     vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
+    vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
 }
 
 SENSOR_SCHEMA = {
diff --git a/homeassistant/components/scrape/config_flow.py b/homeassistant/components/scrape/config_flow.py
index 419dd04f606..1e3635a010c 100644
--- a/homeassistant/components/scrape/config_flow.py
+++ b/homeassistant/components/scrape/config_flow.py
@@ -60,7 +60,15 @@ from homeassistant.helpers.selector import (
 )
 
 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 = {
     vol.Required(CONF_RESOURCE): TextSelector(
@@ -84,6 +92,7 @@ RESOURCE_SETUP = {
     vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): NumberSelector(
         NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX)
     ),
+    vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): TextSelector(),
 }
 
 SENSOR_SETUP = {
diff --git a/homeassistant/components/scrape/const.py b/homeassistant/components/scrape/const.py
index fc433ebb6f0..cd64199fa23 100644
--- a/homeassistant/components/scrape/const.py
+++ b/homeassistant/components/scrape/const.py
@@ -6,11 +6,13 @@ from datetime import timedelta
 from homeassistant.const import Platform
 
 DOMAIN = "scrape"
+DEFAULT_ENCODING = "UTF-8"
 DEFAULT_NAME = "Web scrape"
 DEFAULT_VERIFY_SSL = True
 DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
 
 PLATFORMS = [Platform.SENSOR]
 
+CONF_ENCODING = "encoding"
 CONF_SELECT = "select"
 CONF_INDEX = "index"
diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json
index 061518cb1db..052ef22848f 100644
--- a/homeassistant/components/scrape/strings.json
+++ b/homeassistant/components/scrape/strings.json
@@ -16,14 +16,16 @@
           "password": "[%key:common::config_flow::data::password%]",
           "headers": "Headers",
           "method": "Method",
-          "timeout": "Timeout"
+          "timeout": "Timeout",
+          "encoding": "Character encoding"
         },
         "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"
+          "timeout": "Timeout for connection to website",
+          "encoding": "Character encoding to use. Defaults to UTF-8"
         }
       },
       "sensor": {
@@ -110,14 +112,16 @@
           "password": "[%key:component::scrape::config::step::user::data::password%]",
           "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%]"
+          "timeout": "[%key:component::scrape::config::step::user::data::timeout%]",
+          "encoding": "[%key:component::scrape::config::step::user::data::encoding%]"
         },
         "data_description": {
           "resource": "[%key:component::scrape::config::step::user::data_description::resource%]",
           "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%]",
-          "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%]"
         }
       }
     }
diff --git a/tests/components/scrape/conftest.py b/tests/components/scrape/conftest.py
index fa90786ec2f..5ad4f39844e 100644
--- a/tests/components/scrape/conftest.py
+++ b/tests/components/scrape/conftest.py
@@ -9,7 +9,13 @@ 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.components.scrape.const import (
+    CONF_ENCODING,
+    CONF_INDEX,
+    CONF_SELECT,
+    DEFAULT_ENCODING,
+    DOMAIN,
+)
 from homeassistant.config_entries import SOURCE_USER
 from homeassistant.const import (
     CONF_METHOD,
@@ -38,6 +44,7 @@ async def get_config_to_integration_load() -> dict[str, Any]:
         CONF_METHOD: DEFAULT_METHOD,
         CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
         CONF_TIMEOUT: DEFAULT_TIMEOUT,
+        CONF_ENCODING: DEFAULT_ENCODING,
         "sensor": [
             {
                 CONF_NAME: "Current version",
diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py
index e12a7c15a0c..e508937fed8 100644
--- a/tests/components/scrape/test_config_flow.py
+++ b/tests/components/scrape/test_config_flow.py
@@ -9,8 +9,10 @@ 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_ENCODING,
     CONF_INDEX,
     CONF_SELECT,
+    DEFAULT_ENCODING,
     DEFAULT_VERIFY_SSL,
 )
 from homeassistant.const import (
@@ -75,6 +77,7 @@ async def test_form(hass: HomeAssistant, get_data: MockRestData) -> None:
         CONF_METHOD: "GET",
         CONF_VERIFY_SSL: True,
         CONF_TIMEOUT: 10.0,
+        CONF_ENCODING: "UTF-8",
         "sensor": [
             {
                 CONF_NAME: "Current version",
@@ -165,6 +168,7 @@ async def test_flow_fails(hass: HomeAssistant, get_data: MockRestData) -> None:
         CONF_METHOD: "GET",
         CONF_VERIFY_SSL: True,
         CONF_TIMEOUT: 10.0,
+        CONF_ENCODING: "UTF-8",
         "sensor": [
             {
                 CONF_NAME: "Current version",
@@ -206,6 +210,7 @@ async def test_options_resource_flow(
                 CONF_METHOD: DEFAULT_METHOD,
                 CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
                 CONF_TIMEOUT: DEFAULT_TIMEOUT,
+                CONF_ENCODING: DEFAULT_ENCODING,
                 CONF_USERNAME: "secret_username",
                 CONF_PASSWORD: "secret_password",
             },
@@ -218,6 +223,7 @@ async def test_options_resource_flow(
         CONF_METHOD: "GET",
         CONF_VERIFY_SSL: True,
         CONF_TIMEOUT: 10.0,
+        CONF_ENCODING: "UTF-8",
         CONF_USERNAME: "secret_username",
         CONF_PASSWORD: "secret_password",
         "sensor": [
@@ -282,6 +288,7 @@ async def test_options_add_remove_sensor_flow(
         CONF_METHOD: "GET",
         CONF_VERIFY_SSL: True,
         CONF_TIMEOUT: 10,
+        CONF_ENCODING: "UTF-8",
         "sensor": [
             {
                 CONF_NAME: "Current version",
@@ -341,6 +348,7 @@ async def test_options_add_remove_sensor_flow(
         CONF_METHOD: "GET",
         CONF_VERIFY_SSL: True,
         CONF_TIMEOUT: 10,
+        CONF_ENCODING: "UTF-8",
         "sensor": [
             {
                 CONF_NAME: "Template",
@@ -407,6 +415,7 @@ async def test_options_edit_sensor_flow(
         CONF_METHOD: "GET",
         CONF_VERIFY_SSL: True,
         CONF_TIMEOUT: 10,
+        CONF_ENCODING: "UTF-8",
         "sensor": [
             {
                 CONF_NAME: "Current version",
-- 
GitLab