Speed up generation of template states (#73728)
* Speed up generation of template states
* tweak
* cache
* cache hash
* weaken
* Revert "weaken"
This reverts commit 4856f50080
.
* lower cache size as it tends to be the same ones over and over
* lower cache size as it tends to be the same ones over and over
* lower cache size as it tends to be the same ones over and over
* cover
* Update homeassistant/helpers/template.py
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
* id reuse is possible
* account for iterting all sensors
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
57efa9569c
commit
32e0d9f47c
3 changed files with 63 additions and 6 deletions
|
@ -1110,6 +1110,13 @@ class State:
|
|||
self.domain, self.object_id = split_entity_id(self.entity_id)
|
||||
self._as_dict: ReadOnlyDict[str, Collection[Any]] | None = None
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Make the state hashable.
|
||||
|
||||
State objects are effectively immutable.
|
||||
"""
|
||||
return hash((id(self), self.last_updated))
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Name of this state."""
|
||||
|
|
|
@ -9,7 +9,7 @@ from collections.abc import Callable, Generator, Iterable
|
|||
from contextlib import contextmanager, suppress
|
||||
from contextvars import ContextVar
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial, wraps
|
||||
from functools import cache, lru_cache, partial, wraps
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
|
@ -98,6 +98,9 @@ template_cv: ContextVar[tuple[str, str] | None] = ContextVar(
|
|||
"template_cv", default=None
|
||||
)
|
||||
|
||||
CACHED_TEMPLATE_STATES = 512
|
||||
EVAL_CACHE_SIZE = 512
|
||||
|
||||
|
||||
@bind_hass
|
||||
def attach(hass: HomeAssistant, obj: Any) -> None:
|
||||
|
@ -222,6 +225,9 @@ def _false(arg: str) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
_cached_literal_eval = lru_cache(maxsize=EVAL_CACHE_SIZE)(literal_eval)
|
||||
|
||||
|
||||
class RenderInfo:
|
||||
"""Holds information about a template render."""
|
||||
|
||||
|
@ -318,6 +324,7 @@ class Template:
|
|||
"_exc_info",
|
||||
"_limited",
|
||||
"_strict",
|
||||
"_hash_cache",
|
||||
)
|
||||
|
||||
def __init__(self, template, hass=None):
|
||||
|
@ -333,6 +340,7 @@ class Template:
|
|||
self._exc_info = None
|
||||
self._limited = None
|
||||
self._strict = None
|
||||
self._hash_cache: int = hash(self.template)
|
||||
|
||||
@property
|
||||
def _env(self) -> TemplateEnvironment:
|
||||
|
@ -421,7 +429,7 @@ class Template:
|
|||
def _parse_result(self, render_result: str) -> Any:
|
||||
"""Parse the result."""
|
||||
try:
|
||||
result = literal_eval(render_result)
|
||||
result = _cached_literal_eval(render_result)
|
||||
|
||||
if type(result) in RESULT_WRAPPERS:
|
||||
result = RESULT_WRAPPERS[type(result)](
|
||||
|
@ -618,16 +626,30 @@ class Template:
|
|||
|
||||
def __hash__(self) -> int:
|
||||
"""Hash code for template."""
|
||||
return hash(self.template)
|
||||
return self._hash_cache
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Representation of Template."""
|
||||
return 'Template("' + self.template + '")'
|
||||
|
||||
|
||||
@cache
|
||||
def _domain_states(hass: HomeAssistant, name: str) -> DomainStates:
|
||||
return DomainStates(hass, name)
|
||||
|
||||
|
||||
def _readonly(*args: Any, **kwargs: Any) -> Any:
|
||||
"""Raise an exception when a states object is modified."""
|
||||
raise RuntimeError(f"Cannot modify template States object: {args} {kwargs}")
|
||||
|
||||
|
||||
class AllStates:
|
||||
"""Class to expose all HA states as attributes."""
|
||||
|
||||
__setitem__ = _readonly
|
||||
__delitem__ = _readonly
|
||||
__slots__ = ("_hass",)
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize all states."""
|
||||
self._hass = hass
|
||||
|
@ -643,7 +665,7 @@ class AllStates:
|
|||
if not valid_entity_id(f"{name}.entity"):
|
||||
raise TemplateError(f"Invalid domain name '{name}'")
|
||||
|
||||
return DomainStates(self._hass, name)
|
||||
return _domain_states(self._hass, name)
|
||||
|
||||
# Jinja will try __getitem__ first and it avoids the need
|
||||
# to call is_safe_attribute
|
||||
|
@ -682,6 +704,11 @@ class AllStates:
|
|||
class DomainStates:
|
||||
"""Class to expose a specific HA domain as attributes."""
|
||||
|
||||
__slots__ = ("_hass", "_domain")
|
||||
|
||||
__setitem__ = _readonly
|
||||
__delitem__ = _readonly
|
||||
|
||||
def __init__(self, hass: HomeAssistant, domain: str) -> None:
|
||||
"""Initialize the domain states."""
|
||||
self._hass = hass
|
||||
|
@ -727,6 +754,9 @@ class TemplateStateBase(State):
|
|||
|
||||
_state: State
|
||||
|
||||
__setitem__ = _readonly
|
||||
__delitem__ = _readonly
|
||||
|
||||
# Inheritance is done so functions that check against State keep working
|
||||
# pylint: disable=super-init-not-called
|
||||
def __init__(self, hass: HomeAssistant, collect: bool, entity_id: str) -> None:
|
||||
|
@ -865,10 +895,15 @@ def _collect_state(hass: HomeAssistant, entity_id: str) -> None:
|
|||
entity_collect.entities.add(entity_id)
|
||||
|
||||
|
||||
@lru_cache(maxsize=CACHED_TEMPLATE_STATES)
|
||||
def _template_state_no_collect(hass: HomeAssistant, state: State) -> TemplateState:
|
||||
return TemplateState(hass, state, collect=False)
|
||||
|
||||
|
||||
def _state_generator(hass: HomeAssistant, domain: str | None) -> Generator:
|
||||
"""State generator for a domain or all states."""
|
||||
for state in sorted(hass.states.async_all(domain), key=attrgetter("entity_id")):
|
||||
yield TemplateState(hass, state, collect=False)
|
||||
yield _template_state_no_collect(hass, state)
|
||||
|
||||
|
||||
def _get_state_if_valid(hass: HomeAssistant, entity_id: str) -> TemplateState | None:
|
||||
|
@ -882,6 +917,11 @@ def _get_state(hass: HomeAssistant, entity_id: str) -> TemplateState | None:
|
|||
return _get_template_state_from_state(hass, entity_id, hass.states.get(entity_id))
|
||||
|
||||
|
||||
@lru_cache(maxsize=CACHED_TEMPLATE_STATES)
|
||||
def _template_state(hass: HomeAssistant, state: State) -> TemplateState:
|
||||
return TemplateState(hass, state)
|
||||
|
||||
|
||||
def _get_template_state_from_state(
|
||||
hass: HomeAssistant, entity_id: str, state: State | None
|
||||
) -> TemplateState | None:
|
||||
|
@ -890,7 +930,7 @@ def _get_template_state_from_state(
|
|||
# access to the state properties in the state wrapper.
|
||||
_collect_state(hass, entity_id)
|
||||
return None
|
||||
return TemplateState(hass, state)
|
||||
return _template_state(hass, state)
|
||||
|
||||
|
||||
def _resolve_state(
|
||||
|
|
|
@ -18,6 +18,7 @@ from homeassistant.const import (
|
|||
MASS_GRAMS,
|
||||
PRESSURE_PA,
|
||||
SPEED_KILOMETERS_PER_HOUR,
|
||||
STATE_ON,
|
||||
TEMP_CELSIUS,
|
||||
VOLUME_LITERS,
|
||||
)
|
||||
|
@ -3831,3 +3832,12 @@ async def test_undefined_variable(hass, caplog):
|
|||
"Template variable warning: 'no_such_variable' is undefined when rendering '{{ no_such_variable }}'"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_template_states_blocks_setitem(hass):
|
||||
"""Test we cannot setitem on TemplateStates."""
|
||||
hass.states.async_set("light.new", STATE_ON)
|
||||
state = hass.states.get("light.new")
|
||||
template_state = template.TemplateState(hass, state, True)
|
||||
with pytest.raises(RuntimeError):
|
||||
template_state["any"] = "any"
|
||||
|
|
Loading…
Add table
Reference in a new issue