mirror of
https://github.com/home-assistant/core.git
synced 2026-05-17 05:51:33 +01:00
2309 lines
72 KiB
Python
2309 lines
72 KiB
Python
"""Test Home Assistant template helper methods."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
from datetime import datetime
|
|
from unittest.mock import patch
|
|
|
|
from freezegun import freeze_time
|
|
import pytest
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components import group
|
|
from homeassistant.const import (
|
|
ATTR_UNIT_OF_MEASUREMENT,
|
|
STATE_UNAVAILABLE,
|
|
UnitOfArea,
|
|
UnitOfLength,
|
|
UnitOfMass,
|
|
UnitOfPrecipitationDepth,
|
|
UnitOfPressure,
|
|
UnitOfSpeed,
|
|
UnitOfTemperature,
|
|
UnitOfVolume,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import TemplateError
|
|
from homeassistant.helpers import entity_registry as er, template, translation
|
|
from homeassistant.helpers.template.render_info import (
|
|
ALL_STATES_RATE_LIMIT,
|
|
DOMAIN_STATES_RATE_LIMIT,
|
|
)
|
|
from homeassistant.setup import async_setup_component
|
|
from homeassistant.util import dt as dt_util
|
|
from homeassistant.util.unit_system import UnitSystem
|
|
|
|
from .helpers import assert_result_info, render, render_to_info
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
|
|
def _set_up_units(hass: HomeAssistant) -> None:
|
|
"""Set up the tests."""
|
|
hass.config.units = UnitSystem(
|
|
"custom",
|
|
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
|
|
area=UnitOfArea.SQUARE_METERS,
|
|
conversions={},
|
|
length=UnitOfLength.METERS,
|
|
mass=UnitOfMass.GRAMS,
|
|
pressure=UnitOfPressure.PA,
|
|
temperature=UnitOfTemperature.CELSIUS,
|
|
volume=UnitOfVolume.LITERS,
|
|
wind_speed=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
|
)
|
|
|
|
|
|
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()
|
|
|
|
|
|
@pytest.mark.usefixtures("hass")
|
|
def test_template_equality() -> None:
|
|
"""Test template comparison and hashing."""
|
|
template_one = template.Template("{{ template_one }}")
|
|
template_one_1 = template.Template("{{ template_one }}")
|
|
template_two = template.Template("{{ template_two }}")
|
|
|
|
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 }}"])
|
|
|
|
|
|
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_referring_states_by_entity_id(hass: HomeAssistant) -> None:
|
|
"""Test referring states by entity id."""
|
|
hass.states.async_set("test.object", "happy")
|
|
assert render(hass, "{{ states.test.object.state }}") == "happy"
|
|
|
|
assert render(hass, '{{ states["test.object"].state }}') == "happy"
|
|
|
|
assert render(hass, '{{ states("test.object") }}') == "happy"
|
|
|
|
|
|
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").ensure_valid()
|
|
|
|
|
|
def test_iterating_all_states(hass: HomeAssistant) -> None:
|
|
"""Test iterating all states."""
|
|
tmpl_str = "{% for state in states | sort(attribute='entity_id') %}{{ state.state }}{% endfor %}"
|
|
|
|
info = render_to_info(hass, tmpl_str)
|
|
assert_result_info(info, "", all_states=True)
|
|
assert info.rate_limit == ALL_STATES_RATE_LIMIT
|
|
|
|
hass.states.async_set("test.object", "happy")
|
|
hass.states.async_set("sensor.temperature", 10)
|
|
|
|
info = render_to_info(hass, tmpl_str)
|
|
assert_result_info(info, "10happy", entities=[], all_states=True)
|
|
|
|
|
|
def test_iterating_all_states_unavailable(hass: HomeAssistant) -> None:
|
|
"""Test iterating all states unavailable."""
|
|
hass.states.async_set("test.object", "on")
|
|
|
|
tmpl_str = (
|
|
"{{"
|
|
" states"
|
|
" | selectattr('state', 'in', ['unavailable', 'unknown', 'none'])"
|
|
" | list"
|
|
" | count"
|
|
"}}"
|
|
)
|
|
|
|
info = render_to_info(hass, tmpl_str)
|
|
|
|
assert info.all_states is True
|
|
assert info.rate_limit == ALL_STATES_RATE_LIMIT
|
|
|
|
hass.states.async_set("test.object", "unknown")
|
|
hass.states.async_set("sensor.temperature", 10)
|
|
|
|
info = render_to_info(hass, tmpl_str)
|
|
assert_result_info(info, 1, entities=[], all_states=True)
|
|
|
|
|
|
def test_iterating_domain_states(hass: HomeAssistant) -> None:
|
|
"""Test iterating domain states."""
|
|
tmpl_str = "{% for state in states.sensor %}{{ state.state }}{% endfor %}"
|
|
|
|
info = render_to_info(hass, tmpl_str)
|
|
assert_result_info(info, "", domains=["sensor"])
|
|
assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT
|
|
|
|
hass.states.async_set("test.object", "happy")
|
|
hass.states.async_set("sensor.back_door", "open")
|
|
hass.states.async_set("sensor.temperature", 10)
|
|
|
|
info = render_to_info(hass, tmpl_str)
|
|
assert_result_info(
|
|
info,
|
|
"open10",
|
|
entities=[],
|
|
domains=["sensor"],
|
|
)
|
|
|
|
|
|
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)
|
|
|
|
|
|
def test_if_state_exists(hass: HomeAssistant) -> None:
|
|
"""Test if state exists works."""
|
|
hass.states.async_set("test.object", "available")
|
|
|
|
result = render(
|
|
hass, "{% if states.test.object %}exists{% else %}not exists{% endif %}"
|
|
)
|
|
assert result == "exists"
|
|
|
|
|
|
def test_is_state(hass: HomeAssistant) -> None:
|
|
"""Test is_state method."""
|
|
hass.states.async_set("test.object", "available")
|
|
|
|
result = render(
|
|
hass, '{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}'
|
|
)
|
|
assert result == "yes"
|
|
|
|
result = render(hass, """{{ is_state("test.noobject", "available") }}""")
|
|
assert result is False
|
|
|
|
result = render(
|
|
hass,
|
|
'{% if "test.object" is is_state("available") %}yes{% else %}no{% endif %}',
|
|
)
|
|
assert result == "yes"
|
|
|
|
result = render(
|
|
hass,
|
|
"""{{ ['test.object'] | select("is_state", "available") | first | default }}""",
|
|
)
|
|
assert result == "test.object"
|
|
|
|
result = render(hass, '{{ is_state("test.object", ["on", "off", "available"]) }}')
|
|
assert result is True
|
|
|
|
|
|
def test_is_state_attr(hass: HomeAssistant) -> None:
|
|
"""Test is_state_attr method."""
|
|
hass.states.async_set("test.object", "available", {"mode": "on", "exists": None})
|
|
|
|
result = render(
|
|
hass,
|
|
"""{% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %}""",
|
|
)
|
|
assert result == "yes"
|
|
|
|
result = render(hass, """{{ is_state_attr("test.noobject", "mode", "on") }}""")
|
|
assert result is False
|
|
|
|
result = render(
|
|
hass,
|
|
"""{% if "test.object" is is_state_attr("mode", "on") %}yes{% else %}no{% endif %}""",
|
|
)
|
|
assert result == "yes"
|
|
|
|
result = render(
|
|
hass,
|
|
"""{{ ['test.object'] | select("is_state_attr", "mode", "on") | first | default }}""",
|
|
)
|
|
assert result == "test.object"
|
|
|
|
result = render(
|
|
hass,
|
|
"""{% if is_state_attr("test.object", "exists", None) %}yes{% else %}no{% endif %}""",
|
|
)
|
|
assert result == "yes"
|
|
|
|
result = render(
|
|
hass,
|
|
"""{% if is_state_attr("test.object", "noexist", None) %}yes{% else %}no{% endif %}""",
|
|
)
|
|
assert result == "no"
|
|
|
|
|
|
def test_state_attr(hass: HomeAssistant) -> None:
|
|
"""Test state_attr method."""
|
|
hass.states.async_set(
|
|
"test.object", "available", {"effect": "action", "mode": "on"}
|
|
)
|
|
|
|
result = render(
|
|
hass,
|
|
"""{% if state_attr("test.object", "mode") == "on" %}yes{% else %}no{% endif %}""",
|
|
)
|
|
assert result == "yes"
|
|
|
|
result = render(hass, """{{ state_attr("test.noobject", "mode") == None }}""")
|
|
assert result is True
|
|
|
|
result = render(
|
|
hass,
|
|
"""{% if "test.object" | state_attr("mode") == "on" %}yes{% else %}no{% endif %}""",
|
|
)
|
|
assert result == "yes"
|
|
|
|
result = render(
|
|
hass,
|
|
"""{{ ['test.object'] | map("state_attr", "effect") | first | default }}""",
|
|
)
|
|
assert result == "action"
|
|
|
|
|
|
def test_states_function(hass: HomeAssistant) -> None:
|
|
"""Test using states as a function."""
|
|
hass.states.async_set("test.object", "available")
|
|
|
|
result = render(hass, '{{ states("test.object") }}')
|
|
assert result == "available"
|
|
|
|
result = render(hass, '{{ states("test.object2") }}')
|
|
assert result == "unknown"
|
|
|
|
result = render(
|
|
hass,
|
|
"""{% if "test.object" | states == "available" %}yes{% else %}no{% endif %}""",
|
|
)
|
|
assert result == "yes"
|
|
|
|
result = render(hass, """{{ ['test.object'] | map("states") | first | default }}""")
|
|
assert result == "available"
|
|
|
|
|
|
async def test_state_translated(
|
|
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
|
) -> None:
|
|
"""Test state_translated method."""
|
|
assert await async_setup_component(
|
|
hass,
|
|
"binary_sensor",
|
|
{
|
|
"binary_sensor": {
|
|
"platform": "group",
|
|
"name": "Grouped",
|
|
"entities": ["binary_sensor.first", "binary_sensor.second"],
|
|
}
|
|
},
|
|
)
|
|
await hass.async_block_till_done()
|
|
await translation._async_get_translations_cache(hass).async_load("en", set())
|
|
|
|
hass.states.async_set("switch.without_translations", "on", attributes={})
|
|
hass.states.async_set("binary_sensor.without_device_class", "on", attributes={})
|
|
hass.states.async_set(
|
|
"binary_sensor.with_device_class", "on", attributes={"device_class": "motion"}
|
|
)
|
|
hass.states.async_set(
|
|
"binary_sensor.with_unknown_device_class",
|
|
"on",
|
|
attributes={"device_class": "unknown_class"},
|
|
)
|
|
hass.states.async_set(
|
|
"some_domain.with_device_class_1",
|
|
"off",
|
|
attributes={"device_class": "some_device_class"},
|
|
)
|
|
hass.states.async_set(
|
|
"some_domain.with_device_class_2",
|
|
"foo",
|
|
attributes={"device_class": "some_device_class"},
|
|
)
|
|
hass.states.async_set("domain.is_unavailable", "unavailable", attributes={})
|
|
hass.states.async_set("domain.is_unknown", "unknown", attributes={})
|
|
|
|
config_entry = MockConfigEntry(domain="light")
|
|
config_entry.add_to_hass(hass)
|
|
entity_registry.async_get_or_create(
|
|
"light",
|
|
"hue",
|
|
"5678",
|
|
config_entry=config_entry,
|
|
translation_key="translation_key",
|
|
)
|
|
hass.states.async_set("light.hue_5678", "on", attributes={})
|
|
|
|
result = render(hass, '{{ state_translated("switch.without_translations") }}')
|
|
assert result == "on"
|
|
|
|
result = render(
|
|
hass, '{{ state_translated("binary_sensor.without_device_class") }}'
|
|
)
|
|
assert result == "On"
|
|
|
|
result = render(hass, '{{ state_translated("binary_sensor.with_device_class") }}')
|
|
assert result == "Detected"
|
|
|
|
result = render(
|
|
hass, '{{ state_translated("binary_sensor.with_unknown_device_class") }}'
|
|
)
|
|
assert result == "On"
|
|
|
|
with pytest.raises(TemplateError):
|
|
render(hass, '{{ state_translated("contextfunction") }}')
|
|
|
|
result = render(hass, '{{ state_translated("switch.invalid") }}')
|
|
assert result == "unknown"
|
|
|
|
with pytest.raises(TemplateError):
|
|
render(hass, '{{ state_translated("-invalid") }}')
|
|
|
|
def mock_get_cached_translations(
|
|
_hass: HomeAssistant,
|
|
_language: str,
|
|
category: str,
|
|
_integrations: Iterable[str] | None = None,
|
|
):
|
|
if category == "entity":
|
|
return {
|
|
"component.hue.entity.light.translation_key.state.on": "state_is_on",
|
|
}
|
|
return {}
|
|
|
|
with patch(
|
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
|
side_effect=mock_get_cached_translations,
|
|
):
|
|
result = render(hass, '{{ state_translated("light.hue_5678") }}')
|
|
assert result == "state_is_on"
|
|
|
|
result = render(hass, '{{ state_translated("domain.is_unavailable") }}')
|
|
assert result == "unavailable"
|
|
|
|
result = render(hass, '{{ state_translated("domain.is_unknown") }}')
|
|
assert result == "unknown"
|
|
|
|
|
|
async def test_state_attr_translated(
|
|
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
|
) -> None:
|
|
"""Test state_attr_translated method."""
|
|
await translation._async_get_translations_cache(hass).async_load("en", set())
|
|
|
|
hass.states.async_set(
|
|
"climate.living_room",
|
|
"heat",
|
|
attributes={"fan_mode": "auto", "hvac_action": "heating"},
|
|
)
|
|
hass.states.async_set(
|
|
"switch.test",
|
|
"on",
|
|
attributes={"some_attr": "some_value", "numeric_attr": 42, "bool_attr": True},
|
|
)
|
|
|
|
result = render(
|
|
hass,
|
|
'{{ state_attr_translated("switch.test", "some_attr") }}',
|
|
)
|
|
assert result == "some_value"
|
|
|
|
# Non-string attributes should be returned as-is without type conversion
|
|
result = render(
|
|
hass,
|
|
'{{ state_attr_translated("switch.test", "numeric_attr") }}',
|
|
)
|
|
assert result == 42
|
|
assert isinstance(result, int)
|
|
|
|
result = render(
|
|
hass,
|
|
'{{ state_attr_translated("switch.test", "bool_attr") }}',
|
|
)
|
|
assert result is True
|
|
|
|
result = render(
|
|
hass,
|
|
'{{ state_attr_translated("climate.non_existent", "fan_mode") }}',
|
|
)
|
|
assert result is None
|
|
|
|
with pytest.raises(TemplateError):
|
|
render(hass, '{{ state_attr_translated("-invalid", "fan_mode") }}')
|
|
|
|
result = render(
|
|
hass,
|
|
'{{ state_attr_translated("climate.living_room", "non_existent") }}',
|
|
)
|
|
assert result is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"entity_id",
|
|
"attribute",
|
|
"translations",
|
|
"expected_result",
|
|
),
|
|
[
|
|
(
|
|
"climate.test_platform_5678",
|
|
"fan_mode",
|
|
{
|
|
"component.test_platform.entity.climate.my_climate.state_attributes.fan_mode.state.auto": "Platform Automatic",
|
|
},
|
|
"Platform Automatic",
|
|
),
|
|
(
|
|
"climate.living_room",
|
|
"fan_mode",
|
|
{
|
|
"component.climate.entity_component._.state_attributes.fan_mode.state.auto": "Automatic",
|
|
},
|
|
"Automatic",
|
|
),
|
|
(
|
|
"climate.living_room",
|
|
"hvac_action",
|
|
{
|
|
"component.climate.entity_component._.state_attributes.hvac_action.state.heating": "Heating",
|
|
},
|
|
"Heating",
|
|
),
|
|
],
|
|
)
|
|
async def test_state_attr_translated_translation_lookups(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
entity_id: str,
|
|
attribute: str,
|
|
translations: dict[str, str],
|
|
expected_result: str,
|
|
) -> None:
|
|
"""Test state_attr_translated translation lookups."""
|
|
await translation._async_get_translations_cache(hass).async_load("en", set())
|
|
|
|
hass.states.async_set(
|
|
"climate.living_room",
|
|
"heat",
|
|
attributes={"fan_mode": "auto", "hvac_action": "heating"},
|
|
)
|
|
|
|
config_entry = MockConfigEntry(domain="climate")
|
|
config_entry.add_to_hass(hass)
|
|
entity_registry.async_get_or_create(
|
|
"climate",
|
|
"test_platform",
|
|
"5678",
|
|
config_entry=config_entry,
|
|
translation_key="my_climate",
|
|
)
|
|
hass.states.async_set(
|
|
"climate.test_platform_5678",
|
|
"heat",
|
|
attributes={"fan_mode": "auto"},
|
|
)
|
|
|
|
with patch(
|
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
|
return_value=translations,
|
|
):
|
|
result = render(
|
|
hass,
|
|
f'{{{{ state_attr_translated("{entity_id}", "{attribute}") }}}}',
|
|
)
|
|
assert result == expected_result
|
|
|
|
|
|
def test_has_value(hass: HomeAssistant) -> None:
|
|
"""Test has_value method."""
|
|
hass.states.async_set("test.value1", 1)
|
|
hass.states.async_set("test.unavailable", STATE_UNAVAILABLE)
|
|
|
|
result = render(hass, """{{ has_value("test.value1") }}""")
|
|
assert result is True
|
|
|
|
result = render(hass, """{{ has_value("test.unavailable") }}""")
|
|
assert result is False
|
|
|
|
result = render(hass, """{{ has_value("test.unknown") }}""")
|
|
assert result is False
|
|
|
|
result = render(
|
|
hass, """{% if "test.value1" is has_value %}yes{% else %}no{% endif %}"""
|
|
)
|
|
assert result == "yes"
|
|
|
|
|
|
@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_distance_function_with_1_state(hass: HomeAssistant) -> None:
|
|
"""Test distance function with 1 state."""
|
|
_set_up_units(hass)
|
|
hass.states.async_set(
|
|
"test.object", "happy", {"latitude": 32.87336, "longitude": -117.22943}
|
|
)
|
|
|
|
result = render(hass, "{{ distance(states.test.object) | round }}")
|
|
assert result == 187
|
|
|
|
|
|
def test_distance_function_with_2_states(hass: HomeAssistant) -> None:
|
|
"""Test distance function with 2 states."""
|
|
_set_up_units(hass)
|
|
hass.states.async_set(
|
|
"test.object", "happy", {"latitude": 32.87336, "longitude": -117.22943}
|
|
)
|
|
hass.states.async_set(
|
|
"test.object_2",
|
|
"happy",
|
|
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
|
|
)
|
|
|
|
result = render(
|
|
hass, "{{ distance(states.test.object, states.test.object_2) | round }}"
|
|
)
|
|
assert result == 187
|
|
|
|
|
|
def test_distance_function_with_1_coord(hass: HomeAssistant) -> None:
|
|
"""Test distance function with 1 coord."""
|
|
_set_up_units(hass)
|
|
|
|
result = render(hass, '{{ distance("32.87336", "-117.22943") | round }}')
|
|
assert result == 187
|
|
|
|
|
|
def test_distance_function_with_2_coords(hass: HomeAssistant) -> None:
|
|
"""Test distance function with 2 coords."""
|
|
_set_up_units(hass)
|
|
tpl = f'{{{{ distance("32.87336", "-117.22943", {hass.config.latitude}, {hass.config.longitude}) | round }}}}'
|
|
assert render(hass, tpl) == 187
|
|
|
|
|
|
def test_distance_function_with_1_state_1_coord(hass: HomeAssistant) -> None:
|
|
"""Test distance function with 1 state 1 coord."""
|
|
_set_up_units(hass)
|
|
hass.states.async_set(
|
|
"test.object_2",
|
|
"happy",
|
|
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
|
|
)
|
|
|
|
result = render(
|
|
hass, '{{ distance("32.87336", "-117.22943", states.test.object_2) | round }}'
|
|
)
|
|
assert result == 187
|
|
|
|
result = render(
|
|
hass, '{{ distance(states.test.object_2, "32.87336", "-117.22943") | round }}'
|
|
)
|
|
assert result == 187
|
|
|
|
|
|
def test_distance_function_return_none_if_invalid_state(hass: HomeAssistant) -> None:
|
|
"""Test distance function return None if invalid state."""
|
|
hass.states.async_set("test.object_2", "happy", {"latitude": 10})
|
|
with pytest.raises(TemplateError):
|
|
render(hass, "{{ distance(states.test.object_2) | round }}")
|
|
|
|
|
|
def test_distance_function_return_none_if_invalid_coord(hass: HomeAssistant) -> None:
|
|
"""Test distance function return None if invalid coord."""
|
|
assert render(hass, '{{ distance("123", "abc") }}') is None
|
|
|
|
assert render(hass, '{{ distance("123") }}') is None
|
|
|
|
hass.states.async_set(
|
|
"test.object_2",
|
|
"happy",
|
|
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
|
|
)
|
|
|
|
result = render(hass, '{{ distance("123", states.test_object_2) }}')
|
|
assert result is None
|
|
|
|
|
|
def test_distance_function_with_2_entity_ids(hass: HomeAssistant) -> None:
|
|
"""Test distance function with 2 entity ids."""
|
|
_set_up_units(hass)
|
|
hass.states.async_set(
|
|
"test.object", "happy", {"latitude": 32.87336, "longitude": -117.22943}
|
|
)
|
|
hass.states.async_set(
|
|
"test.object_2",
|
|
"happy",
|
|
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
|
|
)
|
|
|
|
result = render(hass, '{{ distance("test.object", "test.object_2") | round }}')
|
|
assert result == 187
|
|
|
|
|
|
def test_distance_function_with_1_entity_1_coord(hass: HomeAssistant) -> None:
|
|
"""Test distance function with 1 entity_id and 1 coord."""
|
|
_set_up_units(hass)
|
|
hass.states.async_set(
|
|
"test.object",
|
|
"happy",
|
|
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
|
|
)
|
|
|
|
result = render(
|
|
hass, '{{ distance("test.object", "32.87336", "-117.22943") | round }}'
|
|
)
|
|
assert result == 187
|
|
|
|
|
|
def test_closest_function_home_vs_domain(hass: HomeAssistant) -> None:
|
|
"""Test closest function home vs domain."""
|
|
hass.states.async_set(
|
|
"test_domain.object",
|
|
"happy",
|
|
{
|
|
"latitude": hass.config.latitude + 0.1,
|
|
"longitude": hass.config.longitude + 0.1,
|
|
},
|
|
)
|
|
|
|
hass.states.async_set(
|
|
"not_test_domain.but_closer",
|
|
"happy",
|
|
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
|
|
)
|
|
|
|
assert (
|
|
render(hass, "{{ closest(states.test_domain).entity_id }}")
|
|
== "test_domain.object"
|
|
)
|
|
|
|
assert (
|
|
render(hass, "{{ (states.test_domain | closest).entity_id }}")
|
|
== "test_domain.object"
|
|
)
|
|
|
|
|
|
def test_closest_function_home_vs_all_states(hass: HomeAssistant) -> None:
|
|
"""Test closest function home vs all states."""
|
|
hass.states.async_set(
|
|
"test_domain.object",
|
|
"happy",
|
|
{
|
|
"latitude": hass.config.latitude + 0.1,
|
|
"longitude": hass.config.longitude + 0.1,
|
|
},
|
|
)
|
|
|
|
hass.states.async_set(
|
|
"test_domain_2.and_closer",
|
|
"happy",
|
|
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
|
|
)
|
|
|
|
assert render(hass, "{{ closest(states).entity_id }}") == "test_domain_2.and_closer"
|
|
|
|
assert (
|
|
render(hass, "{{ (states | closest).entity_id }}") == "test_domain_2.and_closer"
|
|
)
|
|
|
|
|
|
async def test_closest_function_home_vs_group_entity_id(hass: HomeAssistant) -> None:
|
|
"""Test closest function home vs group entity id."""
|
|
hass.states.async_set(
|
|
"test_domain.object",
|
|
"happy",
|
|
{
|
|
"latitude": hass.config.latitude + 0.1,
|
|
"longitude": hass.config.longitude + 0.1,
|
|
},
|
|
)
|
|
|
|
hass.states.async_set(
|
|
"not_in_group.but_closer",
|
|
"happy",
|
|
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
|
|
)
|
|
|
|
assert await async_setup_component(hass, "group", {})
|
|
await hass.async_block_till_done()
|
|
await group.Group.async_create_group(
|
|
hass,
|
|
"location group",
|
|
created_by_service=False,
|
|
entity_ids=["test_domain.object"],
|
|
icon=None,
|
|
mode=None,
|
|
object_id=None,
|
|
order=None,
|
|
)
|
|
|
|
info = render_to_info(hass, '{{ closest("group.location_group").entity_id }}')
|
|
assert_result_info(
|
|
info, "test_domain.object", {"group.location_group", "test_domain.object"}
|
|
)
|
|
assert info.rate_limit is None
|
|
|
|
|
|
async def test_closest_function_home_vs_group_state(hass: HomeAssistant) -> None:
|
|
"""Test closest function home vs group state."""
|
|
hass.states.async_set(
|
|
"test_domain.object",
|
|
"happy",
|
|
{
|
|
"latitude": hass.config.latitude + 0.1,
|
|
"longitude": hass.config.longitude + 0.1,
|
|
},
|
|
)
|
|
|
|
hass.states.async_set(
|
|
"not_in_group.but_closer",
|
|
"happy",
|
|
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
|
|
)
|
|
|
|
assert await async_setup_component(hass, "group", {})
|
|
await hass.async_block_till_done()
|
|
await group.Group.async_create_group(
|
|
hass,
|
|
"location group",
|
|
created_by_service=False,
|
|
entity_ids=["test_domain.object"],
|
|
icon=None,
|
|
mode=None,
|
|
object_id=None,
|
|
order=None,
|
|
)
|
|
|
|
info = render_to_info(hass, '{{ closest("group.location_group").entity_id }}')
|
|
assert_result_info(
|
|
info, "test_domain.object", {"group.location_group", "test_domain.object"}
|
|
)
|
|
assert info.rate_limit is None
|
|
|
|
info = render_to_info(hass, "{{ closest(states.group.location_group).entity_id }}")
|
|
assert_result_info(
|
|
info, "test_domain.object", {"test_domain.object", "group.location_group"}
|
|
)
|
|
assert info.rate_limit is None
|
|
|
|
|
|
async def test_expand(hass: HomeAssistant) -> None:
|
|
"""Test expand function."""
|
|
info = render_to_info(hass, "{{ expand('test.object') }}")
|
|
assert_result_info(info, [], ["test.object"])
|
|
assert info.rate_limit is None
|
|
|
|
info = render_to_info(hass, "{{ expand(56) }}")
|
|
assert_result_info(info, [])
|
|
assert info.rate_limit is None
|
|
|
|
hass.states.async_set("test.object", "happy")
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
"{{ expand('test.object') | sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}",
|
|
)
|
|
assert_result_info(info, "test.object", ["test.object"])
|
|
assert info.rate_limit is None
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
"{{ expand('group.new_group') | sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}",
|
|
)
|
|
assert_result_info(info, "", ["group.new_group"])
|
|
assert info.rate_limit is None
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
"{{ expand(states.group) | sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}",
|
|
)
|
|
assert_result_info(info, "", [], ["group"])
|
|
assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT
|
|
|
|
assert await async_setup_component(hass, "group", {})
|
|
await hass.async_block_till_done()
|
|
await group.Group.async_create_group(
|
|
hass,
|
|
"new group",
|
|
created_by_service=False,
|
|
entity_ids=["test.object"],
|
|
icon=None,
|
|
mode=None,
|
|
object_id=None,
|
|
order=None,
|
|
)
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
"{{ expand('group.new_group') | sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}",
|
|
)
|
|
assert_result_info(info, "test.object", {"group.new_group", "test.object"})
|
|
assert info.rate_limit is None
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
"{{ expand(states.group) | sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}",
|
|
)
|
|
assert_result_info(info, "test.object", {"test.object"}, ["group"])
|
|
assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
(
|
|
"{{ expand('group.new_group', 'test.object')"
|
|
" | sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}"
|
|
),
|
|
)
|
|
assert_result_info(info, "test.object", {"test.object", "group.new_group"})
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
(
|
|
"{{ ['group.new_group', 'test.object'] | expand"
|
|
" | sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}"
|
|
),
|
|
)
|
|
assert_result_info(info, "test.object", {"test.object", "group.new_group"})
|
|
assert info.rate_limit is None
|
|
|
|
hass.states.async_set("sensor.power_1", 0)
|
|
hass.states.async_set("sensor.power_2", 200.2)
|
|
hass.states.async_set("sensor.power_3", 400.4)
|
|
|
|
assert await async_setup_component(hass, "group", {})
|
|
await hass.async_block_till_done()
|
|
await group.Group.async_create_group(
|
|
hass,
|
|
"power sensors",
|
|
created_by_service=False,
|
|
entity_ids=["sensor.power_1", "sensor.power_2", "sensor.power_3"],
|
|
icon=None,
|
|
mode=None,
|
|
object_id=None,
|
|
order=None,
|
|
)
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
(
|
|
"{{ states.group.power_sensors.attributes.entity_id | expand "
|
|
"| sort(attribute='entity_id') | map(attribute='state')|map('float')|sum }}"
|
|
),
|
|
)
|
|
assert_result_info(
|
|
info,
|
|
200.2 + 400.4,
|
|
{"group.power_sensors", "sensor.power_1", "sensor.power_2", "sensor.power_3"},
|
|
)
|
|
assert info.rate_limit is None
|
|
|
|
# With group entities
|
|
hass.states.async_set("light.first", "on")
|
|
hass.states.async_set("light.second", "off")
|
|
|
|
assert await async_setup_component(
|
|
hass,
|
|
"light",
|
|
{
|
|
"light": {
|
|
"platform": "group",
|
|
"name": "Grouped",
|
|
"entities": ["light.first", "light.second"],
|
|
}
|
|
},
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
"{{ expand('light.grouped') | sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}",
|
|
)
|
|
assert_result_info(
|
|
info,
|
|
"light.first, light.second",
|
|
["light.grouped", "light.first", "light.second"],
|
|
)
|
|
|
|
assert await async_setup_component(
|
|
hass,
|
|
"zone",
|
|
{
|
|
"zone": {
|
|
"name": "Test",
|
|
"latitude": 32.880837,
|
|
"longitude": -117.237561,
|
|
"radius": 250,
|
|
"passive": False,
|
|
}
|
|
},
|
|
)
|
|
info = render_to_info(
|
|
hass,
|
|
"{{ expand('zone.test') | sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}",
|
|
)
|
|
assert_result_info(
|
|
info,
|
|
"",
|
|
["zone.test"],
|
|
)
|
|
|
|
hass.states.async_set(
|
|
"person.person1",
|
|
"test",
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
"{{ expand('zone.test') | sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}",
|
|
)
|
|
assert_result_info(
|
|
info,
|
|
"person.person1",
|
|
["zone.test", "person.person1"],
|
|
)
|
|
|
|
hass.states.async_set(
|
|
"person.person2",
|
|
"test",
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
"{{ expand('zone.test') | sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}",
|
|
)
|
|
assert_result_info(
|
|
info,
|
|
"person.person1, person.person2",
|
|
["zone.test", "person.person1", "person.person2"],
|
|
)
|
|
|
|
|
|
def test_closest_function_to_coord(hass: HomeAssistant) -> None:
|
|
"""Test closest function to coord."""
|
|
hass.states.async_set(
|
|
"test_domain.closest_home",
|
|
"happy",
|
|
{
|
|
"latitude": hass.config.latitude + 0.1,
|
|
"longitude": hass.config.longitude + 0.1,
|
|
},
|
|
)
|
|
|
|
hass.states.async_set(
|
|
"test_domain.closest_zone",
|
|
"happy",
|
|
{
|
|
"latitude": hass.config.latitude + 0.2,
|
|
"longitude": hass.config.longitude + 0.2,
|
|
},
|
|
)
|
|
|
|
hass.states.async_set(
|
|
"zone.far_away",
|
|
"zoning",
|
|
{
|
|
"latitude": hass.config.latitude + 0.3,
|
|
"longitude": hass.config.longitude + 0.3,
|
|
},
|
|
)
|
|
|
|
result = render(
|
|
hass,
|
|
f'{{{{ closest("{hass.config.latitude + 0.3}", {hass.config.longitude + 0.3}, states.test_domain).entity_id }}}}',
|
|
)
|
|
assert result == "test_domain.closest_zone"
|
|
|
|
result = render(
|
|
hass,
|
|
f'{{{{ (states.test_domain | closest("{hass.config.latitude + 0.3}", {hass.config.longitude + 0.3})).entity_id }}}}',
|
|
)
|
|
assert result == "test_domain.closest_zone"
|
|
|
|
|
|
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
|
|
|
|
|
|
def test_closest_function_to_entity_id(hass: HomeAssistant) -> None:
|
|
"""Test closest function to entity id."""
|
|
hass.states.async_set(
|
|
"test_domain.closest_home",
|
|
"happy",
|
|
{
|
|
"latitude": hass.config.latitude + 0.1,
|
|
"longitude": hass.config.longitude + 0.1,
|
|
},
|
|
)
|
|
|
|
hass.states.async_set(
|
|
"test_domain.closest_zone",
|
|
"happy",
|
|
{
|
|
"latitude": hass.config.latitude + 0.2,
|
|
"longitude": hass.config.longitude + 0.2,
|
|
},
|
|
)
|
|
|
|
hass.states.async_set(
|
|
"zone.far_away",
|
|
"zoning",
|
|
{
|
|
"latitude": hass.config.latitude + 0.3,
|
|
"longitude": hass.config.longitude + 0.3,
|
|
},
|
|
)
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
"{{ closest(zone, states.test_domain).entity_id }}",
|
|
{"zone": "zone.far_away"},
|
|
)
|
|
|
|
assert_result_info(
|
|
info,
|
|
"test_domain.closest_zone",
|
|
["test_domain.closest_home", "test_domain.closest_zone", "zone.far_away"],
|
|
["test_domain"],
|
|
)
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
(
|
|
"{{ ([states.test_domain, 'test_domain.closest_zone'] "
|
|
"| closest(zone)).entity_id }}"
|
|
),
|
|
{"zone": "zone.far_away"},
|
|
)
|
|
|
|
assert_result_info(
|
|
info,
|
|
"test_domain.closest_zone",
|
|
["test_domain.closest_home", "test_domain.closest_zone", "zone.far_away"],
|
|
["test_domain"],
|
|
)
|
|
|
|
|
|
def test_closest_function_to_state(hass: HomeAssistant) -> None:
|
|
"""Test closest function to state."""
|
|
hass.states.async_set(
|
|
"test_domain.closest_home",
|
|
"happy",
|
|
{
|
|
"latitude": hass.config.latitude + 0.1,
|
|
"longitude": hass.config.longitude + 0.1,
|
|
},
|
|
)
|
|
|
|
hass.states.async_set(
|
|
"test_domain.closest_zone",
|
|
"happy",
|
|
{
|
|
"latitude": hass.config.latitude + 0.2,
|
|
"longitude": hass.config.longitude + 0.2,
|
|
},
|
|
)
|
|
|
|
hass.states.async_set(
|
|
"zone.far_away",
|
|
"zoning",
|
|
{
|
|
"latitude": hass.config.latitude + 0.3,
|
|
"longitude": hass.config.longitude + 0.3,
|
|
},
|
|
)
|
|
|
|
assert (
|
|
render(
|
|
hass, "{{ closest(states.zone.far_away, states.test_domain).entity_id }}"
|
|
)
|
|
== "test_domain.closest_zone"
|
|
)
|
|
|
|
|
|
def test_closest_function_invalid_state(hass: HomeAssistant) -> None:
|
|
"""Test closest function invalid state."""
|
|
hass.states.async_set(
|
|
"test_domain.closest_home",
|
|
"happy",
|
|
{
|
|
"latitude": hass.config.latitude + 0.1,
|
|
"longitude": hass.config.longitude + 0.1,
|
|
},
|
|
)
|
|
|
|
for state in ("states.zone.non_existing", '"zone.non_existing"'):
|
|
assert render(hass, f"{{{{ closest({state}, states) }}}}") is None
|
|
|
|
|
|
def test_closest_function_state_with_invalid_location(hass: HomeAssistant) -> None:
|
|
"""Test closest function state with invalid location."""
|
|
hass.states.async_set(
|
|
"test_domain.closest_home",
|
|
"happy",
|
|
{"latitude": "invalid latitude", "longitude": hass.config.longitude + 0.1},
|
|
)
|
|
|
|
assert (
|
|
render(hass, "{{ closest(states.test_domain.closest_home, states) }}") is None
|
|
)
|
|
|
|
|
|
def test_closest_function_invalid_coordinates(hass: HomeAssistant) -> None:
|
|
"""Test closest function invalid coordinates."""
|
|
hass.states.async_set(
|
|
"test_domain.closest_home",
|
|
"happy",
|
|
{
|
|
"latitude": hass.config.latitude + 0.1,
|
|
"longitude": hass.config.longitude + 0.1,
|
|
},
|
|
)
|
|
|
|
assert render(hass, '{{ closest("invalid", "coord", states) }}') is None
|
|
assert render(hass, '{{ states | closest("invalid", "coord") }}') is None
|
|
|
|
|
|
def test_closest_function_no_location_states(hass: HomeAssistant) -> None:
|
|
"""Test closest function without location states."""
|
|
assert render(hass, "{{ closest(states).entity_id }}") == ""
|
|
|
|
|
|
def test_generate_filter_iterators(hass: HomeAssistant) -> None:
|
|
"""Test extract entities function with none entities stuff."""
|
|
info = render_to_info(
|
|
hass,
|
|
"""
|
|
{% for state in states %}
|
|
{{ state.entity_id }}
|
|
{% endfor %}
|
|
""",
|
|
)
|
|
assert_result_info(info, "", all_states=True)
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
"""
|
|
{% for state in states.sensor %}
|
|
{{ state.entity_id }}
|
|
{% endfor %}
|
|
""",
|
|
)
|
|
assert_result_info(info, "", domains=["sensor"])
|
|
|
|
hass.states.async_set("sensor.test_sensor", "off", {"attr": "value"})
|
|
|
|
# Don't need the entity because the state is not accessed
|
|
info = render_to_info(
|
|
hass,
|
|
"""
|
|
{% for state in states.sensor %}
|
|
{{ state.entity_id }}
|
|
{% endfor %}
|
|
""",
|
|
)
|
|
assert_result_info(info, "sensor.test_sensor", domains=["sensor"])
|
|
|
|
# But we do here because the state gets accessed
|
|
info = render_to_info(
|
|
hass,
|
|
"""
|
|
{% for state in states.sensor %}
|
|
{{ state.entity_id }}={{ state.state }},
|
|
{% endfor %}
|
|
""",
|
|
)
|
|
assert_result_info(info, "sensor.test_sensor=off,", [], ["sensor"])
|
|
|
|
info = render_to_info(
|
|
hass,
|
|
"""
|
|
{% for state in states.sensor %}
|
|
{{ state.entity_id }}={{ state.attributes.attr }},
|
|
{% endfor %}
|
|
""",
|
|
)
|
|
assert_result_info(info, "sensor.test_sensor=value,", [], ["sensor"])
|
|
|
|
|
|
def test_generate_select(hass: HomeAssistant) -> None:
|
|
"""Test extract entities function with none entities stuff."""
|
|
template_str = """
|
|
{{ states.sensor|selectattr("state","equalto","off")
|
|
|join(",", attribute="entity_id") }}
|
|
"""
|
|
|
|
info = render_to_info(hass, template_str)
|
|
assert_result_info(info, "", [], [])
|
|
assert info.domains_lifecycle == {"sensor"}
|
|
|
|
hass.states.async_set("sensor.test_sensor", "off", {"attr": "value"})
|
|
hass.states.async_set("sensor.test_sensor_on", "on")
|
|
|
|
info = render_to_info(hass, template_str)
|
|
assert_result_info(
|
|
info,
|
|
"sensor.test_sensor",
|
|
[],
|
|
["sensor"],
|
|
)
|
|
assert info.domains_lifecycle == {"sensor"}
|
|
|
|
|
|
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"
|
|
|
|
|
|
def test_state_with_unit(hass: HomeAssistant) -> None:
|
|
"""Test the state_with_unit property helper."""
|
|
hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
|
|
hass.states.async_set("sensor.test2", "wow")
|
|
|
|
result = render(hass, "{{ states.sensor.test.state_with_unit }}")
|
|
assert result == "23 beers"
|
|
|
|
result = render(hass, "{{ states.sensor.test2.state_with_unit }}")
|
|
assert result == "wow"
|
|
|
|
result = render(
|
|
hass, "{% for state in states %}{{ state.state_with_unit }} {% endfor %}"
|
|
)
|
|
assert result == "23 beers wow"
|
|
|
|
result = render(hass, "{{ states.sensor.non_existing.state_with_unit }}")
|
|
assert result == ""
|
|
|
|
|
|
def test_state_with_unit_and_rounding(
|
|
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
|
) -> 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"})
|
|
hass.states.async_set("sensor.test3", "-0.0", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
|
|
hass.states.async_set("sensor.test4", "-0", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
|
|
|
|
# state_with_unit property
|
|
tpl = template.Template("{{ states.sensor.test.state_with_unit }}", hass)
|
|
tpl2 = template.Template("{{ states.sensor.test2.state_with_unit }}", hass)
|
|
|
|
# AllStates.__call__ defaults
|
|
tpl3 = template.Template("{{ states('sensor.test') }}", hass)
|
|
tpl4 = template.Template("{{ states('sensor.test2') }}", hass)
|
|
|
|
# AllStates.__call__ and with_unit=True
|
|
tpl5 = template.Template("{{ states('sensor.test', with_unit=True) }}", hass)
|
|
tpl6 = template.Template("{{ states('sensor.test2', with_unit=True) }}", hass)
|
|
|
|
# AllStates.__call__ and rounded=True
|
|
tpl7 = template.Template("{{ states('sensor.test', rounded=True) }}", hass)
|
|
tpl8 = template.Template("{{ states('sensor.test2', rounded=True) }}", hass)
|
|
tpl9 = template.Template("{{ states('sensor.test3', rounded=True) }}", hass)
|
|
tpl10 = template.Template("{{ states('sensor.test4', rounded=True) }}", hass)
|
|
|
|
assert tpl.async_render() == "23.00 beers"
|
|
assert tpl2.async_render() == "23 beers"
|
|
assert tpl3.async_render() == 23
|
|
assert tpl4.async_render() == 23
|
|
assert tpl5.async_render() == "23.00 beers"
|
|
assert tpl6.async_render() == "23 beers"
|
|
assert tpl7.async_render() == 23.0
|
|
assert tpl8.async_render() == 23
|
|
assert tpl9.async_render() == 0.0
|
|
assert tpl10.async_render() == 0
|
|
|
|
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() == "23.02 beers"
|
|
assert tpl2.async_render() == "23.015 beers"
|
|
assert tpl3.async_render() == 23.015
|
|
assert tpl4.async_render() == 23.015
|
|
assert tpl5.async_render() == "23.02 beers"
|
|
assert tpl6.async_render() == "23.015 beers"
|
|
assert tpl7.async_render() == 23.02
|
|
assert tpl8.async_render() == 23.015
|
|
|
|
|
|
@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}
|
|
|
|
|
|
@pytest.mark.usefixtures("hass")
|
|
async def test_cache_garbage_collection() -> None:
|
|
"""Test caching a template."""
|
|
template_string = (
|
|
"{% set dict = {'foo': 'x&y', 'bar': 42} %} {{ dict | urlencode }}"
|
|
)
|
|
tpl = template.Template(
|
|
(template_string),
|
|
)
|
|
tpl.ensure_valid()
|
|
assert template._NO_HASS_ENV.template_cache.get(template_string)
|
|
|
|
tpl2 = template.Template(
|
|
(template_string),
|
|
)
|
|
tpl2.ensure_valid()
|
|
assert template._NO_HASS_ENV.template_cache.get(template_string)
|
|
|
|
del tpl
|
|
assert template._NO_HASS_ENV.template_cache.get(template_string)
|
|
del tpl2
|
|
assert not template._NO_HASS_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 }}")
|
|
|
|
|
|
def test_warn_no_hass(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None:
|
|
"""Test deprecation warning when instantiating Template without hass."""
|
|
|
|
message = "Detected code that creates a template object without passing hass"
|
|
template.Template("blah")
|
|
assert message in caplog.text
|
|
caplog.clear()
|
|
|
|
template.Template("blah", None)
|
|
assert message in caplog.text
|
|
caplog.clear()
|
|
|
|
template.Template("blah", hass)
|
|
assert message not in caplog.text
|
|
caplog.clear()
|