core/tests/helpers/test_template.py

6587 lines
205 KiB
Python

"""Test Home Assistant template helper methods."""
from __future__ import annotations
from collections.abc import Iterable
from datetime import datetime, timedelta
import json
import logging
import math
import random
from types import MappingProxyType
from typing import Any
from unittest.mock import patch
from freezegun import freeze_time
import orjson
import pytest
from syrupy import SnapshotAssertion
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import group
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
STATE_ON,
STATE_UNAVAILABLE,
UnitOfArea,
UnitOfLength,
UnitOfMass,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
entity,
entity_registry as er,
floor_registry as fr,
issue_registry as ir,
label_registry as lr,
template,
translation,
)
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from homeassistant.util.read_only_dict import ReadOnlyDict
from homeassistant.util.unit_system import UnitSystem
from tests.common import MockConfigEntry, async_fire_time_changed
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,
)
def render(
hass: HomeAssistant, template_str: str, variables: TemplateVarsType | None = None
) -> Any:
"""Create render info from template."""
tmp = template.Template(template_str, hass)
return tmp.async_render(variables)
def render_to_info(
hass: HomeAssistant, template_str: str, variables: TemplateVarsType | None = None
) -> template.RenderInfo:
"""Create render info from template."""
tmp = template.Template(template_str, hass)
return tmp.async_render_to_info(variables)
def extract_entities(
hass: HomeAssistant, template_str: str, variables: TemplateVarsType | None = None
) -> set[str]:
"""Extract entities from a template."""
info = render_to_info(hass, template_str, variables)
return info.entities
def assert_result_info(
info: template.RenderInfo,
result: Any,
entities: Iterable[str] | None = None,
domains: Iterable[str] | None = None,
all_states: bool = False,
) -> None:
"""Check result info."""
assert info.result() == result
assert info.all_states == all_states
assert info.filter("invalid_entity_name.somewhere") == all_states
if entities is not None:
assert info.entities == frozenset(entities)
assert all(info.filter(entity) for entity in entities)
if not all_states:
assert not info.filter("invalid_entity_name.somewhere")
else:
assert not info.entities
if domains is not None:
assert info.domains == frozenset(domains)
assert all(info.filter(domain + ".entity") for domain in domains)
else:
assert not hasattr(info, "_domains")
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_str = "{{ states('sensor.test') }}"
template_obj = template.Template(template_str, None)
template._render_info.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_str = "{{ states('sensor.test') }}"
template_obj = template.Template(template_str, None)
template_obj.hass = hass
template._render_info.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() -> 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 (
template.Template("{{ states.test.object.state }}", hass).async_render()
== "happy"
)
assert (
template.Template('{{ states["test.object"].state }}', hass).async_render()
== "happy"
)
assert (
template.Template('{{ states("test.object") }}', hass).async_render() == "happy"
)
def test_invalid_entity_id(hass: HomeAssistant) -> None:
"""Test referring states by entity id."""
with pytest.raises(TemplateError):
template.Template('{{ states["big.fat..."] }}', hass).async_render()
with pytest.raises(TemplateError):
template.Template('{{ states.test["big.fat..."] }}', hass).async_render()
with pytest.raises(TemplateError):
template.Template('{{ states["invalid/domain"] }}', hass).async_render()
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 == template.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 == template.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 == template.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 (
template.Template(
"""
{% import 'test.jinja' as t %}
{{ t.test_macro() }} {{ t.test_variable }}
""",
hass,
).async_render()
== "macro variable"
)
assert (
template.Template(
"""
{% import 'inner/inner_test.jinja' as t %}
{{ t.test_macro() }} {{ t.test_variable }}
""",
hass,
).async_render()
== "inner macro inner variable"
)
with pytest.raises(TemplateError):
template.Template(
"""
{% import 'notfound.jinja' as t %}
{{ t.test_macro() }} {{ t.test_variable }}
""",
hass,
).async_render()
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."""
assert (
template.Template(
"""
{%- for v in range(10) %}
{%- if v == 1 -%}
{%- continue -%}
{%- elif v == 3 -%}
{%- break -%}
{%- endif -%}
{{ v }}
{%- endfor -%}
""",
hass,
).async_render()
== "02"
)
def test_float_function(hass: HomeAssistant) -> None:
"""Test float function."""
hass.states.async_set("sensor.temperature", "12")
assert (
template.Template(
"{{ float(states.sensor.temperature.state) }}", hass
).async_render()
== 12.0
)
assert (
template.Template(
"{{ float(states.sensor.temperature.state) > 11 }}", hass
).async_render()
is True
)
# Test handling of invalid input
with pytest.raises(TemplateError):
template.Template("{{ float('forgiving') }}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ float('bad', 1) }}") == 1
assert render(hass, "{{ float('bad', default=1) }}") == 1
def test_float_filter(hass: HomeAssistant) -> None:
"""Test float filter."""
hass.states.async_set("sensor.temperature", "12")
assert render(hass, "{{ states.sensor.temperature.state | float }}") == 12.0
assert render(hass, "{{ states.sensor.temperature.state | float > 11 }}") is True
# Test handling of invalid input
with pytest.raises(TemplateError):
render(hass, "{{ 'bad' | float }}")
# Test handling of default return value
assert render(hass, "{{ 'bad' | float(1) }}") == 1
assert render(hass, "{{ 'bad' | float(default=1) }}") == 1
def test_int_filter(hass: HomeAssistant) -> None:
"""Test int filter."""
hass.states.async_set("sensor.temperature", "12.2")
assert render(hass, "{{ states.sensor.temperature.state | int }}") == 12
assert render(hass, "{{ states.sensor.temperature.state | int > 11 }}") is True
hass.states.async_set("sensor.temperature", "0x10")
assert render(hass, "{{ states.sensor.temperature.state | int(base=16) }}") == 16
# Test handling of invalid input
with pytest.raises(TemplateError):
render(hass, "{{ 'bad' | int }}")
# Test handling of default return value
assert render(hass, "{{ 'bad' | int(1) }}") == 1
assert render(hass, "{{ 'bad' | int(default=1) }}") == 1
def test_int_function(hass: HomeAssistant) -> None:
"""Test int filter."""
hass.states.async_set("sensor.temperature", "12.2")
assert render(hass, "{{ int(states.sensor.temperature.state) }}") == 12
assert render(hass, "{{ int(states.sensor.temperature.state) > 11 }}") is True
hass.states.async_set("sensor.temperature", "0x10")
assert render(hass, "{{ int(states.sensor.temperature.state, base=16) }}") == 16
# Test handling of invalid input
with pytest.raises(TemplateError):
render(hass, "{{ int('bad') }}")
# Test handling of default return value
assert render(hass, "{{ int('bad', 1) }}") == 1
assert render(hass, "{{ int('bad', default=1) }}") == 1
def test_bool_function(hass: HomeAssistant) -> None:
"""Test bool function."""
assert render(hass, "{{ bool(true) }}") is True
assert render(hass, "{{ bool(false) }}") is False
assert render(hass, "{{ bool('on') }}") is True
assert render(hass, "{{ bool('off') }}") is False
with pytest.raises(TemplateError):
render(hass, "{{ bool('unknown') }}")
with pytest.raises(TemplateError):
render(hass, "{{ bool(none) }}")
assert render(hass, "{{ bool('unavailable', none) }}") is None
assert render(hass, "{{ bool('unavailable', default=none) }}") is None
def test_bool_filter(hass: HomeAssistant) -> None:
"""Test bool filter."""
assert render(hass, "{{ true | bool }}") is True
assert render(hass, "{{ false | bool }}") is False
assert render(hass, "{{ 'on' | bool }}") is True
assert render(hass, "{{ 'off' | bool }}") is False
with pytest.raises(TemplateError):
render(hass, "{{ 'unknown' | bool }}")
with pytest.raises(TemplateError):
render(hass, "{{ none | bool }}")
assert render(hass, "{{ 'unavailable' | bool(none) }}") is None
assert render(hass, "{{ 'unavailable' | bool(default=none) }}") is None
@pytest.mark.parametrize(
("value", "expected"),
[
(0, True),
(0.0, True),
("0", True),
("0.0", True),
(True, True),
(False, True),
("True", False),
("False", False),
(None, False),
("None", False),
("horse", False),
(math.pi, True),
(math.nan, False),
(math.inf, False),
("nan", False),
("inf", False),
],
)
def test_isnumber(hass: HomeAssistant, value, expected) -> None:
"""Test is_number."""
assert (
template.Template("{{ is_number(value) }}", hass).async_render({"value": value})
== expected
)
assert (
template.Template("{{ value | is_number }}", hass).async_render(
{"value": value}
)
== expected
)
assert (
template.Template("{{ value is is_number }}", hass).async_render(
{"value": value}
)
== expected
)
@pytest.mark.parametrize(
("value", "expected"),
[
([1, 2], True),
({1, 2}, False),
({"a": 1, "b": 2}, False),
(ReadOnlyDict({"a": 1, "b": 2}), False),
(MappingProxyType({"a": 1, "b": 2}), False),
("abc", False),
(b"abc", False),
((1, 2), False),
(datetime(2024, 1, 1, 0, 0, 0), False),
],
)
def test_is_list(hass: HomeAssistant, value: Any, expected: bool) -> None:
"""Test is list."""
assert (
template.Template("{{ value is list }}", hass).async_render({"value": value})
== expected
)
@pytest.mark.parametrize(
("value", "expected"),
[
([1, 2], False),
({1, 2}, True),
({"a": 1, "b": 2}, False),
(ReadOnlyDict({"a": 1, "b": 2}), False),
(MappingProxyType({"a": 1, "b": 2}), False),
("abc", False),
(b"abc", False),
((1, 2), False),
(datetime(2024, 1, 1, 0, 0, 0), False),
],
)
def test_is_set(hass: HomeAssistant, value: Any, expected: bool) -> None:
"""Test is set."""
assert (
template.Template("{{ value is set }}", hass).async_render({"value": value})
== expected
)
@pytest.mark.parametrize(
("value", "expected"),
[
([1, 2], False),
({1, 2}, False),
({"a": 1, "b": 2}, False),
(ReadOnlyDict({"a": 1, "b": 2}), False),
(MappingProxyType({"a": 1, "b": 2}), False),
("abc", False),
(b"abc", False),
((1, 2), True),
(datetime(2024, 1, 1, 0, 0, 0), False),
],
)
def test_is_tuple(hass: HomeAssistant, value: Any, expected: bool) -> None:
"""Test is tuple."""
assert (
template.Template("{{ value is tuple }}", hass).async_render({"value": value})
== expected
)
@pytest.mark.parametrize(
("value", "expected"),
[
([1, 2], {1, 2}),
({1, 2}, {1, 2}),
({"a": 1, "b": 2}, {"a", "b"}),
(ReadOnlyDict({"a": 1, "b": 2}), {"a", "b"}),
(MappingProxyType({"a": 1, "b": 2}), {"a", "b"}),
("abc", {"a", "b", "c"}),
(b"abc", {97, 98, 99}),
((1, 2), {1, 2}),
],
)
def test_set(hass: HomeAssistant, value: Any, expected: bool) -> None:
"""Test convert to set function."""
assert (
template.Template("{{ set(value) }}", hass).async_render({"value": value})
== expected
)
@pytest.mark.parametrize(
("value", "expected"),
[
([1, 2], (1, 2)),
({1, 2}, (1, 2)),
({"a": 1, "b": 2}, ("a", "b")),
(ReadOnlyDict({"a": 1, "b": 2}), ("a", "b")),
(MappingProxyType({"a": 1, "b": 2}), ("a", "b")),
("abc", ("a", "b", "c")),
(b"abc", (97, 98, 99)),
((1, 2), (1, 2)),
],
)
def test_tuple(hass: HomeAssistant, value: Any, expected: bool) -> None:
"""Test convert to tuple function."""
assert (
template.Template("{{ tuple(value) }}", hass).async_render({"value": value})
== expected
)
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):
template.Template("{{ tuple(value) }}", hass).async_render({"value": dt_})
with pytest.raises(TemplateError):
template.Template("{{ set(value) }}", hass).async_render({"value": dt_})
@pytest.mark.parametrize(
("value", "expected"),
[
([1, 2], False),
({1, 2}, False),
({"a": 1, "b": 2}, False),
(ReadOnlyDict({"a": 1, "b": 2}), False),
(MappingProxyType({"a": 1, "b": 2}), False),
("abc", False),
(b"abc", False),
((1, 2), False),
(datetime(2024, 1, 1, 0, 0, 0), True),
],
)
def test_is_datetime(hass: HomeAssistant, value, expected) -> None:
"""Test is datetime."""
assert (
template.Template("{{ value is datetime }}", hass).async_render(
{"value": value}
)
== expected
)
@pytest.mark.parametrize(
("value", "expected"),
[
([1, 2], False),
({1, 2}, False),
({"a": 1, "b": 2}, False),
(ReadOnlyDict({"a": 1, "b": 2}), False),
(MappingProxyType({"a": 1, "b": 2}), False),
("abc", True),
(b"abc", True),
((1, 2), False),
(datetime(2024, 1, 1, 0, 0, 0), False),
],
)
def test_is_string_like(hass: HomeAssistant, value, expected) -> None:
"""Test is string_like."""
assert (
template.Template("{{ value is string_like }}", hass).async_render(
{"value": value}
)
== expected
)
def test_rounding_value(hass: HomeAssistant) -> None:
"""Test rounding value."""
hass.states.async_set("sensor.temperature", 12.78)
assert (
template.Template(
"{{ states.sensor.temperature.state | round(1) }}", hass
).async_render()
== 12.8
)
assert (
template.Template(
"{{ states.sensor.temperature.state | multiply(10) | round }}", hass
).async_render()
== 128
)
assert (
template.Template(
'{{ states.sensor.temperature.state | round(1, "floor") }}', hass
).async_render()
== 12.7
)
assert (
template.Template(
'{{ states.sensor.temperature.state | round(1, "ceil") }}', hass
).async_render()
== 12.8
)
assert (
template.Template(
'{{ states.sensor.temperature.state | round(1, "half") }}', hass
).async_render()
== 13.0
)
def test_rounding_value_on_error(hass: HomeAssistant) -> None:
"""Test rounding value handling of error."""
# Test handling of invalid input
with pytest.raises(TemplateError):
template.Template("{{ None | round }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template('{{ "no_number" | round }}', hass).async_render()
# Test handling of default return value
assert render(hass, "{{ 'no_number' | round(default=1) }}") == 1
def test_multiply(hass: HomeAssistant) -> None:
"""Test multiply."""
tests = {10: 100}
for inp, out in tests.items():
assert (
template.Template(
f"{{{{ {inp} | multiply(10) | round }}}}", hass
).async_render()
== out
)
# Test handling of invalid input
with pytest.raises(TemplateError):
template.Template("{{ abcd | multiply(10) }}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ 'no_number' | multiply(10, 1) }}") == 1
assert render(hass, "{{ 'no_number' | multiply(10, default=1) }}") == 1
def test_add(hass: HomeAssistant) -> None:
"""Test add."""
tests = {10: 42}
for inp, out in tests.items():
assert (
template.Template(f"{{{{ {inp} | add(32) | round }}}}", hass).async_render()
== out
)
# Test handling of invalid input
with pytest.raises(TemplateError):
template.Template("{{ abcd | add(10) }}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ 'no_number' | add(10, 1) }}") == 1
assert render(hass, "{{ 'no_number' | add(10, default=1) }}") == 1
def test_logarithm(hass: HomeAssistant) -> None:
"""Test logarithm."""
tests = [
(4, 2, 2.0),
(1000, 10, 3.0),
(math.e, "", 1.0), # The "" means the default base (e) will be used
]
for value, base, expected in tests:
assert (
template.Template(
f"{{{{ {value} | log({base}) | round(1) }}}}", hass
).async_render()
== expected
)
assert (
template.Template(
f"{{{{ log({value}, {base}) | round(1) }}}}", hass
).async_render()
== expected
)
# Test handling of invalid input
with pytest.raises(TemplateError):
template.Template("{{ invalid | log(_) }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ log(invalid, _) }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ 10 | log(invalid) }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ log(10, invalid) }}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ 'no_number' | log(10, 1) }}") == 1
assert render(hass, "{{ 'no_number' | log(10, default=1) }}") == 1
assert render(hass, "{{ log('no_number', 10, 1) }}") == 1
assert render(hass, "{{ log('no_number', 10, default=1) }}") == 1
assert render(hass, "{{ log(0, 10, 1) }}") == 1
assert render(hass, "{{ log(0, 10, default=1) }}") == 1
def test_sine(hass: HomeAssistant) -> None:
"""Test sine."""
tests = [
(0, 0.0),
(math.pi / 2, 1.0),
(math.pi, 0.0),
(math.pi * 1.5, -1.0),
(math.pi / 10, 0.309),
]
for value, expected in tests:
assert (
template.Template(
f"{{{{ {value} | sin | round(3) }}}}", hass
).async_render()
== expected
)
assert render(hass, f"{{{{ sin({value}) | round(3) }}}}") == expected
# Test handling of invalid input
with pytest.raises(TemplateError):
template.Template("{{ 'duck' | sin }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ invalid | sin('duck') }}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ 'no_number' | sin(1) }}") == 1
assert render(hass, "{{ 'no_number' | sin(default=1) }}") == 1
assert render(hass, "{{ sin('no_number', 1) }}") == 1
assert render(hass, "{{ sin('no_number', default=1) }}") == 1
def test_cos(hass: HomeAssistant) -> None:
"""Test cosine."""
tests = [
(0, 1.0),
(math.pi / 2, 0.0),
(math.pi, -1.0),
(math.pi * 1.5, -0.0),
(math.pi / 10, 0.951),
]
for value, expected in tests:
assert (
template.Template(
f"{{{{ {value} | cos | round(3) }}}}", hass
).async_render()
== expected
)
assert render(hass, f"{{{{ cos({value}) | round(3) }}}}") == expected
# Test handling of invalid input
with pytest.raises(TemplateError):
template.Template("{{ 'error' | cos }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ invalid | cos('error') }}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ 'no_number' | cos(1) }}") == 1
assert render(hass, "{{ 'no_number' | cos(default=1) }}") == 1
assert render(hass, "{{ cos('no_number', 1) }}") == 1
assert render(hass, "{{ cos('no_number', default=1) }}") == 1
def test_tan(hass: HomeAssistant) -> None:
"""Test tangent."""
tests = [
(0, 0.0),
(math.pi, -0.0),
(math.pi / 180 * 45, 1.0),
(math.pi / 180 * 90, "1.633123935319537e+16"),
(math.pi / 180 * 135, -1.0),
]
for value, expected in tests:
assert (
template.Template(
f"{{{{ {value} | tan | round(3) }}}}", hass
).async_render()
== expected
)
assert render(hass, f"{{{{ tan({value}) | round(3) }}}}") == expected
# Test handling of invalid input
with pytest.raises(TemplateError):
template.Template("{{ 'error' | tan }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ invalid | tan('error') }}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ 'no_number' | tan(1) }}") == 1
assert render(hass, "{{ 'no_number' | tan(default=1) }}") == 1
assert render(hass, "{{ tan('no_number', 1) }}") == 1
assert render(hass, "{{ tan('no_number', default=1) }}") == 1
def test_sqrt(hass: HomeAssistant) -> None:
"""Test square root."""
tests = [
(0, 0.0),
(1, 1.0),
(2, 1.414),
(10, 3.162),
(100, 10.0),
]
for value, expected in tests:
assert (
template.Template(
f"{{{{ {value} | sqrt | round(3) }}}}", hass
).async_render()
== expected
)
assert render(hass, f"{{{{ sqrt({value}) | round(3) }}}}") == expected
# Test handling of invalid input
with pytest.raises(TemplateError):
template.Template("{{ 'error' | sqrt }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ invalid | sqrt('error') }}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ 'no_number' | sqrt(1) }}") == 1
assert render(hass, "{{ 'no_number' | sqrt(default=1) }}") == 1
assert render(hass, "{{ sqrt('no_number', 1) }}") == 1
assert render(hass, "{{ sqrt('no_number', default=1) }}") == 1
def test_arc_sine(hass: HomeAssistant) -> None:
"""Test arcus sine."""
tests = [
(-1.0, -1.571),
(-0.5, -0.524),
(0.0, 0.0),
(0.5, 0.524),
(1.0, 1.571),
]
for value, expected in tests:
assert (
template.Template(
f"{{{{ {value} | asin | round(3) }}}}", hass
).async_render()
== expected
)
assert render(hass, f"{{{{ asin({value}) | round(3) }}}}") == expected
# Test handling of invalid input
invalid_tests = [
-2.0, # value error
2.0, # value error
'"error"',
]
for value in invalid_tests:
with pytest.raises(TemplateError):
template.Template(
f"{{{{ {value} | asin | round(3) }}}}", hass
).async_render()
with pytest.raises(TemplateError):
assert render(hass, f"{{{{ asin({value}) | round(3) }}}}")
# Test handling of default return value
assert render(hass, "{{ 'no_number' | asin(1) }}") == 1
assert render(hass, "{{ 'no_number' | asin(default=1) }}") == 1
assert render(hass, "{{ asin('no_number', 1) }}") == 1
assert render(hass, "{{ asin('no_number', default=1) }}") == 1
def test_arc_cos(hass: HomeAssistant) -> None:
"""Test arcus cosine."""
tests = [
(-1.0, 3.142),
(-0.5, 2.094),
(0.0, 1.571),
(0.5, 1.047),
(1.0, 0.0),
]
for value, expected in tests:
assert (
template.Template(
f"{{{{ {value} | acos | round(3) }}}}", hass
).async_render()
== expected
)
assert render(hass, f"{{{{ acos({value}) | round(3) }}}}") == expected
# Test handling of invalid input
invalid_tests = [
-2.0, # value error
2.0, # value error
'"error"',
]
for value in invalid_tests:
with pytest.raises(TemplateError):
template.Template(
f"{{{{ {value} | acos | round(3) }}}}", hass
).async_render()
with pytest.raises(TemplateError):
assert render(hass, f"{{{{ acos({value}) | round(3) }}}}")
# Test handling of default return value
assert render(hass, "{{ 'no_number' | acos(1) }}") == 1
assert render(hass, "{{ 'no_number' | acos(default=1) }}") == 1
assert render(hass, "{{ acos('no_number', 1) }}") == 1
assert render(hass, "{{ acos('no_number', default=1) }}") == 1
def test_arc_tan(hass: HomeAssistant) -> None:
"""Test arcus tangent."""
tests = [
(-10.0, -1.471),
(-2.0, -1.107),
(-1.0, -0.785),
(-0.5, -0.464),
(0.0, 0.0),
(0.5, 0.464),
(1.0, 0.785),
(2.0, 1.107),
(10.0, 1.471),
]
for value, expected in tests:
assert (
template.Template(
f"{{{{ {value} | atan | round(3) }}}}", hass
).async_render()
== expected
)
assert render(hass, f"{{{{ atan({value}) | round(3) }}}}") == expected
# Test handling of invalid input
with pytest.raises(TemplateError):
template.Template("{{ 'error' | atan }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ invalid | atan('error') }}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ 'no_number' | atan(1) }}") == 1
assert render(hass, "{{ 'no_number' | atan(default=1) }}") == 1
assert render(hass, "{{ atan('no_number', 1) }}") == 1
assert render(hass, "{{ atan('no_number', default=1) }}") == 1
def test_arc_tan2(hass: HomeAssistant) -> None:
"""Test two parameter version of arcus tangent."""
tests = [
(-10.0, -10.0, -2.356),
(-10.0, 0.0, -1.571),
(-10.0, 10.0, -0.785),
(0.0, -10.0, 3.142),
(0.0, 0.0, 0.0),
(0.0, 10.0, 0.0),
(10.0, -10.0, 2.356),
(10.0, 0.0, 1.571),
(10.0, 10.0, 0.785),
(-4.0, 3.0, -0.927),
(-1.0, 2.0, -0.464),
(2.0, 1.0, 1.107),
]
for y, x, expected in tests:
assert (
template.Template(
f"{{{{ ({y}, {x}) | atan2 | round(3) }}}}", hass
).async_render()
== expected
)
assert (
template.Template(
f"{{{{ atan2({y}, {x}) | round(3) }}}}", hass
).async_render()
== expected
)
# Test handling of invalid input
with pytest.raises(TemplateError):
template.Template("{{ ('duck', 'goose') | atan2 }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ atan2('duck', 'goose') }}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ ('duck', 'goose') | atan2(1) }}") == 1
assert render(hass, "{{ ('duck', 'goose') | atan2(default=1) }}") == 1
assert render(hass, "{{ atan2('duck', 'goose', 1) }}") == 1
assert render(hass, "{{ atan2('duck', 'goose', default=1) }}") == 1
def test_strptime(hass: HomeAssistant) -> None:
"""Test the parse timestamp method."""
tests = [
("2016-10-19 15:22:05.588122 UTC", "%Y-%m-%d %H:%M:%S.%f %Z", None),
("2016-10-19 15:22:05.588122+0100", "%Y-%m-%d %H:%M:%S.%f%z", None),
("2016-10-19 15:22:05.588122", "%Y-%m-%d %H:%M:%S.%f", None),
("2016-10-19", "%Y-%m-%d", None),
("2016", "%Y", None),
("15:22:05", "%H:%M:%S", None),
]
for inp, fmt, expected in tests:
if expected is None:
expected = str(datetime.strptime(inp, fmt))
temp = f"{{{{ strptime('{inp}', '{fmt}') }}}}"
assert template.Template(temp, hass).async_render() == expected
# Test handling of invalid input
invalid_tests = [
("1469119144", "%Y"),
("invalid", "%Y"),
]
for inp, fmt in invalid_tests:
temp = f"{{{{ strptime('{inp}', '{fmt}') }}}}"
with pytest.raises(TemplateError):
template.Template(temp, hass).async_render()
# Test handling of default return value
assert render(hass, "{{ strptime('invalid', '%Y', 1) }}") == 1
assert render(hass, "{{ strptime('invalid', '%Y', default=1) }}") == 1
async def test_timestamp_custom(hass: HomeAssistant) -> None:
"""Test the timestamps to custom filter."""
await hass.config.async_set_time_zone("UTC")
now = dt_util.utcnow()
tests = [
(1469119144, None, True, "2016-07-21 16:39:04"),
(1469119144, "%Y", True, 2016),
(1469119144, "invalid", True, "invalid"),
(dt_util.as_timestamp(now), None, False, now.strftime("%Y-%m-%d %H:%M:%S")),
]
for inp, fmt, local, out in tests:
if fmt:
fil = f"timestamp_custom('{fmt}')"
elif fmt and local:
fil = f"timestamp_custom('{fmt}', {local})"
else:
fil = "timestamp_custom"
assert template.Template(f"{{{{ {inp} | {fil} }}}}", hass).async_render() == out
# Test handling of invalid input
invalid_tests = [
(None, None, None),
]
for inp, fmt, local in invalid_tests:
if fmt:
fil = f"timestamp_custom('{fmt}')"
elif fmt and local:
fil = f"timestamp_custom('{fmt}', {local})"
else:
fil = "timestamp_custom"
with pytest.raises(TemplateError):
template.Template(f"{{{{ {inp} | {fil} }}}}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ None | timestamp_custom('invalid', True, 1) }}") == 1
assert render(hass, "{{ None | timestamp_custom(default=1) }}") == 1
async def test_timestamp_local(hass: HomeAssistant) -> None:
"""Test the timestamps to local filter."""
await hass.config.async_set_time_zone("UTC")
tests = [
(1469119144, "2016-07-21T16:39:04+00:00"),
]
for inp, out in tests:
assert (
template.Template(f"{{{{ {inp} | timestamp_local }}}}", hass).async_render()
== out
)
# Test handling of invalid input
invalid_tests = [
None,
]
for inp in invalid_tests:
with pytest.raises(TemplateError):
template.Template(f"{{{{ {inp} | timestamp_local }}}}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ None | timestamp_local(1) }}") == 1
assert render(hass, "{{ None | timestamp_local(default=1) }}") == 1
@pytest.mark.parametrize(
"input",
[
"2021-06-03 13:00:00.000000+00:00",
"1986-07-09T12:00:00Z",
"2016-10-19 15:22:05.588122+0100",
"2016-10-19",
"2021-01-01 00:00:01",
"invalid",
],
)
def test_as_datetime(hass: HomeAssistant, input) -> None:
"""Test converting a timestamp string to a date object."""
expected = dt_util.parse_datetime(input)
if expected is not None:
expected = str(expected)
assert (
template.Template(f"{{{{ as_datetime('{input}') }}}}", hass).async_render()
== expected
)
assert (
template.Template(f"{{{{ '{input}' | as_datetime }}}}", hass).async_render()
== expected
)
@pytest.mark.parametrize(
("input", "output"),
[
(1469119144, "2016-07-21 16:39:04+00:00"),
(1469119144.0, "2016-07-21 16:39:04+00:00"),
(-1, "1969-12-31 23:59:59+00:00"),
],
)
def test_as_datetime_from_timestamp(
hass: HomeAssistant,
input: float,
output: str,
) -> None:
"""Test converting a UNIX timestamp to a date object."""
assert (
template.Template(f"{{{{ as_datetime({input}) }}}}", hass).async_render()
== output
)
assert (
template.Template(f"{{{{ {input} | as_datetime }}}}", hass).async_render()
== output
)
assert (
template.Template(f"{{{{ as_datetime('{input}') }}}}", hass).async_render()
== output
)
assert (
template.Template(f"{{{{ '{input}' | as_datetime }}}}", hass).async_render()
== output
)
@pytest.mark.parametrize(
("input", "output"),
[
(
"{% set dt = as_datetime('2024-01-01 16:00:00-08:00') %}",
"2024-01-01 16:00:00-08:00",
),
(
"{% set dt = as_datetime('2024-01-29').date() %}",
"2024-01-29 00:00:00",
),
],
)
def test_as_datetime_from_datetime(
hass: HomeAssistant, input: str, output: str
) -> None:
"""Test using datetime.datetime or datetime.date objects as input."""
assert (
template.Template(f"{input}{{{{ dt | as_datetime }}}}", hass).async_render()
== output
)
assert (
template.Template(f"{input}{{{{ as_datetime(dt) }}}}", hass).async_render()
== output
)
@pytest.mark.parametrize(
("input", "default", "output"),
[
(1469119144, 123, "2016-07-21 16:39:04+00:00"),
('"invalid"', ["default output"], ["default output"]),
(["a", "list"], 0, 0),
({"a": "dict"}, None, None),
],
)
def test_as_datetime_default(
hass: HomeAssistant, input: Any, default: Any, output: str
) -> None:
"""Test invalid input and return default value."""
assert (
template.Template(
f"{{{{ as_datetime({input}, default={default}) }}}}", hass
).async_render()
== output
)
assert (
template.Template(
f"{{{{ {input} | as_datetime({default}) }}}}", hass
).async_render()
== output
)
def test_as_local(hass: HomeAssistant) -> None:
"""Test converting time to local."""
hass.states.async_set("test.object", "available")
last_updated = hass.states.get("test.object").last_updated
assert template.Template(
"{{ as_local(states.test.object.last_updated) }}", hass
).async_render() == str(dt_util.as_local(last_updated))
assert template.Template(
"{{ states.test.object.last_updated | as_local }}", hass
).async_render() == str(dt_util.as_local(last_updated))
def test_to_json(hass: HomeAssistant) -> None:
"""Test the object to JSON string filter."""
# Note that we're not testing the actual json.loads and json.dumps methods,
# only the filters, so we don't need to be exhaustive with our sample JSON.
expected_result = {"Foo": "Bar"}
actual_result = template.Template(
"{{ {'Foo': 'Bar'} | to_json }}", hass
).async_render()
assert actual_result == expected_result
expected_result = orjson.dumps({"Foo": "Bar"}, option=orjson.OPT_INDENT_2).decode()
actual_result = template.Template(
"{{ {'Foo': 'Bar'} | to_json(pretty_print=True) }}", hass
).async_render(parse_result=False)
assert actual_result == expected_result
expected_result = orjson.dumps(
{"Z": 26, "A": 1, "M": 13}, option=orjson.OPT_SORT_KEYS
).decode()
actual_result = template.Template(
"{{ {'Z': 26, 'A': 1, 'M': 13} | to_json(sort_keys=True) }}", hass
).async_render(parse_result=False)
assert actual_result == expected_result
with pytest.raises(TemplateError):
template.Template("{{ {'Foo': now()} | to_json }}", hass).async_render()
# Test special case where substring class cannot be rendered
# See: https://github.com/ijl/orjson/issues/445
class MyStr(str):
__slots__ = ()
expected_result = '{"mykey1":11.0,"mykey2":"myvalue2","mykey3":["opt3b","opt3a"]}'
test_dict = {
MyStr("mykey2"): "myvalue2",
MyStr("mykey1"): 11.0,
MyStr("mykey3"): ["opt3b", "opt3a"],
}
actual_result = template.Template(
"{{ test_dict | to_json(sort_keys=True) }}", hass
).async_render(parse_result=False, variables={"test_dict": test_dict})
assert actual_result == expected_result
def test_to_json_ensure_ascii(hass: HomeAssistant) -> None:
"""Test the object to JSON string filter."""
# Note that we're not testing the actual json.loads and json.dumps methods,
# only the filters, so we don't need to be exhaustive with our sample JSON.
actual_value_ascii = template.Template(
"{{ 'Bar ҝ éèà' | to_json(ensure_ascii=True) }}", hass
).async_render()
assert actual_value_ascii == '"Bar \\u049d \\u00e9\\u00e8\\u00e0"'
actual_value = template.Template(
"{{ 'Bar ҝ éèà' | to_json(ensure_ascii=False) }}", hass
).async_render()
assert actual_value == '"Bar ҝ éèà"'
expected_result = json.dumps({"Foo": "Bar"}, indent=2)
actual_result = template.Template(
"{{ {'Foo': 'Bar'} | to_json(pretty_print=True, ensure_ascii=True) }}", hass
).async_render(parse_result=False)
assert actual_result == expected_result
expected_result = json.dumps({"Z": 26, "A": 1, "M": 13}, sort_keys=True)
actual_result = template.Template(
"{{ {'Z': 26, 'A': 1, 'M': 13} | to_json(sort_keys=True, ensure_ascii=True) }}",
hass,
).async_render(parse_result=False)
assert actual_result == expected_result
def test_from_json(hass: HomeAssistant) -> None:
"""Test the JSON string to object filter."""
# Note that we're not testing the actual json.loads and json.dumps methods,
# only the filters, so we don't need to be exhaustive with our sample JSON.
expected_result = "Bar"
actual_result = template.Template(
'{{ (\'{"Foo": "Bar"}\' | from_json).Foo }}', hass
).async_render()
assert actual_result == expected_result
def test_average(hass: HomeAssistant) -> None:
"""Test the average filter."""
assert template.Template("{{ [1, 2, 3] | average }}", hass).async_render() == 2
assert template.Template("{{ average([1, 2, 3]) }}", hass).async_render() == 2
assert template.Template("{{ average(1, 2, 3) }}", hass).async_render() == 2
# Testing of default values
assert template.Template("{{ average([1, 2, 3], -1) }}", hass).async_render() == 2
assert template.Template("{{ average([], -1) }}", hass).async_render() == -1
assert template.Template("{{ average([], default=-1) }}", hass).async_render() == -1
assert (
template.Template("{{ average([], 5, default=-1) }}", hass).async_render() == -1
)
assert (
template.Template("{{ average(1, 'a', 3, default=-1) }}", hass).async_render()
== -1
)
with pytest.raises(TemplateError):
template.Template("{{ 1 | average }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ average() }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ average([]) }}", hass).async_render()
def test_median(hass: HomeAssistant) -> None:
"""Test the median filter."""
assert template.Template("{{ [1, 3, 2] | median }}", hass).async_render() == 2
assert template.Template("{{ median([1, 3, 2, 4]) }}", hass).async_render() == 2.5
assert template.Template("{{ median(1, 3, 2) }}", hass).async_render() == 2
assert template.Template("{{ median('cdeba') }}", hass).async_render() == "c"
# Testing of default values
assert template.Template("{{ median([1, 2, 3], -1) }}", hass).async_render() == 2
assert template.Template("{{ median([], -1) }}", hass).async_render() == -1
assert template.Template("{{ median([], default=-1) }}", hass).async_render() == -1
assert template.Template("{{ median('abcd', -1) }}", hass).async_render() == -1
assert (
template.Template("{{ median([], 5, default=-1) }}", hass).async_render() == -1
)
assert (
template.Template("{{ median(1, 'a', 3, default=-1) }}", hass).async_render()
== -1
)
with pytest.raises(TemplateError):
template.Template("{{ 1 | median }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ median() }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ median([]) }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ median('abcd') }}", hass).async_render()
def test_statistical_mode(hass: HomeAssistant) -> None:
"""Test the mode filter."""
assert (
template.Template("{{ [1, 2, 2, 3] | statistical_mode }}", hass).async_render()
== 2
)
assert (
template.Template("{{ statistical_mode([1, 2, 3]) }}", hass).async_render() == 1
)
assert (
template.Template(
"{{ statistical_mode('hello', 'bye', 'hello') }}", hass
).async_render()
== "hello"
)
assert (
template.Template("{{ statistical_mode('banana') }}", hass).async_render()
== "a"
)
# Testing of default values
assert (
template.Template("{{ statistical_mode([1, 2, 3], -1) }}", hass).async_render()
== 1
)
assert (
template.Template("{{ statistical_mode([], -1) }}", hass).async_render() == -1
)
assert (
template.Template("{{ statistical_mode([], default=-1) }}", hass).async_render()
== -1
)
assert (
template.Template(
"{{ statistical_mode([], 5, default=-1) }}", hass
).async_render()
== -1
)
with pytest.raises(TemplateError):
template.Template("{{ 1 | statistical_mode }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ statistical_mode() }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ statistical_mode([]) }}", hass).async_render()
def test_min(hass: HomeAssistant) -> None:
"""Test the min filter."""
assert template.Template("{{ [1, 2, 3] | min }}", hass).async_render() == 1
assert template.Template("{{ min([1, 2, 3]) }}", hass).async_render() == 1
assert template.Template("{{ min(1, 2, 3) }}", hass).async_render() == 1
with pytest.raises(TemplateError):
template.Template("{{ 1 | min }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ min() }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ min(1) }}", hass).async_render()
def test_max(hass: HomeAssistant) -> None:
"""Test the max filter."""
assert template.Template("{{ [1, 2, 3] | max }}", hass).async_render() == 3
assert template.Template("{{ max([1, 2, 3]) }}", hass).async_render() == 3
assert template.Template("{{ max(1, 2, 3) }}", hass).async_render() == 3
with pytest.raises(TemplateError):
template.Template("{{ 1 | max }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ max() }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ max(1) }}", hass).async_render()
@pytest.mark.parametrize(
"attribute",
[
"a",
"b",
"c",
],
)
def test_min_max_attribute(hass: HomeAssistant, attribute) -> None:
"""Test the min and max filters with attribute."""
hass.states.async_set(
"test.object",
"test",
{
"objects": [
{
"a": 1,
"b": 2,
"c": 3,
},
{
"a": 2,
"b": 1,
"c": 2,
},
{
"a": 3,
"b": 3,
"c": 1,
},
],
},
)
assert (
template.Template(
f"{{{{ (state_attr('test.object', 'objects') | min(attribute='{attribute}'))['{attribute}']}}}}",
hass,
).async_render()
== 1
)
assert (
template.Template(
f"{{{{ (min(state_attr('test.object', 'objects'), attribute='{attribute}'))['{attribute}']}}}}",
hass,
).async_render()
== 1
)
assert (
template.Template(
f"{{{{ (state_attr('test.object', 'objects') | max(attribute='{attribute}'))['{attribute}']}}}}",
hass,
).async_render()
== 3
)
assert (
template.Template(
f"{{{{ (max(state_attr('test.object', 'objects'), attribute='{attribute}'))['{attribute}']}}}}",
hass,
).async_render()
== 3
)
def test_ord(hass: HomeAssistant) -> None:
"""Test the ord filter."""
assert template.Template('{{ "d" | ord }}', hass).async_render() == 100
def test_base64_encode(hass: HomeAssistant) -> None:
"""Test the base64_encode filter."""
assert (
template.Template('{{ "homeassistant" | base64_encode }}', hass).async_render()
== "aG9tZWFzc2lzdGFudA=="
)
def test_base64_decode(hass: HomeAssistant) -> None:
"""Test the base64_decode filter."""
assert (
template.Template(
'{{ "aG9tZWFzc2lzdGFudA==" | base64_decode }}', hass
).async_render()
== "homeassistant"
)
assert (
template.Template(
'{{ "aG9tZWFzc2lzdGFudA==" | base64_decode(None) }}', hass
).async_render()
== b"homeassistant"
)
assert (
template.Template(
'{{ "aG9tZWFzc2lzdGFudA==" | base64_decode("ascii") }}', hass
).async_render()
== "homeassistant"
)
def test_slugify(hass: HomeAssistant) -> None:
"""Test the slugify filter."""
assert (
template.Template('{{ slugify("Home Assistant") }}', hass).async_render()
== "home_assistant"
)
assert (
template.Template('{{ "Home Assistant" | slugify }}', hass).async_render()
== "home_assistant"
)
assert (
template.Template('{{ slugify("Home Assistant", "-") }}', hass).async_render()
== "home-assistant"
)
assert (
template.Template('{{ "Home Assistant" | slugify("-") }}', hass).async_render()
== "home-assistant"
)
def test_ordinal(hass: HomeAssistant) -> None:
"""Test the ordinal filter."""
tests = [
(1, "1st"),
(2, "2nd"),
(3, "3rd"),
(4, "4th"),
(5, "5th"),
(12, "12th"),
(100, "100th"),
(101, "101st"),
]
for value, expected in tests:
assert (
template.Template(f"{{{{ {value} | ordinal }}}}", hass).async_render()
== expected
)
def test_timestamp_utc(hass: HomeAssistant) -> None:
"""Test the timestamps to local filter."""
now = dt_util.utcnow()
tests = [
(1469119144, "2016-07-21T16:39:04+00:00"),
(dt_util.as_timestamp(now), now.isoformat()),
]
for inp, out in tests:
assert (
template.Template(f"{{{{ {inp} | timestamp_utc }}}}", hass).async_render()
== out
)
# Test handling of invalid input
invalid_tests = [
None,
]
for inp in invalid_tests:
with pytest.raises(TemplateError):
template.Template(f"{{{{ {inp} | timestamp_utc }}}}", hass).async_render()
# Test handling of default return value
assert render(hass, "{{ None | timestamp_utc(1) }}") == 1
assert render(hass, "{{ None | timestamp_utc(default=1) }}") == 1
def test_as_timestamp(hass: HomeAssistant) -> None:
"""Test the as_timestamp function."""
with pytest.raises(TemplateError):
template.Template('{{ as_timestamp("invalid") }}', hass).async_render()
hass.states.async_set("test.object", None)
with pytest.raises(TemplateError):
template.Template("{{ as_timestamp(states.test.object) }}", hass).async_render()
tpl = (
'{{ as_timestamp(strptime("2024-02-03T09:10:24+0000", '
'"%Y-%m-%dT%H:%M:%S%z")) }}'
)
assert template.Template(tpl, hass).async_render() == 1706951424.0
# Test handling of default return value
assert render(hass, "{{ 'invalid' | as_timestamp(1) }}") == 1
assert render(hass, "{{ 'invalid' | as_timestamp(default=1) }}") == 1
assert render(hass, "{{ as_timestamp('invalid', 1) }}") == 1
assert render(hass, "{{ as_timestamp('invalid', default=1) }}") == 1
@patch.object(random, "choice")
def test_random_every_time(test_choice, hass: HomeAssistant) -> None:
"""Ensure the random filter runs every time, not just once."""
tpl = template.Template("{{ [1,2] | random }}", hass)
test_choice.return_value = "foo"
assert tpl.async_render() == "foo"
test_choice.return_value = "bar"
assert tpl.async_render() == "bar"
def test_passing_vars_as_keywords(hass: HomeAssistant) -> None:
"""Test passing variables as keywords."""
assert template.Template("{{ hello }}", hass).async_render(hello=127) == 127
def test_passing_vars_as_vars(hass: HomeAssistant) -> None:
"""Test passing variables as variables."""
assert template.Template("{{ hello }}", hass).async_render({"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."""
assert (
template.render_complex(
template.Template("{{ hello[1] }}", hass), {"hello": ["foo", "bar"]}
)
== "bar"
)
def test_passing_vars_as_dict_element(hass: HomeAssistant) -> None:
"""Test passing variables as list."""
assert (
template.render_complex(
template.Template("{{ hello.foo }}", hass), {"hello": {"foo": "bar"}}
)
== "bar"
)
def test_passing_vars_as_dict(hass: HomeAssistant) -> None:
"""Test passing variables as list."""
assert template.render_complex(
template.Template("{{ hello }}", hass), {"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_valid_with_is_defined(
hass: HomeAssistant,
) -> None:
"""Render with possible JSON value with known JSON object."""
tpl = template.Template("{{ value_json.hello|is_defined }}", hass)
assert tpl.async_render_with_possible_json_value('{"hello": "world"}') == "world"
def test_render_with_possible_json_value_undefined_json(hass: HomeAssistant) -> None:
"""Render with possible JSON value with unknown JSON object."""
tpl = template.Template("{{ value_json.bye|is_defined }}", hass)
assert (
tpl.async_render_with_possible_json_value('{"hello": "world"}')
== '{"hello": "world"}'
)
def test_render_with_possible_json_value_undefined_json_error_value(
hass: HomeAssistant,
) -> None:
"""Render with possible JSON value with unknown JSON object."""
tpl = template.Template("{{ value_json.bye|is_defined }}", 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")
tpl = template.Template(
"{% if states.test.object %}exists{% else %}not exists{% endif %}", hass
)
assert tpl.async_render() == "exists"
def test_is_hidden_entity(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
) -> None:
"""Test is_hidden_entity method."""
hidden_entity = entity_registry.async_get_or_create(
"sensor", "mock", "hidden", hidden_by=er.RegistryEntryHider.USER
)
visible_entity = entity_registry.async_get_or_create("sensor", "mock", "visible")
assert template.Template(
f"{{{{ is_hidden_entity('{hidden_entity.entity_id}') }}}}",
hass,
).async_render()
assert not template.Template(
f"{{{{ is_hidden_entity('{visible_entity.entity_id}') }}}}",
hass,
).async_render()
assert not template.Template(
f"{{{{ ['{visible_entity.entity_id}'] | select('is_hidden_entity') | first }}}}",
hass,
).async_render()
def test_is_state(hass: HomeAssistant) -> None:
"""Test is_state method."""
hass.states.async_set("test.object", "available")
tpl = template.Template(
"""
{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}
""",
hass,
)
assert tpl.async_render() == "yes"
tpl = template.Template(
"""
{{ is_state("test.noobject", "available") }}
""",
hass,
)
assert tpl.async_render() is False
tpl = template.Template(
"""
{% if "test.object" is is_state("available") %}yes{% else %}no{% endif %}
""",
hass,
)
assert tpl.async_render() == "yes"
tpl = template.Template(
"""
{{ ['test.object'] | select("is_state", "available") | first | default }}
""",
hass,
)
assert tpl.async_render() == "test.object"
tpl = template.Template(
"""
{{ is_state("test.object", ["on", "off", "available"]) }}
""",
hass,
)
assert tpl.async_render() is True
def test_is_state_attr(hass: HomeAssistant) -> None:
"""Test is_state_attr method."""
hass.states.async_set("test.object", "available", {"mode": "on"})
tpl = template.Template(
"""
{% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %}
""",
hass,
)
assert tpl.async_render() == "yes"
tpl = template.Template(
"""
{{ is_state_attr("test.noobject", "mode", "on") }}
""",
hass,
)
assert tpl.async_render() is False
tpl = template.Template(
"""
{% if "test.object" is is_state_attr("mode", "on") %}yes{% else %}no{% endif %}
""",
hass,
)
assert tpl.async_render() == "yes"
tpl = template.Template(
"""
{{ ['test.object'] | select("is_state_attr", "mode", "on") | first | default }}
""",
hass,
)
assert tpl.async_render() == "test.object"
def test_state_attr(hass: HomeAssistant) -> None:
"""Test state_attr method."""
hass.states.async_set(
"test.object", "available", {"effect": "action", "mode": "on"}
)
tpl = template.Template(
"""
{% if state_attr("test.object", "mode") == "on" %}yes{% else %}no{% endif %}
""",
hass,
)
assert tpl.async_render() == "yes"
tpl = template.Template(
"""
{{ state_attr("test.noobject", "mode") == None }}
""",
hass,
)
assert tpl.async_render() is True
tpl = template.Template(
"""
{% if "test.object" | state_attr("mode") == "on" %}yes{% else %}no{% endif %}
""",
hass,
)
assert tpl.async_render() == "yes"
tpl = template.Template(
"""
{{ ['test.object'] | map("state_attr", "effect") | first | default }}
""",
hass,
)
assert tpl.async_render() == "action"
def test_states_function(hass: HomeAssistant) -> None:
"""Test using states as a function."""
hass.states.async_set("test.object", "available")
tpl = template.Template('{{ states("test.object") }}', hass)
assert tpl.async_render() == "available"
tpl2 = template.Template('{{ states("test.object2") }}', hass)
assert tpl2.async_render() == "unknown"
tpl = template.Template(
"""
{% if "test.object" | states == "available" %}yes{% else %}no{% endif %}
""",
hass,
)
assert tpl.async_render() == "yes"
tpl = template.Template(
"""
{{ ['test.object'] | map("states") | first | default }}
""",
hass,
)
assert tpl.async_render() == "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")
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={})
tpl = template.Template(
'{{ state_translated("switch.without_translations") }}', hass
)
assert tpl.async_render() == "on"
tp2 = template.Template(
'{{ state_translated("binary_sensor.without_device_class") }}', hass
)
assert tp2.async_render() == "On"
tpl3 = template.Template(
'{{ state_translated("binary_sensor.with_device_class") }}', hass
)
assert tpl3.async_render() == "Detected"
tpl4 = template.Template(
'{{ state_translated("binary_sensor.with_unknown_device_class") }}', hass
)
assert tpl4.async_render() == "On"
with pytest.raises(TemplateError):
template.Template(
'{{ state_translated("contextfunction") }}', hass
).async_render()
tpl6 = template.Template('{{ state_translated("switch.invalid") }}', hass)
assert tpl6.async_render() == "unknown"
with pytest.raises(TemplateError):
template.Template('{{ state_translated("-invalid") }}', hass).async_render()
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,
):
tpl8 = template.Template('{{ state_translated("light.hue_5678") }}', hass)
assert tpl8.async_render() == "state_is_on"
tpl11 = template.Template('{{ state_translated("domain.is_unavailable") }}', hass)
assert tpl11.async_render() == "unavailable"
tpl12 = template.Template('{{ state_translated("domain.is_unknown") }}', hass)
assert tpl12.async_render() == "unknown"
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)
tpl = template.Template(
"""
{{ has_value("test.value1") }}
""",
hass,
)
assert tpl.async_render() is True
tpl = template.Template(
"""
{{ has_value("test.unavailable") }}
""",
hass,
)
assert tpl.async_render() is False
tpl = template.Template(
"""
{{ has_value("test.unknown") }}
""",
hass,
)
assert tpl.async_render() is False
tpl = template.Template(
"""
{% if "test.value1" is has_value %}yes{% else %}no{% endif %}
""",
hass,
)
assert tpl.async_render() == "yes"
@patch(
"homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
return_value=True,
)
def test_now(mock_is_safe, hass: HomeAssistant) -> None:
"""Test now method."""
now = dt_util.now()
with freeze_time(now):
info = template.Template("{{ now().isoformat() }}", hass).async_render_to_info()
assert now.isoformat() == info.result()
assert info.has_time is True
@patch(
"homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
return_value=True,
)
def test_utcnow(mock_is_safe, hass: HomeAssistant) -> None:
"""Test now method."""
utcnow = dt_util.utcnow()
with freeze_time(utcnow):
info = template.Template(
"{{ utcnow().isoformat() }}", hass
).async_render_to_info()
assert utcnow.isoformat() == info.result()
assert info.has_time is True
@pytest.mark.parametrize(
("now", "expected", "expected_midnight", "timezone_str"),
[
# Host clock in UTC
(
"2021-11-24 03:00:00+00:00",
"2021-11-23T10:00:00-08:00",
"2021-11-23T00:00:00-08:00",
"America/Los_Angeles",
),
# Host clock in local time
(
"2021-11-23 19:00:00-08:00",
"2021-11-23T10:00:00-08:00",
"2021-11-23T00:00:00-08:00",
"America/Los_Angeles",
),
],
)
@patch(
"homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
return_value=True,
)
async def test_today_at(
mock_is_safe, hass: HomeAssistant, now, expected, expected_midnight, timezone_str
) -> None:
"""Test today_at method."""
freezer = freeze_time(now)
freezer.start()
await hass.config.async_set_time_zone(timezone_str)
result = template.Template(
"{{ today_at('10:00').isoformat() }}",
hass,
).async_render()
assert result == expected
result = template.Template(
"{{ today_at('10:00:00').isoformat() }}",
hass,
).async_render()
assert result == expected
result = template.Template(
"{{ ('10:00:00' | today_at).isoformat() }}",
hass,
).async_render()
assert result == expected
result = template.Template(
"{{ today_at().isoformat() }}",
hass,
).async_render()
assert result == expected_midnight
with pytest.raises(TemplateError):
template.Template("{{ today_at('bad') }}", hass).async_render()
info = template.Template(
"{{ today_at('10:00').isoformat() }}", hass
).async_render_to_info()
assert info.has_time is True
freezer.stop()
@patch(
"homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
return_value=True,
)
async def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None:
"""Test relative_time method."""
await hass.config.async_set_time_zone("UTC")
now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
relative_time_template = (
'{{relative_time(strptime("2000-01-01 09:00:00", "%Y-%m-%d %H:%M:%S"))}}'
)
with freeze_time(now):
result = template.Template(
relative_time_template,
hass,
).async_render()
assert result == "1 hour"
result = template.Template(
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 09:00:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
hass,
).async_render()
assert result == "2 hours"
result = template.Template(
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 03:00:00 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
hass,
).async_render()
assert result == "1 hour"
result1 = str(
template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = template.Template(
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 11:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
hass,
).async_render()
assert result1 == result2
result = template.Template(
'{{relative_time("string")}}',
hass,
).async_render()
assert result == "string"
# Test behavior when current time is same as the input time
result = template.Template(
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 10:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
hass,
).async_render()
assert result == "0 seconds"
# Test behavior when the input time is in the future
result = template.Template(
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 11:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
hass,
).async_render()
assert result == "2000-01-01 11:00:00+00:00"
info = template.Template(relative_time_template, hass).async_render_to_info()
assert info.has_time is True
@patch(
"homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
return_value=True,
)
async def test_time_since(mock_is_safe, hass: HomeAssistant) -> None:
"""Test time_since method."""
await hass.config.async_set_time_zone("UTC")
now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
time_since_template = (
'{{time_since(strptime("2000-01-01 09:00:00", "%Y-%m-%d %H:%M:%S"))}}'
)
with freeze_time(now):
result = template.Template(
time_since_template,
hass,
).async_render()
assert result == "1 hour"
result = template.Template(
(
"{{"
" time_since("
" strptime("
' "2000-01-01 09:00:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
hass,
).async_render()
assert result == "2 hours"
result = template.Template(
(
"{{"
" time_since("
" strptime("
' "2000-01-01 03:00:00 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
hass,
).async_render()
assert result == "1 hour"
result1 = str(
template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = template.Template(
(
"{{"
" time_since("
" strptime("
' "2000-01-01 11:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 2"
" )"
"}}"
),
hass,
).async_render()
assert result1 == result2
result = template.Template(
(
"{{"
" time_since("
" strptime("
' "2000-01-01 09:05:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision=2"
" )"
"}}"
),
hass,
).async_render()
assert result == "1 hour 55 minutes"
result = template.Template(
(
"{{"
" time_since("
" strptime("
' "2000-01-01 02:05:27 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 3"
" )"
"}}"
),
hass,
).async_render()
assert result == "1 hour 54 minutes 33 seconds"
result = template.Template(
(
"{{"
" time_since("
" strptime("
' "2000-01-01 02:05:27 -06:00",'
' "%Y-%m-%d %H:%M:%S %z")'
" )"
"}}"
),
hass,
).async_render()
assert result == "2 hours"
result = template.Template(
(
"{{"
" time_since("
" strptime("
' "1999-02-01 02:05:27 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 0"
" )"
"}}"
),
hass,
).async_render()
assert result == "11 months 4 days 1 hour 54 minutes 33 seconds"
result = template.Template(
(
"{{"
" time_since("
" strptime("
' "1999-02-01 02:05:27 -06:00",'
' "%Y-%m-%d %H:%M:%S %z")'
" )"
"}}"
),
hass,
).async_render()
assert result == "11 months"
result1 = str(
template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = template.Template(
(
"{{"
" time_since("
" strptime("
' "2000-01-01 11:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision=3"
" )"
"}}"
),
hass,
).async_render()
assert result1 == result2
result = template.Template(
'{{time_since("string")}}',
hass,
).async_render()
assert result == "string"
info = template.Template(time_since_template, hass).async_render_to_info()
assert info.has_time is True
@patch(
"homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
return_value=True,
)
async def test_time_until(mock_is_safe, hass: HomeAssistant) -> None:
"""Test time_until method."""
await hass.config.async_set_time_zone("UTC")
now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
time_until_template = (
'{{time_until(strptime("2000-01-01 11:00:00", "%Y-%m-%d %H:%M:%S"))}}'
)
with freeze_time(now):
result = template.Template(
time_until_template,
hass,
).async_render()
assert result == "1 hour"
result = template.Template(
(
"{{"
" time_until("
" strptime("
' "2000-01-01 13:00:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
hass,
).async_render()
assert result == "2 hours"
result = template.Template(
(
"{{"
" time_until("
" strptime("
' "2000-01-01 05:00:00 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
hass,
).async_render()
assert result == "1 hour"
result1 = str(
template.strptime("2000-01-01 09:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = template.Template(
(
"{{"
" time_until("
" strptime("
' "2000-01-01 09:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 2"
" )"
"}}"
),
hass,
).async_render()
assert result1 == result2
result = template.Template(
(
"{{"
" time_until("
" strptime("
' "2000-01-01 12:05:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision=2"
" )"
"}}"
),
hass,
).async_render()
assert result == "1 hour 5 minutes"
result = template.Template(
(
"{{"
" time_until("
" strptime("
' "2000-01-01 05:54:33 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 3"
" )"
"}}"
),
hass,
).async_render()
assert result == "1 hour 54 minutes 33 seconds"
result = template.Template(
(
"{{"
" time_until("
" strptime("
' "2000-01-01 05:54:33 -06:00",'
' "%Y-%m-%d %H:%M:%S %z")'
" )"
"}}"
),
hass,
).async_render()
assert result == "2 hours"
result = template.Template(
(
"{{"
" time_until("
" strptime("
' "2001-02-01 05:54:33 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 0"
" )"
"}}"
),
hass,
).async_render()
assert result == "1 year 1 month 2 days 1 hour 54 minutes 33 seconds"
result = template.Template(
(
"{{"
" time_until("
" strptime("
' "2001-02-01 05:54:33 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 4"
" )"
"}}"
),
hass,
).async_render()
assert result == "1 year 1 month 2 days 2 hours"
result1 = str(
template.strptime("2000-01-01 09:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = template.Template(
(
"{{"
" time_until("
" strptime("
' "2000-01-01 09:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision=3"
" )"
"}}"
),
hass,
).async_render()
assert result1 == result2
result = template.Template(
'{{time_until("string")}}',
hass,
).async_render()
assert result == "string"
info = template.Template(time_until_template, hass).async_render_to_info()
assert info.has_time is True
@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 = template.Template(
"{{timedelta(seconds=120)}}",
hass,
).async_render()
assert result == "0:02:00"
result = template.Template(
"{{timedelta(seconds=86400)}}",
hass,
).async_render()
assert result == "1 day, 0:00:00"
result = template.Template(
"{{timedelta(days=1, hours=4)}}", hass
).async_render()
assert result == "1 day, 4:00:00"
result = template.Template(
"{{relative_time(now() - timedelta(seconds=3600))}}",
hass,
).async_render()
assert result == "1 hour"
result = template.Template(
"{{relative_time(now() - timedelta(seconds=86400))}}",
hass,
).async_render()
assert result == "1 day"
result = template.Template(
"{{relative_time(now() - timedelta(seconds=86401))}}",
hass,
).async_render()
assert result == "1 day"
result = template.Template(
"{{relative_time(now() - timedelta(weeks=2, days=1))}}",
hass,
).async_render()
assert result == "15 days"
def test_version(hass: HomeAssistant) -> None:
"""Test version filter and function."""
filter_result = template.Template(
"{{ '2099.9.9' | version}}",
hass,
).async_render()
function_result = template.Template(
"{{ version('2099.9.9')}}",
hass,
).async_render()
assert filter_result == function_result == "2099.9.9"
filter_result = template.Template(
"{{ '2099.9.9' | version < '2099.9.10' }}",
hass,
).async_render()
function_result = template.Template(
"{{ version('2099.9.9') < '2099.9.10' }}",
hass,
).async_render()
assert filter_result is function_result is True
filter_result = template.Template(
"{{ '2099.9.9' | version == '2099.9.9' }}",
hass,
).async_render()
function_result = template.Template(
"{{ version('2099.9.9') == '2099.9.9' }}",
hass,
).async_render()
assert filter_result is function_result is True
with pytest.raises(TemplateError):
template.Template(
"{{ version(None) < '2099.9.10' }}",
hass,
).async_render()
def test_regex_match(hass: HomeAssistant) -> None:
"""Test regex_match method."""
tpl = template.Template(
r"""
{{ '123-456-7890' | regex_match('(\\d{3})-(\\d{3})-(\\d{4})') }}
""",
hass,
)
assert tpl.async_render() is True
tpl = template.Template(
"""
{{ 'Home Assistant test' | regex_match('home', True) }}
""",
hass,
)
assert tpl.async_render() is True
tpl = template.Template(
"""
{{ 'Another Home Assistant test' | regex_match('Home') }}
""",
hass,
)
assert tpl.async_render() is False
tpl = template.Template(
"""
{{ ['Home Assistant test'] | regex_match('.*Assist') }}
""",
hass,
)
assert tpl.async_render() is True
def test_match_test(hass: HomeAssistant) -> None:
"""Test match test."""
tpl = template.Template(
r"""
{{ '123-456-7890' is match('(\\d{3})-(\\d{3})-(\\d{4})') }}
""",
hass,
)
assert tpl.async_render() is True
def test_regex_search(hass: HomeAssistant) -> None:
"""Test regex_search method."""
tpl = template.Template(
r"""
{{ '123-456-7890' | regex_search('(\\d{3})-(\\d{3})-(\\d{4})') }}
""",
hass,
)
assert tpl.async_render() is True
tpl = template.Template(
"""
{{ 'Home Assistant test' | regex_search('home', True) }}
""",
hass,
)
assert tpl.async_render() is True
tpl = template.Template(
"""
{{ 'Another Home Assistant test' | regex_search('Home') }}
""",
hass,
)
assert tpl.async_render() is True
tpl = template.Template(
"""
{{ ['Home Assistant test'] | regex_search('Assist') }}
""",
hass,
)
assert tpl.async_render() is True
def test_search_test(hass: HomeAssistant) -> None:
"""Test search test."""
tpl = template.Template(
r"""
{{ '123-456-7890' is search('(\\d{3})-(\\d{3})-(\\d{4})') }}
""",
hass,
)
assert tpl.async_render() is True
def test_regex_replace(hass: HomeAssistant) -> None:
"""Test regex_replace method."""
tpl = template.Template(
r"""
{{ 'Hello World' | regex_replace('(Hello\\s)',) }}
""",
hass,
)
assert tpl.async_render() == "World"
tpl = template.Template(
"""
{{ ['Home hinderant test'] | regex_replace('hinder', 'Assist') }}
""",
hass,
)
assert tpl.async_render() == ["Home Assistant test"]
def test_regex_findall(hass: HomeAssistant) -> None:
"""Test regex_findall method."""
tpl = template.Template(
"""
{{ 'Flight from JFK to LHR' | regex_findall('([A-Z]{3})') }}
""",
hass,
)
assert tpl.async_render() == ["JFK", "LHR"]
def test_regex_findall_index(hass: HomeAssistant) -> None:
"""Test regex_findall_index method."""
tpl = template.Template(
"""
{{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 0) }}
""",
hass,
)
assert tpl.async_render() == "JFK"
tpl = template.Template(
"""
{{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 1) }}
""",
hass,
)
assert tpl.async_render() == "LHR"
tpl = template.Template(
"""
{{ ['JFK', 'LHR'] | regex_findall_index('([A-Z]{3})', 1) }}
""",
hass,
)
assert tpl.async_render() == "LHR"
def test_bitwise_and(hass: HomeAssistant) -> None:
"""Test bitwise_and method."""
tpl = template.Template(
"""
{{ 8 | bitwise_and(8) }}
""",
hass,
)
assert tpl.async_render() == 8 & 8
tpl = template.Template(
"""
{{ 10 | bitwise_and(2) }}
""",
hass,
)
assert tpl.async_render() == 10 & 2
tpl = template.Template(
"""
{{ 8 | bitwise_and(2) }}
""",
hass,
)
assert tpl.async_render() == 8 & 2
def test_bitwise_or(hass: HomeAssistant) -> None:
"""Test bitwise_or method."""
tpl = template.Template(
"""
{{ 8 | bitwise_or(8) }}
""",
hass,
)
assert tpl.async_render() == 8 | 8
tpl = template.Template(
"""
{{ 10 | bitwise_or(2) }}
""",
hass,
)
assert tpl.async_render() == 10 | 2
tpl = template.Template(
"""
{{ 8 | bitwise_or(2) }}
""",
hass,
)
assert tpl.async_render() == 8 | 2
@pytest.mark.parametrize(
("value", "xor_value", "expected"),
[(8, 8, 0), (10, 2, 8), (0x8000, 0xFAFA, 31482), (True, False, 1), (True, True, 0)],
)
def test_bitwise_xor(
hass: HomeAssistant, value: Any, xor_value: Any, expected: int
) -> None:
"""Test bitwise_xor method."""
assert (
template.Template("{{ value | bitwise_xor(xor_value) }}", hass).async_render(
{"value": value, "xor_value": xor_value}
)
== expected
)
def test_pack(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None:
"""Test struct pack method."""
# render as filter
tpl = template.Template(
"""
{{ value | pack('>I') }}
""",
hass,
)
variables = {
"value": 0xDEADBEEF,
}
assert tpl.async_render(variables=variables) == b"\xde\xad\xbe\xef"
# render as function
tpl = template.Template(
"""
{{ pack(value, '>I') }}
""",
hass,
)
variables = {
"value": 0xDEADBEEF,
}
assert tpl.async_render(variables=variables) == b"\xde\xad\xbe\xef"
# test with None value
tpl = template.Template(
"""
{{ pack(value, '>I') }}
""",
hass,
)
variables = {
"value": None,
}
# "Template warning: 'pack' unable to pack object with type '%s' and format_string '%s' see https://docs.python.org/3/library/struct.html for more information"
assert tpl.async_render(variables=variables) is None
assert (
"Template warning: 'pack' unable to pack object 'None' with type 'NoneType' and"
" format_string '>I' see https://docs.python.org/3/library/struct.html for more"
" information" in caplog.text
)
# test with invalid filter
tpl = template.Template(
"""
{{ pack(value, 'invalid filter') }}
""",
hass,
)
variables = {
"value": 0xDEADBEEF,
}
# "Template warning: 'pack' unable to pack object with type '%s' and format_string '%s' see https://docs.python.org/3/library/struct.html for more information"
assert tpl.async_render(variables=variables) is None
assert (
"Template warning: 'pack' unable to pack object '3735928559' with type 'int'"
" and format_string 'invalid filter' see"
" https://docs.python.org/3/library/struct.html for more information"
in caplog.text
)
def test_unpack(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None:
"""Test struct unpack method."""
# render as filter
tpl = template.Template(
"""
{{ value | unpack('>I') }}
""",
hass,
)
variables = {
"value": b"\xde\xad\xbe\xef",
}
assert tpl.async_render(variables=variables) == 0xDEADBEEF
# render as function
tpl = template.Template(
"""
{{ unpack(value, '>I') }}
""",
hass,
)
variables = {
"value": b"\xde\xad\xbe\xef",
}
assert tpl.async_render(variables=variables) == 0xDEADBEEF
# unpack with offset
tpl = template.Template(
"""
{{ unpack(value, '>H', offset=2) }}
""",
hass,
)
variables = {
"value": b"\xde\xad\xbe\xef",
}
assert tpl.async_render(variables=variables) == 0xBEEF
# test with an empty bytes object
tpl = template.Template(
"""
{{ unpack(value, '>I') }}
""",
hass,
)
variables = {
"value": b"",
}
assert tpl.async_render(variables=variables) is None
assert (
"Template warning: 'unpack' unable to unpack object 'b''' with format_string"
" '>I' and offset 0 see https://docs.python.org/3/library/struct.html for more"
" information" in caplog.text
)
# test with invalid filter
tpl = template.Template(
"""
{{ unpack(value, 'invalid filter') }}
""",
hass,
)
variables = {
"value": b"",
}
assert tpl.async_render(variables=variables) is None
assert (
"Template warning: 'unpack' unable to unpack object 'b''' with format_string"
" 'invalid filter' and offset 0 see"
" https://docs.python.org/3/library/struct.html for more information"
in caplog.text
)
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}
)
tpl = template.Template("{{ distance(states.test.object) | round }}", hass)
assert tpl.async_render() == 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},
)
tpl = template.Template(
"{{ distance(states.test.object, states.test.object_2) | round }}", hass
)
assert tpl.async_render() == 187
def test_distance_function_with_1_coord(hass: HomeAssistant) -> None:
"""Test distance function with 1 coord."""
_set_up_units(hass)
tpl = template.Template('{{ distance("32.87336", "-117.22943") | round }}', hass)
assert tpl.async_render() == 187
def test_distance_function_with_2_coords(hass: HomeAssistant) -> None:
"""Test distance function with 2 coords."""
_set_up_units(hass)
assert (
template.Template(
f'{{{{ distance("32.87336", "-117.22943", {hass.config.latitude}, {hass.config.longitude}) | round }}}}',
hass,
).async_render()
== 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},
)
tpl = template.Template(
'{{ distance("32.87336", "-117.22943", states.test.object_2) | round }}',
hass,
)
assert tpl.async_render() == 187
tpl2 = template.Template(
'{{ distance(states.test.object_2, "32.87336", "-117.22943") | round }}',
hass,
)
assert tpl2.async_render() == 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})
tpl = template.Template("{{ distance(states.test.object_2) | round }}", hass)
with pytest.raises(TemplateError):
tpl.async_render()
def test_distance_function_return_none_if_invalid_coord(hass: HomeAssistant) -> None:
"""Test distance function return None if invalid coord."""
assert (
template.Template('{{ distance("123", "abc") }}', hass).async_render() is None
)
assert template.Template('{{ distance("123") }}', hass).async_render() is None
hass.states.async_set(
"test.object_2",
"happy",
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
)
tpl = template.Template('{{ distance("123", states.test_object_2) }}', hass)
assert tpl.async_render() 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},
)
tpl = template.Template(
'{{ distance("test.object", "test.object_2") | round }}', hass
)
assert tpl.async_render() == 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},
)
tpl = template.Template(
'{{ distance("test.object", "32.87336", "-117.22943") | round }}', hass
)
assert tpl.async_render() == 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 (
template.Template(
"{{ closest(states.test_domain).entity_id }}", hass
).async_render()
== "test_domain.object"
)
assert (
template.Template(
"{{ (states.test_domain | closest).entity_id }}", hass
).async_render()
== "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 (
template.Template("{{ closest(states).entity_id }}", hass).async_render()
== "test_domain_2.and_closer"
)
assert (
template.Template("{{ (states | closest).entity_id }}", hass).async_render()
== "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 == template.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 == template.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"],
)
async def test_device_entities(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device_entities function."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
# Test non existing device ids
info = render_to_info(hass, "{{ device_entities('abc123') }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ device_entities(56) }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test device without entities
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test device with single entity, which has no state
entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}")
assert_result_info(info, ["light.hue_5678"], [])
assert info.rate_limit is None
info = render_to_info(
hass,
(
f"{{{{ device_entities('{device_entry.id}') | expand "
"| sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(info, "", ["light.hue_5678"])
assert info.rate_limit is None
# Test device with single entity, with state
hass.states.async_set("light.hue_5678", "happy")
info = render_to_info(
hass,
(
f"{{{{ device_entities('{device_entry.id}') | expand "
"| sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(info, "light.hue_5678", ["light.hue_5678"])
assert info.rate_limit is None
# Test device with multiple entities, which have a state
entity_registry.async_get_or_create(
"light",
"hue",
"ABCD",
config_entry=config_entry,
device_id=device_entry.id,
)
hass.states.async_set("light.hue_abcd", "camper")
info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}")
assert_result_info(info, ["light.hue_5678", "light.hue_abcd"], [])
assert info.rate_limit is None
info = render_to_info(
hass,
(
f"{{{{ device_entities('{device_entry.id}') | expand "
"| sort(attribute='entity_id') | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(
info, "light.hue_5678, light.hue_abcd", ["light.hue_5678", "light.hue_abcd"]
)
assert info.rate_limit is None
async def test_integration_entities(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test integration_entities function."""
# test entities for untitled config entry
config_entry = MockConfigEntry(domain="mock", title="")
config_entry.add_to_hass(hass)
entity_registry.async_get_or_create(
"sensor", "mock", "untitled", config_entry=config_entry
)
info = render_to_info(hass, "{{ integration_entities('') }}")
assert_result_info(info, [])
assert info.rate_limit is None
# test entities for given config entry title
config_entry = MockConfigEntry(domain="mock", title="Mock bridge 2")
config_entry.add_to_hass(hass)
entity_entry = entity_registry.async_get_or_create(
"sensor", "mock", "test", config_entry=config_entry
)
info = render_to_info(hass, "{{ integration_entities('Mock bridge 2') }}")
assert_result_info(info, [entity_entry.entity_id])
assert info.rate_limit is None
# test entities for given non unique config entry title
config_entry = MockConfigEntry(domain="mock", title="Not unique")
config_entry.add_to_hass(hass)
entity_entry_not_unique_1 = entity_registry.async_get_or_create(
"sensor", "mock", "not_unique_1", config_entry=config_entry
)
config_entry = MockConfigEntry(domain="mock", title="Not unique")
config_entry.add_to_hass(hass)
entity_entry_not_unique_2 = entity_registry.async_get_or_create(
"sensor", "mock", "not_unique_2", config_entry=config_entry
)
info = render_to_info(hass, "{{ integration_entities('Not unique') }}")
assert_result_info(
info, [entity_entry_not_unique_1.entity_id, entity_entry_not_unique_2.entity_id]
)
assert info.rate_limit is None
# test integration entities not in entity registry
mock_entity = entity.Entity()
mock_entity.hass = hass
mock_entity.entity_id = "light.test_entity"
mock_entity.platform = EntityPlatform(
hass=hass,
logger=logging.getLogger(__name__),
domain="light",
platform_name="entryless_integration",
platform=None,
scan_interval=timedelta(seconds=30),
entity_namespace=None,
)
await mock_entity.async_internal_added_to_hass()
info = render_to_info(hass, "{{ integration_entities('entryless_integration') }}")
assert_result_info(info, ["light.test_entity"])
assert info.rate_limit is None
# Test non existing integration/entry title
info = render_to_info(hass, "{{ integration_entities('abc123') }}")
assert_result_info(info, [])
assert info.rate_limit is None
async def test_config_entry_id(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test config_entry_id function."""
config_entry = MockConfigEntry(domain="light", title="Some integration")
config_entry.add_to_hass(hass)
entity_entry = entity_registry.async_get_or_create(
"sensor", "test", "test", suggested_object_id="test", config_entry=config_entry
)
info = render_to_info(hass, "{{ 'sensor.fail' | config_entry_id }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 56 | config_entry_id }}")
assert_result_info(info, None)
info = render_to_info(hass, "{{ 'not_a_real_entity_id' | config_entry_id }}")
assert_result_info(info, None)
info = render_to_info(
hass, f"{{{{ config_entry_id('{entity_entry.entity_id}') }}}}"
)
assert_result_info(info, config_entry.entry_id)
assert info.rate_limit is None
async def test_device_id(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device_id function."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
model="test",
name="test",
)
entity_entry = entity_registry.async_get_or_create(
"sensor", "test", "test", suggested_object_id="test", device_id=device_entry.id
)
entity_entry_no_device = entity_registry.async_get_or_create(
"sensor", "test", "test_no_device", suggested_object_id="test"
)
info = render_to_info(hass, "{{ 'sensor.fail' | device_id }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 56 | device_id }}")
assert_result_info(info, None)
info = render_to_info(hass, "{{ 'not_a_real_entity_id' | device_id }}")
assert_result_info(info, None)
info = render_to_info(
hass, f"{{{{ device_id('{entity_entry_no_device.entity_id}') }}}}"
)
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ device_id('{entity_entry.entity_id}') }}}}")
assert_result_info(info, device_entry.id)
assert info.rate_limit is None
info = render_to_info(hass, "{{ device_id('test') }}")
assert_result_info(info, device_entry.id)
assert info.rate_limit is None
async def test_device_attr(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device_attr and is_device_attr functions."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
# Test non existing device ids (device_attr)
info = render_to_info(hass, "{{ device_attr('abc123', 'id') }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ device_attr(56, 'id') }}")
with pytest.raises(TemplateError):
assert_result_info(info, None)
# Test non existing device ids (is_device_attr)
info = render_to_info(hass, "{{ is_device_attr('abc123', 'id', 'test') }}")
assert_result_info(info, False)
assert info.rate_limit is None
info = render_to_info(hass, "{{ is_device_attr(56, 'id', 'test') }}")
with pytest.raises(TemplateError):
assert_result_info(info, False)
# Test non existing entity id (device_attr)
info = render_to_info(hass, "{{ device_attr('entity.test', 'id') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existing entity id (is_device_attr)
info = render_to_info(hass, "{{ is_device_attr('entity.test', 'id', 'test') }}")
assert_result_info(info, False)
assert info.rate_limit is None
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
model="test",
)
entity_entry = entity_registry.async_get_or_create(
"sensor", "test", "test", suggested_object_id="test", device_id=device_entry.id
)
# Test non existent device attribute (device_attr)
info = render_to_info(
hass, f"{{{{ device_attr('{device_entry.id}', 'invalid_attr') }}}}"
)
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existent device attribute (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'invalid_attr', 'test') }}}}"
)
assert_result_info(info, False)
assert info.rate_limit is None
# Test None device attribute (device_attr)
info = render_to_info(
hass, f"{{{{ device_attr('{device_entry.id}', 'manufacturer') }}}}"
)
assert_result_info(info, None)
assert info.rate_limit is None
# Test None device attribute mismatch (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'manufacturer', 'test') }}}}"
)
assert_result_info(info, False)
assert info.rate_limit is None
# Test None device attribute match (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'manufacturer', None) }}}}"
)
assert_result_info(info, True)
assert info.rate_limit is None
# Test valid device attribute match (device_attr)
info = render_to_info(hass, f"{{{{ device_attr('{device_entry.id}', 'model') }}}}")
assert_result_info(info, "test")
assert info.rate_limit is None
# Test valid device attribute match (device_attr)
info = render_to_info(
hass, f"{{{{ device_attr('{entity_entry.entity_id}', 'model') }}}}"
)
assert_result_info(info, "test")
assert info.rate_limit is None
# Test valid device attribute mismatch (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'model', 'fail') }}}}"
)
assert_result_info(info, False)
assert info.rate_limit is None
# Test valid device attribute match (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'model', 'test') }}}}"
)
assert_result_info(info, True)
assert info.rate_limit is None
# Test filter syntax (device_attr)
info = render_to_info(
hass, f"{{{{ '{entity_entry.entity_id}' | device_attr('model') }}}}"
)
assert_result_info(info, "test")
assert info.rate_limit is None
# Test test syntax (is_device_attr)
info = render_to_info(
hass,
(
f"{{{{ ['{device_entry.id}'] | select('is_device_attr', 'model', 'test') "
"| list }}"
),
)
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None
async def test_config_entry_attr(hass: HomeAssistant) -> None:
"""Test config entry attr."""
info = {
"domain": "mock_light",
"title": "mock title",
"source": config_entries.SOURCE_BLUETOOTH,
"disabled_by": config_entries.ConfigEntryDisabler.USER,
}
config_entry = MockConfigEntry(**info)
config_entry.add_to_hass(hass)
info["state"] = config_entries.ConfigEntryState.NOT_LOADED
for key, value in info.items():
tpl = template.Template(
"{{ config_entry_attr('" + config_entry.entry_id + "', '" + key + "') }}",
hass,
)
assert tpl.async_render(parse_result=False) == str(value)
for config_entry_id, key in (
(config_entry.entry_id, "invalid_key"),
(56, "domain"),
):
with pytest.raises(TemplateError):
template.Template(
"{{ config_entry_attr("
+ json.dumps(config_entry_id)
+ ", '"
+ key
+ "') }}",
hass,
).async_render()
assert (
template.Template(
"{{ config_entry_attr('invalid_id', 'domain') }}", hass
).async_render(parse_result=False)
== "None"
)
async def test_issues(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None:
"""Test issues function."""
# Test no issues
info = render_to_info(hass, "{{ issues() }}")
assert_result_info(info, {})
assert info.rate_limit is None
# Test persistent issue
ir.async_create_issue(
hass,
"test",
"issue 1",
breaks_in_ha_version="2023.7",
is_fixable=True,
is_persistent=True,
learn_more_url="https://theuselessweb.com",
severity="error",
translation_key="abc_1234",
translation_placeholders={"abc": "123"},
)
await hass.async_block_till_done()
created_issue = issue_registry.async_get_issue("test", "issue 1")
info = render_to_info(hass, "{{ issues()['test', 'issue 1'] }}")
assert_result_info(info, created_issue.to_json())
assert info.rate_limit is None
# Test fixed issue
ir.async_delete_issue(hass, "test", "issue 1")
await hass.async_block_till_done()
info = render_to_info(hass, "{{ issues() }}")
assert_result_info(info, {})
assert info.rate_limit is None
async def test_issue(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None:
"""Test issue function."""
# Test non existent issue
info = render_to_info(hass, "{{ issue('non_existent', 'issue') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test existing issue
ir.async_create_issue(
hass,
"test",
"issue 1",
breaks_in_ha_version="2023.7",
is_fixable=True,
is_persistent=True,
learn_more_url="https://theuselessweb.com",
severity="error",
translation_key="abc_1234",
translation_placeholders={"abc": "123"},
)
await hass.async_block_till_done()
created_issue = issue_registry.async_get_issue("test", "issue 1")
info = render_to_info(hass, "{{ issue('test', 'issue 1') }}")
assert_result_info(info, created_issue.to_json())
assert info.rate_limit is None
async def test_areas(hass: HomeAssistant, area_registry: ar.AreaRegistry) -> None:
"""Test areas function."""
# Test no areas
info = render_to_info(hass, "{{ areas() }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test one area
area1 = area_registry.async_get_or_create("area1")
info = render_to_info(hass, "{{ areas() }}")
assert_result_info(info, [area1.id])
assert info.rate_limit is None
# Test multiple areas
area2 = area_registry.async_get_or_create("area2")
info = render_to_info(hass, "{{ areas() }}")
assert_result_info(info, [area1.id, area2.id])
assert info.rate_limit is None
async def test_area_id(
hass: HomeAssistant,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test area_id function."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
# Test non existing entity id
info = render_to_info(hass, "{{ area_id('sensor.fake') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existing device id (hex value)
info = render_to_info(hass, "{{ area_id('123abc') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existing area name
info = render_to_info(hass, "{{ area_id('fake area name') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ area_id(56) }}")
assert_result_info(info, None)
assert info.rate_limit is None
area_entry_entity_id = area_registry.async_get_or_create("sensor.fake")
# Test device with single entity, which has no area
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
info = render_to_info(hass, f"{{{{ area_id('{device_entry.id}') }}}}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test device ID, entity ID and area name as input with area name that looks like
# a device ID. Try a filter too
area_entry_hex = area_registry.async_get_or_create("123abc")
device_entry = device_registry.async_update_device(
device_entry.id, area_id=area_entry_hex.id
)
entity_entry = entity_registry.async_update_entity(
entity_entry.entity_id, area_id=area_entry_hex.id
)
info = render_to_info(hass, f"{{{{ '{device_entry.id}' | area_id }}}}")
assert_result_info(info, area_entry_hex.id)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}")
assert_result_info(info, area_entry_hex.id)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ area_id('{area_entry_hex.name}') }}}}")
assert_result_info(info, area_entry_hex.id)
assert info.rate_limit is None
# Test device ID, entity ID and area name as input with area name that looks like an
# entity ID
area_entry_entity_id = area_registry.async_get_or_create("sensor.fake")
device_entry = device_registry.async_update_device(
device_entry.id, area_id=area_entry_entity_id.id
)
entity_entry = entity_registry.async_update_entity(
entity_entry.entity_id, area_id=area_entry_entity_id.id
)
info = render_to_info(hass, f"{{{{ area_id('{device_entry.id}') }}}}")
assert_result_info(info, area_entry_entity_id.id)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}")
assert_result_info(info, area_entry_entity_id.id)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ area_id('{area_entry_entity_id.name}') }}}}")
assert_result_info(info, area_entry_entity_id.id)
assert info.rate_limit is None
# Make sure that when entity doesn't have an area but its device does, that's what
# gets returned
entity_entry = entity_registry.async_update_entity(
entity_entry.entity_id, area_id=area_entry_entity_id.id
)
info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}")
assert_result_info(info, area_entry_entity_id.id)
assert info.rate_limit is None
async def test_area_name(
hass: HomeAssistant,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test area_name function."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
# Test non existing entity id
info = render_to_info(hass, "{{ area_name('sensor.fake') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existing device id (hex value)
info = render_to_info(hass, "{{ area_name('123abc') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existing area id
info = render_to_info(hass, "{{ area_name('1234567890') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ area_name(56) }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test device with single entity, which has no area
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
info = render_to_info(hass, f"{{{{ area_name('{device_entry.id}') }}}}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test device ID, entity ID and area id as input. Try a filter too
area_entry = area_registry.async_get_or_create("123abc")
device_entry = device_registry.async_update_device(
device_entry.id, area_id=area_entry.id
)
entity_entry = entity_registry.async_update_entity(
entity_entry.entity_id, area_id=area_entry.id
)
info = render_to_info(hass, f"{{{{ '{device_entry.id}' | area_name }}}}")
assert_result_info(info, area_entry.name)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}")
assert_result_info(info, area_entry.name)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ area_name('{area_entry.id}') }}}}")
assert_result_info(info, area_entry.name)
assert info.rate_limit is None
# Make sure that when entity doesn't have an area but its device does, that's what
# gets returned
entity_entry = entity_registry.async_update_entity(
entity_entry.entity_id, area_id=None
)
info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}")
assert_result_info(info, area_entry.name)
assert info.rate_limit is None
async def test_area_entities(
hass: HomeAssistant,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test area_entities function."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
# Test non existing device id
info = render_to_info(hass, "{{ area_entities('deadbeef') }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ area_entities(56) }}")
assert_result_info(info, [])
assert info.rate_limit is None
area_entry = area_registry.async_get_or_create("sensor.fake")
entity_entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
)
entity_registry.async_update_entity(entity_entry.entity_id, area_id=area_entry.id)
info = render_to_info(hass, f"{{{{ area_entities('{area_entry.id}') }}}}")
assert_result_info(info, ["light.hue_5678"])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_entities }}}}")
assert_result_info(info, ["light.hue_5678"])
assert info.rate_limit is None
# Test for entities that inherit area from device
device_entry = device_registry.async_get_or_create(
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
config_entry_id=config_entry.entry_id,
suggested_area="sensor.fake",
)
entity_registry.async_get_or_create(
"light",
"hue_light",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_entities }}}}")
assert_result_info(info, ["light.hue_5678", "light.hue_light_5678"])
assert info.rate_limit is None
async def test_area_devices(
hass: HomeAssistant,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test area_devices function."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
# Test non existing device id
info = render_to_info(hass, "{{ area_devices('deadbeef') }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ area_devices(56) }}")
assert_result_info(info, [])
assert info.rate_limit is None
area_entry = area_registry.async_get_or_create("sensor.fake")
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
suggested_area=area_entry.name,
)
info = render_to_info(hass, f"{{{{ area_devices('{area_entry.id}') }}}}")
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_devices }}}}")
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None
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,
},
)
tpl = template.Template(
f'{{{{ closest("{hass.config.latitude + 0.3}", {hass.config.longitude + 0.3}, states.test_domain).entity_id }}}}',
hass,
)
assert tpl.async_render() == "test_domain.closest_zone"
tpl = template.Template(
f'{{{{ (states.test_domain | closest("{hass.config.latitude + 0.3}", {hass.config.longitude + 0.3})).entity_id }}}}',
hass,
)
assert tpl.async_render() == "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 == template.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 state.entity_id | regex_match('.*\\.office_') %}
{{ 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 == template.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 | regex_match('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 == template.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 == template.ALL_STATES_RATE_LIMIT
template_cover_str = """
{% for state in states.cover %}
{% if state.state | regex_match('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 == template.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 (
template.Template(
"{{ closest(states.zone.far_away, states.test_domain).entity_id }}", hass
).async_render()
== "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 (
template.Template(
f"{{{{ closest({state}, states) }}}}", hass
).async_render()
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 (
template.Template(
"{{ closest(states.test_domain.closest_home, states) }}", hass
).async_render()
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 (
template.Template(
'{{ closest("invalid", "coord", states) }}', hass
).async_render()
is None
)
assert (
template.Template(
'{{ states | closest("invalid", "coord") }}', hass
).async_render()
is None
)
def test_closest_function_no_location_states(hass: HomeAssistant) -> None:
"""Test closest function without location states."""
assert (
template.Template("{{ closest(states).entity_id }}", hass).async_render() == ""
)
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") }}
"""
tmp = template.Template(template_str, hass)
info = tmp.async_render_to_info()
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 = tmp.async_render_to_info()
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."""
template_str = """
{{ states("sensor.xyz") == "dog" }}
"""
tmp = template.Template(template_str, hass)
info = tmp.async_render_to_info()
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 %}
"""
tmp = template.Template(template_str, hass)
info = tmp.async_render_to_info()
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()
tmp = template.Template(template_str, hass)
info = tmp.async_render_to_info()
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")
tpl = template.Template("{{ states.sensor.test.state_with_unit }}", hass)
assert tpl.async_render() == "23 beers"
tpl = template.Template("{{ states.sensor.test2.state_with_unit }}", hass)
assert tpl.async_render() == "wow"
tpl = template.Template(
"{% for state in states %}{{ state.state_with_unit }} {% endfor %}", hass
)
assert tpl.async_render() == "23 beers wow"
tpl = template.Template("{{ states.sensor.non_existing.state_with_unit }}", hass)
assert tpl.async_render() == ""
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")
tpl = template.Template("{{ states | length }}", hass)
assert tpl.async_render() == 3
tpl = template.Template("{{ states.sensor | length }}", hass)
assert tpl.async_render() == 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}
def test_urlencode(hass: HomeAssistant) -> None:
"""Test the urlencode method."""
tpl = template.Template(
"{% set dict = {'foo': 'x&y', 'bar': 42} %}{{ dict | urlencode }}",
hass,
)
assert tpl.async_render() == "foo=x%26y&bar=42"
tpl = template.Template(
"{% set string = 'the quick brown fox = true' %}{{ string | urlencode }}",
hass,
)
assert tpl.async_render() == "the%20quick%20brown%20fox%20%3D%20true"
def test_as_timedelta(hass: HomeAssistant) -> None:
"""Test the as_timedelta function/filter."""
tpl = template.Template("{{ as_timedelta('PT10M') }}", hass)
assert tpl.async_render() == "0:10:00"
tpl = template.Template("{{ 'PT10M' | as_timedelta }}", hass)
assert tpl.async_render() == "0:10:00"
tpl = template.Template("{{ 'T10M' | as_timedelta }}", hass)
assert tpl.async_render() is None
def test_iif(hass: HomeAssistant) -> None:
"""Test the immediate if function/filter."""
tpl = template.Template("{{ (1 == 1) | iif }}", hass)
assert tpl.async_render() is True
tpl = template.Template("{{ (1 == 2) | iif }}", hass)
assert tpl.async_render() is False
tpl = template.Template("{{ (1 == 1) | iif('yes') }}", hass)
assert tpl.async_render() == "yes"
tpl = template.Template("{{ (1 == 2) | iif('yes') }}", hass)
assert tpl.async_render() is False
tpl = template.Template("{{ (1 == 2) | iif('yes', 'no') }}", hass)
assert tpl.async_render() == "no"
tpl = template.Template("{{ not_exists | default(None) | iif('yes', 'no') }}", hass)
assert tpl.async_render() == "no"
tpl = template.Template(
"{{ not_exists | default(None) | iif('yes', 'no', 'unknown') }}", hass
)
assert tpl.async_render() == "unknown"
tpl = template.Template("{{ iif(1 == 1) }}", hass)
assert tpl.async_render() is True
tpl = template.Template("{{ iif(1 == 2, 'yes', 'no') }}", hass)
assert tpl.async_render() == "no"
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."""
tmp = template.Template('{{ states.__getattr__("any") }}', hass)
with pytest.raises(TemplateError):
tmp.async_render()
tmp = template.Template('{{ states.sensor.__getattr__("any") }}', hass)
with pytest.raises(TemplateError):
tmp.async_render()
tmp = template.Template('{{ states.sensor.any.__getattr__("any") }}', hass)
with pytest.raises(TemplateError):
tmp.async_render()
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 %}.
"""
tmp = template.Template(demo_template_str, hass)
result = tmp.async_render()
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")
tpl = template.Template(
(
"{% for states in states | slice(1) -%}{% set state = states | first %}"
"{{ state.entity_id }}"
"{%- endfor %}"
),
hass,
)
assert tpl.async_render() == "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()
tmp = template.Template("{{ states | count }}", hass)
info = tmp.async_render_to_info()
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")
tmp = template.Template(tmpl, hass)
info = tmp.async_render_to_info()
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):
template.Template("{{ now() | rando }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ utcnow() | rando }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ now() | random }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ utcnow() | random }}", hass).async_render()
async def test_state_attributes(hass: HomeAssistant) -> None:
"""Test state attributes."""
hass.states.async_set("sensor.test", "23")
tpl = template.Template(
"{{ states.sensor.test.last_changed }}",
hass,
)
assert tpl.async_render() == str(hass.states.get("sensor.test").last_changed)
tpl = template.Template(
"{{ states.sensor.test.object_id }}",
hass,
)
assert tpl.async_render() == hass.states.get("sensor.test").object_id
tpl = template.Template(
"{{ states.sensor.test.domain }}",
hass,
)
assert tpl.async_render() == hass.states.get("sensor.test").domain
tpl = template.Template(
"{{ states.sensor.test.context.id }}",
hass,
)
assert tpl.async_render() == hass.states.get("sensor.test").context.id
tpl = template.Template(
"{{ states.sensor.test.state_with_unit }}",
hass,
)
assert tpl.async_render() == 23
tpl = template.Template(
"{{ states.sensor.test.invalid_prop }}",
hass,
)
assert tpl.async_render() == ""
tpl = template.Template(
"{{ states.sensor.test.invalid_prop.xx }}",
hass,
)
with pytest.raises(TemplateError):
tpl.async_render()
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")
tpl = template.Template(
(
"{{ states | selectattr('state', 'in', ['unavailable','unknown','none']) "
"| sort(attribute='entity_id') | map(attribute='entity_id') | list | join(', ') }}"
),
hass,
)
assert tpl.async_render() == "light.none, light.unavailable, light.unknown"
tpl = template.Template(
(
"{{ states.light "
"| selectattr('state', 'in', ['unavailable','unknown','none']) "
"| sort(attribute='entity_id') | map(attribute='entity_id') | list "
"| join(', ') }}"
),
hass,
)
assert tpl.async_render() == "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 (
template.Template("{{ states.sensor.temperature.state }}", hass).async_render(
parse_result=False
)
== "12"
)
assert (
template.Template("{{ false }}", hass).async_render(parse_result=False)
== "False"
)
assert (
template.Template("{{ [1, 2, 3] }}", hass).async_render(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})),
):
tpl = template.Template(text, hass)
result = tpl.async_render()
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 template.Template(tpl, hass).async_render() == 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."""
tpl = template.Template(template_string, hass)
assert tpl.async_render() == ""
assert (
"Template variable warning: 'no_such_variable' is undefined when rendering "
f"'{template_string}'" in caplog.text
)
async def test_template_states_blocks_setitem(hass: HomeAssistant) -> None:
"""Test we cannot setitem on TemplateStates."""
hass.states.async_set("light.new", STATE_ON)
state = hass.states.get("light.new")
template_state = template.TemplateState(hass, state, True)
with pytest.raises(RuntimeError):
template_state["any"] = "any"
async def test_template_states_can_serialize(hass: HomeAssistant) -> None:
"""Test TemplateState is serializable."""
hass.states.async_set("light.new", STATE_ON)
state = hass.states.get("light.new")
template_state = template.TemplateState(hass, state, True)
assert template_state.as_dict() is template_state.as_dict()
assert json_dumps(template_state) == json_dumps(template_state)
@pytest.mark.parametrize(
("seq", "value", "expected"),
[
([0], 0, True),
([1], 0, False),
([False], 0, True),
([True], 0, False),
([0], [0], False),
(["toto", 1], "toto", True),
(["toto", 1], "tata", False),
([], 0, False),
([], None, False),
],
)
def test_contains(hass: HomeAssistant, seq, value, expected) -> None:
"""Test contains."""
assert (
template.Template("{{ seq | contains(value) }}", hass).async_render(
{"seq": seq, "value": value}
)
== expected
)
assert (
template.Template("{{ seq is contains(value) }}", hass).async_render(
{"seq": seq, "value": value}
)
== expected
)
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_lru_increases_with_many_entities(hass: HomeAssistant) -> None:
"""Test that the template internal LRU cache increases with many entities."""
# We do not actually want to record 4096 entities so we mock the entity count
mock_entity_count = 16
assert template.CACHED_TEMPLATE_LRU.get_size() == template.CACHED_TEMPLATE_STATES
assert (
template.CACHED_TEMPLATE_NO_COLLECT_LRU.get_size()
== template.CACHED_TEMPLATE_STATES
)
template.CACHED_TEMPLATE_LRU.set_size(8)
template.CACHED_TEMPLATE_NO_COLLECT_LRU.set_size(8)
template.async_setup(hass)
for i in range(mock_entity_count):
hass.states.async_set(f"sensor.sensor{i}", "on")
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10))
await hass.async_block_till_done()
assert template.CACHED_TEMPLATE_LRU.get_size() == int(
round(mock_entity_count * template.ENTITY_COUNT_GROWTH_FACTOR)
)
assert template.CACHED_TEMPLATE_NO_COLLECT_LRU.get_size() == int(
round(mock_entity_count * template.ENTITY_COUNT_GROWTH_FACTOR)
)
await hass.async_stop()
for i in range(mock_entity_count):
hass.states.async_set(f"sensor.sensor_add_{i}", "on")
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20))
await hass.async_block_till_done()
assert template.CACHED_TEMPLATE_LRU.get_size() == int(
round(mock_entity_count * template.ENTITY_COUNT_GROWTH_FACTOR)
)
assert template.CACHED_TEMPLATE_NO_COLLECT_LRU.get_size() == int(
round(mock_entity_count * template.ENTITY_COUNT_GROWTH_FACTOR)
)
async def test_floors(
hass: HomeAssistant,
floor_registry: fr.FloorRegistry,
) -> None:
"""Test floors function."""
# Test no floors
info = render_to_info(hass, "{{ floors() }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test one floor
floor1 = floor_registry.async_create("First floor")
info = render_to_info(hass, "{{ floors() }}")
assert_result_info(info, [floor1.floor_id])
assert info.rate_limit is None
# Test multiple floors
floor2 = floor_registry.async_create("Second floor")
info = render_to_info(hass, "{{ floors() }}")
assert_result_info(info, [floor1.floor_id, floor2.floor_id])
assert info.rate_limit is None
async def test_floor_id(
hass: HomeAssistant,
floor_registry: fr.FloorRegistry,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test floor_id function."""
def test(value: str, expected: str | None) -> None:
info = render_to_info(hass, f"{{{{ floor_id('{value}') }}}}")
assert_result_info(info, expected)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{value}' | floor_id }}}}")
assert_result_info(info, expected)
assert info.rate_limit is None
# Test non existing floor name
test("Third floor", None)
# Test wrong value type
info = render_to_info(hass, "{{ floor_id(42) }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 42 | floor_id }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test with an actual floor
floor = floor_registry.async_create("First floor")
test("First floor", floor.floor_id)
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
area_entry_hex = area_registry.async_get_or_create("123abc")
# Create area, device, entity and assign area to device and entity
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
device_entry = device_registry.async_update_device(
device_entry.id, area_id=area_entry_hex.id
)
entity_entry = entity_registry.async_update_entity(
entity_entry.entity_id, area_id=area_entry_hex.id
)
test(area_entry_hex.id, None)
test(device_entry.id, None)
test(entity_entry.entity_id, None)
# Add floor to area
area_entry_hex = area_registry.async_update(
area_entry_hex.id, floor_id=floor.floor_id
)
test(area_entry_hex.id, floor.floor_id)
test(device_entry.id, floor.floor_id)
test(entity_entry.entity_id, floor.floor_id)
async def test_floor_name(
hass: HomeAssistant,
floor_registry: fr.FloorRegistry,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test floor_name function."""
def test(value: str, expected: str | None) -> None:
info = render_to_info(hass, f"{{{{ floor_name('{value}') }}}}")
assert_result_info(info, expected)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{value}' | floor_name }}}}")
assert_result_info(info, expected)
assert info.rate_limit is None
# Test non existing floor name
test("Third floor", None)
# Test wrong value type
info = render_to_info(hass, "{{ floor_name(42) }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 42 | floor_name }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test existing floor ID
floor = floor_registry.async_create("First floor")
test(floor.floor_id, floor.name)
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
area_entry_hex = area_registry.async_get_or_create("123abc")
# Create area, device, entity and assign area to device and entity
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
device_entry = device_registry.async_update_device(
device_entry.id, area_id=area_entry_hex.id
)
entity_entry = entity_registry.async_update_entity(
entity_entry.entity_id, area_id=area_entry_hex.id
)
test(area_entry_hex.id, None)
test(device_entry.id, None)
test(entity_entry.entity_id, None)
# Add floor to area
area_entry_hex = area_registry.async_update(
area_entry_hex.id, floor_id=floor.floor_id
)
test(area_entry_hex.id, floor.name)
test(device_entry.id, floor.name)
test(entity_entry.entity_id, floor.name)
async def test_floor_areas(
hass: HomeAssistant,
floor_registry: fr.FloorRegistry,
area_registry: ar.AreaRegistry,
) -> None:
"""Test floor_areas function."""
# Test non existing floor ID
info = render_to_info(hass, "{{ floor_areas('skyring') }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 'skyring' | floor_areas }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ floor_areas(42) }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 42 | floor_areas }}")
assert_result_info(info, [])
assert info.rate_limit is None
floor = floor_registry.async_create("First floor")
area = area_registry.async_create("Living room")
area_registry.async_update(area.id, floor_id=floor.floor_id)
# Get areas by floor ID
info = render_to_info(hass, f"{{{{ floor_areas('{floor.floor_id}') }}}}")
assert_result_info(info, [area.id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{floor.floor_id}' | floor_areas }}}}")
assert_result_info(info, [area.id])
assert info.rate_limit is None
# Get entities by floor name
info = render_to_info(hass, f"{{{{ floor_areas('{floor.name}') }}}}")
assert_result_info(info, [area.id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{floor.name}' | floor_areas }}}}")
assert_result_info(info, [area.id])
assert info.rate_limit is None
async def test_labels(
hass: HomeAssistant,
label_registry: lr.LabelRegistry,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test labels function."""
# Test no labels
info = render_to_info(hass, "{{ labels() }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test one label
label1 = label_registry.async_create("label1")
info = render_to_info(hass, "{{ labels() }}")
assert_result_info(info, [label1.label_id])
assert info.rate_limit is None
# Test multiple label
label2 = label_registry.async_create("label2")
info = render_to_info(hass, "{{ labels() }}")
assert_result_info(info, [label1.label_id, label2.label_id])
assert info.rate_limit is None
# Test non-exsting entity ID
info = render_to_info(hass, "{{ labels('sensor.fake') }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 'sensor.fake' | labels }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test non existing device ID (hex value)
info = render_to_info(hass, "{{ labels('123abc') }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ '123abc' | labels }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Create a device & entity for testing
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
device_id=device_entry.id,
)
# Test entity, which has no labels
info = render_to_info(hass, f"{{{{ labels('{entity_entry.entity_id}') }}}}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{entity_entry.entity_id}' | labels }}}}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test device, which has no labels
info = render_to_info(hass, f"{{{{ labels('{device_entry.id}') }}}}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{device_entry.id}' | labels }}}}")
assert_result_info(info, [])
assert info.rate_limit is None
# Add labels to the entity & device
device_entry = device_registry.async_update_device(
device_entry.id, labels=[label1.label_id]
)
entity_entry = entity_registry.async_update_entity(
entity_entry.entity_id, labels=[label2.label_id]
)
# Test entity, which now has a label
info = render_to_info(hass, f"{{{{ '{entity_entry.entity_id}' | labels }}}}")
assert_result_info(info, [label2.label_id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ labels('{entity_entry.entity_id}') }}}}")
assert_result_info(info, [label2.label_id])
assert info.rate_limit is None
# Test device, which now has a label
info = render_to_info(hass, f"{{{{ '{device_entry.id}' | labels }}}}")
assert_result_info(info, [label1.label_id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ labels('{device_entry.id}') }}}}")
assert_result_info(info, [label1.label_id])
assert info.rate_limit is None
# Create area for testing
area = area_registry.async_create("living room")
# Test area, which has no labels
info = render_to_info(hass, f"{{{{ '{area.id}' | labels }}}}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ labels('{area.id}') }}}}")
assert_result_info(info, [])
assert info.rate_limit is None
# Add label to the area
area_registry.async_update(area.id, labels=[label1.label_id, label2.label_id])
# Test area, which now has labels
info = render_to_info(hass, f"{{{{ '{area.id}' | labels }}}}")
assert_result_info(info, [label1.label_id, label2.label_id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ labels('{area.id}') }}}}")
assert_result_info(info, [label1.label_id, label2.label_id])
assert info.rate_limit is None
async def test_label_id(
hass: HomeAssistant,
label_registry: lr.LabelRegistry,
) -> None:
"""Test label_id function."""
# Test non existing label name
info = render_to_info(hass, "{{ label_id('non-existing label') }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 'non-existing label' | label_id }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ label_id(42) }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 42 | label_id }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test with an actual label
label = label_registry.async_create("existing label")
info = render_to_info(hass, "{{ label_id('existing label') }}")
assert_result_info(info, label.label_id)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 'existing label' | label_id }}")
assert_result_info(info, label.label_id)
assert info.rate_limit is None
async def test_label_name(
hass: HomeAssistant,
label_registry: lr.LabelRegistry,
) -> None:
"""Test label_name function."""
# Test non existing label ID
info = render_to_info(hass, "{{ label_name('1234567890') }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ '1234567890' | label_name }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ label_name(42) }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 42 | label_name }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existing label ID
label = label_registry.async_create("choo choo")
info = render_to_info(hass, f"{{{{ label_name('{label.label_id}') }}}}")
assert_result_info(info, label.name)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{label.label_id}' | label_name }}}}")
assert_result_info(info, label.name)
assert info.rate_limit is None
async def test_label_entities(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
label_registry: lr.LabelRegistry,
) -> None:
"""Test label_entities function."""
# Test non existing device ID
info = render_to_info(hass, "{{ label_entities('deadbeef') }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 'deadbeef' | label_entities }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ label_entities(42) }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 42 | label_entities }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Create a fake config entry with a entity
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
entity_entry = entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
)
# Add a label to the entity
label = label_registry.async_create("Romantic Lights")
entity_registry.async_update_entity(entity_entry.entity_id, labels={label.label_id})
# Get entities by label ID
info = render_to_info(hass, f"{{{{ label_entities('{label.label_id}') }}}}")
assert_result_info(info, ["light.hue_5678"])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{label.label_id}' | label_entities }}}}")
assert_result_info(info, ["light.hue_5678"])
assert info.rate_limit is None
# Get entities by label name
info = render_to_info(hass, f"{{{{ label_entities('{label.name}') }}}}")
assert_result_info(info, ["light.hue_5678"])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{label.name}' | label_entities }}}}")
assert_result_info(info, ["light.hue_5678"])
assert info.rate_limit is None
async def test_label_devices(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
label_registry: ar.AreaRegistry,
) -> None:
"""Test label_devices function."""
# Test non existing device ID
info = render_to_info(hass, "{{ label_devices('deadbeef') }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 'deadbeef' | label_devices }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ label_devices(42) }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 42 | label_devices }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Create a fake config entry with a device
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
# Add a label to it
label = label_registry.async_create("Romantic Lights")
device_registry.async_update_device(device_entry.id, labels=[label.label_id])
# Get the devices from a label by its ID
info = render_to_info(hass, f"{{{{ label_devices('{label.label_id}') }}}}")
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{label.label_id}' | label_devices }}}}")
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None
# Get the devices from a label by its name
info = render_to_info(hass, f"{{{{ label_devices('{label.name}') }}}}")
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{label.name}' | label_devices }}}}")
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None
async def test_label_areas(
hass: HomeAssistant,
area_registry: ar.AreaRegistry,
label_registry: lr.LabelRegistry,
) -> None:
"""Test label_areas function."""
# Test non existing area ID
info = render_to_info(hass, "{{ label_areas('deadbeef') }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 'deadbeef' | label_areas }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ label_areas(42) }}")
assert_result_info(info, [])
assert info.rate_limit is None
info = render_to_info(hass, "{{ 42 | label_areas }}")
assert_result_info(info, [])
assert info.rate_limit is None
# Create an area with an label
label = label_registry.async_create("Upstairs")
master_bedroom = area_registry.async_create(
"Master Bedroom", labels=[label.label_id]
)
# Get areas by label ID
info = render_to_info(hass, f"{{{{ label_areas('{label.label_id}') }}}}")
assert_result_info(info, [master_bedroom.id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{label.label_id}' | label_areas }}}}")
assert_result_info(info, [master_bedroom.id])
assert info.rate_limit is None
# Get areas by label name
info = render_to_info(hass, f"{{{{ label_areas('{label.name}') }}}}")
assert_result_info(info, [master_bedroom.id])
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ '{label.name}' | label_areas }}}}")
assert_result_info(info, [master_bedroom.id])
assert info.rate_limit is None
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
@pytest.mark.parametrize(
("cola", "colb", "expected"),
[
([1, 2], [3, 4], [(1, 3), (2, 4)]),
([1, 2], [3, 4, 5], [(1, 3), (2, 4)]),
([1, 2, 3, 4], [3, 4], [(1, 3), (2, 4)]),
],
)
def test_zip(hass: HomeAssistant, cola, colb, expected) -> None:
"""Test zip."""
assert (
template.Template("{{ zip(cola, colb) | list }}", hass).async_render(
{"cola": cola, "colb": colb}
)
== expected
)
assert (
template.Template(
"[{% for a, b in zip(cola, colb) %}({{a}}, {{b}}), {% endfor %}]", hass
).async_render({"cola": cola, "colb": colb})
== expected
)
@pytest.mark.parametrize(
("col", "expected"),
[
([(1, 3), (2, 4)], [(1, 2), (3, 4)]),
(["ax", "by", "cz"], [("a", "b", "c"), ("x", "y", "z")]),
],
)
def test_unzip(hass: HomeAssistant, col, expected) -> None:
"""Test unzipping using zip."""
assert (
template.Template("{{ zip(*col) | list }}", hass).async_render({"col": col})
== expected
)
assert (
template.Template(
"{% set a, b = zip(*col) %}[{{a}}, {{b}}]", hass
).async_render({"col": col})
== expected
)
def test_template_output_exceeds_maximum_size(hass: HomeAssistant) -> None:
"""Test template output exceeds maximum size."""
tpl = template.Template("{{ 'a' * 1024 * 257 }}", hass)
with pytest.raises(TemplateError):
tpl.async_render()
@pytest.mark.parametrize(
("service_response"),
[
{
"calendar.sports": {
"events": [
{
"start": "2024-02-27T17:00:00-06:00",
"end": "2024-02-27T18:00:00-06:00",
"summary": "Basketball vs. Rockets",
"description": "",
}
]
},
"calendar.local_furry_events": {"events": []},
"calendar.yap_house_schedules": {
"events": [
{
"start": "2024-02-26T08:00:00-06:00",
"end": "2024-02-26T09:00:00-06:00",
"summary": "Dr. Appt",
"description": "",
},
{
"start": "2024-02-28T20:00:00-06:00",
"end": "2024-02-28T21:00:00-06:00",
"summary": "Bake a cake",
"description": "something good",
},
]
},
},
{
"binary_sensor.workday": {"workday": True},
"binary_sensor.workday2": {"workday": False},
},
{
"weather.smhi_home": {
"forecast": [
{
"datetime": "2024-03-31T16:00:00",
"condition": "cloudy",
"wind_bearing": 79,
"cloud_coverage": 100,
"temperature": 10,
"templow": 4,
"pressure": 998,
"wind_gust_speed": 21.6,
"wind_speed": 11.88,
"precipitation": 0.2,
"humidity": 87,
},
{
"datetime": "2024-04-01T12:00:00",
"condition": "rainy",
"wind_bearing": 17,
"cloud_coverage": 100,
"temperature": 6,
"templow": 1,
"pressure": 999,
"wind_gust_speed": 20.52,
"wind_speed": 8.64,
"precipitation": 2.2,
"humidity": 88,
},
{
"datetime": "2024-04-02T12:00:00",
"condition": "cloudy",
"wind_bearing": 17,
"cloud_coverage": 100,
"temperature": 0,
"templow": -3,
"pressure": 1003,
"wind_gust_speed": 57.24,
"wind_speed": 30.6,
"precipitation": 1.3,
"humidity": 71,
},
]
},
"weather.forecast_home": {
"forecast": [
{
"condition": "cloudy",
"precipitation_probability": 6.6,
"datetime": "2024-03-31T10:00:00+00:00",
"wind_bearing": 71.8,
"temperature": 10.9,
"templow": 6.5,
"wind_gust_speed": 24.1,
"wind_speed": 13.7,
"precipitation": 0,
"humidity": 71,
},
{
"condition": "cloudy",
"precipitation_probability": 8,
"datetime": "2024-04-01T10:00:00+00:00",
"wind_bearing": 350.6,
"temperature": 10.2,
"templow": 3.4,
"wind_gust_speed": 38.2,
"wind_speed": 21.6,
"precipitation": 0,
"humidity": 79,
},
{
"condition": "snowy",
"precipitation_probability": 67.4,
"datetime": "2024-04-02T10:00:00+00:00",
"wind_bearing": 24.5,
"temperature": 3,
"templow": 0,
"wind_gust_speed": 64.8,
"wind_speed": 37.4,
"precipitation": 2.3,
"humidity": 77,
},
]
},
},
{
"vacuum.deebot_n8_plus_1": {
"payloadType": "j",
"resp": {
"body": {
"msg": "ok",
}
},
"header": {
"ver": "0.0.1",
},
},
"vacuum.deebot_n8_plus_2": {
"payloadType": "j",
"resp": {
"body": {
"msg": "ok",
}
},
"header": {
"ver": "0.0.1",
},
},
},
],
ids=["calendar", "workday", "weather", "vacuum"],
)
async def test_merge_response(
hass: HomeAssistant,
service_response: dict,
snapshot: SnapshotAssertion,
) -> None:
"""Test the merge_response function/filter."""
_template = "{{ merge_response(" + str(service_response) + ") }}"
tpl = template.Template(_template, hass)
assert service_response == snapshot(name="a_response")
assert tpl.async_render() == snapshot(name="b_rendered")
async def test_merge_response_with_entity_id_in_response(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Test the merge_response function/filter with empty lists."""
service_response = {
"test.response": {"some_key": True, "entity_id": "test.response"},
"test.response2": {"some_key": False, "entity_id": "test.response2"},
}
_template = "{{ merge_response(" + str(service_response) + ") }}"
with pytest.raises(
TemplateError,
match="ValueError: Response dictionary already contains key 'entity_id'",
):
template.Template(_template, hass).async_render()
service_response = {
"test.response": {
"happening": [
{
"start": "2024-02-27T17:00:00-06:00",
"end": "2024-02-27T18:00:00-06:00",
"summary": "Magic day",
"entity_id": "test.response",
}
]
}
}
_template = "{{ merge_response(" + str(service_response) + ") }}"
with pytest.raises(
TemplateError,
match="ValueError: Response dictionary already contains key 'entity_id'",
):
template.Template(_template, hass).async_render()
async def test_merge_response_with_empty_response(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Test the merge_response function/filter with empty lists."""
service_response = {
"calendar.sports": {"events": []},
"calendar.local_furry_events": {"events": []},
"calendar.yap_house_schedules": {"events": []},
}
_template = "{{ merge_response(" + str(service_response) + ") }}"
tpl = template.Template(_template, hass)
assert service_response == snapshot(name="a_response")
assert tpl.async_render() == snapshot(name="b_rendered")
async def test_response_empty_dict(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Test the merge_response function/filter with empty dict."""
service_response = {}
_template = "{{ merge_response(" + str(service_response) + ") }}"
tpl = template.Template(_template, hass)
assert tpl.async_render() == []
async def test_response_incorrect_value(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Test the merge_response function/filter with incorrect response."""
service_response = "incorrect"
_template = "{{ merge_response(" + str(service_response) + ") }}"
with pytest.raises(TemplateError, match="TypeError: Response is not a dictionary"):
template.Template(_template, hass).async_render()
async def test_merge_response_with_incorrect_response(hass: HomeAssistant) -> None:
"""Test the merge_response function/filter with empty response should raise."""
service_response = {"calendar.sports": []}
_template = "{{ merge_response(" + str(service_response) + ") }}"
tpl = template.Template(_template, hass)
with pytest.raises(TemplateError, match="TypeError: Response is not a dictionary"):
tpl.async_render()
service_response = {
"binary_sensor.workday": [],
}
_template = "{{ merge_response(" + str(service_response) + ") }}"
tpl = template.Template(_template, hass)
with pytest.raises(TemplateError, match="TypeError: Response is not a dictionary"):
tpl.async_render()
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()
async def test_merge_response_not_mutate_original_object(
hass: HomeAssistant, snapshot: SnapshotAssertion
) -> None:
"""Test the merge_response does not mutate original service response value."""
value = '{"calendar.family": {"events": [{"summary": "An event"}]}'
_template = (
"{% set calendar_response = " + value + "} %}"
"{{ merge_response(calendar_response) }}"
# We should be able to merge the same response again
# as the merge is working on a copy of the original object (response)
"{{ merge_response(calendar_response) }}"
)
tpl = template.Template(_template, hass)
assert tpl.async_render()