From 48b93e03ee8b2d929c35e5b53fb6ab9d6bbb96da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Mar 2023 16:31:12 -1000 Subject: [PATCH] Cache transient templates compiles provided via api (#89065) * Cache transient templates compiles provided via api partially fixes #89047 (there is more going on here) * add a bit more coverage just to be sure * switch method * Revert "switch method" This reverts commit 0e9e1c8cbe8753159f4fd6775cdc9cf217d66f0e. * tweak * hold hass * empty for github flakey --- homeassistant/components/api/__init__.py | 9 +++- .../components/mobile_app/webhook.py | 10 +++- .../components/websocket_api/commands.py | 9 +++- tests/components/api/test_init.py | 46 +++++++++++++++++++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 56a07a6bcf0..5c0a60ecef7 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -1,5 +1,6 @@ """Rest API for Home Assistant.""" import asyncio +from functools import lru_cache from http import HTTPStatus import logging @@ -350,6 +351,12 @@ class APIComponentsView(HomeAssistantView): return self.json(request.app["hass"].config.components) +@lru_cache +def _cached_template(template_str: str, hass: ha.HomeAssistant) -> template.Template: + """Return a cached template.""" + return template.Template(template_str, hass) + + class APITemplateView(HomeAssistantView): """View to handle Template requests.""" @@ -362,7 +369,7 @@ class APITemplateView(HomeAssistantView): raise Unauthorized() try: data = await request.json() - tpl = template.Template(data["template"], request.app["hass"]) + tpl = _cached_template(data["template"], request.app["hass"]) return tpl.async_render(variables=data.get("variables"), parse_result=False) except (ValueError, TemplateError) as ex: return self.json_message( diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index c7fc375008a..90e244aaf06 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine from contextlib import suppress -from functools import wraps +from functools import lru_cache, wraps from http import HTTPStatus import logging import secrets @@ -365,6 +365,12 @@ async def webhook_stream_camera( return webhook_response(resp, registration=config_entry.data) +@lru_cache +def _cached_template(template_str: str, hass: HomeAssistant) -> template.Template: + """Return a cached template.""" + return template.Template(template_str, hass) + + @WEBHOOK_COMMANDS.register("render_template") @validate_schema( { @@ -381,7 +387,7 @@ async def webhook_render_template( resp = {} for key, item in data.items(): try: - tpl = template.Template(item[ATTR_TEMPLATE], hass) + tpl = _cached_template(item[ATTR_TEMPLATE], hass) resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) except TemplateError as ex: resp[key] = {"error": str(ex)} diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index e8008eb49b6..fa5c6aac294 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from contextlib import suppress import datetime as dt +from functools import lru_cache import json from typing import Any, cast @@ -424,6 +425,12 @@ def handle_ping( connection.send_message(pong_message(msg["id"])) +@lru_cache +def _cached_template(template_str: str, hass: HomeAssistant) -> template.Template: + """Return a cached template.""" + return template.Template(template_str, hass) + + @decorators.websocket_command( { vol.Required("type"): "render_template", @@ -440,7 +447,7 @@ async def handle_render_template( ) -> None: """Handle render_template command.""" template_str = msg["template"] - template_obj = template.Template(template_str, hass) + template_obj = _cached_template(template_str, hass) variables = msg.get("variables") timeout = msg.get("timeout") info = None diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index 570bb980aba..61da000fc07 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -349,6 +349,52 @@ async def test_api_template(hass: HomeAssistant, mock_api_client: TestClient) -> assert body == "10" + hass.states.async_set("sensor.temperature", 20) + resp = await mock_api_client.post( + const.URL_API_TEMPLATE, + json={"template": "{{ states.sensor.temperature.state }}"}, + ) + + body = await resp.text() + + assert body == "20" + + hass.states.async_remove("sensor.temperature") + resp = await mock_api_client.post( + const.URL_API_TEMPLATE, + json={"template": "{{ states.sensor.temperature.state }}"}, + ) + + body = await resp.text() + + assert body == "" + + +async def test_api_template_cached( + hass: HomeAssistant, mock_api_client: TestClient +) -> None: + """Test the template API uses the cache.""" + hass.states.async_set("sensor.temperature", 30) + + resp = await mock_api_client.post( + const.URL_API_TEMPLATE, + json={"template": "{{ states.sensor.temperature.state }}"}, + ) + + body = await resp.text() + + assert body == "30" + + hass.states.async_set("sensor.temperature", 40) + resp = await mock_api_client.post( + const.URL_API_TEMPLATE, + json={"template": "{{ states.sensor.temperature.state }}"}, + ) + + body = await resp.text() + + assert body == "40" + async def test_api_template_error( hass: HomeAssistant, mock_api_client: TestClient