mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 11:47:06 +00:00
Ensure 'this' variable is always defined for template entities (#70911)
This commit is contained in:
parent
08b683dafd
commit
eba125b093
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user