Add support for payload_template in rest component (#107464)

* Add support for payload_template in rest component

* Update homeassistant/components/rest/schema.py

* Update homeassistant/components/rest/data.py

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Lukasz Szmit 2024-04-18 14:22:58 +01:00 committed by GitHub
parent 8ba1340c2e
commit ceaf8f2402
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 86 additions and 6 deletions

View file

@ -45,6 +45,7 @@ from homeassistant.util.async_ import create_eager_task
from .const import ( from .const import (
CONF_ENCODING, CONF_ENCODING,
CONF_PAYLOAD_TEMPLATE,
CONF_SSL_CIPHER_LIST, CONF_SSL_CIPHER_LIST,
COORDINATOR, COORDINATOR,
DEFAULT_SSL_CIPHER_LIST, DEFAULT_SSL_CIPHER_LIST,
@ -108,8 +109,11 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> bool
for rest_idx, conf in enumerate(rest_config): for rest_idx, conf in enumerate(rest_config):
scan_interval: timedelta = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) scan_interval: timedelta = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
resource_template: template.Template | None = conf.get(CONF_RESOURCE_TEMPLATE) resource_template: template.Template | None = conf.get(CONF_RESOURCE_TEMPLATE)
payload_template: template.Template | None = conf.get(CONF_PAYLOAD_TEMPLATE)
rest = create_rest_data_from_config(hass, conf) rest = create_rest_data_from_config(hass, conf)
coordinator = _rest_coordinator(hass, rest, resource_template, scan_interval) coordinator = _rest_coordinator(
hass, rest, resource_template, payload_template, scan_interval
)
refresh_coroutines.append(coordinator.async_refresh()) refresh_coroutines.append(coordinator.async_refresh())
hass.data[DOMAIN][REST_DATA].append({REST: rest, COORDINATOR: coordinator}) hass.data[DOMAIN][REST_DATA].append({REST: rest, COORDINATOR: coordinator})
@ -156,16 +160,20 @@ def _rest_coordinator(
hass: HomeAssistant, hass: HomeAssistant,
rest: RestData, rest: RestData,
resource_template: template.Template | None, resource_template: template.Template | None,
payload_template: template.Template | None,
update_interval: timedelta, update_interval: timedelta,
) -> DataUpdateCoordinator[None]: ) -> DataUpdateCoordinator[None]:
"""Wrap a DataUpdateCoordinator around the rest object.""" """Wrap a DataUpdateCoordinator around the rest object."""
if resource_template: if resource_template or payload_template:
async def _async_refresh_with_resource_template() -> None: async def _async_refresh_with_templates() -> None:
rest.set_url(resource_template.async_render(parse_result=False)) if resource_template:
rest.set_url(resource_template.async_render(parse_result=False))
if payload_template:
rest.set_payload(payload_template.async_render(parse_result=False))
await rest.async_update() await rest.async_update()
update_method = _async_refresh_with_resource_template update_method = _async_refresh_with_templates
else: else:
update_method = rest.async_update update_method = rest.async_update
@ -184,6 +192,7 @@ def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> Res
resource_template: template.Template | None = config.get(CONF_RESOURCE_TEMPLATE) resource_template: template.Template | None = config.get(CONF_RESOURCE_TEMPLATE)
method: str = config[CONF_METHOD] method: str = config[CONF_METHOD]
payload: str | None = config.get(CONF_PAYLOAD) payload: str | None = config.get(CONF_PAYLOAD)
payload_template: template.Template | None = config.get(CONF_PAYLOAD_TEMPLATE)
verify_ssl: bool = config[CONF_VERIFY_SSL] verify_ssl: bool = config[CONF_VERIFY_SSL]
ssl_cipher_list: str = config.get(CONF_SSL_CIPHER_LIST, DEFAULT_SSL_CIPHER_LIST) ssl_cipher_list: str = config.get(CONF_SSL_CIPHER_LIST, DEFAULT_SSL_CIPHER_LIST)
username: str | None = config.get(CONF_USERNAME) username: str | None = config.get(CONF_USERNAME)
@ -196,6 +205,10 @@ def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> Res
resource_template.hass = hass resource_template.hass = hass
resource = resource_template.async_render(parse_result=False) resource = resource_template.async_render(parse_result=False)
if payload_template is not None:
payload_template.hass = hass
payload = payload_template.async_render(parse_result=False)
if not resource: if not resource:
raise HomeAssistantError("Resource not set for RestData") raise HomeAssistantError("Resource not set for RestData")

View file

@ -33,3 +33,5 @@ XML_MIME_TYPES = (
"application/xml", "application/xml",
"text/xml", "text/xml",
) )
CONF_PAYLOAD_TEMPLATE = "payload_template"

View file

@ -56,6 +56,10 @@ class RestData:
self.last_exception: Exception | None = None self.last_exception: Exception | None = None
self.headers: httpx.Headers | None = None self.headers: httpx.Headers | None = None
def set_payload(self, payload: str) -> None:
"""Set request data."""
self._request_data = payload
@property @property
def url(self) -> str: def url(self) -> str:
"""Get url.""" """Get url."""

View file

@ -38,6 +38,7 @@ from .const import (
CONF_ENCODING, CONF_ENCODING,
CONF_JSON_ATTRS, CONF_JSON_ATTRS,
CONF_JSON_ATTRS_PATH, CONF_JSON_ATTRS_PATH,
CONF_PAYLOAD_TEMPLATE,
CONF_SSL_CIPHER_LIST, CONF_SSL_CIPHER_LIST,
DEFAULT_ENCODING, DEFAULT_ENCODING,
DEFAULT_FORCE_UPDATE, DEFAULT_FORCE_UPDATE,
@ -60,7 +61,8 @@ RESOURCE_SCHEMA = {
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,
vol.Optional(CONF_PAYLOAD): cv.string, vol.Exclusive(CONF_PAYLOAD, CONF_PAYLOAD): cv.string,
vol.Exclusive(CONF_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
vol.Optional( vol.Optional(
CONF_SSL_CIPHER_LIST, CONF_SSL_CIPHER_LIST,

View file

@ -475,3 +475,62 @@ async def test_config_schema_via_packages(hass: HomeAssistant) -> None:
assert len(config["rest"]) == 2 assert len(config["rest"]) == 2
assert config["rest"][0]["resource"] == "http://url1" assert config["rest"][0]["resource"] == "http://url1"
assert config["rest"][1]["resource"] == "http://url2" assert config["rest"][1]["resource"] == "http://url2"
@respx.mock
async def test_setup_minimum_payload_template(hass: HomeAssistant) -> None:
"""Test setup with minimum configuration (payload_template)."""
respx.post("http://localhost", json={"data": "value"}).respond(
status_code=HTTPStatus.OK,
json={
"sensor1": "1",
"sensor2": "2",
"binary_sensor1": "on",
"binary_sensor2": "off",
},
)
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"resource": "http://localhost",
"payload_template": '{% set payload = {"data": "value"} %}{{ payload | to_json }}',
"method": "POST",
"verify_ssl": "false",
"timeout": 30,
"sensor": [
{
"unit_of_measurement": UnitOfInformation.MEGABYTES,
"name": "sensor1",
"value_template": "{{ value_json.sensor1 }}",
},
{
"unit_of_measurement": UnitOfInformation.MEGABYTES,
"name": "sensor2",
"value_template": "{{ value_json.sensor2 }}",
},
],
"binary_sensor": [
{
"name": "binary_sensor1",
"value_template": "{{ value_json.binary_sensor1 }}",
},
{
"name": "binary_sensor2",
"value_template": "{{ value_json.binary_sensor2 }}",
},
],
}
]
},
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 4
assert hass.states.get("sensor.sensor1").state == "1"
assert hass.states.get("sensor.sensor2").state == "2"
assert hass.states.get("binary_sensor.binary_sensor1").state == "on"
assert hass.states.get("binary_sensor.binary_sensor2").state == "off"