Ensure 'this' variable is always defined for template entities (#70911)

This commit is contained in:
Erik Montnemery 2022-05-03 16:43:44 +02:00 committed by GitHub
parent 08b683dafd
commit eba125b093
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 140 additions and 7 deletions

View File

@ -17,8 +17,9 @@ from homeassistant.const import (
CONF_ICON_TEMPLATE, CONF_ICON_TEMPLATE,
CONF_NAME, CONF_NAME,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
STATE_UNKNOWN,
) )
from homeassistant.core import CoreState, Event, callback from homeassistant.core import CoreState, Event, State, callback
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -251,13 +252,28 @@ class TemplateEntity(Entity):
self._entity_picture_template = config.get(CONF_PICTURE) self._entity_picture_template = config.get(CONF_PICTURE)
self._friendly_name_template = config.get(CONF_NAME) self._friendly_name_template = config.get(CONF_NAME)
class DummyState(State):
"""None-state for template entities not yet added to the state machine."""
def __init__(self) -> None:
"""Initialize a new state."""
super().__init__("unknown.unknown", STATE_UNKNOWN)
self.entity_id = None # type: ignore[assignment]
@property
def name(self) -> str:
"""Name of this state."""
return "<None>"
variables = {"this": DummyState()}
# Try to render the name as it can influence the entity ID # Try to render the name as it can influence the entity ID
self._attr_name = fallback_name self._attr_name = fallback_name
if self._friendly_name_template: if self._friendly_name_template:
self._friendly_name_template.hass = hass self._friendly_name_template.hass = hass
with contextlib.suppress(TemplateError): with contextlib.suppress(TemplateError):
self._attr_name = self._friendly_name_template.async_render( self._attr_name = self._friendly_name_template.async_render(
parse_result=False variables=variables, parse_result=False
) )
# Templates will not render while the entity is unavailable, try to render the # Templates will not render while the entity is unavailable, try to render the
@ -266,13 +282,15 @@ class TemplateEntity(Entity):
self._entity_picture_template.hass = hass self._entity_picture_template.hass = hass
with contextlib.suppress(TemplateError): with contextlib.suppress(TemplateError):
self._attr_entity_picture = self._entity_picture_template.async_render( self._attr_entity_picture = self._entity_picture_template.async_render(
parse_result=False variables=variables, parse_result=False
) )
if self._icon_template: if self._icon_template:
self._icon_template.hass = hass self._icon_template.hass = hass
with contextlib.suppress(TemplateError): with contextlib.suppress(TemplateError):
self._attr_icon = self._icon_template.async_render(parse_result=False) self._attr_icon = self._icon_template.async_render(
variables=variables, parse_result=False
)
@callback @callback
def _update_available(self, result): def _update_available(self, result):
@ -373,10 +391,10 @@ class TemplateEntity(Entity):
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)} variables = {"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, values) template_var_tup = TrackTemplate(template, variables)
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

View File

@ -850,7 +850,8 @@ class TemplateStateFromEntityId(TemplateStateBase):
@property @property
def _state(self) -> State: # type: ignore[override] # mypy issue 4125 def _state(self) -> State: # type: ignore[override] # mypy issue 4125
state = self._hass.states.get(self._entity_id) state = self._hass.states.get(self._entity_id)
assert state if not state:
state = State(self._entity_id, STATE_UNKNOWN)
return state return state
def __repr__(self) -> str: def __repr__(self) -> str:

View File

@ -653,6 +653,120 @@ async def test_this_variable(hass, start_ha):
assert hass.states.get(TEST_NAME).state == "It Works: " + TEST_NAME assert hass.states.get(TEST_NAME).state == "It Works: " + TEST_NAME
@pytest.mark.parametrize("count,domain", [(1, "template")])
@pytest.mark.parametrize(
"config",
[
{
"template": {
"sensor": {
"state": "{{ this.attributes.get('test', 'no-test!') }}: {{ this.entity_id }}",
"icon": "mdi:{% if this.entity_id in states and 'friendly_name' in this.attributes %} {{this.attributes['friendly_name']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}",
"name": "{% if this.entity_id in states and 'friendly_name' in this.attributes %} {{this.attributes['friendly_name']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}",
"picture": "{% if this.entity_id in states and 'entity_picture' in this.attributes %} {{this.attributes['entity_picture']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}",
"attributes": {"test": "{{ this.entity_id }}"},
},
},
},
],
)
async def test_this_variable_early_hass_not_running(hass, config, count, domain):
"""Test referencing 'this' variable before the entity is in the state machine.
Hass is not yet started when the entity is added.
Icon, name and picture templates are rendered once in the constructor.
"""
entity_id = "sensor.none_false"
hass.state = CoreState.not_running
# Setup template
with assert_setup_component(count, domain):
assert await async_setup_component(
hass,
domain,
config,
)
await hass.async_block_till_done()
await hass.async_block_till_done()
# Sensor state not rendered, icon, name and picture
# templates rendered in constructor with entity_id set to None
state = hass.states.get(entity_id)
assert state.state == "unknown"
assert state.attributes == {
"entity_picture": "None:False",
"friendly_name": "None:False",
"icon": "mdi:None:False",
}
# Signal hass started
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
# Re-render icon, name, pciture + other templates now rendered
state = hass.states.get(entity_id)
assert state.state == "sensor.none_false: sensor.none_false"
assert state.attributes == {
"entity_picture": "sensor.none_false:False",
"friendly_name": "sensor.none_false:False",
"icon": "mdi:sensor.none_false:False",
"test": "sensor.none_false",
}
@pytest.mark.parametrize("count,domain", [(1, "template")])
@pytest.mark.parametrize(
"config",
[
{
"template": {
"sensor": {
"state": "{{ this.attributes.get('test', 'no-test!') }}: {{ this.entity_id }}",
"icon": "mdi:{% if this.entity_id in states and 'friendly_name' in this.attributes %} {{this.attributes['friendly_name']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}",
"name": "{% if this.entity_id in states and 'friendly_name' in this.attributes %} {{this.attributes['friendly_name']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}",
"picture": "{% if this.entity_id in states and 'entity_picture' in this.attributes %} {{this.attributes['entity_picture']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}",
"attributes": {"test": "{{ this.entity_id }}"},
},
},
},
],
)
async def test_this_variable_early_hass_running(hass, config, count, domain):
"""Test referencing 'this' variable before the entity is in the state machine.
Hass is already started when the entity is added.
Icon, name and picture templates are rendered in the constructor, and again
before the entity is added to hass.
"""
# Start hass
assert hass.state == CoreState.running
await hass.async_start()
await hass.async_block_till_done()
# Setup template
with assert_setup_component(count, domain):
assert await async_setup_component(
hass,
domain,
config,
)
await hass.async_block_till_done()
await hass.async_block_till_done()
entity_id = "sensor.none_false"
# All templated rendered
state = hass.states.get(entity_id)
assert state.state == "sensor.none_false: sensor.none_false"
assert state.attributes == {
"entity_picture": "sensor.none_false:False",
"friendly_name": "sensor.none_false:False",
"icon": "mdi:sensor.none_false:False",
"test": "sensor.none_false",
}
@pytest.mark.parametrize("count,domain", [(1, sensor.DOMAIN)]) @pytest.mark.parametrize("count,domain", [(1, sensor.DOMAIN)])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config", "config",