mirror of
https://github.com/home-assistant/core.git
synced 2026-02-21 18:38:17 +00:00
325 lines
11 KiB
Python
325 lines
11 KiB
Python
"""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 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)
|