core/tests/components/template/test_sensor.py

1532 lines
52 KiB
Python

"""The test for the Template sensor platform."""
from asyncio import Event
from datetime import timedelta
from unittest.mock import patch
import pytest
from homeassistant.bootstrap import async_from_config_dict
from homeassistant.components import sensor
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
ATTR_ICON,
EVENT_COMPONENT_LOADED,
EVENT_HOMEASSISTANT_START,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import Context, CoreState, HomeAssistant, State, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.helpers.template import Template
from homeassistant.setup import ATTR_COMPONENT, async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import (
assert_setup_component,
async_fire_time_changed,
mock_restore_cache_with_extra_data,
)
TEST_NAME = "sensor.test_template_sensor"
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"value_template": "It {{ states.sensor.test_state.state }}."
}
},
},
},
],
)
async def test_template_legacy(hass: HomeAssistant, start_ha) -> None:
"""Test template."""
assert hass.states.get(TEST_NAME).state == "It ."
hass.states.async_set("sensor.test_state", "Works")
await hass.async_block_till_done()
assert hass.states.get(TEST_NAME).state == "It Works."
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"value_template": "{{ states.sensor.test_state.state }}",
"icon_template": "{% if states.sensor.test_state.state == "
"'Works' %}"
"mdi:check"
"{% endif %}",
}
},
},
},
],
)
async def test_icon_template(hass: HomeAssistant, start_ha) -> None:
"""Test icon template."""
assert hass.states.get(TEST_NAME).attributes.get("icon") == ""
hass.states.async_set("sensor.test_state", "Works")
await hass.async_block_till_done()
assert hass.states.get(TEST_NAME).attributes["icon"] == "mdi:check"
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"value_template": "{{ states.sensor.test_state.state }}",
"entity_picture_template": "{% if states.sensor.test_state.state == "
"'Works' %}"
"/local/sensor.png"
"{% endif %}",
}
},
},
},
],
)
async def test_entity_picture_template(hass: HomeAssistant, start_ha) -> None:
"""Test entity_picture template."""
assert hass.states.get(TEST_NAME).attributes.get("entity_picture") == ""
hass.states.async_set("sensor.test_state", "Works")
await hass.async_block_till_done()
assert (
hass.states.get(TEST_NAME).attributes["entity_picture"] == "/local/sensor.png"
)
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
("attribute", "config", "expected"),
[
(
"friendly_name",
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"value_template": "{{ states.sensor.test_state.state }}",
"friendly_name_template": "It {{ states.sensor.test_state.state }}.",
}
},
},
},
("It .", "It Works."),
),
(
"friendly_name",
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"value_template": "{{ states.sensor.test_state.state }}",
"friendly_name_template": "{{ 'It ' + states.sensor.test_state.state + '.'}}",
}
},
},
},
(None, "It Works."),
),
(
"friendly_name",
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"value_template": "{{ states.fourohfour.state }}",
"friendly_name_template": "It {{ states.sensor.test_state.state }}.",
}
},
},
},
("It .", "It Works."),
),
(
"test_attribute",
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"value_template": "{{ states.sensor.test_state.state }}",
"attribute_templates": {
"test_attribute": "It {{ states.sensor.test_state.state }}."
},
}
},
},
},
("It .", "It Works."),
),
],
)
async def test_friendly_name_template(
hass: HomeAssistant, attribute, expected, start_ha
) -> None:
"""Test friendly_name template with an unknown value_template."""
assert hass.states.get(TEST_NAME).attributes.get(attribute) == expected[0]
hass.states.async_set("sensor.test_state", "Works")
await hass.async_block_till_done()
assert hass.states.get(TEST_NAME).attributes[attribute] == expected[1]
@pytest.mark.parametrize(("count", "domain"), [(0, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {"value_template": "{% if rubbish %}"}
},
},
},
{
"sensor": {
"platform": "template",
"sensors": {
"test INVALID sensor": {
"value_template": "{{ states.sensor.test_state.state }}"
}
},
},
},
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {"invalid"},
},
},
},
{
"sensor": {
"platform": "template",
},
},
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"not_value_template": "{{ states.sensor.test_state.state }}"
}
},
},
},
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"test": {
"value_template": "{{ states.sensor.test_sensor.state }}",
"device_class": "foobarnotreal",
}
}
},
},
},
],
)
async def test_template_syntax_error(hass: HomeAssistant, start_ha) -> None:
"""Test setup with invalid device_class."""
assert hass.states.async_all("sensor") == []
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"value_template": "It {{ states.sensor.test_state"
".attributes.missing }}."
}
},
},
},
],
)
async def test_template_attribute_missing(hass: HomeAssistant, start_ha) -> None:
"""Test missing attribute template."""
assert hass.states.get(TEST_NAME).state == STATE_UNAVAILABLE
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test1": {
"value_template": "{{ states.sensor.test_sensor.state }}",
"unit_of_measurement": "°C",
"device_class": "temperature",
},
"test2": {
"value_template": "{{ states.sensor.test_sensor.state }}"
},
},
},
},
],
)
async def test_setup_valid_device_class(hass: HomeAssistant, start_ha) -> None:
"""Test setup with valid device_class."""
hass.states.async_set("sensor.test_sensor", "75")
await hass.async_block_till_done()
assert hass.states.get("sensor.test1").attributes["device_class"] == "temperature"
assert "device_class" not in hass.states.get("sensor.test2").attributes
@pytest.mark.parametrize("load_registries", [False])
async def test_creating_sensor_loads_group(hass: HomeAssistant) -> None:
"""Test setting up template sensor loads group component first."""
order = []
after_dep_event = Event()
async def async_setup_group(hass, config):
# Make sure group takes longer to load, so that it won't
# be loaded first by chance
await after_dep_event.wait()
order.append("group")
return True
async def async_setup_template(
hass, config, async_add_entities, discovery_info=None
):
order.append("sensor.template")
return True
async def set_after_dep_event(event):
if event.data[ATTR_COMPONENT] == "sensor":
after_dep_event.set()
hass.bus.async_listen(EVENT_COMPONENT_LOADED, set_after_dep_event)
with patch(
"homeassistant.components.group.async_setup",
new=async_setup_group,
), patch(
"homeassistant.components.template.sensor.async_setup_platform",
new=async_setup_template,
):
await async_from_config_dict(
{"sensor": {"platform": "template", "sensors": {}}, "group": {}}, hass
)
await hass.async_block_till_done()
assert order == ["group", "sensor.template"]
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"value_template": "{{ states.sensor.test_sensor.state }}",
"availability_template": "{{ is_state('sensor.availability_sensor', 'on') }}",
}
},
},
},
],
)
async def test_available_template_with_entities(hass: HomeAssistant, start_ha) -> None:
"""Test availability tempalates with values from other entities."""
hass.states.async_set("sensor.availability_sensor", STATE_OFF)
# When template returns true..
hass.states.async_set("sensor.availability_sensor", STATE_ON)
await hass.async_block_till_done()
# Device State should not be unavailable
assert hass.states.get(TEST_NAME).state != STATE_UNAVAILABLE
# When Availability template returns false
hass.states.async_set("sensor.availability_sensor", STATE_OFF)
await hass.async_block_till_done()
# device state should be unavailable
assert hass.states.get(TEST_NAME).state == STATE_UNAVAILABLE
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"invalid_template": {
"value_template": "{{ states.sensor.test_sensor.state }}",
"attribute_templates": {
"test_attribute": "{{ states.sensor.unknown.attributes.picture }}"
},
}
},
},
},
],
)
async def test_invalid_attribute_template(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, start_ha, caplog_setup_text
) -> None:
"""Test that errors are logged if rendering template fails."""
hass.states.async_set("sensor.test_sensor", "startup")
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 2
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
await async_update_entity(hass, "sensor.invalid_template")
assert "TemplateError" in caplog_setup_text
assert "test_attribute" in caplog.text
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"my_sensor": {
"value_template": "{{ states.sensor.test_state.state }}",
"availability_template": "{{ x - 12 }}",
}
},
},
},
],
)
async def test_invalid_availability_template_keeps_component_available(
hass: HomeAssistant, start_ha, caplog_setup_text
) -> None:
"""Test that an invalid availability keeps the device available."""
assert hass.states.get("sensor.my_sensor").state != STATE_UNAVAILABLE
assert "UndefinedError: 'x' is undefined" in caplog_setup_text
async def test_no_template_match_all(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that we allow static templates."""
hass.states.async_set("sensor.test_sensor", "startup")
hass.state = CoreState.not_running
await async_setup_component(
hass,
sensor.DOMAIN,
{
"sensor": {
"platform": "template",
"sensors": {
"invalid_state": {"value_template": "{{ 1 + 1 }}"},
"invalid_icon": {
"value_template": "{{ states.sensor.test_sensor.state }}",
"icon_template": "{{ 1 + 1 }}",
},
"invalid_entity_picture": {
"value_template": "{{ states.sensor.test_sensor.state }}",
"entity_picture_template": "{{ 1 + 1 }}",
},
"invalid_friendly_name": {
"value_template": "{{ states.sensor.test_sensor.state }}",
"friendly_name_template": "{{ 1 + 1 }}",
},
"invalid_attribute": {
"value_template": "{{ states.sensor.test_sensor.state }}",
"attribute_templates": {"test_attribute": "{{ 1 + 1 }}"},
},
},
}
},
)
await hass.async_block_till_done()
assert hass.states.get("sensor.invalid_state").state == "unknown"
assert hass.states.get("sensor.invalid_icon").state == "unknown"
assert hass.states.get("sensor.invalid_entity_picture").state == "unknown"
assert hass.states.get("sensor.invalid_friendly_name").state == "unknown"
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 6
assert hass.states.get("sensor.invalid_state").state == "unknown"
assert hass.states.get("sensor.invalid_icon").state == "unknown"
assert hass.states.get("sensor.invalid_entity_picture").state == "unknown"
assert hass.states.get("sensor.invalid_friendly_name").state == "unknown"
assert hass.states.get("sensor.invalid_attribute").state == "unknown"
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert hass.states.get("sensor.invalid_state").state == "2"
assert hass.states.get("sensor.invalid_icon").state == "startup"
assert hass.states.get("sensor.invalid_entity_picture").state == "startup"
assert hass.states.get("sensor.invalid_friendly_name").state == "startup"
assert hass.states.get("sensor.invalid_attribute").state == "startup"
hass.states.async_set("sensor.test_sensor", "hello")
await hass.async_block_till_done()
assert hass.states.get("sensor.invalid_state").state == "2"
# Will now process because we have at least one valid template
assert hass.states.get("sensor.invalid_icon").state == "hello"
assert hass.states.get("sensor.invalid_entity_picture").state == "hello"
assert hass.states.get("sensor.invalid_friendly_name").state == "hello"
assert hass.states.get("sensor.invalid_attribute").state == "hello"
await async_update_entity(hass, "sensor.invalid_state")
await async_update_entity(hass, "sensor.invalid_icon")
await async_update_entity(hass, "sensor.invalid_entity_picture")
await async_update_entity(hass, "sensor.invalid_friendly_name")
await async_update_entity(hass, "sensor.invalid_attribute")
assert hass.states.get("sensor.invalid_state").state == "2"
assert hass.states.get("sensor.invalid_icon").state == "hello"
assert hass.states.get("sensor.invalid_entity_picture").state == "hello"
assert hass.states.get("sensor.invalid_friendly_name").state == "hello"
assert hass.states.get("sensor.invalid_attribute").state == "hello"
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
@pytest.mark.parametrize(
"config",
[
{
"template": {
"unique_id": "group-id",
"sensor": {"name": "top-level", "unique_id": "sensor-id", "state": "5"},
},
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor_01": {
"unique_id": "not-so-unique-anymore",
"value_template": "{{ true }}",
},
"test_template_sensor_02": {
"unique_id": "not-so-unique-anymore",
"value_template": "{{ false }}",
},
},
},
},
],
)
async def test_unique_id(
hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry
) -> None:
"""Test unique_id option only creates one sensor per id."""
assert len(hass.states.async_all()) == 2
assert len(entity_registry.entities) == 2
assert entity_registry.async_get_entity_id(
"sensor", "template", "group-id-sensor-id"
)
assert entity_registry.async_get_entity_id(
"sensor", "template", "not-so-unique-anymore"
)
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"solar_angle": {
"friendly_name": "Sun angle",
"unit_of_measurement": "degrees",
"value_template": "{{ state_attr('sun.sun', 'elevation') }}",
},
"sunrise": {
"value_template": "{{ state_attr('sun.sun', 'next_rising') }}"
},
},
}
},
],
)
async def test_sun_renders_once_per_sensor(hass: HomeAssistant, start_ha) -> None:
"""Test sun change renders the template only once per sensor."""
now = dt_util.utcnow()
hass.states.async_set(
"sun.sun", "above_horizon", {"elevation": 45.3, "next_rising": now}
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 3
assert hass.states.get("sensor.solar_angle").state == "45.3"
assert hass.states.get("sensor.sunrise").state == str(now)
async_render_calls = []
@callback
def _record_async_render(self, *args, **kwargs):
"""Catch async_render."""
async_render_calls.append(self.template)
return "75"
later = dt_util.utcnow()
with patch.object(Template, "async_render", _record_async_render):
hass.states.async_set("sun.sun", {"elevation": 50, "next_rising": later})
await hass.async_block_till_done()
assert hass.states.get("sensor.solar_angle").state == "75"
assert hass.states.get("sensor.sunrise").state == "75"
assert len(async_render_calls) == 2
assert set(async_render_calls) == {
"{{ state_attr('sun.sun', 'elevation') }}",
"{{ state_attr('sun.sun', 'next_rising') }}",
}
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
@pytest.mark.parametrize(
"config",
[
{
"template": {
"sensor": {
"name": "test_template_sensor",
"state": "{{ this.attributes.test }}: {{ this.entity_id }}",
"attributes": {"test": "It {{ states.sensor.test_state.state }}"},
},
},
},
{
"template": {
"trigger": {
"platform": "state",
"entity_id": [
"sensor.test_state",
"sensor.test_template_sensor",
],
},
"sensor": {
"name": "test_template_sensor",
"state": "{{ this.attributes.test }}: {{ this.entity_id }}",
"attributes": {"test": "It {{ states.sensor.test_state.state }}"},
},
},
},
],
)
async def test_this_variable(hass: HomeAssistant, start_ha) -> None:
"""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, "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: HomeAssistant, config, count, domain
) -> None:
"""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: HomeAssistant, config, count, domain
) -> None:
"""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",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test": {
"value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}",
},
},
}
},
],
)
async def test_self_referencing_sensor_loop(
hass: HomeAssistant, start_ha, caplog_setup_text
) -> None:
"""Test a self referencing sensor does not loop forever."""
assert len(hass.states.async_all()) == 1
await hass.async_block_till_done()
await hass.async_block_till_done()
assert "Template loop detected" in caplog_setup_text
assert int(hass.states.get("sensor.test").state) == 2
await hass.async_block_till_done()
assert int(hass.states.get("sensor.test").state) == 2
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test": {
"value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}",
"icon_template": "{% if ((states.sensor.test.state or 0) | int) >= 1 %}mdi:greater{% else %}mdi:less{% endif %}",
},
},
}
},
],
)
async def test_self_referencing_sensor_with_icon_loop(
hass: HomeAssistant, start_ha, caplog_setup_text
) -> None:
"""Test a self referencing sensor loops forever with a valid self referencing icon."""
assert len(hass.states.async_all()) == 1
await hass.async_block_till_done()
await hass.async_block_till_done()
assert "Template loop detected" in caplog_setup_text
state = hass.states.get("sensor.test")
assert int(state.state) == 3
assert state.attributes[ATTR_ICON] == "mdi:greater"
await hass.async_block_till_done()
state = hass.states.get("sensor.test")
assert int(state.state) == 3
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test": {
"value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}",
"icon_template": "{% if ((states.sensor.test.state or 0) | int) > 3 %}mdi:greater{% else %}mdi:less{% endif %}",
"entity_picture_template": "{% if ((states.sensor.test.state or 0) | int) >= 1 %}bigpic{% else %}smallpic{% endif %}",
},
},
}
},
],
)
async def test_self_referencing_sensor_with_icon_and_picture_entity_loop(
hass: HomeAssistant, start_ha, caplog_setup_text
) -> None:
"""Test a self referencing sensor loop forevers with a valid self referencing icon."""
assert len(hass.states.async_all()) == 1
await hass.async_block_till_done()
await hass.async_block_till_done()
assert "Template loop detected" in caplog_setup_text
state = hass.states.get("sensor.test")
assert int(state.state) == 4
assert state.attributes[ATTR_ICON] == "mdi:less"
assert state.attributes[ATTR_ENTITY_PICTURE] == "bigpic"
await hass.async_block_till_done()
assert int(state.state) == 4
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test": {
"value_template": "{{ 1 }}",
"entity_picture_template": "{{ ((states.sensor.test.attributes['entity_picture'] or 0) | int) + 1 }}",
},
},
}
},
],
)
async def test_self_referencing_entity_picture_loop(
hass: HomeAssistant, start_ha, caplog_setup_text
) -> None:
"""Test a self referencing sensor does not loop forever with a looping self referencing entity picture."""
assert len(hass.states.async_all()) == 1
next_time = dt_util.utcnow() + timedelta(seconds=1.2)
with patch(
"homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time
):
async_fire_time_changed(hass, next_time)
await hass.async_block_till_done()
await hass.async_block_till_done()
assert "Template loop detected" in caplog_setup_text
state = hass.states.get("sensor.test")
assert int(state.state) == 1
assert state.attributes[ATTR_ENTITY_PICTURE] == 2
await hass.async_block_till_done()
assert int(state.state) == 1
async def test_self_referencing_icon_with_no_loop(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test a self referencing icon that does not loop."""
hass.states.async_set("sensor.heartworm_high_80", 10)
hass.states.async_set("sensor.heartworm_low_57", 10)
hass.states.async_set("sensor.heartworm_avg_64", 10)
hass.states.async_set("sensor.heartworm_avg_57", 10)
value_template_str = """{% if (states.sensor.heartworm_high_80.state|int >= 10) and (states.sensor.heartworm_low_57.state|int >= 10) %}
extreme
{% elif (states.sensor.heartworm_avg_64.state|int >= 30) %}
high
{% elif (states.sensor.heartworm_avg_64.state|int >= 14) %}
moderate
{% elif (states.sensor.heartworm_avg_64.state|int >= 5) %}
slight
{% elif (states.sensor.heartworm_avg_57.state|int >= 5) %}
marginal
{% elif (states.sensor.heartworm_avg_57.state|int < 5) %}
none
{% endif %}"""
icon_template_str = """{% if is_state('sensor.heartworm_risk',"extreme") %}
mdi:hazard-lights
{% elif is_state('sensor.heartworm_risk',"high") %}
mdi:triangle-outline
{% elif is_state('sensor.heartworm_risk',"moderate") %}
mdi:alert-circle-outline
{% elif is_state('sensor.heartworm_risk',"slight") %}
mdi:exclamation
{% elif is_state('sensor.heartworm_risk',"marginal") %}
mdi:heart
{% elif is_state('sensor.heartworm_risk',"none") %}
mdi:snowflake
{% endif %}"""
await async_setup_component(
hass,
sensor.DOMAIN,
{
"sensor": {
"platform": "template",
"sensors": {
"heartworm_risk": {
"value_template": value_template_str,
"icon_template": icon_template_str,
},
},
}
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 5
hass.states.async_set("sensor.heartworm_high_80", 10)
await hass.async_block_till_done()
await hass.async_block_till_done()
assert "Template loop detected" not in caplog.text
state = hass.states.get("sensor.heartworm_risk")
assert state.state == "extreme"
assert state.attributes[ATTR_ICON] == "mdi:hazard-lights"
await hass.async_block_till_done()
assert state.state == "extreme"
assert state.attributes[ATTR_ICON] == "mdi:hazard-lights"
assert "Template loop detected" not in caplog.text
@pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"value_template": "{{ states.sensor.test_state.state }}",
"friendly_name_template": "{{ states.sensor.test_state.state }}",
}
},
}
},
],
)
async def test_duplicate_templates(hass: HomeAssistant, start_ha) -> None:
"""Test template entity where the value and friendly name as the same template."""
hass.states.async_set("sensor.test_state", "Abc")
await hass.async_block_till_done()
state = hass.states.get(TEST_NAME)
assert state.attributes["friendly_name"] == "Abc"
assert state.state == "Abc"
hass.states.async_set("sensor.test_state", "Def")
await hass.async_block_till_done()
state = hass.states.get(TEST_NAME)
assert state.attributes["friendly_name"] == "Def"
assert state.state == "Def"
@pytest.mark.parametrize(("count", "domain"), [(2, "template")])
@pytest.mark.parametrize(
"config",
[
{
"template": [
{"invalid": "config"},
# Config after invalid should still be set up
{
"unique_id": "listening-test-event",
"trigger": {"platform": "event", "event_type": "test_event"},
"sensors": {
"hello": {
"friendly_name": "Hello Name",
"unique_id": "hello_name-id",
"device_class": "battery",
"unit_of_measurement": "%",
"value_template": "{{ trigger.event.data.beer }}",
"entity_picture_template": "{{ '/local/dogs.png' }}",
"icon_template": "{{ 'mdi:pirate' }}",
"attribute_templates": {
"plus_one": "{{ trigger.event.data.beer + 1 }}"
},
},
},
"sensor": [
{
"name": "via list",
"unique_id": "via_list-id",
"device_class": "battery",
"unit_of_measurement": "%",
"availability": "{{ True }}",
"state": "{{ trigger.event.data.beer + 1 }}",
"picture": "{{ '/local/dogs.png' }}",
"icon": "{{ 'mdi:pirate' }}",
"attributes": {
"plus_one": "{{ trigger.event.data.beer + 1 }}"
},
"state_class": "measurement",
}
],
},
{
"trigger": [],
"sensors": {
"bare_minimum": {
"value_template": "{{ trigger.event.data.beer }}"
},
},
},
],
},
],
)
async def test_trigger_entity(
hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry
) -> None:
"""Test trigger entity works."""
state = hass.states.get("sensor.hello_name")
assert state is not None
assert state.state == STATE_UNKNOWN
state = hass.states.get("sensor.bare_minimum")
assert state is not None
assert state.state == STATE_UNKNOWN
context = Context()
hass.bus.async_fire("test_event", {"beer": 2}, context=context)
await hass.async_block_till_done()
state = hass.states.get("sensor.hello_name")
assert state.state == "2"
assert state.attributes.get("device_class") == "battery"
assert state.attributes.get("icon") == "mdi:pirate"
assert state.attributes.get("entity_picture") == "/local/dogs.png"
assert state.attributes.get("plus_one") == 3
assert state.attributes.get("unit_of_measurement") == "%"
assert state.context is context
assert len(entity_registry.entities) == 2
assert (
entity_registry.entities["sensor.hello_name"].unique_id
== "listening-test-event-hello_name-id"
)
assert (
entity_registry.entities["sensor.via_list"].unique_id
== "listening-test-event-via_list-id"
)
state = hass.states.get("sensor.via_list")
assert state.state == "3"
assert state.attributes.get("device_class") == "battery"
assert state.attributes.get("icon") == "mdi:pirate"
assert state.attributes.get("entity_picture") == "/local/dogs.png"
assert state.attributes.get("plus_one") == 3
assert state.attributes.get("unit_of_measurement") == "%"
assert state.attributes.get("state_class") == "measurement"
assert state.context is context
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
@pytest.mark.parametrize(
"config",
[
{
"template": {
"trigger": {"platform": "event", "event_type": "test_event"},
"sensors": {
"hello": {
"unique_id": "no-base-id",
"friendly_name": "Hello",
"value_template": "{{ non_existing + 1 }}",
}
},
},
},
],
)
async def test_trigger_entity_render_error(
hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry
) -> None:
"""Test trigger entity handles render error."""
state = hass.states.get("sensor.hello")
assert state is not None
assert state.state == STATE_UNKNOWN
context = Context()
hass.bus.async_fire("test_event", {"beer": 2}, context=context)
await hass.async_block_till_done()
state = hass.states.get("sensor.hello")
assert state.state == STATE_UNAVAILABLE
assert len(entity_registry.entities) == 1
assert entity_registry.entities["sensor.hello"].unique_id == "no-base-id"
@pytest.mark.parametrize(("count", "domain"), [(0, sensor.DOMAIN)])
@pytest.mark.parametrize(
"config",
[
{
"sensor": {
"platform": "template",
"trigger": {"platform": "event", "event_type": "test_event"},
"sensors": {
"test_template_sensor": {
"value_template": "{{ states.sensor.test_state.state }}",
"friendly_name_template": "{{ states.sensor.test_state.state }}",
}
},
}
},
],
)
async def test_trigger_not_allowed_platform_config(
hass: HomeAssistant, start_ha, caplog_setup_text
) -> None:
"""Test we throw a helpful warning if a trigger is configured in platform config."""
state = hass.states.get(TEST_NAME)
assert state is None
assert (
"You can only add triggers to template entities if they are defined under `template:`."
in caplog_setup_text
)
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
@pytest.mark.parametrize(
"config",
[
{
"template": {
"sensor": {
"name": "top-level",
"device_class": "battery",
"state_class": "measurement",
"state": "5",
"unit_of_measurement": "%",
},
},
},
],
)
async def test_config_top_level(hass: HomeAssistant, start_ha) -> None:
"""Test unique_id option only creates one sensor per id."""
assert len(hass.states.async_all()) == 1
state = hass.states.get("sensor.top_level")
assert state is not None
assert state.state == "5"
assert state.attributes["device_class"] == "battery"
assert state.attributes["state_class"] == "measurement"
async def test_trigger_entity_available(hass: HomeAssistant) -> None:
"""Test trigger entity availability works."""
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"trigger": {"platform": "event", "event_type": "test_event"},
"sensor": [
{
"name": "Maybe Available",
"availability": "{{ trigger and trigger.event.data.beer == 2 }}",
"state": "{{ trigger.event.data.beer }}",
},
],
},
],
},
)
await hass.async_block_till_done()
# Sensors are unknown if never triggered
state = hass.states.get("sensor.maybe_available")
assert state is not None
assert state.state == STATE_UNKNOWN
hass.bus.async_fire("test_event", {"beer": 2})
await hass.async_block_till_done()
state = hass.states.get("sensor.maybe_available")
assert state.state == "2"
hass.bus.async_fire("test_event", {"beer": 1})
await hass.async_block_till_done()
state = hass.states.get("sensor.maybe_available")
assert state.state == "unavailable"
async def test_trigger_entity_device_class_parsing_works(hass: HomeAssistant) -> None:
"""Test trigger entity device class parsing works."""
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"trigger": {"platform": "event", "event_type": "test_event"},
"sensor": [
{
"name": "Date entity",
"state": "{{ now().date() }}",
"device_class": "date",
},
{
"name": "Timestamp entity",
"state": "{{ now() }}",
"device_class": "timestamp",
},
],
},
],
},
)
await hass.async_block_till_done()
# State of timestamp sensors are always in UTC
now = dt_util.utcnow()
with patch("homeassistant.util.dt.now", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
date_state = hass.states.get("sensor.date_entity")
assert date_state is not None
assert date_state.state == now.date().isoformat()
ts_state = hass.states.get("sensor.timestamp_entity")
assert ts_state is not None
assert ts_state.state == now.isoformat(timespec="seconds")
async def test_trigger_entity_device_class_errors_works(hass: HomeAssistant) -> None:
"""Test trigger entity device class errors works."""
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"trigger": {"platform": "event", "event_type": "test_event"},
"sensor": [
{
"name": "Date entity",
"state": "invalid",
"device_class": "date",
},
{
"name": "Timestamp entity",
"state": "invalid",
"device_class": "timestamp",
},
],
},
],
},
)
await hass.async_block_till_done()
now = dt_util.now()
with patch("homeassistant.util.dt.now", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
date_state = hass.states.get("sensor.date_entity")
assert date_state is not None
assert date_state.state == STATE_UNKNOWN
ts_state = hass.states.get("sensor.timestamp_entity")
assert ts_state is not None
assert ts_state.state == STATE_UNKNOWN
async def test_entity_device_class_parsing_works(hass: HomeAssistant) -> None:
"""Test entity device class parsing works."""
# State of timestamp sensors are always in UTC
now = dt_util.utcnow()
with patch("homeassistant.util.dt.now", return_value=now):
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"sensor": [
{
"name": "Date entity",
"state": "{{ now().date() }}",
"device_class": "date",
},
{
"name": "Timestamp entity",
"state": "{{ now() }}",
"device_class": "timestamp",
},
],
},
],
},
)
await hass.async_block_till_done()
date_state = hass.states.get("sensor.date_entity")
assert date_state is not None
assert date_state.state == now.date().isoformat()
ts_state = hass.states.get("sensor.timestamp_entity")
assert ts_state is not None
assert ts_state.state == now.isoformat(timespec="seconds")
async def test_entity_device_class_errors_works(hass: HomeAssistant) -> None:
"""Test entity device class errors works."""
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"sensor": [
{
"name": "Date entity",
"state": "invalid",
"device_class": "date",
},
{
"name": "Timestamp entity",
"state": "invalid",
"device_class": "timestamp",
},
],
},
],
},
)
await hass.async_block_till_done()
now = dt_util.now()
with patch("homeassistant.util.dt.now", return_value=now):
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
date_state = hass.states.get("sensor.date_entity")
assert date_state is not None
assert date_state.state == STATE_UNKNOWN
ts_state = hass.states.get("sensor.timestamp_entity")
assert ts_state is not None
assert ts_state.state == STATE_UNKNOWN
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
@pytest.mark.parametrize(
"config",
[
{
"template": {
"trigger": {"platform": "event", "event_type": "test_event"},
"sensor": {
"name": "test",
"state": "{{ trigger.event.data.beer }}",
"picture": "{{ '/local/dogs.png' }}",
"icon": "{{ 'mdi:pirate' }}",
"attributes": {
"plus_one": "{{ trigger.event.data.beer + 1 }}",
"another": "{{ trigger.event.data.uno_mas or 1 }}",
},
},
},
},
],
)
@pytest.mark.parametrize(
("restored_state", "restored_native_value", "initial_state", "initial_attributes"),
[
# the native value should be used, not the state
("dog", 10, "10", ["entity_picture", "icon", "plus_one"]),
(STATE_UNAVAILABLE, 10, STATE_UNKNOWN, []),
(STATE_UNKNOWN, 10, STATE_UNKNOWN, []),
],
)
async def test_trigger_entity_restore_state(
hass: HomeAssistant,
count,
domain,
config,
restored_state,
restored_native_value,
initial_state,
initial_attributes,
) -> None:
"""Test restoring trigger template binary sensor."""
restored_attributes = {
"entity_picture": "/local/cats.png",
"icon": "mdi:ship",
"plus_one": 55,
}
fake_state = State(
"sensor.test",
restored_state,
restored_attributes,
)
fake_extra_data = {
"native_value": restored_native_value,
"native_unit_of_measurement": None,
}
mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),))
with assert_setup_component(count, domain):
assert await async_setup_component(
hass,
domain,
config,
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
state = hass.states.get("sensor.test")
assert state.state == initial_state
for attr in restored_attributes:
if attr in initial_attributes:
assert state.attributes[attr] == restored_attributes[attr]
else:
assert attr not in state.attributes
assert "another" not in state.attributes
hass.bus.async_fire("test_event", {"beer": 2})
await hass.async_block_till_done()
state = hass.states.get("sensor.test")
assert state.state == "2"
assert state.attributes["icon"] == "mdi:pirate"
assert state.attributes["entity_picture"] == "/local/dogs.png"
assert state.attributes["plus_one"] == 3
assert state.attributes["another"] == 1