Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying a super template for async_track_template_result #58477

Merged
merged 7 commits into from Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 16 additions & 2 deletions homeassistant/components/template/template_entity.py
Expand Up @@ -233,13 +233,27 @@ def _handle_results(

async def _async_template_startup(self, *_) -> None:
template_var_tups = []
has_availability_template = False
for template, attributes in self._template_attrs.items():
template_var_tups.append(TrackTemplate(template, None))
template_var_tup = TrackTemplate(template, None)
is_availability_template = False
for attribute in attributes:
# pylint: disable-next=protected-access
if attribute._attribute == "_attr_available":
has_availability_template = True
is_availability_template = True
attribute.async_setup()
# Insert the availability template first in the list
if is_availability_template:
template_var_tups.insert(0, template_var_tup)
else:
template_var_tups.append(template_var_tup)

result_info = async_track_template_result(
self.hass, template_var_tups, self._handle_results
self.hass,
template_var_tups,
self._handle_results,
has_super_template=has_availability_template,
)
self.async_on_remove(result_info.async_remove)
self._async_update = result_info.async_refresh
Expand Down
81 changes: 69 additions & 12 deletions homeassistant/helpers/event.py
Expand Up @@ -2,7 +2,7 @@
from __future__ import annotations

import asyncio
from collections.abc import Awaitable, Iterable
from collections.abc import Awaitable, Iterable, Sequence
import copy
from dataclasses import dataclass
from datetime import datetime, timedelta
Expand Down Expand Up @@ -767,8 +767,9 @@ class _TrackTemplateResultInfo:
def __init__(
self,
hass: HomeAssistant,
track_templates: Iterable[TrackTemplate],
track_templates: Sequence[TrackTemplate],
action: Callable,
has_super_template: bool = False,
) -> None:
"""Handle removal / refresh of tracker init."""
self.hass = hass
Expand All @@ -777,8 +778,9 @@ def __init__(
for track_template_ in track_templates:
track_template_.template.hass = hass
self._track_templates = track_templates
self._has_super_template = has_super_template

self._last_result: dict[Template, str | TemplateError] = {}
self._last_result: dict[Template, bool | str | TemplateError] = {}

self._rate_limit = KeyedRateLimit(hass)
self._info: dict[Template, RenderInfo] = {}
Expand Down Expand Up @@ -874,7 +876,7 @@ def _render_template_if_ready(
) -> bool | TrackTemplateResult:
"""Re-render the template if conditions match.

Returns False if the template was not be re-rendered
Returns False if the template was not re-rendered.

Returns True if the template re-rendered and did not
change.
Expand Down Expand Up @@ -953,19 +955,68 @@ def _refresh(
info_changed = False
now = event.time_fired if not replayed and event else dt_util.utcnow()

for track_template_ in track_templates or self._track_templates:
update = self._render_template_if_ready(track_template_, now, event)
def _apply_update(
update: bool | TrackTemplateResult, template: Template
) -> bool:
"""Handle updates of a tracked template."""
if not update:
continue
return False

template = track_template_.template
self._setup_time_listener(template, self._info[template].has_time)

info_changed = True

if isinstance(update, TrackTemplateResult):
updates.append(update)

return True

block_updates = False
super_template = self._track_templates[0] if self._has_super_template else None

track_templates = track_templates or self._track_templates

def _super_template_as_boolean(result: bool | str | TemplateError) -> bool:
"""Return True if the result is truthy or a TemplateError."""
if isinstance(result, TemplateError):
return True

return result_as_boolean(result)

# Update the super template first
if super_template is not None:
update = self._render_template_if_ready(super_template, now, event)
info_changed |= _apply_update(update, super_template.template)

if isinstance(update, TrackTemplateResult):
super_result = update.result
else:
super_result = self._last_result.get(super_template.template)

# If the super template did not render to True, don't update other templates
if (
super_result is not None
and _super_template_as_boolean(super_result) is not True
):
block_updates = True

if (
isinstance(update, TrackTemplateResult)
and _super_template_as_boolean(update.last_result) is not True
and _super_template_as_boolean(update.result) is True
):
# Super template changed from not True to True, force re-render
# of all templates in the group
event = None
track_templates = self._track_templates

# Then update the remaining templates unless blocked by the super template
if not block_updates:
for track_template_ in track_templates:
if track_template_ == super_template:
continue

update = self._render_template_if_ready(track_template_, now, event)
info_changed |= _apply_update(update, track_template_.template)

if info_changed:
assert self._track_state_changes
self._track_state_changes.async_update_listeners(
Expand Down Expand Up @@ -1016,10 +1067,11 @@ def _refresh(
@bind_hass
def async_track_template_result(
hass: HomeAssistant,
track_templates: Iterable[TrackTemplate],
track_templates: Sequence[TrackTemplate],
action: TrackTemplateResultListener,
raise_on_template_error: bool = False,
strict: bool = False,
has_super_template: bool = False,
) -> _TrackTemplateResultInfo:
"""Add a listener that fires when the result of a template changes.

Expand Down Expand Up @@ -1050,13 +1102,18 @@ def async_track_template_result(
tracking.
strict
When set to True, raise on undefined variables.
has_super_template
When set to True, the first template will block rendering of other
templates if it doesn't render as True.

Returns
-------
Info object used to unregister the listener, and refresh the template.

"""
tracker = _TrackTemplateResultInfo(hass, track_templates, action)
tracker = _TrackTemplateResultInfo(
hass, track_templates, action, has_super_template
)
tracker.async_setup(raise_on_template_error, strict=strict)
return tracker

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/helpers/template.py
Expand Up @@ -861,7 +861,7 @@ def _resolve_state(
return None


def result_as_boolean(template_result: str | None) -> bool:
def result_as_boolean(template_result: Any | None) -> bool:
"""Convert the template result to a boolean.

True/not 0/'1'/'true'/'yes'/'on'/'enable' are considered truthy
Expand Down