From ceaf8f240249baf9d1d4c842998850581da8c2af Mon Sep 17 00:00:00 2001 From: Lukasz Szmit <2490317+ptashek@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:22:58 +0100 Subject: [PATCH] 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 --- homeassistant/components/rest/__init__.py | 23 +++++++-- homeassistant/components/rest/const.py | 2 + homeassistant/components/rest/data.py | 4 ++ homeassistant/components/rest/schema.py | 4 +- tests/components/rest/test_init.py | 59 +++++++++++++++++++++++ 5 files changed, 86 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index 1c33b4592df..b7cdee2e039 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -45,6 +45,7 @@ from homeassistant.util.async_ import create_eager_task from .const import ( CONF_ENCODING, + CONF_PAYLOAD_TEMPLATE, CONF_SSL_CIPHER_LIST, COORDINATOR, 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): scan_interval: timedelta = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) 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) - 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()) hass.data[DOMAIN][REST_DATA].append({REST: rest, COORDINATOR: coordinator}) @@ -156,16 +160,20 @@ def _rest_coordinator( hass: HomeAssistant, rest: RestData, resource_template: template.Template | None, + payload_template: template.Template | None, update_interval: timedelta, ) -> DataUpdateCoordinator[None]: """Wrap a DataUpdateCoordinator around the rest object.""" - if resource_template: + if resource_template or payload_template: - async def _async_refresh_with_resource_template() -> None: - rest.set_url(resource_template.async_render(parse_result=False)) + async def _async_refresh_with_templates() -> None: + 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() - update_method = _async_refresh_with_resource_template + update_method = _async_refresh_with_templates else: 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) method: str = config[CONF_METHOD] payload: str | None = config.get(CONF_PAYLOAD) + payload_template: template.Template | None = config.get(CONF_PAYLOAD_TEMPLATE) verify_ssl: bool = config[CONF_VERIFY_SSL] ssl_cipher_list: str = config.get(CONF_SSL_CIPHER_LIST, DEFAULT_SSL_CIPHER_LIST) 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 = 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: raise HomeAssistantError("Resource not set for RestData") diff --git a/homeassistant/components/rest/const.py b/homeassistant/components/rest/const.py index 8fb08f766fa..d10b3f3f74e 100644 --- a/homeassistant/components/rest/const.py +++ b/homeassistant/components/rest/const.py @@ -33,3 +33,5 @@ XML_MIME_TYPES = ( "application/xml", "text/xml", ) + +CONF_PAYLOAD_TEMPLATE = "payload_template" diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 06be7a4f6ff..4c9667e7651 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -56,6 +56,10 @@ class RestData: self.last_exception: Exception | None = None self.headers: httpx.Headers | None = None + def set_payload(self, payload: str) -> None: + """Set request data.""" + self._request_data = payload + @property def url(self) -> str: """Get url.""" diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index d6011a43efd..f7fd8a36113 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -38,6 +38,7 @@ from .const import ( CONF_ENCODING, CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH, + CONF_PAYLOAD_TEMPLATE, CONF_SSL_CIPHER_LIST, DEFAULT_ENCODING, DEFAULT_FORCE_UPDATE, @@ -60,7 +61,8 @@ RESOURCE_SCHEMA = { vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS), vol.Optional(CONF_USERNAME): 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_SSL_CIPHER_LIST, diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 38a1661a831..0fda89cc329 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -475,3 +475,62 @@ async def test_config_schema_via_packages(hass: HomeAssistant) -> None: assert len(config["rest"]) == 2 assert config["rest"][0]["resource"] == "http://url1" 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"