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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
324
homeassistant/helpers/template/extensions/datetime.py
Normal file
324
homeassistant/helpers/template/extensions/datetime.py
Normal 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)
|
||||
790
tests/helpers/template/extensions/test_datetime.py
Normal file
790
tests/helpers/template/extensions/test_datetime.py
Normal 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
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user