mirror of
https://github.com/home-assistant/core.git
synced 2026-05-20 07:20:14 +01:00
Extract type casting template functions into a type cast Jinja2 extension (#167280)
This commit is contained in:
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal, DecimalException, InvalidOperation
|
||||
import logging
|
||||
import math
|
||||
from typing import Any, Self
|
||||
|
||||
from cronsim import CronSim
|
||||
@@ -52,7 +53,6 @@ from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
)
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.helpers.template import is_number
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
@@ -113,8 +113,11 @@ COLLECTING = "collecting"
|
||||
|
||||
def validate_is_number(value):
|
||||
"""Validate value is a number."""
|
||||
if is_number(value):
|
||||
return value
|
||||
try:
|
||||
if math.isfinite(float(value)):
|
||||
return value
|
||||
except ValueError, TypeError:
|
||||
pass
|
||||
raise vol.Invalid("Value is not a number")
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ from jinja2.utils import Namespace
|
||||
from lru import LRU
|
||||
import orjson
|
||||
from propcache.api import under_cached_property
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
@@ -76,7 +75,7 @@ from .context import (
|
||||
template_context_manager,
|
||||
template_cv,
|
||||
)
|
||||
from .helpers import raise_no_default
|
||||
from .helpers import raise_no_default, result_as_boolean as result_as_boolean
|
||||
from .render_info import RenderInfo, render_info_cv
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -1127,42 +1126,6 @@ def _resolve_state(
|
||||
return None
|
||||
|
||||
|
||||
@overload
|
||||
def forgiving_boolean(value: Any) -> bool | object: ...
|
||||
|
||||
|
||||
@overload
|
||||
def forgiving_boolean[_T](value: Any, default: _T) -> bool | _T: ...
|
||||
|
||||
|
||||
def forgiving_boolean[_T](
|
||||
value: Any, default: _T | object = _SENTINEL
|
||||
) -> bool | _T | object:
|
||||
"""Try to convert value to a boolean."""
|
||||
try:
|
||||
# Import here, not at top-level to avoid circular import
|
||||
from homeassistant.helpers import config_validation as cv # noqa: PLC0415
|
||||
|
||||
return cv.boolean(value)
|
||||
except vol.Invalid:
|
||||
if default is _SENTINEL:
|
||||
raise_no_default("bool", value)
|
||||
return default
|
||||
|
||||
|
||||
def result_as_boolean(template_result: Any | None) -> bool:
|
||||
"""Convert the template result to a boolean.
|
||||
|
||||
True/not 0/'1'/'true'/'yes'/'on'/'enable' are considered truthy
|
||||
False/0/None/'0'/'false'/'no'/'off'/'disable' are considered falsy
|
||||
All other values are falsy
|
||||
"""
|
||||
if template_result is None:
|
||||
return False
|
||||
|
||||
return forgiving_boolean(template_result, default=False)
|
||||
|
||||
|
||||
def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]:
|
||||
"""Expand out any groups and zones into entity states."""
|
||||
# circular import.
|
||||
@@ -1601,58 +1564,6 @@ def fail_when_undefined(value):
|
||||
return value
|
||||
|
||||
|
||||
def forgiving_float(value, default=_SENTINEL):
|
||||
"""Try to convert value to a float."""
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError, TypeError:
|
||||
if default is _SENTINEL:
|
||||
raise_no_default("float", value)
|
||||
return default
|
||||
|
||||
|
||||
def forgiving_float_filter(value, default=_SENTINEL):
|
||||
"""Try to convert value to a float."""
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError, TypeError:
|
||||
if default is _SENTINEL:
|
||||
raise_no_default("float", value)
|
||||
return default
|
||||
|
||||
|
||||
def forgiving_int(value, default=_SENTINEL, base=10):
|
||||
"""Try to convert value to an int, and raise if it fails."""
|
||||
result = jinja2.filters.do_int(value, default=default, base=base)
|
||||
if result is _SENTINEL:
|
||||
raise_no_default("int", value)
|
||||
return result
|
||||
|
||||
|
||||
def forgiving_int_filter(value, default=_SENTINEL, base=10):
|
||||
"""Try to convert value to an int, and raise if it fails."""
|
||||
result = jinja2.filters.do_int(value, default=default, base=base)
|
||||
if result is _SENTINEL:
|
||||
raise_no_default("int", value)
|
||||
return result
|
||||
|
||||
|
||||
def is_number(value):
|
||||
"""Try to convert value to a float."""
|
||||
try:
|
||||
fvalue = float(value)
|
||||
except ValueError, TypeError:
|
||||
return False
|
||||
if not math.isfinite(fvalue):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
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 struct_pack(value: Any | None, format_string: str) -> bytes | None:
|
||||
"""Pack an object into a bytes object."""
|
||||
try:
|
||||
@@ -1944,15 +1855,14 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
self.add_extension("homeassistant.helpers.template.extensions.MathExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.RegexExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.StringExtension")
|
||||
self.add_extension(
|
||||
"homeassistant.helpers.template.extensions.TypeCastExtension"
|
||||
)
|
||||
|
||||
self.globals["apply"] = apply
|
||||
self.globals["as_function"] = as_function
|
||||
self.globals["bool"] = forgiving_boolean
|
||||
self.globals["combine"] = combine
|
||||
self.globals["float"] = forgiving_float
|
||||
self.globals["iif"] = iif
|
||||
self.globals["int"] = forgiving_int
|
||||
self.globals["is_number"] = is_number
|
||||
self.globals["merge_response"] = merge_response
|
||||
self.globals["pack"] = struct_pack
|
||||
self.globals["typeof"] = typeof
|
||||
@@ -1963,16 +1873,12 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
self.filters["add"] = add
|
||||
self.filters["apply"] = apply
|
||||
self.filters["as_function"] = as_function
|
||||
self.filters["bool"] = forgiving_boolean
|
||||
self.filters["combine"] = combine
|
||||
self.filters["contains"] = contains
|
||||
self.filters["float"] = forgiving_float_filter
|
||||
self.filters["from_json"] = from_json
|
||||
self.filters["from_hex"] = from_hex
|
||||
self.filters["iif"] = iif
|
||||
self.filters["int"] = forgiving_int_filter
|
||||
self.filters["is_defined"] = fail_when_undefined
|
||||
self.filters["is_number"] = is_number
|
||||
self.filters["multiply"] = multiply
|
||||
self.filters["ord"] = ord
|
||||
self.filters["pack"] = struct_pack
|
||||
@@ -1985,8 +1891,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
|
||||
self.tests["apply"] = apply
|
||||
self.tests["contains"] = contains
|
||||
self.tests["is_number"] = is_number
|
||||
self.tests["string_like"] = _is_string_like
|
||||
|
||||
if hass is None:
|
||||
return
|
||||
|
||||
@@ -12,6 +12,7 @@ from .labels import LabelExtension
|
||||
from .math import MathExtension
|
||||
from .regex import RegexExtension
|
||||
from .string import StringExtension
|
||||
from .type_cast import TypeCastExtension
|
||||
|
||||
__all__ = [
|
||||
"AreaExtension",
|
||||
@@ -26,4 +27,5 @@ __all__ = [
|
||||
"MathExtension",
|
||||
"RegexExtension",
|
||||
"StringExtension",
|
||||
"TypeCastExtension",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Type casting functions for Home Assistant templates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import jinja2.filters
|
||||
|
||||
from homeassistant.helpers.template.helpers import forgiving_boolean, raise_no_default
|
||||
|
||||
from .base import BaseTemplateExtension, TemplateFunction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.helpers.template import TemplateEnvironment
|
||||
|
||||
_SENTINEL = object()
|
||||
|
||||
|
||||
class TypeCastExtension(BaseTemplateExtension):
|
||||
"""Jinja2 extension for type casting functions."""
|
||||
|
||||
def __init__(self, environment: TemplateEnvironment) -> None:
|
||||
"""Initialize the type cast extension."""
|
||||
super().__init__(
|
||||
environment,
|
||||
functions=[
|
||||
TemplateFunction(
|
||||
"bool",
|
||||
forgiving_boolean,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"float",
|
||||
self.forgiving_float,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"int",
|
||||
self.forgiving_int,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"is_number",
|
||||
self.is_number,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
as_test=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"string_like",
|
||||
self.is_string_like,
|
||||
as_test=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def forgiving_float(value: Any, default: Any = _SENTINEL) -> Any:
|
||||
"""Try to convert value to a float."""
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError, TypeError:
|
||||
if default is _SENTINEL:
|
||||
raise_no_default("float", value)
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def forgiving_int(value: Any, default: Any = _SENTINEL, base: int = 10) -> Any:
|
||||
"""Try to convert value to an int, and raise if it fails."""
|
||||
result = jinja2.filters.do_int(value, default=default, base=base)
|
||||
if result is _SENTINEL:
|
||||
raise_no_default("int", value)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def is_number(value: Any) -> bool:
|
||||
"""Try to convert value to a float."""
|
||||
try:
|
||||
fvalue = float(value)
|
||||
except ValueError, TypeError:
|
||||
return False
|
||||
if not math.isfinite(fvalue):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_string_like(value: Any) -> bool:
|
||||
"""Return whether a value is a string or string like object."""
|
||||
return isinstance(value, (str, bytes, bytearray))
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, NoReturn
|
||||
from typing import TYPE_CHECKING, Any, NoReturn, overload
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
@@ -17,6 +18,8 @@ from .context import template_cv
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
_SENTINEL = object()
|
||||
|
||||
|
||||
def raise_no_default(function: str, value: Any) -> NoReturn:
|
||||
"""Raise ValueError when no default is specified for template functions."""
|
||||
@@ -47,9 +50,6 @@ def resolve_area_id(hass: HomeAssistant, lookup_value: Any) -> str | None:
|
||||
if areas_list:
|
||||
return areas_list[0].id
|
||||
|
||||
# Import here, not at top-level to avoid circular import
|
||||
from homeassistant.helpers import config_validation as cv # noqa: PLC0415
|
||||
|
||||
# Check if it's an entity ID
|
||||
try:
|
||||
cv.entity_id(lookup_value)
|
||||
@@ -69,3 +69,36 @@ def resolve_area_id(hass: HomeAssistant, lookup_value: Any) -> str | None:
|
||||
return device.area_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@overload
|
||||
def forgiving_boolean(value: Any) -> bool | object: ...
|
||||
|
||||
|
||||
@overload
|
||||
def forgiving_boolean[_T](value: Any, default: _T) -> bool | _T: ...
|
||||
|
||||
|
||||
def forgiving_boolean[_T](
|
||||
value: Any, default: _T | object = _SENTINEL
|
||||
) -> bool | _T | object:
|
||||
"""Try to convert value to a boolean."""
|
||||
try:
|
||||
return cv.boolean(value)
|
||||
except vol.Invalid:
|
||||
if default is _SENTINEL:
|
||||
raise_no_default("bool", value)
|
||||
return default
|
||||
|
||||
|
||||
def result_as_boolean(template_result: Any | None) -> bool:
|
||||
"""Convert the template result to a boolean.
|
||||
|
||||
True/not 0/'1'/'true'/'yes'/'on'/'enable' are considered truthy
|
||||
False/0/None/'0'/'false'/'no'/'off'/'disable' are considered falsy
|
||||
All other values are falsy
|
||||
"""
|
||||
if template_result is None:
|
||||
return False
|
||||
|
||||
return forgiving_boolean(template_result, default=False)
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
"""Test type casting functions for Home Assistant templates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import TemplateError
|
||||
|
||||
from tests.helpers.template.helpers import render
|
||||
|
||||
|
||||
def test_float_function(hass: HomeAssistant) -> None:
|
||||
"""Test float function."""
|
||||
hass.states.async_set("sensor.temperature", "12")
|
||||
|
||||
assert render(hass, "{{ float(states.sensor.temperature.state) }}") == 12.0
|
||||
|
||||
assert render(hass, "{{ float(states.sensor.temperature.state) > 11 }}") is True
|
||||
|
||||
# Test handling of invalid input
|
||||
with pytest.raises(TemplateError):
|
||||
render(hass, "{{ float('forgiving') }}")
|
||||
|
||||
# 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: object, expected: bool) -> None:
|
||||
"""Test is_number."""
|
||||
assert render(hass, "{{ is_number(value) }}", {"value": value}) == expected
|
||||
assert render(hass, "{{ value | is_number }}", {"value": value}) == expected
|
||||
assert render(hass, "{{ value is is_number }}", {"value": value}) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("hello", True),
|
||||
(b"hello", True),
|
||||
(bytearray(b"hello"), True),
|
||||
(42, False),
|
||||
([1, 2], False),
|
||||
(None, False),
|
||||
],
|
||||
)
|
||||
def test_string_like(hass: HomeAssistant, value: object, expected: bool) -> None:
|
||||
"""Test string_like."""
|
||||
assert render(hass, "{{ value is string_like }}", {"value": value}) == expected
|
||||
@@ -6,7 +6,6 @@ from collections.abc import Iterable
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -299,131 +298,6 @@ def test_loop_controls(hass: HomeAssistant) -> None:
|
||||
assert render(hass, tpl) == "02"
|
||||
|
||||
|
||||
def test_float_function(hass: HomeAssistant) -> None:
|
||||
"""Test float function."""
|
||||
hass.states.async_set("sensor.temperature", "12")
|
||||
|
||||
assert render(hass, "{{ float(states.sensor.temperature.state) }}") == 12.0
|
||||
|
||||
assert render(hass, "{{ float(states.sensor.temperature.state) > 11 }}") is True
|
||||
|
||||
# Test handling of invalid input
|
||||
with pytest.raises(TemplateError):
|
||||
render(hass, "{{ float('forgiving') }}")
|
||||
|
||||
# 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 render(hass, "{{ is_number(value) }}", {"value": value}) == expected
|
||||
assert render(hass, "{{ value | is_number }}", {"value": value}) == expected
|
||||
assert render(hass, "{{ value is is_number }}", {"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)
|
||||
|
||||
Reference in New Issue
Block a user