Add timedelta_string template function similar to time_since/time_until/relative_time (#169773)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Clément Notin
2026-09-08 13:34:13 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent 7c9cc36a6d
commit 1d9ae4b184
4 changed files with 98 additions and 0 deletions
@@ -117,6 +117,14 @@ class DateTimeExtension(BaseTemplateExtension):
requires_hass=True,
limited_ok=False,
),
TemplateFunction(
"timedelta_string",
self.timedelta_string,
as_global=True,
as_filter=True,
requires_hass=True,
limited_ok=False,
),
TemplateFunction(
"today_at",
self.today_at,
@@ -319,3 +327,19 @@ class DateTimeExtension(BaseTemplateExtension):
If the value not a datetime object the input will be returned unmodified.
"""
return self._datetime_as_string(value, precision=precision, future=True)
def timedelta_string(self, value: Any, precision: int = 1) -> Any:
"""Take a timedelta and return a human-readable string representation.
The result can be in seconds, minutes, hours, days, months and years.
precision is the number of units to return, with the last unit rounded.
precision=0 returns all units (no early rounding, except for sub-second values).
Negative timedeltas are formatted using their absolute value.
If the value is not a timedelta object the input will be returned unmodified.
"""
if not isinstance(value, timedelta):
return value
return dt_util.timedelta_string(value, precision)
+14
View File
@@ -387,6 +387,20 @@ def get_time_remaining(date: dt.datetime, precision: int = 1) -> str:
return _get_timestring(rounded_delta, precision)
def timedelta_string(delta: dt.timedelta, precision: int = 1) -> str:
"""Return a string representation of a timedelta.
The result can be in seconds, minutes, hours, days, months and years.
precision is the number of units to return, with the last unit rounded.
precision=0 returns all units (no early rounding, except for sub-second values).
Negative timedeltas are treated as their absolute value.
"""
rounded_delta = round(abs(delta.total_seconds()))
return _get_timestring(rounded_delta, precision)
def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> list[int]:
"""Parse the time expression part and return a list of times to match."""
if parameter is None or parameter == "*":
@@ -786,3 +786,35 @@ async def test_time_until(mock_is_safe, hass: HomeAssistant) -> None:
info = render_to_info(hass, time_until_template)
assert info.has_time is True
@pytest.mark.parametrize(
("template_str", "expected"),
[
("{{ timedelta(seconds=0) | timedelta_string }}", "0 seconds"),
("{{ timedelta(seconds=1) | timedelta_string }}", "1 second"),
("{{ timedelta(hours=1) | timedelta_string }}", "1 hour"),
("{{ timedelta(hours=2) | timedelta_string }}", "2 hours"),
("{{ timedelta(hours=1, minutes=30) | timedelta_string }}", "2 hours"),
(
"{{ timedelta(hours=1, minutes=30) | timedelta_string(precision=2) }}",
"1 hour 30 minutes",
),
(
"{{ timedelta(hours=1, minutes=54, seconds=33) | timedelta_string(precision=3) }}",
"1 hour 54 minutes 33 seconds",
),
(
"{{ timedelta(hours=1, minutes=54, seconds=33) | timedelta_string(precision=0) }}",
"1 hour 54 minutes 33 seconds",
),
("{{ timedelta_string(timedelta(hours=1), precision=2) }}", "1 hour"),
("{{ timedelta_string('string') }}", "string"),
("{{ timedelta_string(42) }}", 42),
],
)
def test_timedelta_string(
hass: HomeAssistant, template_str: str, expected: str | int
) -> None:
"""Test timedelta_string function and filter."""
assert render(hass, template_str) == expected
+28
View File
@@ -292,6 +292,34 @@ def test_time_remaining() -> None:
assert dt_util.get_time_remaining(diff) == "1 year"
@pytest.mark.parametrize(
("delta", "precision", "expected"),
[
(timedelta(seconds=0), 1, "0 seconds"),
(timedelta(milliseconds=400), 1, "0 seconds"),
(timedelta(milliseconds=600), 1, "1 second"),
(timedelta(seconds=1), 1, "1 second"),
(timedelta(seconds=30), 1, "30 seconds"),
(timedelta(minutes=1), 1, "1 minute"),
(timedelta(minutes=5), 1, "5 minutes"),
(timedelta(hours=1), 1, "1 hour"),
(timedelta(hours=5), 1, "5 hours"),
(timedelta(hours=1, minutes=30), 1, "2 hours"),
(timedelta(hours=1, minutes=30), 2, "1 hour 30 minutes"),
(timedelta(days=2), 1, "2 days"),
(timedelta(days=32), 1, "1 month"),
(timedelta(days=365), 1, "1 year"),
(timedelta(hours=1, minutes=54, seconds=33), 3, "1 hour 54 minutes 33 seconds"),
(timedelta(hours=1, minutes=54, seconds=33), 0, "1 hour 54 minutes 33 seconds"),
(timedelta(hours=-1), 1, "1 hour"),
(timedelta(hours=-1, minutes=-30), 2, "1 hour 30 minutes"),
],
)
def test_timedelta_string(delta: timedelta, precision: int, expected: str) -> None:
"""Test timedelta_string."""
assert dt_util.timedelta_string(delta, precision) == expected
def test_parse_time_expression() -> None:
"""Test parse_time_expression."""
assert list(range(60)) == dt_util.parse_time_expression("*", 0, 59)