mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 04:37:06 +00:00
Add REST sensor/binary_sensor/switch templated headers & params (#54426)
This commit is contained in:
parent
944a7c09c4
commit
b1b782419b
@ -37,6 +37,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|||||||
from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX
|
from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX
|
||||||
from .data import RestData
|
from .data import RestData
|
||||||
from .schema import CONFIG_SCHEMA # noqa: F401
|
from .schema import CONFIG_SCHEMA # noqa: F401
|
||||||
|
from .utils import inject_hass_in_templates_list
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -161,6 +162,8 @@ def create_rest_data_from_config(hass, config):
|
|||||||
resource_template.hass = hass
|
resource_template.hass = hass
|
||||||
resource = resource_template.async_render(parse_result=False)
|
resource = resource_template.async_render(parse_result=False)
|
||||||
|
|
||||||
|
inject_hass_in_templates_list(hass, [headers, params])
|
||||||
|
|
||||||
if username and password:
|
if username and password:
|
||||||
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
|
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
|
||||||
auth = httpx.DigestAuth(username, password)
|
auth = httpx.DigestAuth(username, password)
|
||||||
|
@ -3,6 +3,7 @@ import logging
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
from homeassistant.components.rest.utils import render_templates
|
||||||
from homeassistant.helpers.httpx_client import get_async_client
|
from homeassistant.helpers.httpx_client import get_async_client
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
@ -51,13 +52,16 @@ class RestData:
|
|||||||
self._hass, verify_ssl=self._verify_ssl
|
self._hass, verify_ssl=self._verify_ssl
|
||||||
)
|
)
|
||||||
|
|
||||||
|
rendered_headers = render_templates(self._headers)
|
||||||
|
rendered_params = render_templates(self._params)
|
||||||
|
|
||||||
_LOGGER.debug("Updating from %s", self._resource)
|
_LOGGER.debug("Updating from %s", self._resource)
|
||||||
try:
|
try:
|
||||||
response = await self._async_client.request(
|
response = await self._async_client.request(
|
||||||
self._method,
|
self._method,
|
||||||
self._resource,
|
self._resource,
|
||||||
headers=self._headers,
|
headers=rendered_headers,
|
||||||
params=self._params,
|
params=rendered_params,
|
||||||
auth=self._auth,
|
auth=self._auth,
|
||||||
data=self._request_data,
|
data=self._request_data,
|
||||||
timeout=self._timeout,
|
timeout=self._timeout,
|
||||||
|
@ -54,8 +54,8 @@ RESOURCE_SCHEMA = {
|
|||||||
vol.Optional(CONF_AUTHENTICATION): vol.In(
|
vol.Optional(CONF_AUTHENTICATION): vol.In(
|
||||||
[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
|
[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
|
||||||
),
|
),
|
||||||
vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}),
|
vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.template}),
|
||||||
vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.string}),
|
vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.template}),
|
||||||
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS),
|
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS),
|
||||||
vol.Optional(CONF_USERNAME): cv.string,
|
vol.Optional(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_PASSWORD): cv.string,
|
vol.Optional(CONF_PASSWORD): cv.string,
|
||||||
|
@ -27,6 +27,8 @@ from homeassistant.const import (
|
|||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
from .utils import inject_hass_in_templates_list, render_templates
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
CONF_BODY_OFF = "body_off"
|
CONF_BODY_OFF = "body_off"
|
||||||
CONF_BODY_ON = "body_on"
|
CONF_BODY_ON = "body_on"
|
||||||
@ -46,8 +48,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
{
|
{
|
||||||
vol.Required(CONF_RESOURCE): cv.url,
|
vol.Required(CONF_RESOURCE): cv.url,
|
||||||
vol.Optional(CONF_STATE_RESOURCE): cv.url,
|
vol.Optional(CONF_STATE_RESOURCE): cv.url,
|
||||||
vol.Optional(CONF_HEADERS): {cv.string: cv.string},
|
vol.Optional(CONF_HEADERS): {cv.string: cv.template},
|
||||||
vol.Optional(CONF_PARAMS): {cv.string: cv.string},
|
vol.Optional(CONF_PARAMS): {cv.string: cv.template},
|
||||||
vol.Optional(CONF_BODY_OFF, default=DEFAULT_BODY_OFF): cv.template,
|
vol.Optional(CONF_BODY_OFF, default=DEFAULT_BODY_OFF): cv.template,
|
||||||
vol.Optional(CONF_BODY_ON, default=DEFAULT_BODY_ON): cv.template,
|
vol.Optional(CONF_BODY_ON, default=DEFAULT_BODY_ON): cv.template,
|
||||||
vol.Optional(CONF_IS_ON_TEMPLATE): cv.template,
|
vol.Optional(CONF_IS_ON_TEMPLATE): cv.template,
|
||||||
@ -90,6 +92,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
body_on.hass = hass
|
body_on.hass = hass
|
||||||
if body_off is not None:
|
if body_off is not None:
|
||||||
body_off.hass = hass
|
body_off.hass = hass
|
||||||
|
inject_hass_in_templates_list(hass, [headers, params])
|
||||||
timeout = config.get(CONF_TIMEOUT)
|
timeout = config.get(CONF_TIMEOUT)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -204,13 +207,16 @@ class RestSwitch(SwitchEntity):
|
|||||||
"""Send a state update to the device."""
|
"""Send a state update to the device."""
|
||||||
websession = async_get_clientsession(self.hass, self._verify_ssl)
|
websession = async_get_clientsession(self.hass, self._verify_ssl)
|
||||||
|
|
||||||
|
rendered_headers = render_templates(self._headers)
|
||||||
|
rendered_params = render_templates(self._params)
|
||||||
|
|
||||||
with async_timeout.timeout(self._timeout):
|
with async_timeout.timeout(self._timeout):
|
||||||
req = await getattr(websession, self._method)(
|
req = await getattr(websession, self._method)(
|
||||||
self._resource,
|
self._resource,
|
||||||
auth=self._auth,
|
auth=self._auth,
|
||||||
data=bytes(body, "utf-8"),
|
data=bytes(body, "utf-8"),
|
||||||
headers=self._headers,
|
headers=rendered_headers,
|
||||||
params=self._params,
|
params=rendered_params,
|
||||||
)
|
)
|
||||||
return req
|
return req
|
||||||
|
|
||||||
@ -227,12 +233,15 @@ class RestSwitch(SwitchEntity):
|
|||||||
"""Get the latest data from REST API and update the state."""
|
"""Get the latest data from REST API and update the state."""
|
||||||
websession = async_get_clientsession(hass, self._verify_ssl)
|
websession = async_get_clientsession(hass, self._verify_ssl)
|
||||||
|
|
||||||
|
rendered_headers = render_templates(self._headers)
|
||||||
|
rendered_params = render_templates(self._params)
|
||||||
|
|
||||||
with async_timeout.timeout(self._timeout):
|
with async_timeout.timeout(self._timeout):
|
||||||
req = await websession.get(
|
req = await websession.get(
|
||||||
self._state_resource,
|
self._state_resource,
|
||||||
auth=self._auth,
|
auth=self._auth,
|
||||||
headers=self._headers,
|
headers=rendered_headers,
|
||||||
params=self._params,
|
params=rendered_params,
|
||||||
)
|
)
|
||||||
text = await req.text()
|
text = await req.text()
|
||||||
|
|
||||||
|
27
homeassistant/components/rest/utils.py
Normal file
27
homeassistant/components/rest/utils.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"""Reusable utilities for the Rest component."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.template import Template
|
||||||
|
|
||||||
|
|
||||||
|
def inject_hass_in_templates_list(
|
||||||
|
hass: HomeAssistant, tpl_dict_list: list[dict[str, Template] | None]
|
||||||
|
):
|
||||||
|
"""Inject hass in a list of dict of templates."""
|
||||||
|
for tpl_dict in tpl_dict_list:
|
||||||
|
if tpl_dict is not None:
|
||||||
|
for tpl in tpl_dict.values():
|
||||||
|
tpl.hass = hass
|
||||||
|
|
||||||
|
|
||||||
|
def render_templates(tpl_dict: dict[str, Template] | None):
|
||||||
|
"""Render a dict of templates."""
|
||||||
|
if tpl_dict is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
rendered_items = {}
|
||||||
|
for item_name, template_header in tpl_dict.items():
|
||||||
|
if (value := template_header.async_render()) is not None:
|
||||||
|
rendered_items[item_name] = value
|
||||||
|
return rendered_items
|
@ -179,6 +179,40 @@ async def test_setup_get(hass):
|
|||||||
assert state.attributes[ATTR_DEVICE_CLASS] == binary_sensor.DEVICE_CLASS_PLUG
|
assert state.attributes[ATTR_DEVICE_CLASS] == binary_sensor.DEVICE_CLASS_PLUG
|
||||||
|
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
async def test_setup_get_template_headers_params(hass):
|
||||||
|
"""Test setup with valid configuration."""
|
||||||
|
respx.get("http://localhost").respond(status_code=200, json={})
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"sensor",
|
||||||
|
{
|
||||||
|
"sensor": {
|
||||||
|
"platform": "rest",
|
||||||
|
"resource": "http://localhost",
|
||||||
|
"method": "GET",
|
||||||
|
"value_template": "{{ value_json.key }}",
|
||||||
|
"name": "foo",
|
||||||
|
"verify_ssl": "true",
|
||||||
|
"timeout": 30,
|
||||||
|
"headers": {
|
||||||
|
"Accept": CONTENT_TYPE_JSON,
|
||||||
|
"User-Agent": "Mozilla/{{ 3 + 2 }}.0",
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"start": 0,
|
||||||
|
"end": "{{ 3 + 2 }}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await async_setup_component(hass, "homeassistant", {})
|
||||||
|
|
||||||
|
assert respx.calls.last.request.headers["Accept"] == CONTENT_TYPE_JSON
|
||||||
|
assert respx.calls.last.request.headers["User-Agent"] == "Mozilla/5.0"
|
||||||
|
assert respx.calls.last.request.url.query == b"start=0&end=5"
|
||||||
|
|
||||||
|
|
||||||
@respx.mock
|
@respx.mock
|
||||||
async def test_setup_get_digest_auth(hass):
|
async def test_setup_get_digest_auth(hass):
|
||||||
"""Test setup with valid configuration."""
|
"""Test setup with valid configuration."""
|
||||||
|
@ -217,6 +217,40 @@ async def test_setup_get(hass):
|
|||||||
assert state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_MEASUREMENT
|
assert state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_MEASUREMENT
|
||||||
|
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
async def test_setup_get_templated_headers_params(hass):
|
||||||
|
"""Test setup with valid configuration."""
|
||||||
|
respx.get("http://localhost").respond(status_code=200, json={})
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"sensor",
|
||||||
|
{
|
||||||
|
"sensor": {
|
||||||
|
"platform": "rest",
|
||||||
|
"resource": "http://localhost",
|
||||||
|
"method": "GET",
|
||||||
|
"value_template": "{{ value_json.key }}",
|
||||||
|
"name": "foo",
|
||||||
|
"verify_ssl": "true",
|
||||||
|
"timeout": 30,
|
||||||
|
"headers": {
|
||||||
|
"Accept": CONTENT_TYPE_JSON,
|
||||||
|
"User-Agent": "Mozilla/{{ 3 + 2 }}.0",
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"start": 0,
|
||||||
|
"end": "{{ 3 + 2 }}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await async_setup_component(hass, "homeassistant", {})
|
||||||
|
|
||||||
|
assert respx.calls.last.request.headers["Accept"] == CONTENT_TYPE_JSON
|
||||||
|
assert respx.calls.last.request.headers["User-Agent"] == "Mozilla/5.0"
|
||||||
|
assert respx.calls.last.request.url.query == b"start=0&end=5"
|
||||||
|
|
||||||
|
|
||||||
@respx.mock
|
@respx.mock
|
||||||
async def test_setup_get_digest_auth(hass):
|
async def test_setup_get_digest_auth(hass):
|
||||||
"""Test setup with valid configuration."""
|
"""Test setup with valid configuration."""
|
||||||
|
@ -27,7 +27,6 @@ DEVICE_CLASS = DEVICE_CLASS_SWITCH
|
|||||||
METHOD = "post"
|
METHOD = "post"
|
||||||
RESOURCE = "http://localhost/"
|
RESOURCE = "http://localhost/"
|
||||||
STATE_RESOURCE = RESOURCE
|
STATE_RESOURCE = RESOURCE
|
||||||
HEADERS = {"Content-type": CONTENT_TYPE_JSON}
|
|
||||||
AUTH = None
|
AUTH = None
|
||||||
PARAMS = None
|
PARAMS = None
|
||||||
|
|
||||||
@ -151,19 +150,51 @@ async def test_setup_with_state_resource(hass, aioclient_mock):
|
|||||||
assert_setup_component(1, SWITCH_DOMAIN)
|
assert_setup_component(1, SWITCH_DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_with_templated_headers_params(hass, aioclient_mock):
|
||||||
|
"""Test setup with valid configuration."""
|
||||||
|
aioclient_mock.get("http://localhost", status=HTTPStatus.OK)
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
{
|
||||||
|
SWITCH_DOMAIN: {
|
||||||
|
CONF_PLATFORM: DOMAIN,
|
||||||
|
CONF_NAME: "foo",
|
||||||
|
CONF_RESOURCE: "http://localhost",
|
||||||
|
CONF_HEADERS: {
|
||||||
|
"Accept": CONTENT_TYPE_JSON,
|
||||||
|
"User-Agent": "Mozilla/{{ 3 + 2 }}.0",
|
||||||
|
},
|
||||||
|
CONF_PARAMS: {
|
||||||
|
"start": 0,
|
||||||
|
"end": "{{ 3 + 2 }}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert aioclient_mock.call_count == 1
|
||||||
|
assert aioclient_mock.mock_calls[-1][3].get("Accept") == CONTENT_TYPE_JSON
|
||||||
|
assert aioclient_mock.mock_calls[-1][3].get("User-Agent") == "Mozilla/5.0"
|
||||||
|
assert aioclient_mock.mock_calls[-1][1].query["start"] == "0"
|
||||||
|
assert aioclient_mock.mock_calls[-1][1].query["end"] == "5"
|
||||||
|
assert_setup_component(1, SWITCH_DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
"""Tests for REST switch platform."""
|
"""Tests for REST switch platform."""
|
||||||
|
|
||||||
|
|
||||||
def _setup_test_switch(hass):
|
def _setup_test_switch(hass):
|
||||||
body_on = Template("on", hass)
|
body_on = Template("on", hass)
|
||||||
body_off = Template("off", hass)
|
body_off = Template("off", hass)
|
||||||
|
headers = {"Content-type": Template(CONTENT_TYPE_JSON, hass)}
|
||||||
switch = rest.RestSwitch(
|
switch = rest.RestSwitch(
|
||||||
NAME,
|
NAME,
|
||||||
DEVICE_CLASS,
|
DEVICE_CLASS,
|
||||||
RESOURCE,
|
RESOURCE,
|
||||||
STATE_RESOURCE,
|
STATE_RESOURCE,
|
||||||
METHOD,
|
METHOD,
|
||||||
HEADERS,
|
headers,
|
||||||
PARAMS,
|
PARAMS,
|
||||||
AUTH,
|
AUTH,
|
||||||
body_on,
|
body_on,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user