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:
parent
9316909e60
commit
d20a620590
3 changed files with 81 additions and 10 deletions
|
@ -27,7 +27,11 @@ from homeassistant.helpers.event import (
|
||||||
TrackTemplateResult,
|
TrackTemplateResult,
|
||||||
async_track_template_result,
|
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 (
|
from .const import (
|
||||||
CONF_ATTRIBUTE_TEMPLATES,
|
CONF_ATTRIBUTE_TEMPLATES,
|
||||||
|
@ -368,8 +372,11 @@ class TemplateEntity(Entity):
|
||||||
async def _async_template_startup(self, *_) -> None:
|
async def _async_template_startup(self, *_) -> None:
|
||||||
template_var_tups: list[TrackTemplate] = []
|
template_var_tups: list[TrackTemplate] = []
|
||||||
has_availability_template = False
|
has_availability_template = False
|
||||||
|
|
||||||
|
values = {"this": TemplateStateFromEntityId(self.hass, self.entity_id)}
|
||||||
|
|
||||||
for template, attributes in self._template_attrs.items():
|
for template, attributes in self._template_attrs.items():
|
||||||
template_var_tup = TrackTemplate(template, None)
|
template_var_tup = TrackTemplate(template, values)
|
||||||
is_availability_template = False
|
is_availability_template = False
|
||||||
for attribute in attributes:
|
for attribute in attributes:
|
||||||
# pylint: disable-next=protected-access
|
# pylint: disable-next=protected-access
|
||||||
|
|
|
@ -719,22 +719,24 @@ class DomainStates:
|
||||||
return f"<template DomainStates('{self._domain}')>"
|
return f"<template DomainStates('{self._domain}')>"
|
||||||
|
|
||||||
|
|
||||||
class TemplateState(State):
|
class TemplateStateBase(State):
|
||||||
"""Class to represent a state object in a template."""
|
"""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
|
# Inheritance is done so functions that check against State keep working
|
||||||
# pylint: disable=super-init-not-called
|
# 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."""
|
"""Initialize template state."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._state = state
|
|
||||||
self._collect = collect
|
self._collect = collect
|
||||||
|
self._entity_id = entity_id
|
||||||
|
|
||||||
def _collect_state(self) -> None:
|
def _collect_state(self) -> None:
|
||||||
if self._collect and _RENDER_INFO in self._hass.data:
|
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
|
# Jinja will try __getitem__ first and it avoids the need
|
||||||
# to call is_safe_attribute
|
# to call is_safe_attribute
|
||||||
|
@ -743,10 +745,10 @@ class TemplateState(State):
|
||||||
if item in _COLLECTABLE_STATE_ATTRIBUTES:
|
if item in _COLLECTABLE_STATE_ATTRIBUTES:
|
||||||
# _collect_state inlined here for performance
|
# _collect_state inlined here for performance
|
||||||
if self._collect and _RENDER_INFO in self._hass.data:
|
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)
|
return getattr(self._state, item)
|
||||||
if item == "entity_id":
|
if item == "entity_id":
|
||||||
return self._state.entity_id
|
return self._entity_id
|
||||||
if item == "state_with_unit":
|
if item == "state_with_unit":
|
||||||
return self.state_with_unit
|
return self.state_with_unit
|
||||||
raise KeyError
|
raise KeyError
|
||||||
|
@ -757,7 +759,7 @@ class TemplateState(State):
|
||||||
|
|
||||||
Intentionally does not collect state
|
Intentionally does not collect state
|
||||||
"""
|
"""
|
||||||
return self._state.entity_id
|
return self._entity_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
|
@ -819,11 +821,44 @@ class TemplateState(State):
|
||||||
self._collect_state()
|
self._collect_state()
|
||||||
return self._state.__eq__(other)
|
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:
|
def __repr__(self) -> str:
|
||||||
"""Representation of Template State."""
|
"""Representation of Template State."""
|
||||||
return f"<template TemplateState({self._state!r})>"
|
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:
|
def _collect_state(hass: HomeAssistant, entity_id: str) -> None:
|
||||||
if (entity_collect := hass.data.get(_RENDER_INFO)) is not None:
|
if (entity_collect := hass.data.get(_RENDER_INFO)) is not None:
|
||||||
entity_collect.entities.add(entity_id)
|
entity_collect.entities.add(entity_id)
|
||||||
|
|
|
@ -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("count,domain", [(1, sensor.DOMAIN)])
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"config",
|
"config",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue