async def _async_template_startup(self, *_) -> None: template_var_tups = [] for template, attributes in self._template_attrs.items(): template_var_tups.append(TrackTemplate(template, None)) for attribute in attributes: attribute.async_setup() result_info = async_track_template_result(self.opp, template_var_tups, self._handle_results) self.async_on_remove(result_info.async_remove) self._async_update = result_info.async_refresh result_info.async_refresh()
async def async_added_to_opp(self): """Subscribe to children and template state changes.""" @callback def _async_on_dependency_update(event): """Update ha state when dependencies update.""" self.async_set_context(event.context) self.async_schedule_update_op_state(True) @callback def _async_on_template_update(event, updates): """Update ha state when dependencies update.""" result = updates.pop().result if isinstance(result, TemplateError): self._state_template_result = None else: self._state_template_result = result if event: self.async_set_context(event.context) self.async_schedule_update_op_state(True) if self._state_template is not None: result = async_track_template_result( self.opp, [TrackTemplate(self._state_template, None)], _async_on_template_update, ) self.opp.bus.async_listen_once( EVENT_OPENPEERPOWER_START, callback(lambda _: result.async_refresh())) self.async_on_remove(result.async_remove) depend = copy(self._children) for entity in self._attrs.values(): depend.append(entity[0]) self.async_on_remove( self.opp.helpers.event.async_track_state_change_event( list(set(depend)), _async_on_dependency_update))
async def handle_render_template(opp: OpenPeerPower, connection: ActiveConnection, msg: dict[str, Any]) -> None: """Handle render_template command.""" template_str = msg["template"] template_obj = template.Template(template_str, opp) # type: ignore[no-untyped-call] variables = msg.get("variables") timeout = msg.get("timeout") info = None if timeout: try: timed_out = await template_obj.async_render_will_timeout( timeout, strict=msg["strict"]) except TemplateError as ex: connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) return if timed_out: connection.send_error( msg["id"], const.ERR_TEMPLATE_ERROR, f"Exceeded maximum execution time of {timeout}s", ) return @callback def _template_listener(event: Event, updates: list[TrackTemplateResult]) -> None: nonlocal info track_template_result = updates.pop() result = track_template_result.result if isinstance(result, TemplateError): connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(result)) return connection.send_message( messages.event_message( msg["id"], { "result": result, "listeners": info.listeners } # type: ignore[attr-defined] )) try: info = async_track_template_result( opp, [TrackTemplate(template_obj, variables)], _template_listener, raise_on_template_error=True, strict=msg["strict"], ) except TemplateError as ex: connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) return connection.subscriptions[msg["id"]] = info.async_remove connection.send_result(msg["id"]) opp.loop.call_soon_threadsafe(info.async_refresh)
async def async_added_to_opp(self): """ Call when entity about to be added. All relevant update logic for instance attributes occurs within this closure. Other methods in this class are designed to avoid directly modifying instance attributes, by instead focusing on returning relevant data back to this method. The goal of this method is to ensure that `self.current_observations` and `self.probability` are set on a best-effort basis when this entity is register with opp. In addition, this method must register the state listener defined within, which will be called any time a relevant entity changes its state. """ @callback def async_threshold_sensor_state_listener(event): """ Handle sensor state changes. When a state changes, we must update our list of current observations, then calculate the new probability. """ new_state = event.data.get("new_state") if new_state is None or new_state.state == STATE_UNKNOWN: return entity = event.data.get("entity_id") self.current_observations.update( self._record_entity_observations(entity)) self.async_set_context(event.context) self._recalculate_and_write_state() self.async_on_remove( async_track_state_change_event( self.opp, list(self.observations_by_entity), async_threshold_sensor_state_listener, )) @callback def _async_template_result_changed(event, updates): track_template_result = updates.pop() template = track_template_result.template result = track_template_result.result entity = event and event.data.get("entity_id") if isinstance(result, TemplateError): _LOGGER.error( "TemplateError('%s') " "while processing template '%s' " "in entity '%s'", result, template, self.entity_id, ) should_trigger = False else: should_trigger = result_as_boolean(result) for obs in self.observations_by_template[template]: if should_trigger: obs_entry = {"entity_id": entity, **obs} else: obs_entry = None self.current_observations[obs["id"]] = obs_entry if event: self.async_set_context(event.context) self._recalculate_and_write_state() for template in self.observations_by_template: info = async_track_template_result( self.opp, [TrackTemplate(template, None)], _async_template_result_changed, ) self._callbacks.append(info) self.async_on_remove(info.async_remove) info.async_refresh() self.current_observations.update( self._initialize_current_observations()) self.probability = self._calculate_new_probability() self._deviation = bool(self.probability >= self._probability_threshold)
async def async_attach_trigger(opp, config, action, automation_info, *, platform_type="template"): """Listen for state changes based on configuration.""" trigger_id = automation_info.get("trigger_id") if automation_info else None value_template = config.get(CONF_VALUE_TEMPLATE) value_template.opp = opp time_delta = config.get(CONF_FOR) template.attach(opp, time_delta) delay_cancel = None job = OppJob(action) armed = False # Arm at setup if the template is already false. try: if not result_as_boolean( value_template.async_render(automation_info["variables"])): armed = True except exceptions.TemplateError as ex: _LOGGER.warning( "Error initializing 'template' trigger for '%s': %s", automation_info["name"], ex, ) @callback def template_listener(event, updates): """Listen for state changes and calls action.""" nonlocal delay_cancel, armed result = updates.pop().result if isinstance(result, exceptions.TemplateError): _LOGGER.warning( "Error evaluating 'template' trigger for '%s': %s", automation_info["name"], result, ) return if delay_cancel: # pylint: disable=not-callable delay_cancel() delay_cancel = None if not result_as_boolean(result): armed = True return # Only fire when previously armed. if not armed: return # Fire! armed = False entity_id = event and event.data.get("entity_id") from_s = event and event.data.get("old_state") to_s = event and event.data.get("new_state") if entity_id is not None: description = f"{entity_id} via template" else: description = "time change or manual update via template" template_variables = { "platform": platform_type, "entity_id": entity_id, "from_state": from_s, "to_state": to_s, } trigger_variables = { "for": time_delta, "description": description, "id": trigger_id, } @callback def call_action(*_): """Call action with right context.""" nonlocal trigger_variables opp.async_run_opp_job( job, {"trigger": { **template_variables, **trigger_variables }}, (to_s.context if to_s else None), ) if not time_delta: call_action() return try: period = cv.positive_time_period( template.render_complex(time_delta, {"trigger": template_variables})) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error("Error rendering '%s' for template: %s", automation_info["name"], ex) return trigger_variables["for"] = period delay_cancel = async_call_later(opp, period.total_seconds(), call_action) info = async_track_template_result( opp, [TrackTemplate(value_template, automation_info["variables"])], template_listener, ) unsub = info.async_remove @callback def async_remove(): """Remove state listeners async.""" unsub() if delay_cancel: # pylint: disable=not-callable delay_cancel() return async_remove