Ensure 'this' variable is always defined for template entities (#70911)
parent
08b683dafd
commit
eba125b093
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue