Esempio n. 1
0
    def __init__(
        self,
        hass: HomeAssistant,
        track_templates: Iterable[TrackTemplate],
        action: Callable,
    ):
        """Handle removal / refresh of tracker init."""
        self.hass = hass
        self._action = action

        for track_template_ in track_templates:
            track_template_.template.hass = hass
        self._track_templates = track_templates

        self._last_result: Dict[Template, Union[str, TemplateError]] = {}

        self._rate_limit = KeyedRateLimit(hass)
        self._info: Dict[Template, RenderInfo] = {}
        self._track_state_changes: Optional[_TrackStateChangeFiltered] = None
Esempio n. 2
0
    def __init__(
        self,
        hass: HomeAssistant,
        track_templates: Iterable[TrackTemplate],
        action: Callable,
    ):
        """Handle removal / refresh of tracker init."""
        self.hass = hass
        self._job = HassJob(action)

        for track_template_ in track_templates:
            track_template_.template.hass = hass
        self._track_templates = track_templates

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

        self._rate_limit = KeyedRateLimit(hass)
        self._info: dict[Template, RenderInfo] = {}
        self._track_state_changes: _TrackStateChangeFiltered | None = None
        self._time_listeners: dict[Template, Callable] = {}
Esempio n. 3
0
class _TrackTemplateResultInfo:
    """Handle removal / refresh of tracker."""
    def __init__(
        self,
        hass: HomeAssistant,
        track_templates: Iterable[TrackTemplate],
        action: Callable,
    ):
        """Handle removal / refresh of tracker init."""
        self.hass = hass
        self._action = action

        for track_template_ in track_templates:
            track_template_.template.hass = hass
        self._track_templates = track_templates

        self._last_result: Dict[Template, Union[str, TemplateError]] = {}

        self._rate_limit = KeyedRateLimit(hass)
        self._info: Dict[Template, RenderInfo] = {}
        self._track_state_changes: Optional[_TrackStateChangeFiltered] = None

    def async_setup(self, raise_on_template_error: bool) -> None:
        """Activation of template tracking."""
        for track_template_ in self._track_templates:
            template = track_template_.template
            variables = track_template_.variables

            self._info[template] = template.async_render_to_info(variables)
            if self._info[template].exception:
                if raise_on_template_error:
                    raise self._info[template].exception
                _LOGGER.error(
                    "Error while processing template: %s",
                    track_template_.template,
                    exc_info=self._info[template].exception,
                )

        self._track_state_changes = async_track_state_change_filtered(
            self.hass, _render_infos_to_track_states(self._info.values()),
            self._refresh)
        _LOGGER.debug(
            "Template group %s listens for %s",
            self._track_templates,
            self.listeners,
        )

    @property
    def listeners(self) -> Dict:
        """State changes that will cause a re-render."""
        assert self._track_state_changes
        return self._track_state_changes.listeners

    @callback
    def async_remove(self) -> None:
        """Cancel the listener."""
        assert self._track_state_changes
        self._track_state_changes.async_remove()
        self._rate_limit.async_remove()

    @callback
    def async_refresh(self) -> None:
        """Force recalculate the template."""
        self._refresh(None)

    def _render_template_if_ready(
        self,
        track_template_: TrackTemplate,
        now: datetime,
        event: Optional[Event],
    ) -> Union[bool, TrackTemplateResult]:
        """Re-render the template if conditions match.

        Returns False if the template was not be re-rendered

        Returns True if the template re-rendered and did not
        change.

        Returns TrackTemplateResult if the template re-render
        generates a new result.
        """
        template = track_template_.template

        if event:
            info = self._info[template]

            if not self._rate_limit.async_has_timer(
                    template) and not _event_triggers_rerender(event, info):
                return False

            if self._rate_limit.async_schedule_action(
                    template,
                    _rate_limit_for_event(event, info, track_template_),
                    now,
                    self._refresh,
                    event,
            ):
                return False

            _LOGGER.debug(
                "Template update %s triggered by event: %s",
                template.template,
                event,
            )

        self._rate_limit.async_triggered(template, now)
        self._info[template] = template.async_render_to_info(
            track_template_.variables)

        try:
            result: Union[str, TemplateError] = self._info[template].result()
        except TemplateError as ex:
            result = ex

        last_result = self._last_result.get(template)

        # Check to see if the result has changed
        if result == last_result:
            return True

        if isinstance(result, TemplateError) and isinstance(
                last_result, TemplateError):
            return True

        return TrackTemplateResult(template, last_result, result)

    @callback
    def _refresh(self, event: Optional[Event]) -> None:
        updates = []
        info_changed = False
        now = dt_util.utcnow()

        for track_template_ in self._track_templates:
            update = self._render_template_if_ready(track_template_, now,
                                                    event)
            if not update:
                continue

            info_changed = True
            if isinstance(update, TrackTemplateResult):
                updates.append(update)

        if info_changed:
            assert self._track_state_changes
            self._track_state_changes.async_update_listeners(
                _render_infos_to_track_states(self._info.values()), )
            _LOGGER.debug(
                "Template group %s listens for %s",
                self._track_templates,
                self.listeners,
            )

        if not updates:
            return

        for track_result in updates:
            self._last_result[track_result.template] = track_result.result

        self.hass.async_run_job(self._action, event, updates)
Esempio n. 4
0
class _TrackTemplateResultInfo:
    """Handle removal / refresh of tracker."""

    def __init__(
        self,
        hass: HomeAssistant,
        track_templates: Iterable[TrackTemplate],
        action: Callable,
    ):
        """Handle removal / refresh of tracker init."""
        self.hass = hass
        self._job = HassJob(action)

        for track_template_ in track_templates:
            track_template_.template.hass = hass
        self._track_templates = track_templates

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

        self._rate_limit = KeyedRateLimit(hass)
        self._info: dict[Template, RenderInfo] = {}
        self._track_state_changes: _TrackStateChangeFiltered | None = None
        self._time_listeners: dict[Template, Callable] = {}

    def async_setup(self, raise_on_template_error: bool, strict: bool = False) -> None:
        """Activation of template tracking."""
        for track_template_ in self._track_templates:
            template = track_template_.template
            variables = track_template_.variables
            self._info[template] = info = template.async_render_to_info(
                variables, strict=strict
            )

            if info.exception:
                if raise_on_template_error:
                    raise info.exception
                _LOGGER.error(
                    "Error while processing template: %s",
                    track_template_.template,
                    exc_info=info.exception,
                )

        self._track_state_changes = async_track_state_change_filtered(
            self.hass, _render_infos_to_track_states(self._info.values()), self._refresh
        )
        self._update_time_listeners()
        _LOGGER.debug(
            "Template group %s listens for %s",
            self._track_templates,
            self.listeners,
        )

    @property
    def listeners(self) -> dict:
        """State changes that will cause a re-render."""
        assert self._track_state_changes
        return {
            **self._track_state_changes.listeners,
            "time": bool(self._time_listeners),
        }

    @callback
    def _setup_time_listener(self, template: Template, has_time: bool) -> None:
        if not has_time:
            if template in self._time_listeners:
                # now() or utcnow() has left the scope of the template
                self._time_listeners.pop(template)()
            return

        if template in self._time_listeners:
            return

        track_templates = [
            track_template_
            for track_template_ in self._track_templates
            if track_template_.template == template
        ]

        @callback
        def _refresh_from_time(now: datetime) -> None:
            self._refresh(None, track_templates=track_templates)

        self._time_listeners[template] = async_track_utc_time_change(
            self.hass, _refresh_from_time, second=0
        )

    @callback
    def _update_time_listeners(self) -> None:
        for template, info in self._info.items():
            self._setup_time_listener(template, info.has_time)

    @callback
    def async_remove(self) -> None:
        """Cancel the listener."""
        assert self._track_state_changes
        self._track_state_changes.async_remove()
        self._rate_limit.async_remove()
        for template in list(self._time_listeners):
            self._time_listeners.pop(template)()

    @callback
    def async_refresh(self) -> None:
        """Force recalculate the template."""
        self._refresh(None)

    def _render_template_if_ready(
        self,
        track_template_: TrackTemplate,
        now: datetime,
        event: Event | None,
    ) -> bool | TrackTemplateResult:
        """Re-render the template if conditions match.

        Returns False if the template was not be re-rendered

        Returns True if the template re-rendered and did not
        change.

        Returns TrackTemplateResult if the template re-render
        generates a new result.
        """
        template = track_template_.template

        if event:
            info = self._info[template]

            if not _event_triggers_rerender(event, info):
                return False

            had_timer = self._rate_limit.async_has_timer(template)

            if self._rate_limit.async_schedule_action(
                template,
                _rate_limit_for_event(event, info, track_template_),
                now,
                self._refresh,
                event,
                (track_template_,),
                True,
            ):
                return not had_timer

            _LOGGER.debug(
                "Template update %s triggered by event: %s",
                template.template,
                event,
            )

        self._rate_limit.async_triggered(template, now)
        self._info[template] = info = template.async_render_to_info(
            track_template_.variables
        )

        try:
            result: str | TemplateError = info.result()
        except TemplateError as ex:
            result = ex

        last_result = self._last_result.get(template)

        # Check to see if the result has changed
        if result == last_result:
            return True

        if isinstance(result, TemplateError) and isinstance(last_result, TemplateError):
            return True

        return TrackTemplateResult(template, last_result, result)

    @callback
    def _refresh(
        self,
        event: Event | None,
        track_templates: Iterable[TrackTemplate] | None = None,
        replayed: bool | None = False,
    ) -> None:
        """Refresh the template.

        The event is the state_changed event that caused the refresh
        to be considered.

        track_templates is an optional list of TrackTemplate objects
        to refresh.  If not provided, all tracked templates will be
        considered.

        replayed is True if the event is being replayed because the
        rate limit was hit.
        """
        updates = []
        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)
            if not update:
                continue

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

            info_changed = True

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

        if info_changed:
            assert self._track_state_changes
            self._track_state_changes.async_update_listeners(
                _render_infos_to_track_states(
                    [
                        _suppress_domain_all_in_render_info(self._info[template])
                        if self._rate_limit.async_has_timer(template)
                        else self._info[template]
                        for template in self._info
                    ]
                )
            )
            _LOGGER.debug(
                "Template group %s listens for %s",
                self._track_templates,
                self.listeners,
            )

        if not updates:
            return

        for track_result in updates:
            self._last_result[track_result.template] = track_result.result

        self.hass.async_run_hass_job(self._job, event, updates)