1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Extract date/time template functions into an datetime Jinja2 extension (#157042)

This commit is contained in:
Franck Nijhof
2025-11-23 11:47:49 +01:00
committed by GitHub
parent 3ef62c97ca
commit d97998e2e1
5 changed files with 1121 additions and 1005 deletions

View File

@@ -7,7 +7,7 @@ import asyncio
import collections.abc
from collections.abc import Callable, Generator, Iterable
from copy import deepcopy
from datetime import date, datetime, time, timedelta
from datetime import datetime, timedelta
from functools import cache, lru_cache, partial, wraps
import json
import logging
@@ -63,7 +63,7 @@ from homeassistant.helpers import (
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.translation import async_translate_state
from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.util import convert, dt as dt_util, location as location_util
from homeassistant.util import convert, location as location_util
from homeassistant.util.async_ import run_callback_threadsafe
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
@@ -1417,22 +1417,6 @@ def has_value(hass: HomeAssistant, entity_id: str) -> bool:
)
def now(hass: HomeAssistant) -> datetime:
"""Record fetching now."""
if (render_info := render_info_cv.get()) is not None:
render_info.has_time = True
return dt_util.now()
def utcnow(hass: HomeAssistant) -> datetime:
"""Record fetching utcnow."""
if (render_info := render_info_cv.get()) is not None:
render_info.has_time = True
return dt_util.utcnow()
def forgiving_round(value, precision=0, method="common", default=_SENTINEL):
"""Filter to round a value."""
try:
@@ -1510,85 +1494,6 @@ def version(value):
return AwesomeVersion(value)
def timestamp_custom(value, date_format=DATE_STR_FORMAT, local=True, default=_SENTINEL):
"""Filter to convert given timestamp to format."""
try:
result = dt_util.utc_from_timestamp(value)
if local:
result = dt_util.as_local(result)
return result.strftime(date_format)
except (ValueError, TypeError):
# If timestamp can't be converted
if default is _SENTINEL:
raise_no_default("timestamp_custom", value)
return default
def timestamp_local(value, default=_SENTINEL):
"""Filter to convert given timestamp to local date/time."""
try:
return dt_util.as_local(dt_util.utc_from_timestamp(value)).isoformat()
except (ValueError, TypeError):
# If timestamp can't be converted
if default is _SENTINEL:
raise_no_default("timestamp_local", value)
return default
def timestamp_utc(value, default=_SENTINEL):
"""Filter to convert given timestamp to UTC date/time."""
try:
return dt_util.utc_from_timestamp(value).isoformat()
except (ValueError, TypeError):
# If timestamp can't be converted
if default is _SENTINEL:
raise_no_default("timestamp_utc", value)
return default
def forgiving_as_timestamp(value, default=_SENTINEL):
"""Filter and function which tries to convert value to timestamp."""
try:
return dt_util.as_timestamp(value)
except (ValueError, TypeError):
if default is _SENTINEL:
raise_no_default("as_timestamp", value)
return default
def as_datetime(value: Any, default: Any = _SENTINEL) -> Any:
"""Filter and to convert a time string or UNIX timestamp to datetime object."""
# Return datetime.datetime object without changes
if type(value) is datetime:
return value
# Add midnight to datetime.date object
if type(value) is date:
return datetime.combine(value, time(0, 0, 0))
try:
# Check for a valid UNIX timestamp string, int or float
timestamp = float(value)
return dt_util.utc_from_timestamp(timestamp)
except (ValueError, TypeError):
# Try to parse datetime string to datetime object
try:
return dt_util.parse_datetime(value, raise_on_error=True)
except (ValueError, TypeError):
if default is _SENTINEL:
# Return None on string input
# to ensure backwards compatibility with HA Core 2024.1 and before.
if isinstance(value, str):
return None
raise_no_default("as_datetime", value)
return default
def as_timedelta(value: str) -> timedelta | None:
"""Parse a ISO8601 duration like 'PT10M' to a timedelta."""
return dt_util.parse_duration(value)
def merge_response(value: ServiceResponse) -> list[Any]:
"""Merge action responses into single list.
@@ -1646,16 +1551,6 @@ def merge_response(value: ServiceResponse) -> list[Any]:
return response_items
def strptime(string, fmt, default=_SENTINEL):
"""Parse a time string to datetime."""
try:
return datetime.strptime(string, fmt)
except (ValueError, AttributeError, TypeError):
if default is _SENTINEL:
raise_no_default("strptime", string)
return default
def fail_when_undefined(value):
"""Filter to force a failure when the value is undefined."""
if isinstance(value, jinja2.Undefined):
@@ -1710,11 +1605,6 @@ def is_number(value):
return True
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))
@@ -1819,94 +1709,6 @@ def random_every_time(context, values):
return random.choice(values)
def today_at(hass: HomeAssistant, time_str: str = "") -> datetime:
"""Record fetching now where the time has been replaced with value."""
if (render_info := render_info_cv.get()) is not None:
render_info.has_time = True
today = dt_util.start_of_local_day()
if not time_str:
return today
if (time_today := dt_util.parse_time(time_str)) is None:
raise ValueError(
f"could not convert {type(time_str).__name__} to datetime: '{time_str}'"
)
return datetime.combine(today, time_today, today.tzinfo)
def relative_time(hass: HomeAssistant, value: Any) -> Any:
"""Take a datetime and return its "age" as a string.
The age can be in second, minute, hour, day, month or year. Only the
biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will
be returned.
If the input datetime is in the future,
the input datetime will be returned.
If the input are not a datetime object the input will be returned unmodified.
Note: This template function is deprecated in favor of `time_until`, but is still
supported so as not to break old templates.
"""
if (render_info := render_info_cv.get()) is not None:
render_info.has_time = True
if not isinstance(value, datetime):
return value
if not value.tzinfo:
value = dt_util.as_local(value)
if dt_util.now() < value:
return value
return dt_util.get_age(value)
def time_since(hass: HomeAssistant, value: Any | datetime, precision: int = 1) -> Any:
"""Take a datetime and return its "age" as a string.
The age can be in seconds, minutes, hours, days, months and year.
precision is the number of units to return, with the last unit rounded.
If the value not a datetime object the input will be returned unmodified.
"""
if (render_info := render_info_cv.get()) is not None:
render_info.has_time = True
if not isinstance(value, datetime):
return value
if not value.tzinfo:
value = dt_util.as_local(value)
if dt_util.now() < value:
return value
return dt_util.get_age(value, precision)
def time_until(hass: HomeAssistant, value: Any | datetime, precision: int = 1) -> Any:
"""Take a datetime and return the amount of time until that time as a string.
The time until can be in seconds, minutes, hours, days, months and years.
precision is the number of units to return, with the last unit rounded.
If the value not a datetime object the input will be returned unmodified.
"""
if (render_info := render_info_cv.get()) is not None:
render_info.has_time = True
if not isinstance(value, datetime):
return value
if not value.tzinfo:
value = dt_util.as_local(value)
if dt_util.now() > value:
return value
return dt_util.get_time_remaining(value, precision)
def iif(
value: Any, if_true: Any = True, if_false: Any = False, if_none: Any = _SENTINEL
) -> Any:
@@ -2089,6 +1891,9 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"homeassistant.helpers.template.extensions.CollectionExtension"
)
self.add_extension("homeassistant.helpers.template.extensions.CryptoExtension")
self.add_extension(
"homeassistant.helpers.template.extensions.DateTimeExtension"
)
self.add_extension("homeassistant.helpers.template.extensions.DeviceExtension")
self.add_extension("homeassistant.helpers.template.extensions.FloorExtension")
self.add_extension("homeassistant.helpers.template.extensions.LabelExtension")
@@ -2097,11 +1902,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.add_extension("homeassistant.helpers.template.extensions.StringExtension")
self.globals["apply"] = apply
self.globals["as_datetime"] = as_datetime
self.globals["as_function"] = as_function
self.globals["as_local"] = dt_util.as_local
self.globals["as_timedelta"] = as_timedelta
self.globals["as_timestamp"] = forgiving_as_timestamp
self.globals["bool"] = forgiving_boolean
self.globals["combine"] = combine
self.globals["float"] = forgiving_float
@@ -2110,8 +1911,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["is_number"] = is_number
self.globals["merge_response"] = merge_response
self.globals["pack"] = struct_pack
self.globals["strptime"] = strptime
self.globals["timedelta"] = timedelta
self.globals["typeof"] = typeof
self.globals["unpack"] = struct_unpack
self.globals["version"] = version
@@ -2119,11 +1918,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.filters["add"] = add
self.filters["apply"] = apply
self.filters["as_datetime"] = as_datetime
self.filters["as_function"] = as_function
self.filters["as_local"] = dt_util.as_local
self.filters["as_timedelta"] = as_timedelta
self.filters["as_timestamp"] = forgiving_as_timestamp
self.filters["bool"] = forgiving_boolean
self.filters["combine"] = combine
self.filters["contains"] = contains
@@ -2139,9 +1934,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.filters["pack"] = struct_pack
self.filters["random"] = random_every_time
self.filters["round"] = forgiving_round
self.filters["timestamp_custom"] = timestamp_custom
self.filters["timestamp_local"] = timestamp_local
self.filters["timestamp_utc"] = timestamp_utc
self.filters["to_json"] = to_json
self.filters["typeof"] = typeof
self.filters["unpack"] = struct_unpack
@@ -2149,7 +1941,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.tests["apply"] = apply
self.tests["contains"] = contains
self.tests["datetime"] = _is_datetime
self.tests["is_number"] = is_number
self.tests["string_like"] = _is_string_like
@@ -2218,15 +2009,9 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"is_hidden_entity",
"is_state_attr",
"is_state",
"now",
"relative_time",
"state_attr",
"state_translated",
"states",
"time_since",
"time_until",
"today_at",
"utcnow",
]
hass_filters = [
"area_id",
@@ -2253,20 +2038,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["distance"] = hassfunction(distance)
self.globals["expand"] = hassfunction(expand)
self.globals["has_value"] = hassfunction(has_value)
self.globals["now"] = hassfunction(now)
self.globals["relative_time"] = hassfunction(relative_time)
self.globals["time_since"] = hassfunction(time_since)
self.globals["time_until"] = hassfunction(time_until)
self.globals["today_at"] = hassfunction(today_at)
self.globals["utcnow"] = hassfunction(utcnow)
self.filters["closest"] = hassfunction(closest_filter)
self.filters["expand"] = self.globals["expand"]
self.filters["has_value"] = self.globals["has_value"]
self.filters["relative_time"] = self.globals["relative_time"]
self.filters["time_since"] = self.globals["time_since"]
self.filters["time_until"] = self.globals["time_until"]
self.filters["today_at"] = self.globals["today_at"]
self.tests["has_value"] = hassfunction(has_value, pass_eval_context)

View File

@@ -4,6 +4,7 @@ from .areas import AreaExtension
from .base64 import Base64Extension
from .collection import CollectionExtension
from .crypto import CryptoExtension
from .datetime import DateTimeExtension
from .devices import DeviceExtension
from .floors import FloorExtension
from .labels import LabelExtension
@@ -16,6 +17,7 @@ __all__ = [
"Base64Extension",
"CollectionExtension",
"CryptoExtension",
"DateTimeExtension",
"DeviceExtension",
"FloorExtension",
"LabelExtension",

View File

@@ -0,0 +1,324 @@
"""DateTime functions for Home Assistant templates."""
from __future__ import annotations
from datetime import date, datetime, time, timedelta
from typing import TYPE_CHECKING, Any
from homeassistant.helpers.template.helpers import raise_no_default
from homeassistant.helpers.template.render_info import render_info_cv
from homeassistant.util import dt as dt_util
from .base import BaseTemplateExtension, TemplateFunction
if TYPE_CHECKING:
from homeassistant.helpers.template import TemplateEnvironment
_SENTINEL = object()
DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S"
class DateTimeExtension(BaseTemplateExtension):
"""Extension for datetime-related template functions."""
def __init__(self, environment: TemplateEnvironment) -> None:
"""Initialize the datetime extension."""
super().__init__(
environment,
functions=[
TemplateFunction(
"as_datetime",
self.as_datetime,
as_global=True,
as_filter=True,
),
TemplateFunction(
"as_local",
self.as_local,
as_global=True,
as_filter=True,
),
TemplateFunction(
"as_timedelta",
self.as_timedelta,
as_global=True,
as_filter=True,
),
TemplateFunction(
"as_timestamp",
self.as_timestamp,
as_global=True,
as_filter=True,
),
TemplateFunction(
"strptime",
self.strptime,
as_global=True,
),
TemplateFunction(
"timedelta",
timedelta,
as_global=True,
),
TemplateFunction(
"timestamp_custom",
self.timestamp_custom,
as_filter=True,
),
TemplateFunction(
"timestamp_local",
self.timestamp_local,
as_filter=True,
),
TemplateFunction(
"timestamp_utc",
self.timestamp_utc,
as_filter=True,
),
TemplateFunction(
"datetime",
self.is_datetime,
as_test=True,
),
# Functions that require hass
TemplateFunction(
"now",
self.now,
as_global=True,
requires_hass=True,
limited_ok=False,
),
TemplateFunction(
"utcnow",
self.utcnow,
as_global=True,
requires_hass=True,
limited_ok=False,
),
TemplateFunction(
"relative_time",
self.relative_time,
as_global=True,
as_filter=True,
requires_hass=True,
limited_ok=False,
),
TemplateFunction(
"time_since",
self.time_since,
as_global=True,
as_filter=True,
requires_hass=True,
limited_ok=False,
),
TemplateFunction(
"time_until",
self.time_until,
as_global=True,
as_filter=True,
requires_hass=True,
limited_ok=False,
),
TemplateFunction(
"today_at",
self.today_at,
as_global=True,
as_filter=True,
requires_hass=True,
limited_ok=False,
),
],
)
def timestamp_custom(
self,
value: Any,
date_format: str = DATE_STR_FORMAT,
local: bool = True,
default: Any = _SENTINEL,
) -> Any:
"""Filter to convert given timestamp to format."""
try:
result = dt_util.utc_from_timestamp(value)
if local:
result = dt_util.as_local(result)
return result.strftime(date_format)
except (ValueError, TypeError):
# If timestamp can't be converted
if default is _SENTINEL:
raise_no_default("timestamp_custom", value)
return default
def timestamp_local(self, value: Any, default: Any = _SENTINEL) -> Any:
"""Filter to convert given timestamp to local date/time."""
try:
return dt_util.as_local(dt_util.utc_from_timestamp(value)).isoformat()
except (ValueError, TypeError):
# If timestamp can't be converted
if default is _SENTINEL:
raise_no_default("timestamp_local", value)
return default
def timestamp_utc(self, value: Any, default: Any = _SENTINEL) -> Any:
"""Filter to convert given timestamp to UTC date/time."""
try:
return dt_util.utc_from_timestamp(value).isoformat()
except (ValueError, TypeError):
# If timestamp can't be converted
if default is _SENTINEL:
raise_no_default("timestamp_utc", value)
return default
def as_timestamp(self, value: Any, default: Any = _SENTINEL) -> Any:
"""Filter and function which tries to convert value to timestamp."""
try:
return dt_util.as_timestamp(value)
except (ValueError, TypeError):
if default is _SENTINEL:
raise_no_default("as_timestamp", value)
return default
def as_datetime(self, value: Any, default: Any = _SENTINEL) -> Any:
"""Filter and to convert a time string or UNIX timestamp to datetime object."""
# Return datetime.datetime object without changes
if type(value) is datetime:
return value
# Add midnight to datetime.date object
if type(value) is date:
return datetime.combine(value, time(0, 0, 0))
try:
# Check for a valid UNIX timestamp string, int or float
timestamp = float(value)
return dt_util.utc_from_timestamp(timestamp)
except (ValueError, TypeError):
# Try to parse datetime string to datetime object
try:
return dt_util.parse_datetime(value, raise_on_error=True)
except (ValueError, TypeError):
if default is _SENTINEL:
# Return None on string input
# to ensure backwards compatibility with HA Core 2024.1 and before.
if isinstance(value, str):
return None
raise_no_default("as_datetime", value)
return default
def as_timedelta(self, value: str) -> timedelta | None:
"""Parse a ISO8601 duration like 'PT10M' to a timedelta."""
return dt_util.parse_duration(value)
def strptime(self, string: str, fmt: str, default: Any = _SENTINEL) -> Any:
"""Parse a time string to datetime."""
try:
return datetime.strptime(string, fmt)
except (ValueError, AttributeError, TypeError):
if default is _SENTINEL:
raise_no_default("strptime", string)
return default
def as_local(self, value: datetime) -> datetime:
"""Filter and function to convert time to local."""
return dt_util.as_local(value)
def is_datetime(self, value: Any) -> bool:
"""Return whether a value is a datetime."""
return isinstance(value, datetime)
def now(self) -> datetime:
"""Record fetching now."""
if (render_info := render_info_cv.get()) is not None:
render_info.has_time = True
return dt_util.now()
def utcnow(self) -> datetime:
"""Record fetching utcnow."""
if (render_info := render_info_cv.get()) is not None:
render_info.has_time = True
return dt_util.utcnow()
def today_at(self, time_str: str = "") -> datetime:
"""Record fetching now where the time has been replaced with value."""
if (render_info := render_info_cv.get()) is not None:
render_info.has_time = True
today = dt_util.start_of_local_day()
if not time_str:
return today
if (time_today := dt_util.parse_time(time_str)) is None:
raise ValueError(
f"could not convert {type(time_str).__name__} to datetime: '{time_str}'"
)
return datetime.combine(today, time_today, today.tzinfo)
def relative_time(self, value: Any) -> Any:
"""Take a datetime and return its "age" as a string.
The age can be in second, minute, hour, day, month or year. Only the
biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will
be returned.
If the input datetime is in the future,
the input datetime will be returned.
If the input are not a datetime object the input will be returned unmodified.
Note: This template function is deprecated in favor of `time_until`, but is still
supported so as not to break old templates.
"""
if (render_info := render_info_cv.get()) is not None:
render_info.has_time = True
if not isinstance(value, datetime):
return value
if not value.tzinfo:
value = dt_util.as_local(value)
if dt_util.now() < value:
return value
return dt_util.get_age(value)
def time_since(self, value: Any | datetime, precision: int = 1) -> Any:
"""Take a datetime and return its "age" as a string.
The age can be in seconds, minutes, hours, days, months and year.
precision is the number of units to return, with the last unit rounded.
If the value not a datetime object the input will be returned unmodified.
"""
if (render_info := render_info_cv.get()) is not None:
render_info.has_time = True
if not isinstance(value, datetime):
return value
if not value.tzinfo:
value = dt_util.as_local(value)
if dt_util.now() < value:
return value
return dt_util.get_age(value, precision)
def time_until(self, value: Any | datetime, precision: int = 1) -> Any:
"""Take a datetime and return the amount of time until that time as a string.
The time until can be in seconds, minutes, hours, days, months and years.
precision is the number of units to return, with the last unit rounded.
If the value not a datetime object the input will be returned unmodified.
"""
if (render_info := render_info_cv.get()) is not None:
render_info.has_time = True
if not isinstance(value, datetime):
return value
if not value.tzinfo:
value = dt_util.as_local(value)
if dt_util.now() > value:
return value
return dt_util.get_time_remaining(value, precision)

View File

@@ -0,0 +1,790 @@
"""Test datetime template functions."""
from __future__ import annotations
from datetime import datetime
from types import MappingProxyType
from typing import Any
from unittest.mock import patch
from freezegun import freeze_time
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import TemplateError
from homeassistant.util import dt as dt_util
from homeassistant.util.read_only_dict import ReadOnlyDict
from tests.helpers.template.helpers import render, render_to_info
@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 render(hass, "{{ value is datetime }}", {"value": value}) == expected
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 render(hass, temp) == 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):
render(hass, temp)
# 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 render(hass, f"{{{{ {inp} | {fil} }}}}") == 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):
render(hass, f"{{{{ {inp} | {fil} }}}}")
# 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 render(hass, f"{{{{ {inp} | timestamp_local }}}}") == out
# Test handling of invalid input
invalid_tests = [
None,
]
for inp in invalid_tests:
with pytest.raises(TemplateError):
render(hass, f"{{{{ {inp} | timestamp_local }}}}")
# 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 render(hass, f"{{{{ as_datetime('{input}') }}}}") == expected
assert render(hass, f"{{{{ '{input}' | as_datetime }}}}") == 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 render(hass, f"{{{{ as_datetime({input}) }}}}") == output
assert render(hass, f"{{{{ {input} | as_datetime }}}}") == output
assert render(hass, f"{{{{ as_datetime('{input}') }}}}") == output
assert render(hass, f"{{{{ '{input}' | as_datetime }}}}") == 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 render(hass, f"{input}{{{{ dt | as_datetime }}}}") == output
assert render(hass, f"{input}{{{{ as_datetime(dt) }}}}") == 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 render(hass, f"{{{{ as_datetime({input}, default={default}) }}}}") == output
assert render(hass, f"{{{{ {input} | as_datetime({default}) }}}}") == 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 render(hass, "{{ as_local(states.test.object.last_updated) }}") == str(
dt_util.as_local(last_updated)
)
assert render(hass, "{{ states.test.object.last_updated | as_local }}") == str(
dt_util.as_local(last_updated)
)
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 render(hass, f"{{{{ {inp} | timestamp_utc }}}}") == out
# Test handling of invalid input
invalid_tests = [
None,
]
for inp in invalid_tests:
with pytest.raises(TemplateError):
render(hass, f"{{{{ {inp} | timestamp_utc }}}}")
# 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):
render(hass, '{{ as_timestamp("invalid") }}')
hass.states.async_set("test.object", None)
with pytest.raises(TemplateError):
render(hass, "{{ as_timestamp(states.test.object) }}")
tpl = (
'{{ as_timestamp(strptime("2024-02-03T09:10:24+0000", '
'"%Y-%m-%dT%H:%M:%S%z")) }}'
)
assert render(hass, tpl) == 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
def test_as_timedelta(hass: HomeAssistant) -> None:
"""Test the as_timedelta function/filter."""
result = render(hass, "{{ as_timedelta('PT10M') }}")
assert result == "0:10:00"
result = render(hass, "{{ 'PT10M' | as_timedelta }}")
assert result == "0:10:00"
result = render(hass, "{{ 'T10M' | as_timedelta }}")
assert result is None
@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 = render_to_info(hass, "{{ now().isoformat() }}")
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 = render_to_info(hass, "{{ utcnow().isoformat() }}")
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 = render(hass, "{{ today_at('10:00').isoformat() }}")
assert result == expected
result = render(hass, "{{ today_at('10:00:00').isoformat() }}")
assert result == expected
result = render(hass, "{{ ('10:00:00' | today_at).isoformat() }}")
assert result == expected
result = render(hass, "{{ today_at().isoformat() }}")
assert result == expected_midnight
with pytest.raises(TemplateError):
render(hass, "{{ today_at('bad') }}")
info = render_to_info(hass, "{{ today_at('10:00').isoformat() }}")
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 = render(hass, relative_time_template)
assert result == "1 hour"
result = render(
hass,
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 09:00:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "2 hours"
result = render(
hass,
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 03:00:00 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "1 hour"
result1 = str(
datetime.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = render(
hass,
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 11:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result1 == result2
result = render(hass, '{{relative_time("string")}}')
assert result == "string"
# Test behavior when current time is same as the input time
result = render(
hass,
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 10:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "0 seconds"
# Test behavior when the input time is in the future
result = render(
hass,
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 11:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "2000-01-01 11:00:00+00:00"
info = render_to_info(hass, relative_time_template)
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 = render(hass, time_since_template)
assert result == "1 hour"
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 09:00:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "2 hours"
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 03:00:00 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "1 hour"
result1 = str(
datetime.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 11:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 2"
" )"
"}}"
),
)
assert result1 == result2
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 09:05:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision=2"
" )"
"}}"
),
)
assert result == "1 hour 55 minutes"
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 02:05:27 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 3"
" )"
"}}"
),
)
assert result == "1 hour 54 minutes 33 seconds"
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 02:05:27 -06:00",'
' "%Y-%m-%d %H:%M:%S %z")'
" )"
"}}"
),
)
assert result == "2 hours"
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "1999-02-01 02:05:27 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 0"
" )"
"}}"
),
)
assert result == "11 months 4 days 1 hour 54 minutes 33 seconds"
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "1999-02-01 02:05:27 -06:00",'
' "%Y-%m-%d %H:%M:%S %z")'
" )"
"}}"
),
)
assert result == "11 months"
result1 = str(
datetime.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 11:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision=3"
" )"
"}}"
),
)
assert result1 == result2
result = render(hass, '{{time_since("string")}}')
assert result == "string"
info = render_to_info(hass, time_since_template)
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 = render(hass, time_until_template)
assert result == "1 hour"
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 13:00:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "2 hours"
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 05:00:00 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "1 hour"
result1 = str(
datetime.strptime("2000-01-01 09:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 09:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 2"
" )"
"}}"
),
)
assert result1 == result2
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 12:05:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision=2"
" )"
"}}"
),
)
assert result == "1 hour 5 minutes"
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 05:54:33 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 3"
" )"
"}}"
),
)
assert result == "1 hour 54 minutes 33 seconds"
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 05:54:33 -06:00",'
' "%Y-%m-%d %H:%M:%S %z")'
" )"
"}}"
),
)
assert result == "2 hours"
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2001-02-01 05:54:33 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 0"
" )"
"}}"
),
)
assert result == "1 year 1 month 2 days 1 hour 54 minutes 33 seconds"
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2001-02-01 05:54:33 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 4"
" )"
"}}"
),
)
assert result == "1 year 1 month 2 days 2 hours"
result1 = str(
datetime.strptime("2000-01-01 09:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 09:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision=3"
" )"
"}}"
),
)
assert result1 == result2
result = render(hass, '{{time_until("string")}}')
assert result == "string"
info = render_to_info(hass, time_until_template)
assert info.has_time is True

View File

@@ -8,8 +8,6 @@ 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
@@ -50,7 +48,6 @@ from homeassistant.helpers.template.render_info import (
)
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from homeassistant.util.read_only_dict import ReadOnlyDict
from homeassistant.util.unit_system import UnitSystem
from .helpers import assert_result_info, render, render_to_info
@@ -436,25 +433,6 @@ def test_converting_datetime_to_iterable(hass: HomeAssistant) -> None:
render(hass, "{{ set(value) }}", {"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 render(hass, "{{ value is datetime }}", {"value": value}) == expected
def test_rounding_value(hass: HomeAssistant) -> None:
"""Test rounding value."""
hass.states.async_set("sensor.temperature", 12.78)
@@ -594,202 +572,6 @@ def test_as_function_no_arguments(hass: HomeAssistant) -> None:
assert render(hass, tpl) == "Hello"
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 render(hass, temp) == 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):
render(hass, temp)
# 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 render(hass, f"{{{{ {inp} | {fil} }}}}") == 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):
render(hass, f"{{{{ {inp} | {fil} }}}}")
# 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 render(hass, f"{{{{ {inp} | timestamp_local }}}}") == out
# Test handling of invalid input
invalid_tests = [
None,
]
for inp in invalid_tests:
with pytest.raises(TemplateError):
render(hass, f"{{{{ {inp} | timestamp_local }}}}")
# 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 render(hass, f"{{{{ as_datetime('{input}') }}}}") == expected
assert render(hass, f"{{{{ '{input}' | as_datetime }}}}") == 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 render(hass, f"{{{{ as_datetime({input}) }}}}") == output
assert render(hass, f"{{{{ {input} | as_datetime }}}}") == output
assert render(hass, f"{{{{ as_datetime('{input}') }}}}") == output
assert render(hass, f"{{{{ '{input}' | as_datetime }}}}") == 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 render(hass, f"{input}{{{{ dt | as_datetime }}}}") == output
assert render(hass, f"{input}{{{{ as_datetime(dt) }}}}") == 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 render(hass, f"{{{{ as_datetime({input}, default={default}) }}}}") == output
assert render(hass, f"{{{{ {input} | as_datetime({default}) }}}}") == 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 render(hass, "{{ as_local(states.test.object.last_updated) }}") == str(
dt_util.as_local(last_updated)
)
assert render(hass, "{{ states.test.object.last_updated | as_local }}") == str(
dt_util.as_local(last_updated)
)
def test_to_json(hass: HomeAssistant) -> None:
"""Test the object to JSON string filter."""
@@ -892,53 +674,6 @@ def test_from_hex(hass: HomeAssistant) -> None:
assert render(hass, "{{ '0F010003' | from_hex }}") == b"\x0f\x01\x00\x03"
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 render(hass, f"{{{{ {inp} | timestamp_utc }}}}") == out
# Test handling of invalid input
invalid_tests = [
None,
]
for inp in invalid_tests:
with pytest.raises(TemplateError):
render(hass, f"{{{{ {inp} | timestamp_utc }}}}")
# 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):
render(hass, '{{ as_timestamp("invalid") }}')
hass.states.async_set("test.object", None)
with pytest.raises(TemplateError):
render(hass, "{{ as_timestamp(states.test.object) }}")
tpl = (
'{{ as_timestamp(strptime("2024-02-03T09:10:24+0000", '
'"%Y-%m-%dT%H:%M:%S%z")) }}'
)
assert render(hass, tpl) == 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."""
@@ -1334,503 +1069,6 @@ def test_has_value(hass: HomeAssistant) -> None:
assert result == "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 = render_to_info(hass, "{{ now().isoformat() }}")
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 = render_to_info(hass, "{{ utcnow().isoformat() }}")
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 = render(hass, "{{ today_at('10:00').isoformat() }}")
assert result == expected
result = render(hass, "{{ today_at('10:00:00').isoformat() }}")
assert result == expected
result = render(hass, "{{ ('10:00:00' | today_at).isoformat() }}")
assert result == expected
result = render(hass, "{{ today_at().isoformat() }}")
assert result == expected_midnight
with pytest.raises(TemplateError):
render(hass, "{{ today_at('bad') }}")
info = render_to_info(hass, "{{ today_at('10:00').isoformat() }}")
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 = render(hass, relative_time_template)
assert result == "1 hour"
result = render(
hass,
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 09:00:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "2 hours"
result = render(
hass,
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 03:00:00 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "1 hour"
result1 = str(
template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = render(
hass,
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 11:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result1 == result2
result = render(hass, '{{relative_time("string")}}')
assert result == "string"
# Test behavior when current time is same as the input time
result = render(
hass,
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 10:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "0 seconds"
# Test behavior when the input time is in the future
result = render(
hass,
(
"{{"
" relative_time("
" strptime("
' "2000-01-01 11:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "2000-01-01 11:00:00+00:00"
info = render_to_info(hass, relative_time_template)
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 = render(hass, time_since_template)
assert result == "1 hour"
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 09:00:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "2 hours"
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 03:00:00 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "1 hour"
result1 = str(
template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 11:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 2"
" )"
"}}"
),
)
assert result1 == result2
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 09:05:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision=2"
" )"
"}}"
),
)
assert result == "1 hour 55 minutes"
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 02:05:27 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 3"
" )"
"}}"
),
)
assert result == "1 hour 54 minutes 33 seconds"
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 02:05:27 -06:00",'
' "%Y-%m-%d %H:%M:%S %z")'
" )"
"}}"
),
)
assert result == "2 hours"
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "1999-02-01 02:05:27 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 0"
" )"
"}}"
),
)
assert result == "11 months 4 days 1 hour 54 minutes 33 seconds"
result = render(
hass,
(
"{{"
" time_since("
" strptime("
' "1999-02-01 02:05:27 -06:00",'
' "%Y-%m-%d %H:%M:%S %z")'
" )"
"}}"
),
)
assert result == "11 months"
result1 = str(
template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = render(
hass,
(
"{{"
" time_since("
" strptime("
' "2000-01-01 11:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision=3"
" )"
"}}"
),
)
assert result1 == result2
result = render(hass, '{{time_since("string")}}')
assert result == "string"
info = render_to_info(hass, time_since_template)
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 = render(hass, time_until_template)
assert result == "1 hour"
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 13:00:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "2 hours"
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 05:00:00 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"'
" )"
" )"
"}}"
),
)
assert result == "1 hour"
result1 = str(
template.strptime("2000-01-01 09:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z")
)
result2 = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 09:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 2"
" )"
"}}"
),
)
assert result1 == result2
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 12:05:00 +01:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision=2"
" )"
"}}"
),
)
assert result == "1 hour 5 minutes"
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 05:54:33 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 3"
" )"
"}}"
),
)
assert result == "1 hour 54 minutes 33 seconds"
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 05:54:33 -06:00",'
' "%Y-%m-%d %H:%M:%S %z")'
" )"
"}}"
),
)
assert result == "2 hours"
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2001-02-01 05:54:33 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 0"
" )"
"}}"
),
)
assert result == "1 year 1 month 2 days 1 hour 54 minutes 33 seconds"
result = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2001-02-01 05:54:33 -06:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision = 4"
" )"
"}}"
),
)
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 = render(
hass,
(
"{{"
" time_until("
" strptime("
' "2000-01-01 09:00:00 +00:00",'
' "%Y-%m-%d %H:%M:%S %z"),'
" precision=3"
" )"
"}}"
),
)
assert result1 == result2
result = render(hass, '{{time_until("string")}}')
assert result == "string"
info = render_to_info(hass, time_until_template)
assert info.has_time is True
@patch(
"homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
return_value=True,
@@ -3294,19 +2532,6 @@ def test_render_complex_handling_non_template_values(hass: HomeAssistant) -> Non
) == {True: 1, False: 2}
def test_as_timedelta(hass: HomeAssistant) -> None:
"""Test the as_timedelta function/filter."""
result = render(hass, "{{ as_timedelta('PT10M') }}")
assert result == "0:10:00"
result = render(hass, "{{ 'PT10M' | as_timedelta }}")
assert result == "0:10:00"
result = render(hass, "{{ 'T10M' | as_timedelta }}")
assert result is None
def test_iif(hass: HomeAssistant) -> None:
"""Test the immediate if function/filter."""