Make this variable available in template entities (#65201)

* feat: make this variable available in template entities

This makes the variable `this` available in template entities.
It will simplify the use of self-referencing template entities.
Because, without this, we have to repeat the entity id every time.
If we can solve this without explicitly spelling the entity id,
code can be re-used much better.

As a side-effect, this will allow to use `variables`-like patterns,
where attributes can be used as variables to calculate subsequent attributes or state.

Example:
```yaml
template:
  sensor:
    - name: test
      state: "{{ this.attributes.test }}"
      # not: "{{ state_attr('sensor.test', 'test' }}"
      attributes:
        test: "{{ now() }}"
```

* expose entity_id instead of this

* add test

* Refactor to expose this variable

* Tweak repr dunder

Co-authored-by: Erik <erik@montnemery.com>
This commit is contained in:
akloeckner 2022-04-20 15:30:17 +02:00 committed by GitHub
parent 9316909e60
commit d20a620590
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 81 additions and 10 deletions

View file

@ -27,7 +27,11 @@ from homeassistant.helpers.event import (
TrackTemplateResult,
async_track_template_result,
)
from homeassistant.helpers.template import Template, result_as_boolean
from homeassistant.helpers.template import (
Template,
TemplateStateFromEntityId,
result_as_boolean,
)
from .const import (
CONF_ATTRIBUTE_TEMPLATES,
@ -368,8 +372,11 @@ class TemplateEntity(Entity):
async def _async_template_startup(self, *_) -> None:
template_var_tups: list[TrackTemplate] = []
has_availability_template = False
values = {"this": TemplateStateFromEntityId(self.hass, self.entity_id)}
for template, attributes in self._template_attrs.items():
template_var_tup = TrackTemplate(template, None)
template_var_tup = TrackTemplate(template, values)
is_availability_template = False
for attribute in attributes:
# pylint: disable-next=protected-access

View file

@ -719,22 +719,24 @@ class DomainStates:
return f"<template DomainStates('{self._domain}')>"
class TemplateState(State):
class TemplateStateBase(State):
"""Class to represent a state object in a template."""
__slots__ = ("_hass", "_state", "_collect")
__slots__ = ("_hass", "_collect", "_entity_id")
_state: State
# Inheritance is done so functions that check against State keep working
# pylint: disable=super-init-not-called
def __init__(self, hass: HomeAssistant, state: State, collect: bool = True) -> None:
def __init__(self, hass: HomeAssistant, collect: bool, entity_id: str) -> None:
"""Initialize template state."""
self._hass = hass
self._state = state
self._collect = collect
self._entity_id = entity_id
def _collect_state(self) -> None:
if self._collect and _RENDER_INFO in self._hass.data:
self._hass.data[_RENDER_INFO].entities.add(self._state.entity_id)
self._hass.data[_RENDER_INFO].entities.add(self._entity_id)
# Jinja will try __getitem__ first and it avoids the need
# to call is_safe_attribute
@ -743,10 +745,10 @@ class TemplateState(State):
if item in _COLLECTABLE_STATE_ATTRIBUTES:
# _collect_state inlined here for performance
if self._collect and _RENDER_INFO in self._hass.data:
self._hass.data[_RENDER_INFO].entities.add(self._state.entity_id)
self._hass.data[_RENDER_INFO].entities.add(self._entity_id)
return getattr(self._state, item)
if item == "entity_id":
return self._state.entity_id
return self._entity_id
if item == "state_with_unit":
return self.state_with_unit
raise KeyError
@ -757,7 +759,7 @@ class TemplateState(State):
Intentionally does not collect state
"""
return self._state.entity_id
return self._entity_id
@property
def state(self):
@ -819,11 +821,44 @@ class TemplateState(State):
self._collect_state()
return self._state.__eq__(other)
class TemplateState(TemplateStateBase):
"""Class to represent a state object in a template."""
__slots__ = ("_state",)
# Inheritance is done so functions that check against State keep working
# pylint: disable=super-init-not-called
def __init__(self, hass: HomeAssistant, state: State, collect: bool = True) -> None:
"""Initialize template state."""
super().__init__(hass, collect, state.entity_id)
self._state = state
def __repr__(self) -> str:
"""Representation of Template State."""
return f"<template TemplateState({self._state!r})>"
class TemplateStateFromEntityId(TemplateStateBase):
"""Class to represent a state object in a template."""
def __init__(
self, hass: HomeAssistant, entity_id: str, collect: bool = True
) -> None:
"""Initialize template state."""
super().__init__(hass, collect, entity_id)
@property
def _state(self) -> State: # type: ignore[override] # mypy issue 4125
state = self._hass.states.get(self._entity_id)
assert state
return state
def __repr__(self) -> str:
"""Representation of Template State."""
return f"<template TemplateStateFromEntityId({self._entity_id})>"
def _collect_state(hass: HomeAssistant, entity_id: str) -> None:
if (entity_collect := hass.data.get(_RENDER_INFO)) is not None:
entity_collect.entities.add(entity_id)

View file

@ -620,6 +620,35 @@ async def test_sun_renders_once_per_sensor(hass, start_ha):
}
@pytest.mark.parametrize("count,domain", [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"value_template": "{{ this.attributes.test }}: {{ this.entity_id }}",
"attribute_templates": {
"test": "It {{ states.sensor.test_state.state }}"
},
}
},
},
},
],
)
async def test_this_variable(hass, start_ha):
"""Test template."""
assert hass.states.get(TEST_NAME).state == "It: " + TEST_NAME
hass.states.async_set("sensor.test_state", "Works")
await hass.async_block_till_done()
await hass.async_block_till_done()
assert hass.states.get(TEST_NAME).state == "It Works: " + TEST_NAME
@pytest.mark.parametrize("count,domain", [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",