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

pull/71250/head
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_NAME,
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
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@ -251,13 +252,28 @@ class TemplateEntity(Entity):
self._entity_picture_template = config.get(CONF_PICTURE)
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
self._attr_name = fallback_name
if self._friendly_name_template:
self._friendly_name_template.hass = hass
with contextlib.suppress(TemplateError):
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
@ -266,13 +282,15 @@ class TemplateEntity(Entity):
self._entity_picture_template.hass = hass
with contextlib.suppress(TemplateError):
self._attr_entity_picture = self._entity_picture_template.async_render(
parse_result=False
variables=variables, parse_result=False
)
if self._icon_template:
self._icon_template.hass = hass
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
def _update_available(self, result):
@ -373,10 +391,10 @@ class TemplateEntity(Entity):
template_var_tups: list[TrackTemplate] = []
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():
template_var_tup = TrackTemplate(template, values)
template_var_tup = TrackTemplate(template, variables)
is_availability_template = False
for attribute in attributes:
# pylint: disable-next=protected-access

View File

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