1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-18 14:29:57 +01:00
Files
core/tests/helpers/template/test_init.py
T

1017 lines
33 KiB
Python

"""Test Home Assistant template helper methods."""
from datetime import datetime
from unittest.mock import patch
from freezegun import freeze_time
import pytest
import voluptuous as vol
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import entity_registry as er, template
from homeassistant.helpers.template.render_info import (
ALL_STATES_RATE_LIMIT,
DOMAIN_STATES_RATE_LIMIT,
)
from homeassistant.util import dt as dt_util
from .helpers import assert_result_info, render, render_to_info
async def test_template_render_missing_hass(hass: HomeAssistant) -> None:
"""Test template render when hass is not set."""
hass.states.async_set("sensor.test", "23")
template_obj = template.Template("{{ states('sensor.test') }}", None)
template.render_info_cv.set(template.RenderInfo(template_obj))
with pytest.raises(RuntimeError, match="hass not set while rendering"):
template_obj.async_render_to_info()
async def test_template_render_info_collision(hass: HomeAssistant) -> None:
"""Test template render info collision.
This usually means the template is being rendered
in the wrong thread.
"""
hass.states.async_set("sensor.test", "23")
template_obj = template.Template("{{ states('sensor.test') }}", None)
template_obj.hass = hass
template.render_info_cv.set(template.RenderInfo(template_obj))
with pytest.raises(RuntimeError, match="RenderInfo already set while rendering"):
template_obj.async_render_to_info()
def test_template_equality(hass: HomeAssistant) -> None:
"""Test template comparison and hashing."""
template_one = template.Template("{{ template_one }}", hass)
template_one_1 = template.Template("{{ template_one }}", hass)
template_two = template.Template("{{ template_two }}", hass)
assert template_one == template_one_1
assert template_one != template_two
assert hash(template_one) == hash(template_one_1)
assert hash(template_one) != hash(template_two)
assert str(template_one_1) == "Template<template=({{ template_one }}) renders=0>"
with pytest.raises(TypeError):
template.Template(["{{ template_one }}"], hass)
def test_invalid_template(hass: HomeAssistant) -> None:
"""Invalid template raises error."""
tmpl = template.Template("{{", hass)
with pytest.raises(TemplateError):
tmpl.ensure_valid()
with pytest.raises(TemplateError):
tmpl.async_render()
info = tmpl.async_render_to_info()
with pytest.raises(TemplateError):
assert info.result() == "impossible"
tmpl = template.Template("{{states(keyword)}}", hass)
tmpl.ensure_valid()
with pytest.raises(TemplateError):
tmpl.async_render()
def test_invalid_entity_id(hass: HomeAssistant) -> None:
"""Test referring states by entity id."""
with pytest.raises(TemplateError):
render(hass, '{{ states["big.fat..."] }}')
with pytest.raises(TemplateError):
render(hass, '{{ states.test["big.fat..."] }}')
with pytest.raises(TemplateError):
render(hass, '{{ states["invalid/domain"] }}')
def test_raise_exception_on_error(hass: HomeAssistant) -> None:
"""Test raising an exception on error."""
with pytest.raises(TemplateError):
template.Template("{{ invalid_syntax", hass).ensure_valid()
async def test_import(hass: HomeAssistant) -> None:
"""Test that imports work from the config/custom_templates folder."""
await template.async_load_custom_templates(hass)
assert "test.jinja" in template._get_hass_loader(hass).sources
assert "inner/inner_test.jinja" in template._get_hass_loader(hass).sources
assert (
render(
hass,
"""
{% import 'test.jinja' as t %}
{{ t.test_macro() }} {{ t.test_variable }}
""",
)
== "macro variable"
)
assert (
render(
hass,
"""
{% import 'inner/inner_test.jinja' as t %}
{{ t.test_macro() }} {{ t.test_variable }}
""",
)
== "inner macro inner variable"
)
with pytest.raises(TemplateError):
render(
hass,
"""
{% import 'notfound.jinja' as t %}
{{ t.test_macro() }} {{ t.test_variable }}
""",
)
async def test_import_change(hass: HomeAssistant) -> None:
"""Test that a change in HassLoader results in updated imports."""
await template.async_load_custom_templates(hass)
to_test = template.Template(
"""
{% import 'test.jinja' as t %}
{{ t.test_macro() }} {{ t.test_variable }}
""",
hass,
)
assert to_test.async_render() == "macro variable"
template._get_hass_loader(hass).sources = {
"test.jinja": """
{% macro test_macro() -%}
macro2
{%- endmacro %}
{% set test_variable = "variable2" %}
"""
}
assert to_test.async_render() == "macro2 variable2"
def test_loop_controls(hass: HomeAssistant) -> None:
"""Test that loop controls are enabled."""
tpl = """
{%- for v in range(10) %}
{%- if v == 1 -%}
{%- continue -%}
{%- elif v == 3 -%}
{%- break -%}
{%- endif -%}
{{ v }}
{%- endfor -%}
"""
assert render(hass, tpl) == "02"
def test_converting_datetime_to_iterable(hass: HomeAssistant) -> None:
"""Test converting a datetime to an iterable raises an error."""
dt_ = datetime(2020, 1, 1, 0, 0, 0)
with pytest.raises(TemplateError):
render(hass, "{{ tuple(value) }}", {"value": dt_})
with pytest.raises(TemplateError):
render(hass, "{{ set(value) }}", {"value": dt_})
def test_passing_vars_as_keywords(hass: HomeAssistant) -> None:
"""Test passing variables as keywords."""
assert render(hass, "{{ hello }}", hello=127) == 127
def test_passing_vars_as_vars(hass: HomeAssistant) -> None:
"""Test passing variables as variables."""
assert render(hass, "{{ hello }}", {"hello": 127}) == 127
def test_passing_vars_as_list(hass: HomeAssistant) -> None:
"""Test passing variables as list."""
assert template.render_complex(
template.Template("{{ hello }}", hass), {"hello": ["foo", "bar"]}
) == ["foo", "bar"]
def test_passing_vars_as_list_element(hass: HomeAssistant) -> None:
"""Test passing variables as list."""
tpl = template.Template("{{ hello[1] }}", hass)
assert template.render_complex(tpl, {"hello": ["foo", "bar"]}) == "bar"
def test_passing_vars_as_dict_element(hass: HomeAssistant) -> None:
"""Test passing variables as list."""
tpl = template.Template("{{ hello.foo }}", hass)
assert template.render_complex(tpl, {"hello": {"foo": "bar"}}) == "bar"
def test_passing_vars_as_dict(hass: HomeAssistant) -> None:
"""Test passing variables as list."""
tpl = template.Template("{{ hello }}", hass)
assert template.render_complex(tpl, {"hello": {"foo": "bar"}}) == {"foo": "bar"}
def test_render_with_possible_json_value_with_valid_json(hass: HomeAssistant) -> None:
"""Render with possible JSON value with valid JSON."""
tpl = template.Template("{{ value_json.hello }}", hass)
assert tpl.async_render_with_possible_json_value('{"hello": "world"}') == "world"
def test_render_with_possible_json_value_with_invalid_json(hass: HomeAssistant) -> None:
"""Render with possible JSON value with invalid JSON."""
tpl = template.Template("{{ value_json }}", hass)
assert tpl.async_render_with_possible_json_value("{ I AM NOT JSON }") == ""
def test_render_with_possible_json_value_with_template_error_value(
hass: HomeAssistant,
) -> None:
"""Render with possible JSON value with template error value."""
tpl = template.Template("{{ non_existing.variable }}", hass)
assert tpl.async_render_with_possible_json_value("hello", "-") == "-"
def test_render_with_possible_json_value_with_missing_json_value(
hass: HomeAssistant,
) -> None:
"""Render with possible JSON value with unknown JSON object."""
tpl = template.Template("{{ value_json.goodbye }}", hass)
assert tpl.async_render_with_possible_json_value('{"hello": "world"}') == ""
def test_render_with_possible_json_value_non_string_value(hass: HomeAssistant) -> None:
"""Render with possible JSON value with non-string value."""
tpl = template.Template(
"""{{ strptime(value~'+0000', '%Y-%m-%d %H:%M:%S%z') }}""",
hass,
)
value = datetime(2019, 1, 18, 12, 13, 14)
expected = str(value.replace(tzinfo=dt_util.UTC))
assert tpl.async_render_with_possible_json_value(value) == expected
def test_render_with_possible_json_value_and_parse_result(hass: HomeAssistant) -> None:
"""Render with possible JSON value with valid JSON."""
tpl = template.Template("{{ value_json.hello }}", hass)
result = tpl.async_render_with_possible_json_value(
"""{"hello": {"world": "value1"}}""", parse_result=True
)
assert isinstance(result, dict)
def test_render_with_possible_json_value_and_dont_parse_result(
hass: HomeAssistant,
) -> None:
"""Render with possible JSON value with valid JSON."""
tpl = template.Template("{{ value_json.hello }}", hass)
result = tpl.async_render_with_possible_json_value(
"""{"hello": {"world": "value1"}}""", parse_result=False
)
assert isinstance(result, str)
@patch(
"homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
return_value=True,
)
def test_timedelta(mock_is_safe, hass: HomeAssistant) -> None:
"""Test relative_time method."""
now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
with freeze_time(now):
result = render(hass, "{{timedelta(seconds=120)}}")
assert result == "0:02:00"
result = render(hass, "{{timedelta(seconds=86400)}}")
assert result == "1 day, 0:00:00"
result = render(hass, "{{timedelta(days=1, hours=4)}}")
assert result == "1 day, 4:00:00"
result = render(hass, "{{relative_time(now() - timedelta(seconds=3600))}}")
assert result == "1 hour"
result = render(hass, "{{relative_time(now() - timedelta(seconds=86400))}}")
assert result == "1 day"
result = render(hass, "{{relative_time(now() - timedelta(seconds=86401))}}")
assert result == "1 day"
result = render(hass, "{{relative_time(now() - timedelta(weeks=2, days=1))}}")
assert result == "15 days"
def test_async_render_to_info_with_branching(hass: HomeAssistant) -> None:
"""Test async_render_to_info function by domain."""
hass.states.async_set("light.a", "off")
hass.states.async_set("light.b", "on")
hass.states.async_set("light.c", "off")
info = render_to_info(
hass,
"""
{% if states.light.a == "on" %}
{{ states.light.b.state }}
{% else %}
{{ states.light.c.state }}
{% endif %}
""",
)
assert_result_info(info, "off", {"light.a", "light.c"})
assert info.rate_limit is None
info = render_to_info(
hass,
"""
{% if states.light.a.state == "off" %}
{% set domain = "light" %}
{{ states[domain].b.state }}
{% endif %}
""",
)
assert_result_info(info, "on", {"light.a", "light.b"})
assert info.rate_limit is None
def test_async_render_to_info_with_complex_branching(hass: HomeAssistant) -> None:
"""Test async_render_to_info function by domain."""
hass.states.async_set("light.a", "off")
hass.states.async_set("light.b", "on")
hass.states.async_set("light.c", "off")
hass.states.async_set("vacuum.a", "off")
hass.states.async_set("device_tracker.a", "off")
hass.states.async_set("device_tracker.b", "off")
hass.states.async_set("lock.a", "off")
hass.states.async_set("sensor.a", "off")
hass.states.async_set("binary_sensor.a", "off")
info = render_to_info(
hass,
"""
{% set domain = "vacuum" %}
{% if states.light.a == "on" %}
{{ states.light.b.state }}
{% elif states.light.a == "on" %}
{{ states.device_tracker }}
{% elif states.light.a == "on" %}
{{ states[domain] | list }}
{% elif states('light.b') == "on" %}
{{ states[otherdomain] | sort(attribute='entity_id') | map(attribute='entity_id') | list }}
{% elif states.light.a == "on" %}
{{ states["nonexist"] | list }}
{% else %}
else
{% endif %}
""",
{"otherdomain": "sensor"},
)
assert_result_info(info, ["sensor.a"], {"light.a", "light.b"}, {"sensor"})
assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT
async def test_async_render_to_info_with_wildcard_matching_entity_id(
hass: HomeAssistant,
) -> None:
"""Test tracking template with a wildcard."""
template_complex_str = r"""
{% for state in states.cover %}
{% if 'office_' in state.entity_id %}
{{ state.entity_id }}={{ state.state }}
{% endif %}
{% endfor %}
"""
hass.states.async_set("cover.office_drapes", "closed")
hass.states.async_set("cover.office_window", "closed")
hass.states.async_set("cover.office_skylight", "open")
info = render_to_info(hass, template_complex_str)
assert info.domains == {"cover"}
assert info.entities == set()
assert info.all_states is False
assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT
async def test_async_render_to_info_with_wildcard_matching_state(
hass: HomeAssistant,
) -> None:
"""Test tracking template with a wildcard."""
template_complex_str = """
{% for state in states %}
{% if state.state.startswith('ope') %}
{{ state.entity_id }}={{ state.state }}
{% endif %}
{% endfor %}
"""
hass.states.async_set("cover.office_drapes", "closed")
hass.states.async_set("cover.office_window", "closed")
hass.states.async_set("cover.office_skylight", "open")
hass.states.async_set("cover.x_skylight", "open")
hass.states.async_set("binary_sensor.door", "on")
await hass.async_block_till_done()
info = render_to_info(hass, template_complex_str)
assert not info.domains
assert info.entities == set()
assert info.all_states is True
assert info.rate_limit == ALL_STATES_RATE_LIMIT
hass.states.async_set("binary_sensor.door", "off")
info = render_to_info(hass, template_complex_str)
assert not info.domains
assert info.entities == set()
assert info.all_states is True
assert info.rate_limit == ALL_STATES_RATE_LIMIT
template_cover_str = """
{% for state in states.cover %}
{% if state.state.startswith('ope') %}
{{ state.entity_id }}={{ state.state }}
{% endif %}
{% endfor %}
"""
hass.states.async_set("cover.x_skylight", "closed")
info = render_to_info(hass, template_cover_str)
assert info.domains == {"cover"}
assert info.entities == set()
assert info.all_states is False
assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT
def test_nested_async_render_to_info_case(hass: HomeAssistant) -> None:
"""Test a deeply nested state with async_render_to_info."""
hass.states.async_set("input_select.picker", "vacuum.a")
hass.states.async_set("vacuum.a", "off")
info = render_to_info(
hass, "{{ states[states['input_select.picker'].state].state }}", {}
)
assert_result_info(info, "off", {"input_select.picker", "vacuum.a"})
assert info.rate_limit is None
def test_result_as_boolean(hass: HomeAssistant) -> None:
"""Test converting a template result to a boolean."""
assert template.result_as_boolean(True) is True
assert template.result_as_boolean(" 1 ") is True
assert template.result_as_boolean(" true ") is True
assert template.result_as_boolean(" TrUE ") is True
assert template.result_as_boolean(" YeS ") is True
assert template.result_as_boolean(" On ") is True
assert template.result_as_boolean(" Enable ") is True
assert template.result_as_boolean(1) is True
assert template.result_as_boolean(-1) is True
assert template.result_as_boolean(500) is True
assert template.result_as_boolean(0.5) is True
assert template.result_as_boolean(0.389) is True
assert template.result_as_boolean(35) is True
assert template.result_as_boolean(False) is False
assert template.result_as_boolean(" 0 ") is False
assert template.result_as_boolean(" false ") is False
assert template.result_as_boolean(" FaLsE ") is False
assert template.result_as_boolean(" no ") is False
assert template.result_as_boolean(" off ") is False
assert template.result_as_boolean(" disable ") is False
assert template.result_as_boolean(0) is False
assert template.result_as_boolean(0.0) is False
assert template.result_as_boolean("0.00") is False
assert template.result_as_boolean(None) is False
async def test_async_render_to_info_in_conditional(hass: HomeAssistant) -> None:
"""Test extract entities function with none entities stuff."""
info = render_to_info(hass, '{{ states("sensor.xyz") == "dog" }}')
assert_result_info(info, False, ["sensor.xyz"], [])
hass.states.async_set("sensor.xyz", "dog")
hass.states.async_set("sensor.cow", "True")
await hass.async_block_till_done()
template_str = """
{% if states("sensor.xyz") == "dog" %}
{{ states("sensor.cow") }}
{% else %}
{{ states("sensor.pig") }}
{% endif %}
"""
info = render_to_info(hass, template_str)
assert_result_info(info, True, ["sensor.xyz", "sensor.cow"], [])
hass.states.async_set("sensor.xyz", "sheep")
hass.states.async_set("sensor.pig", "oink")
await hass.async_block_till_done()
info = render_to_info(hass, template_str)
assert_result_info(info, "oink", ["sensor.xyz", "sensor.pig"], [])
def test_jinja_namespace(hass: HomeAssistant) -> None:
"""Test Jinja's namespace command can be used."""
test_template = template.Template(
(
"{% set ns = namespace(a_key='') %}"
"{% set ns.a_key = states.sensor.dummy.state %}"
"{{ ns.a_key }}"
),
hass,
)
hass.states.async_set("sensor.dummy", "a value")
assert test_template.async_render() == "a value"
hass.states.async_set("sensor.dummy", "another value")
assert test_template.async_render() == "another value"
@pytest.mark.parametrize(
("rounded", "with_unit", "output1_1", "output1_2", "output2_1", "output2_2"),
[
(False, False, 23, 23.015, 23, 23.015),
(False, True, "23 beers", "23.015 beers", "23 beers", "23.015 beers"),
(True, False, 23.0, 23.02, 23, 23.015),
(True, True, "23.00 beers", "23.02 beers", "23 beers", "23.015 beers"),
],
)
def test_state_with_unit_and_rounding_options(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
rounded: str,
with_unit: str,
output1_1,
output1_2,
output2_1,
output2_2,
) -> None:
"""Test formatting the state rounded and with unit."""
entry = entity_registry.async_get_or_create(
"sensor", "test", "very_unique", suggested_object_id="test"
)
entity_registry.async_update_entity_options(
entry.entity_id,
"sensor",
{
"suggested_display_precision": 2,
},
)
assert entry.entity_id == "sensor.test"
hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
hass.states.async_set("sensor.test2", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
tpl = template.Template(
f"{{{{ states('sensor.test', rounded={rounded}, with_unit={with_unit}) }}}}",
hass,
)
tpl2 = template.Template(
f"{{{{ states('sensor.test2', rounded={rounded}, with_unit={with_unit}) }}}}",
hass,
)
assert tpl.async_render() == output1_1
assert tpl2.async_render() == output2_1
hass.states.async_set("sensor.test", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
hass.states.async_set("sensor.test2", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
assert tpl.async_render() == output1_2
assert tpl2.async_render() == output2_2
def test_length_of_states(hass: HomeAssistant) -> None:
"""Test fetching the length of states."""
hass.states.async_set("sensor.test", "23")
hass.states.async_set("sensor.test2", "wow")
hass.states.async_set("climate.test2", "cooling")
result = render(hass, "{{ states | length }}")
assert result == 3
result = render(hass, "{{ states.sensor | length }}")
assert result == 2
def test_render_complex_handling_non_template_values(hass: HomeAssistant) -> None:
"""Test that we can render non-template fields."""
assert template.render_complex(
{True: 1, False: template.Template("{{ hello }}", hass)}, {"hello": 2}
) == {True: 1, False: 2}
async def test_cache_garbage_collection(hass: HomeAssistant) -> None:
"""Test caching a template."""
template_string = (
"{% set dict = {'foo': 'x&y', 'bar': 42} %} {{ dict | urlencode }}"
)
tpl = template.Template(
(template_string),
hass,
)
tpl.ensure_valid()
env = tpl._env
assert env.template_cache.get(template_string)
tpl2 = template.Template(
(template_string),
hass,
)
tpl2.ensure_valid()
assert env.template_cache.get(template_string)
del tpl
assert env.template_cache.get(template_string)
del tpl2
assert not env.template_cache.get(template_string)
def test_is_template_string() -> None:
"""Test is template string."""
assert template.is_template_string("{{ x }}") is True
assert template.is_template_string("{% if x == 2 %}1{% else %}0{%end if %}") is True
assert template.is_template_string("{# a comment #} Hey") is True
assert template.is_template_string("1") is False
assert template.is_template_string("Some Text") is False
async def test_protected_blocked(hass: HomeAssistant) -> None:
"""Test accessing __getattr__ produces a template error."""
with pytest.raises(TemplateError):
render(hass, '{{ states.__getattr__("any") }}')
with pytest.raises(TemplateError):
render(hass, '{{ states.sensor.__getattr__("any") }}')
with pytest.raises(TemplateError):
render(hass, '{{ states.sensor.any.__getattr__("any") }}')
async def test_demo_template(hass: HomeAssistant) -> None:
"""Test the demo template works as expected."""
hass.states.async_set(
"sun.sun",
"above",
{"elevation": 50, "next_rising": "2022-05-12T03:00:08.503651+00:00"},
)
for i in range(2):
hass.states.async_set(f"sensor.sensor{i}", "on")
demo_template_str = """
{## Imitate available variables: ##}
{% set my_test_json = {
"temperature": 25,
"unit": "°C"
} %}
The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}.
{% if is_state("sun.sun", "above_horizon") -%}
The sun rose {{ relative_time(states.sun.sun.last_changed) }} ago.
{%- else -%}
The sun will rise at {{ as_timestamp(state_attr("sun.sun", "next_rising")) | timestamp_local }}.
{%- endif %}
For loop example getting 3 entity values:
{% for states in states | slice(3) -%}
{% set state = states | first %}
{%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}
{{ state.name | lower }} is {{state.state_with_unit}}
{%- endfor %}.
"""
result = render(hass, demo_template_str)
assert "The temperature is 25" in result
assert "is on" in result
assert "sensor0" in result
assert "sensor1" in result
assert "sun" in result
async def test_slice_states(hass: HomeAssistant) -> None:
"""Test iterating states with a slice."""
hass.states.async_set("sensor.test", "23")
result = render(
hass,
(
"{% for states in states | slice(1) -%}{% set state = states | first %}"
"{{ state.entity_id }}"
"{%- endfor %}"
),
)
assert result == "sensor.test"
async def test_lifecycle(hass: HomeAssistant) -> None:
"""Test that we limit template render info for lifecycle events."""
hass.states.async_set("sun.sun", "above", {"elevation": 50, "next_rising": "later"})
for i in range(2):
hass.states.async_set(f"sensor.sensor{i}", "on")
hass.states.async_set("sensor.removed", "off")
await hass.async_block_till_done()
hass.states.async_set("sun.sun", "below", {"elevation": 60, "next_rising": "later"})
for i in range(2):
hass.states.async_set(f"sensor.sensor{i}", "off")
hass.states.async_set("sensor.new", "off")
hass.states.async_remove("sensor.removed")
await hass.async_block_till_done()
info = render_to_info(hass, "{{ states | count }}")
assert info.all_states is False
assert info.all_states_lifecycle is True
assert info.rate_limit is None
assert info.has_time is False
assert info.entities == set()
assert info.domains == set()
assert info.domains_lifecycle == set()
assert info.filter("sun.sun") is False
assert info.filter("sensor.sensor1") is False
assert info.filter_lifecycle("sensor.new") is True
assert info.filter_lifecycle("sensor.removed") is True
async def test_template_timeout(hass: HomeAssistant) -> None:
"""Test to see if a template will timeout."""
for i in range(2):
hass.states.async_set(f"sensor.sensor{i}", "on")
tmp = template.Template("{{ states | count }}", hass)
assert await tmp.async_render_will_timeout(3) is False
tmp3 = template.Template("static", hass)
assert await tmp3.async_render_will_timeout(3) is False
tmp4 = template.Template("{{ var1 }}", hass)
assert await tmp4.async_render_will_timeout(3, {"var1": "ok"}) is False
slow_template_str = """
{% for var in range(1000) -%}
{% for var in range(1000) -%}
{{ var }}
{%- endfor %}
{%- endfor %}
"""
tmp5 = template.Template(slow_template_str, hass)
assert await tmp5.async_render_will_timeout(0.000001) is True
async def test_template_timeout_raise(hass: HomeAssistant) -> None:
"""Test we can raise from."""
tmp2 = template.Template("{{ error_invalid + 1 }}", hass)
with pytest.raises(TemplateError):
assert await tmp2.async_render_will_timeout(3) is False
async def test_lights(hass: HomeAssistant) -> None:
"""Test we can sort lights."""
tmpl = """
{% set lights_on = states.light|selectattr('state','eq','on')|sort(attribute='entity_id')|map(attribute='name')|list %}
{% if lights_on|length == 0 %}
No lights on. Sleep well..
{% elif lights_on|length == 1 %}
The {{lights_on[0]}} light is on.
{% elif lights_on|length == 2 %}
The {{lights_on[0]}} and {{lights_on[1]}} lights are on.
{% else %}
The {{lights_on[:-1]|join(', ')}}, and {{lights_on[-1]}} lights are on.
{% endif %}
"""
states = []
for i in range(10):
states.append(f"light.sensor{i}")
hass.states.async_set(f"light.sensor{i}", "on")
info = render_to_info(hass, tmpl)
assert info.entities == set()
assert info.domains == {"light"}
assert "lights are on" in info.result()
for i in range(10):
assert f"sensor{i}" in info.result()
async def test_template_errors(hass: HomeAssistant) -> None:
"""Test template rendering wraps exceptions with TemplateError."""
with pytest.raises(TemplateError):
render(hass, "{{ now() | rando }}")
with pytest.raises(TemplateError):
render(hass, "{{ utcnow() | rando }}")
with pytest.raises(TemplateError):
render(hass, "{{ now() | random }}")
with pytest.raises(TemplateError):
render(hass, "{{ utcnow() | random }}")
async def test_state_attributes(hass: HomeAssistant) -> None:
"""Test state attributes."""
hass.states.async_set("sensor.test", "23")
result = render(hass, "{{ states.sensor.test.last_changed }}")
assert result == str(hass.states.get("sensor.test").last_changed)
result = render(hass, "{{ states.sensor.test.object_id }}")
assert result == hass.states.get("sensor.test").object_id
result = render(hass, "{{ states.sensor.test.domain }}")
assert result == hass.states.get("sensor.test").domain
result = render(hass, "{{ states.sensor.test.context.id }}")
assert result == hass.states.get("sensor.test").context.id
result = render(hass, "{{ states.sensor.test.state_with_unit }}")
assert result == 23
result = render(hass, "{{ states.sensor.test.invalid_prop }}")
assert result == ""
with pytest.raises(TemplateError):
render(hass, "{{ states.sensor.test.invalid_prop.xx }}")
async def test_unavailable_states(hass: HomeAssistant) -> None:
"""Test watching unavailable states."""
for i in range(10):
hass.states.async_set(f"light.sensor{i}", "on")
hass.states.async_set("light.unavailable", "unavailable")
hass.states.async_set("light.unknown", "unknown")
hass.states.async_set("light.none", "none")
result = render(
hass,
(
"{{ states | selectattr('state', 'in', ['unavailable','unknown','none']) "
"| sort(attribute='entity_id') | map(attribute='entity_id') | list | join(', ') }}"
),
)
assert result == "light.none, light.unavailable, light.unknown"
result = render(
hass,
(
"{{ states.light "
"| selectattr('state', 'in', ['unavailable','unknown','none']) "
"| sort(attribute='entity_id') | map(attribute='entity_id') | list "
"| join(', ') }}"
),
)
assert result == "light.none, light.unavailable, light.unknown"
async def test_no_result_parsing(hass: HomeAssistant) -> None:
"""Test if templates results are not parsed."""
hass.states.async_set("sensor.temperature", "12")
assert (
render(hass, "{{ states.sensor.temperature.state }}", parse_result=False)
== "12"
)
assert render(hass, "{{ false }}", parse_result=False) == "False"
assert render(hass, "{{ [1, 2, 3] }}", parse_result=False) == "[1, 2, 3]"
async def test_is_static_still_ast_evals(hass: HomeAssistant) -> None:
"""Test is_static still converts to native type."""
tpl = template.Template("[1, 2]", hass)
assert tpl.is_static
assert tpl.async_render() == [1, 2]
async def test_result_wrappers(hass: HomeAssistant) -> None:
"""Test result wrappers."""
for text, native, orig_type, schema in (
("[1, 2]", [1, 2], list, vol.Schema([int])),
("{1, 2}", {1, 2}, set, vol.Schema({int})),
("(1, 2)", (1, 2), tuple, vol.ExactSequence([int, int])),
('{"hello": True}', {"hello": True}, dict, vol.Schema({"hello": bool})),
):
result = render(hass, text)
assert isinstance(result, orig_type)
assert isinstance(result, template.ResultWrapper)
assert result == native
assert result.render_result == text
schema(result) # should not raise
# Result with render text stringifies to original text
assert str(result) == text
# Result without render text stringifies same as original type
assert str(template.RESULT_WRAPPERS[orig_type](native)) == str(
orig_type(native)
)
async def test_parse_result(hass: HomeAssistant) -> None:
"""Test parse result."""
for tpl, result in (
('{{ "{{}}" }}', "{{}}"),
("not-something", "not-something"),
("2a", "2a"),
("123E5", "123E5"),
("1j", "1j"),
("1e+100", "1e+100"),
("0xface", "0xface"),
("123", 123),
("10", 10),
("123.0", 123.0),
(".5", 0.5),
("0.5", 0.5),
("-1", -1),
("-1.0", -1.0),
("+1", 1),
("5.", 5.0),
("123_123_123", "123_123_123"),
# ("+48100200300", "+48100200300"), # phone number
("010", "010"),
("0011101.00100001010001", "0011101.00100001010001"),
):
assert render(hass, tpl) == result
@pytest.mark.parametrize(
"template_string",
[
"{{ no_such_variable }}",
"{{ no_such_variable and True }}",
"{{ no_such_variable | join(', ') }}",
],
)
async def test_undefined_symbol_warnings(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
template_string: str,
) -> None:
"""Test a warning is logged on undefined variables."""
assert render(hass, template_string) == ""
assert (
f"Template variable warning: 'no_such_variable' is undefined when rendering '{template_string}'"
in caplog.text
)
async def test_render_to_info_with_exception(hass: HomeAssistant) -> None:
"""Test info is still available if the template has an exception."""
hass.states.async_set("test_domain.object", "dog")
info = render_to_info(hass, '{{ states("test_domain.object") | float }}')
with pytest.raises(TemplateError, match="no default was specified"):
info.result()
assert info.all_states is False
assert info.entities == {"test_domain.object"}
async def test_template_thread_safety_checks(hass: HomeAssistant) -> None:
"""Test template thread safety checks."""
hass.states.async_set("sensor.test", "23")
template_str = "{{ states('sensor.test') }}"
template_obj = template.Template(template_str, None)
template_obj.hass = hass
hass.config.debug = True
with pytest.raises(
RuntimeError,
match="Detected code that calls async_render_to_info from a thread.",
):
await hass.async_add_executor_job(template_obj.async_render_to_info)
assert template_obj.async_render_to_info().result() == 23
def test_template_output_exceeds_maximum_size(hass: HomeAssistant) -> None:
"""Test template output exceeds maximum size."""
with pytest.raises(TemplateError):
render(hass, "{{ 'a' * 1024 * 257 }}")