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 __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] = {}
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)
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)