diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 26b0674a3512ed..06280a26ccd6a9 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1956,6 +1956,41 @@ def is_number(value): return True +def _is_list(value: Any) -> bool: + """Return whether a value is a list.""" + return isinstance(value, list) + + +def _is_set(value: Any) -> bool: + """Return whether a value is a set.""" + return isinstance(value, set) + + +def _is_tuple(value: Any) -> bool: + """Return whether a value is a tuple.""" + return isinstance(value, tuple) + + +def _to_set(value: Any) -> set[Any]: + """Convert value to set.""" + return set(value) + + +def _to_tuple(value): + """Convert value to tuple.""" + return tuple(value) + + +def _is_datetime(value: Any) -> bool: + """Return whether a value is a datetime.""" + return isinstance(value, datetime) + + +def _is_string_like(value: Any) -> bool: + """Return whether a value is a string or string like object.""" + return isinstance(value, (str, bytes, bytearray)) + + def regex_match(value, find="", ignorecase=False): """Match value using regex.""" if not isinstance(value, str): @@ -2387,6 +2422,8 @@ def __init__( self.globals["max"] = min_max_from_filter(self.filters["max"], "max") self.globals["min"] = min_max_from_filter(self.filters["min"], "min") self.globals["is_number"] = is_number + self.globals["set"] = _to_set + self.globals["tuple"] = _to_tuple self.globals["int"] = forgiving_int self.globals["pack"] = struct_pack self.globals["unpack"] = struct_unpack @@ -2395,6 +2432,11 @@ def __init__( self.globals["bool"] = forgiving_boolean self.globals["version"] = version self.tests["is_number"] = is_number + self.tests["list"] = _is_list + self.tests["set"] = _is_set + self.tests["tuple"] = _is_tuple + self.tests["datetime"] = _is_datetime + self.tests["string_like"] = _is_string_like self.tests["match"] = regex_match self.tests["search"] = regex_search self.tests["contains"] = contains diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 58e0c730165b50..c466bfed213a64 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -7,6 +7,7 @@ import logging import math import random +from types import MappingProxyType from typing import Any from unittest.mock import patch @@ -43,6 +44,7 @@ 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 @@ -475,6 +477,171 @@ def test_isnumber(hass: HomeAssistant, value, expected) -> None: ) +@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)