1
0
mirror of https://github.com/home-assistant/core.git synced 2026-06-02 21:54:27 +01:00
Files
core/tests/helpers/template/extensions/test_state.py
T
2026-05-17 10:30:45 -04:00

1352 lines
39 KiB
Python

"""Test state functions for Home Assistant templates."""
from collections.abc import Iterable
from unittest.mock import patch
import pytest
from homeassistant.components import group
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
STATE_UNAVAILABLE,
UnitOfArea,
UnitOfLength,
UnitOfMass,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import entity_registry as er, template, translation
from homeassistant.helpers.template.render_info import (
ALL_STATES_RATE_LIMIT,
DOMAIN_STATES_RATE_LIMIT,
)
from homeassistant.setup import async_setup_component
from homeassistant.util.unit_system import UnitSystem
from tests.common import MockConfigEntry
from tests.helpers.template.helpers import assert_result_info, render, render_to_info
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 test_referring_states_by_entity_id(hass: HomeAssistant) -> None:
"""Test referring states by entity id."""
hass.states.async_set("test.object", "happy")
assert render(hass, "{{ states.test.object.state }}") == "happy"
assert render(hass, '{{ states["test.object"].state }}') == "happy"
assert render(hass, '{{ states("test.object") }}') == "happy"
def test_iterating_all_states(hass: HomeAssistant) -> None:
"""Test iterating all states."""
tmpl_str = (
"{% for state in states | sort(attribute='entity_id') %}"
"{{ state.state }}{% endfor %}"
)
info = render_to_info(hass, tmpl_str)
assert_result_info(info, "", all_states=True)
assert info.rate_limit == ALL_STATES_RATE_LIMIT
hass.states.async_set("test.object", "happy")
hass.states.async_set("sensor.temperature", 10)
info = render_to_info(hass, tmpl_str)
assert_result_info(info, "10happy", entities=[], all_states=True)
def test_iterating_all_states_unavailable(hass: HomeAssistant) -> None:
"""Test iterating all states unavailable."""
hass.states.async_set("test.object", "on")
tmpl_str = (
"{{"
" states"
" | selectattr('state', 'in', ['unavailable', 'unknown', 'none'])"
" | list"
" | count"
"}}"
)
info = render_to_info(hass, tmpl_str)
assert info.all_states is True
assert info.rate_limit == ALL_STATES_RATE_LIMIT
hass.states.async_set("test.object", "unknown")
hass.states.async_set("sensor.temperature", 10)
info = render_to_info(hass, tmpl_str)
assert_result_info(info, 1, entities=[], all_states=True)
def test_iterating_domain_states(hass: HomeAssistant) -> None:
"""Test iterating domain states."""
tmpl_str = "{% for state in states.sensor %}{{ state.state }}{% endfor %}"
info = render_to_info(hass, tmpl_str)
assert_result_info(info, "", domains=["sensor"])
assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT
hass.states.async_set("test.object", "happy")
hass.states.async_set("sensor.back_door", "open")
hass.states.async_set("sensor.temperature", 10)
info = render_to_info(hass, tmpl_str)
assert_result_info(
info,
"open10",
entities=[],
domains=["sensor"],
)
def test_if_state_exists(hass: HomeAssistant) -> None:
"""Test if state exists works."""
hass.states.async_set("test.object", "available")
result = render(
hass, "{% if states.test.object %}exists{% else %}not exists{% endif %}"
)
assert result == "exists"
def test_is_state(hass: HomeAssistant) -> None:
"""Test is_state method."""
hass.states.async_set("test.object", "available")
result = render(
hass, '{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}'
)
assert result == "yes"
result = render(hass, """{{ is_state("test.noobject", "available") }}""")
assert result is False
result = render(
hass,
'{% if "test.object" is is_state("available") %}yes{% else %}no{% endif %}',
)
assert result == "yes"
result = render(
hass,
"""{{ ['test.object'] | select("is_state", "available") | first | default }}""",
)
assert result == "test.object"
result = render(hass, '{{ is_state("test.object", ["on", "off", "available"]) }}')
assert result is True
def test_is_state_attr(hass: HomeAssistant) -> None:
"""Test is_state_attr method."""
hass.states.async_set("test.object", "available", {"mode": "on", "exists": None})
result = render(
hass,
'{% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %}',
)
assert result == "yes"
result = render(hass, """{{ is_state_attr("test.noobject", "mode", "on") }}""")
assert result is False
result = render(
hass,
'{% if "test.object" is is_state_attr("mode", "on") %}'
"yes{% else %}no{% endif %}",
)
assert result == "yes"
result = render(
hass,
"{{ ['test.object']"
' | select("is_state_attr", "mode", "on")'
" | first | default }}",
)
assert result == "test.object"
result = render(
hass,
'{% if is_state_attr("test.object", "exists", None) %}'
"yes{% else %}no{% endif %}",
)
assert result == "yes"
result = render(
hass,
'{% if is_state_attr("test.object", "noexist", None) %}'
"yes{% else %}no{% endif %}",
)
assert result == "no"
def test_state_attr(hass: HomeAssistant) -> None:
"""Test state_attr method."""
hass.states.async_set(
"test.object", "available", {"effect": "action", "mode": "on"}
)
result = render(
hass,
'{% if state_attr("test.object", "mode") == "on" %}yes{% else %}no{% endif %}',
)
assert result == "yes"
result = render(hass, """{{ state_attr("test.noobject", "mode") == None }}""")
assert result is True
result = render(
hass,
'{% if "test.object" | state_attr("mode") == "on" %}yes{% else %}no{% endif %}',
)
assert result == "yes"
result = render(
hass,
"""{{ ['test.object'] | map("state_attr", "effect") | first | default }}""",
)
assert result == "action"
def test_states_function(hass: HomeAssistant) -> None:
"""Test using states as a function."""
hass.states.async_set("test.object", "available")
result = render(hass, '{{ states("test.object") }}')
assert result == "available"
result = render(hass, '{{ states("test.object2") }}')
assert result == "unknown"
result = render(
hass,
"""{% if "test.object" | states == "available" %}yes{% else %}no{% endif %}""",
)
assert result == "yes"
result = render(hass, """{{ ['test.object'] | map("states") | first | default }}""")
assert result == "available"
async def test_state_translated(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test state_translated method."""
assert await async_setup_component(
hass,
"binary_sensor",
{
"binary_sensor": {
"platform": "group",
"name": "Grouped",
"entities": ["binary_sensor.first", "binary_sensor.second"],
}
},
)
await hass.async_block_till_done()
await translation._async_get_translations_cache(hass).async_load("en", set())
hass.states.async_set("switch.without_translations", "on", attributes={})
hass.states.async_set("binary_sensor.without_device_class", "on", attributes={})
hass.states.async_set(
"binary_sensor.with_device_class", "on", attributes={"device_class": "motion"}
)
hass.states.async_set(
"binary_sensor.with_unknown_device_class",
"on",
attributes={"device_class": "unknown_class"},
)
hass.states.async_set(
"some_domain.with_device_class_1",
"off",
attributes={"device_class": "some_device_class"},
)
hass.states.async_set(
"some_domain.with_device_class_2",
"foo",
attributes={"device_class": "some_device_class"},
)
hass.states.async_set("domain.is_unavailable", "unavailable", attributes={})
hass.states.async_set("domain.is_unknown", "unknown", attributes={})
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
entity_registry.async_get_or_create(
"light",
"hue",
"5678",
config_entry=config_entry,
translation_key="translation_key",
)
hass.states.async_set("light.hue_5678", "on", attributes={})
result = render(hass, '{{ state_translated("switch.without_translations") }}')
assert result == "on"
result = render(
hass, '{{ state_translated("binary_sensor.without_device_class") }}'
)
assert result == "On"
result = render(hass, '{{ state_translated("binary_sensor.with_device_class") }}')
assert result == "Detected"
result = render(
hass, '{{ state_translated("binary_sensor.with_unknown_device_class") }}'
)
assert result == "On"
with pytest.raises(TemplateError):
render(hass, '{{ state_translated("contextfunction") }}')
result = render(hass, '{{ state_translated("switch.invalid") }}')
assert result == "unknown"
with pytest.raises(TemplateError):
render(hass, '{{ state_translated("-invalid") }}')
def mock_get_cached_translations(
_hass: HomeAssistant,
_language: str,
category: str,
_integrations: Iterable[str] | None = None,
):
if category == "entity":
return {
"component.hue.entity.light.translation_key.state.on": "state_is_on",
}
return {}
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
side_effect=mock_get_cached_translations,
):
result = render(hass, '{{ state_translated("light.hue_5678") }}')
assert result == "state_is_on"
result = render(hass, '{{ state_translated("domain.is_unavailable") }}')
assert result == "unavailable"
result = render(hass, '{{ state_translated("domain.is_unknown") }}')
assert result == "unknown"
async def test_state_attr_translated(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test state_attr_translated method."""
await translation._async_get_translations_cache(hass).async_load("en", set())
hass.states.async_set(
"climate.living_room",
"heat",
attributes={"fan_mode": "auto", "hvac_action": "heating"},
)
hass.states.async_set(
"switch.test",
"on",
attributes={"some_attr": "some_value", "numeric_attr": 42, "bool_attr": True},
)
result = render(
hass,
'{{ state_attr_translated("switch.test", "some_attr") }}',
)
assert result == "some_value"
# Non-string attributes should be returned as-is without type conversion
result = render(
hass,
'{{ state_attr_translated("switch.test", "numeric_attr") }}',
)
assert result == 42
assert isinstance(result, int)
result = render(
hass,
'{{ state_attr_translated("switch.test", "bool_attr") }}',
)
assert result is True
result = render(
hass,
'{{ state_attr_translated("climate.non_existent", "fan_mode") }}',
)
assert result is None
with pytest.raises(TemplateError):
render(hass, '{{ state_attr_translated("-invalid", "fan_mode") }}')
result = render(
hass,
'{{ state_attr_translated("climate.living_room", "non_existent") }}',
)
assert result is None
@pytest.mark.parametrize(
(
"entity_id",
"attribute",
"translations",
"expected_result",
),
[
(
"climate.test_platform_5678",
"fan_mode",
{
"component.test_platform.entity.climate"
".my_climate.state_attributes"
".fan_mode.state.auto": "Platform Automatic",
},
"Platform Automatic",
),
(
"climate.living_room",
"fan_mode",
{
"component.climate.entity_component"
"._.state_attributes"
".fan_mode.state.auto": "Automatic",
},
"Automatic",
),
(
"climate.living_room",
"hvac_action",
{
"component.climate.entity_component"
"._.state_attributes"
".hvac_action.state.heating": "Heating",
},
"Heating",
),
],
)
async def test_state_attr_translated_translation_lookups(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
entity_id: str,
attribute: str,
translations: dict[str, str],
expected_result: str,
) -> None:
"""Test state_attr_translated translation lookups."""
await translation._async_get_translations_cache(hass).async_load("en", set())
hass.states.async_set(
"climate.living_room",
"heat",
attributes={"fan_mode": "auto", "hvac_action": "heating"},
)
config_entry = MockConfigEntry(domain="climate")
config_entry.add_to_hass(hass)
entity_registry.async_get_or_create(
"climate",
"test_platform",
"5678",
config_entry=config_entry,
translation_key="my_climate",
)
hass.states.async_set(
"climate.test_platform_5678",
"heat",
attributes={"fan_mode": "auto"},
)
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value=translations,
):
result = render(
hass,
f'{{{{ state_attr_translated("{entity_id}", "{attribute}") }}}}',
)
assert result == expected_result
def test_has_value(hass: HomeAssistant) -> None:
"""Test has_value method."""
hass.states.async_set("test.value1", 1)
hass.states.async_set("test.unavailable", STATE_UNAVAILABLE)
result = render(hass, """{{ has_value("test.value1") }}""")
assert result is True
result = render(hass, """{{ has_value("test.unavailable") }}""")
assert result is False
result = render(hass, """{{ has_value("test.unknown") }}""")
assert result is False
result = render(
hass, """{% if "test.value1" is has_value %}yes{% else %}no{% endif %}"""
)
assert result == "yes"
def test_distance_function_with_1_state(hass: HomeAssistant) -> None:
"""Test distance function with 1 state."""
_set_up_units(hass)
hass.states.async_set(
"test.object", "happy", {"latitude": 32.87336, "longitude": -117.22943}
)
result = render(hass, "{{ distance(states.test.object) | round }}")
assert result == 187
def test_distance_function_with_2_states(hass: HomeAssistant) -> None:
"""Test distance function with 2 states."""
_set_up_units(hass)
hass.states.async_set(
"test.object", "happy", {"latitude": 32.87336, "longitude": -117.22943}
)
hass.states.async_set(
"test.object_2",
"happy",
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
)
result = render(
hass, "{{ distance(states.test.object, states.test.object_2) | round }}"
)
assert result == 187
def test_distance_function_with_1_coord(hass: HomeAssistant) -> None:
"""Test distance function with 1 coord."""
_set_up_units(hass)
result = render(hass, '{{ distance("32.87336", "-117.22943") | round }}')
assert result == 187
def test_distance_function_with_2_coords(hass: HomeAssistant) -> None:
"""Test distance function with 2 coords."""
_set_up_units(hass)
tpl = (
f'{{{{ distance("32.87336", "-117.22943",'
f" {hass.config.latitude}, {hass.config.longitude})"
f" | round }}}}"
)
assert render(hass, tpl) == 187
def test_distance_function_with_1_state_1_coord(hass: HomeAssistant) -> None:
"""Test distance function with 1 state 1 coord."""
_set_up_units(hass)
hass.states.async_set(
"test.object_2",
"happy",
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
)
result = render(
hass, '{{ distance("32.87336", "-117.22943", states.test.object_2) | round }}'
)
assert result == 187
result = render(
hass, '{{ distance(states.test.object_2, "32.87336", "-117.22943") | round }}'
)
assert result == 187
def test_distance_function_return_none_if_invalid_state(hass: HomeAssistant) -> None:
"""Test distance function return None if invalid state."""
hass.states.async_set("test.object_2", "happy", {"latitude": 10})
with pytest.raises(TemplateError):
render(hass, "{{ distance(states.test.object_2) | round }}")
def test_distance_function_return_none_if_invalid_coord(hass: HomeAssistant) -> None:
"""Test distance function return None if invalid coord."""
assert render(hass, '{{ distance("123", "abc") }}') is None
assert render(hass, '{{ distance("123") }}') is None
hass.states.async_set(
"test.object_2",
"happy",
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
)
result = render(hass, '{{ distance("123", states.test_object_2) }}')
assert result is None
def test_distance_function_with_2_entity_ids(hass: HomeAssistant) -> None:
"""Test distance function with 2 entity ids."""
_set_up_units(hass)
hass.states.async_set(
"test.object", "happy", {"latitude": 32.87336, "longitude": -117.22943}
)
hass.states.async_set(
"test.object_2",
"happy",
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
)
result = render(hass, '{{ distance("test.object", "test.object_2") | round }}')
assert result == 187
def test_distance_function_with_1_entity_1_coord(hass: HomeAssistant) -> None:
"""Test distance function with 1 entity_id and 1 coord."""
_set_up_units(hass)
hass.states.async_set(
"test.object",
"happy",
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
)
result = render(
hass, '{{ distance("test.object", "32.87336", "-117.22943") | round }}'
)
assert result == 187
def test_closest_function_home_vs_domain(hass: HomeAssistant) -> None:
"""Test closest function home vs domain."""
hass.states.async_set(
"test_domain.object",
"happy",
{
"latitude": hass.config.latitude + 0.1,
"longitude": hass.config.longitude + 0.1,
},
)
hass.states.async_set(
"not_test_domain.but_closer",
"happy",
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
)
assert (
render(hass, "{{ closest(states.test_domain).entity_id }}")
== "test_domain.object"
)
assert (
render(hass, "{{ (states.test_domain | closest).entity_id }}")
== "test_domain.object"
)
def test_closest_function_home_vs_all_states(hass: HomeAssistant) -> None:
"""Test closest function home vs all states."""
hass.states.async_set(
"test_domain.object",
"happy",
{
"latitude": hass.config.latitude + 0.1,
"longitude": hass.config.longitude + 0.1,
},
)
hass.states.async_set(
"test_domain_2.and_closer",
"happy",
{"latitude": hass.config.latitude, "longitude": hass.config.longitude},
)
assert render(hass, "{{ closest(states).entity_id }}") == "test_domain_2.and_closer"
assert (
render(hass, "{{ (states | closest).entity_id }}") == "test_domain_2.and_closer"
)
async def test_expand(hass: HomeAssistant) -> None:
"""Test expand function."""
info = render_to_info(hass, "{{ expand('test.object') }}")
assert_result_info(info, [], ["test.object"])
assert info.rate_limit is None
info = render_to_info(hass, "{{ expand(56) }}")
assert_result_info(info, [])
assert info.rate_limit is None
hass.states.async_set("test.object", "happy")
info = render_to_info(
hass,
(
"{{ expand('test.object')"
" | sort(attribute='entity_id')"
" | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(info, "test.object", ["test.object"])
assert info.rate_limit is None
info = render_to_info(
hass,
(
"{{ expand('group.new_group')"
" | sort(attribute='entity_id')"
" | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(info, "", ["group.new_group"])
assert info.rate_limit is None
info = render_to_info(
hass,
(
"{{ expand(states.group)"
" | sort(attribute='entity_id')"
" | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(info, "", [], ["group"])
assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT
assert await async_setup_component(hass, "group", {})
await hass.async_block_till_done()
await group.Group.async_create_group(
hass,
"new group",
created_by_service=False,
entity_ids=["test.object"],
icon=None,
mode=None,
object_id=None,
order=None,
)
info = render_to_info(
hass,
(
"{{ expand('group.new_group')"
" | sort(attribute='entity_id')"
" | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(info, "test.object", {"group.new_group", "test.object"})
assert info.rate_limit is None
info = render_to_info(
hass,
(
"{{ expand(states.group)"
" | sort(attribute='entity_id')"
" | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(info, "test.object", {"test.object"}, ["group"])
assert info.rate_limit == DOMAIN_STATES_RATE_LIMIT
info = render_to_info(
hass,
(
"{{ expand('group.new_group', 'test.object')"
" | sort(attribute='entity_id')"
" | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(info, "test.object", {"test.object", "group.new_group"})
info = render_to_info(
hass,
(
"{{ ['group.new_group', 'test.object'] | expand"
" | sort(attribute='entity_id')"
" | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(info, "test.object", {"test.object", "group.new_group"})
assert info.rate_limit is None
hass.states.async_set("sensor.power_1", 0)
hass.states.async_set("sensor.power_2", 200.2)
hass.states.async_set("sensor.power_3", 400.4)
assert await async_setup_component(hass, "group", {})
await hass.async_block_till_done()
await group.Group.async_create_group(
hass,
"power sensors",
created_by_service=False,
entity_ids=["sensor.power_1", "sensor.power_2", "sensor.power_3"],
icon=None,
mode=None,
object_id=None,
order=None,
)
info = render_to_info(
hass,
(
"{{ states.group.power_sensors.attributes"
".entity_id | expand"
" | sort(attribute='entity_id')"
" | map(attribute='state')"
"|map('float')|sum }}"
),
)
assert_result_info(
info,
200.2 + 400.4,
{"group.power_sensors", "sensor.power_1", "sensor.power_2", "sensor.power_3"},
)
assert info.rate_limit is None
# With group entities
hass.states.async_set("light.first", "on")
hass.states.async_set("light.second", "off")
assert await async_setup_component(
hass,
"light",
{
"light": {
"platform": "group",
"name": "Grouped",
"entities": ["light.first", "light.second"],
}
},
)
await hass.async_block_till_done()
info = render_to_info(
hass,
(
"{{ expand('light.grouped')"
" | sort(attribute='entity_id')"
" | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(
info,
"light.first, light.second",
["light.grouped", "light.first", "light.second"],
)
assert await async_setup_component(
hass,
"zone",
{
"zone": {
"name": "Test",
"latitude": 32.880837,
"longitude": -117.237561,
"radius": 250,
"passive": False,
}
},
)
info = render_to_info(
hass,
(
"{{ expand('zone.test')"
" | sort(attribute='entity_id')"
" | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(
info,
"",
["zone.test"],
)
hass.states.async_set(
"person.person1",
"test",
)
await hass.async_block_till_done()
info = render_to_info(
hass,
(
"{{ expand('zone.test')"
" | sort(attribute='entity_id')"
" | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(
info,
"person.person1",
["zone.test", "person.person1"],
)
hass.states.async_set(
"person.person2",
"test",
)
await hass.async_block_till_done()
info = render_to_info(
hass,
(
"{{ expand('zone.test')"
" | sort(attribute='entity_id')"
" | map(attribute='entity_id') | join(', ') }}"
),
)
assert_result_info(
info,
"person.person1, person.person2",
["zone.test", "person.person1", "person.person2"],
)
def test_closest_function_to_coord(hass: HomeAssistant) -> None:
"""Test closest function to coord."""
hass.states.async_set(
"test_domain.closest_home",
"happy",
{
"latitude": hass.config.latitude + 0.1,
"longitude": hass.config.longitude + 0.1,
},
)
hass.states.async_set(
"test_domain.closest_zone",
"happy",
{
"latitude": hass.config.latitude + 0.2,
"longitude": hass.config.longitude + 0.2,
},
)
hass.states.async_set(
"zone.far_away",
"zoning",
{
"latitude": hass.config.latitude + 0.3,
"longitude": hass.config.longitude + 0.3,
},
)
result = render(
hass,
f'{{{{ closest("{hass.config.latitude + 0.3}",'
f" {hass.config.longitude + 0.3},"
f" states.test_domain).entity_id }}}}",
)
assert result == "test_domain.closest_zone"
result = render(
hass,
f'{{{{ (states.test_domain | closest("{hass.config.latitude + 0.3}",'
f" {hass.config.longitude + 0.3})).entity_id }}}}",
)
assert result == "test_domain.closest_zone"
def test_closest_function_to_entity_id(hass: HomeAssistant) -> None:
"""Test closest function to entity id."""
hass.states.async_set(
"test_domain.closest_home",
"happy",
{
"latitude": hass.config.latitude + 0.1,
"longitude": hass.config.longitude + 0.1,
},
)
hass.states.async_set(
"test_domain.closest_zone",
"happy",
{
"latitude": hass.config.latitude + 0.2,
"longitude": hass.config.longitude + 0.2,
},
)
hass.states.async_set(
"zone.far_away",
"zoning",
{
"latitude": hass.config.latitude + 0.3,
"longitude": hass.config.longitude + 0.3,
},
)
info = render_to_info(
hass,
"{{ closest(zone, states.test_domain).entity_id }}",
{"zone": "zone.far_away"},
)
assert_result_info(
info,
"test_domain.closest_zone",
["test_domain.closest_home", "test_domain.closest_zone", "zone.far_away"],
["test_domain"],
)
info = render_to_info(
hass,
(
"{{ ([states.test_domain, 'test_domain.closest_zone'] "
"| closest(zone)).entity_id }}"
),
{"zone": "zone.far_away"},
)
assert_result_info(
info,
"test_domain.closest_zone",
["test_domain.closest_home", "test_domain.closest_zone", "zone.far_away"],
["test_domain"],
)
def test_closest_function_to_state(hass: HomeAssistant) -> None:
"""Test closest function to state."""
hass.states.async_set(
"test_domain.closest_home",
"happy",
{
"latitude": hass.config.latitude + 0.1,
"longitude": hass.config.longitude + 0.1,
},
)
hass.states.async_set(
"test_domain.closest_zone",
"happy",
{
"latitude": hass.config.latitude + 0.2,
"longitude": hass.config.longitude + 0.2,
},
)
hass.states.async_set(
"zone.far_away",
"zoning",
{
"latitude": hass.config.latitude + 0.3,
"longitude": hass.config.longitude + 0.3,
},
)
assert (
render(
hass, "{{ closest(states.zone.far_away, states.test_domain).entity_id }}"
)
== "test_domain.closest_zone"
)
def test_closest_function_invalid_state(hass: HomeAssistant) -> None:
"""Test closest function invalid state."""
hass.states.async_set(
"test_domain.closest_home",
"happy",
{
"latitude": hass.config.latitude + 0.1,
"longitude": hass.config.longitude + 0.1,
},
)
for state in ("states.zone.non_existing", '"zone.non_existing"'):
assert render(hass, f"{{{{ closest({state}, states) }}}}") is None
def test_closest_function_state_with_invalid_location(hass: HomeAssistant) -> None:
"""Test closest function state with invalid location."""
hass.states.async_set(
"test_domain.closest_home",
"happy",
{"latitude": "invalid latitude", "longitude": hass.config.longitude + 0.1},
)
assert (
render(hass, "{{ closest(states.test_domain.closest_home, states) }}") is None
)
def test_closest_function_invalid_coordinates(hass: HomeAssistant) -> None:
"""Test closest function invalid coordinates."""
hass.states.async_set(
"test_domain.closest_home",
"happy",
{
"latitude": hass.config.latitude + 0.1,
"longitude": hass.config.longitude + 0.1,
},
)
assert render(hass, '{{ closest("invalid", "coord", states) }}') is None
assert render(hass, '{{ states | closest("invalid", "coord") }}') is None
def test_closest_function_no_location_states(hass: HomeAssistant) -> None:
"""Test closest function without location states."""
assert render(hass, "{{ closest(states).entity_id }}") == ""
def test_generate_filter_iterators(hass: HomeAssistant) -> None:
"""Test extract entities function with none entities stuff."""
info = render_to_info(
hass,
"""
{% for state in states %}
{{ state.entity_id }}
{% endfor %}
""",
)
assert_result_info(info, "", all_states=True)
info = render_to_info(
hass,
"""
{% for state in states.sensor %}
{{ state.entity_id }}
{% endfor %}
""",
)
assert_result_info(info, "", domains=["sensor"])
hass.states.async_set("sensor.test_sensor", "off", {"attr": "value"})
# Don't need the entity because the state is not accessed
info = render_to_info(
hass,
"""
{% for state in states.sensor %}
{{ state.entity_id }}
{% endfor %}
""",
)
assert_result_info(info, "sensor.test_sensor", domains=["sensor"])
# But we do here because the state gets accessed
info = render_to_info(
hass,
"""
{% for state in states.sensor %}
{{ state.entity_id }}={{ state.state }},
{% endfor %}
""",
)
assert_result_info(info, "sensor.test_sensor=off,", [], ["sensor"])
info = render_to_info(
hass,
"""
{% for state in states.sensor %}
{{ state.entity_id }}={{ state.attributes.attr }},
{% endfor %}
""",
)
assert_result_info(info, "sensor.test_sensor=value,", [], ["sensor"])
def test_generate_select(hass: HomeAssistant) -> None:
"""Test extract entities function with none entities stuff."""
template_str = """
{{ states.sensor|selectattr("state","equalto","off")
|join(",", attribute="entity_id") }}
"""
info = render_to_info(hass, template_str)
assert_result_info(info, "", [], [])
assert info.domains_lifecycle == {"sensor"}
hass.states.async_set("sensor.test_sensor", "off", {"attr": "value"})
hass.states.async_set("sensor.test_sensor_on", "on")
info = render_to_info(hass, template_str)
assert_result_info(
info,
"sensor.test_sensor",
[],
["sensor"],
)
assert info.domains_lifecycle == {"sensor"}
def test_state_with_unit(hass: HomeAssistant) -> None:
"""Test the state_with_unit property helper."""
hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
hass.states.async_set("sensor.test2", "wow")
result = render(hass, "{{ states.sensor.test.state_with_unit }}")
assert result == "23 beers"
result = render(hass, "{{ states.sensor.test2.state_with_unit }}")
assert result == "wow"
result = render(
hass, "{% for state in states %}{{ state.state_with_unit }} {% endfor %}"
)
assert result == "23 beers wow"
result = render(hass, "{{ states.sensor.non_existing.state_with_unit }}")
assert result == ""
def test_state_with_unit_and_rounding(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test formatting the state rounded and with unit."""
entry = entity_registry.async_get_or_create(
"sensor", "test", "very_unique", suggested_object_id="test"
)
entity_registry.async_update_entity_options(
entry.entity_id,
"sensor",
{
"suggested_display_precision": 2,
},
)
assert entry.entity_id == "sensor.test"
hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
hass.states.async_set("sensor.test2", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
hass.states.async_set("sensor.test3", "-0.0", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
hass.states.async_set("sensor.test4", "-0", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
# state_with_unit property
tpl = template.Template("{{ states.sensor.test.state_with_unit }}", hass)
tpl2 = template.Template("{{ states.sensor.test2.state_with_unit }}", hass)
# AllStates.__call__ defaults
tpl3 = template.Template("{{ states('sensor.test') }}", hass)
tpl4 = template.Template("{{ states('sensor.test2') }}", hass)
# AllStates.__call__ and with_unit=True
tpl5 = template.Template("{{ states('sensor.test', with_unit=True) }}", hass)
tpl6 = template.Template("{{ states('sensor.test2', with_unit=True) }}", hass)
# AllStates.__call__ and rounded=True
tpl7 = template.Template("{{ states('sensor.test', rounded=True) }}", hass)
tpl8 = template.Template("{{ states('sensor.test2', rounded=True) }}", hass)
tpl9 = template.Template("{{ states('sensor.test3', rounded=True) }}", hass)
tpl10 = template.Template("{{ states('sensor.test4', rounded=True) }}", hass)
assert tpl.async_render() == "23.00 beers"
assert tpl2.async_render() == "23 beers"
assert tpl3.async_render() == 23
assert tpl4.async_render() == 23
assert tpl5.async_render() == "23.00 beers"
assert tpl6.async_render() == "23 beers"
assert tpl7.async_render() == 23.0
assert tpl8.async_render() == 23
assert tpl9.async_render() == 0.0
assert tpl10.async_render() == 0
hass.states.async_set("sensor.test", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
hass.states.async_set("sensor.test2", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"})
assert tpl.async_render() == "23.02 beers"
assert tpl2.async_render() == "23.015 beers"
assert tpl3.async_render() == 23.015
assert tpl4.async_render() == 23.015
assert tpl5.async_render() == "23.02 beers"
assert tpl6.async_render() == "23.015 beers"
assert tpl7.async_render() == 23.02
assert tpl8.async_render() == 23.015
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