async def async_added_to_hass(self): """Register callbacks.""" self.add_template_attribute("_state", self._template, None, self._update_state) if ( self._friendly_name_template is not None and not self._friendly_name_template.is_static ): self.add_template_attribute("_name", self._friendly_name_template) if self._delay_on_raw is not None: try: self._delay_on = cv.positive_time_period(self._delay_on_raw) except vol.Invalid: self.add_template_attribute( "_delay_on", self._delay_on_raw, cv.positive_time_period ) if self._delay_off_raw is not None: try: self._delay_off = cv.positive_time_period(self._delay_off_raw) except vol.Invalid: self.add_template_attribute( "_delay_off", self._delay_off_raw, cv.positive_time_period ) await super().async_added_to_hass()
def _set_state(self, state, _=None): """Set up auto off.""" self._state = state self.async_set_context(self.coordinator.data["context"]) self.async_write_ha_state() if not state: return auto_off_time = self._rendered.get(CONF_AUTO_OFF) or self._config.get( CONF_AUTO_OFF) if auto_off_time is None: return if not isinstance(auto_off_time, timedelta): try: auto_off_time = cv.positive_time_period(auto_off_time) except vol.Invalid as err: logging.getLogger(__name__).warning( "Error rendering %s template: %s", CONF_AUTO_OFF, err) return @callback def _auto_off(_): """Set state of template binary sensor.""" self._state = False self.async_write_ha_state() self._auto_off_cancel = async_call_later(self.hass, auto_off_time.total_seconds(), _auto_off)
def _set_state(self, state, _=None): """Set up auto off.""" self._state = state self.async_set_context(self.coordinator.data["context"]) self.async_write_ha_state() if not state: return auto_off_delay = self._rendered.get(CONF_AUTO_OFF) or self._config.get( CONF_AUTO_OFF) if auto_off_delay is None: return if not isinstance(auto_off_delay, timedelta): try: auto_off_delay = cv.positive_time_period(auto_off_delay) except vol.Invalid as err: logging.getLogger(__name__).warning( "Error rendering %s template: %s", CONF_AUTO_OFF, err) return auto_off_time = dt_util.utcnow() + auto_off_delay self._set_auto_off(auto_off_time)
def state_automation_listener(event): """Listen for state changes and calls action.""" entity_id = event.data.get("entity_id") from_s = event.data.get("old_state") to_s = event.data.get("new_state") @callback def call_action(): """Call action with right context.""" hass.async_run_hass_job( job, { "trigger": { "platform": platform_type, "entity_id": entity_id, "below": below, "above": above, "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period[entity_id], "description": f"numeric state of {entity_id}", } }, to_s.context, ) matching = check_numeric_state(entity_id, from_s, to_s) if not matching: entities_triggered.discard(entity_id) elif entity_id not in entities_triggered: entities_triggered.add(entity_id) if time_delta: try: period[entity_id] = cv.positive_time_period( template.render_complex(time_delta, variables(entity_id))) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", automation_info["name"], ex, ) entities_triggered.discard(entity_id) return unsub_track_same[entity_id] = async_track_same_state( hass, period[entity_id], call_action, entity_ids=entity_id, async_check_same_func=check_numeric_state, ) else: call_action()
def template_listener(event, updates): """Listen for state changes and calls action.""" nonlocal delay_cancel result = updates.pop().result if delay_cancel: # pylint: disable=not-callable delay_cancel() delay_cancel = None if not result_as_boolean(result): return entity_id = event.data.get("entity_id") from_s = event.data.get("old_state") to_s = event.data.get("new_state") @callback def call_action(*_): """Call action with right context.""" hass.async_run_hass_job( job, { "trigger": { "platform": "template", "entity_id": entity_id, "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period, "description": f"{entity_id} via template", } }, (to_s.context if to_s else None), ) if not time_delta: call_action() return variables = { "trigger": { "platform": platform_type, "entity_id": entity_id, "from_state": from_s, "to_state": to_s, } } try: period = cv.positive_time_period( template.render_complex(time_delta, variables)) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error("Error rendering '%s' for template: %s", automation_info["name"], ex) return delay_cancel = async_call_later(hass, period.seconds, call_action)
def _get_pos_time_period_template(self, key): try: return cv.positive_time_period( template.render_complex(self._action[key], self._variables)) except (exceptions.TemplateError, vol.Invalid) as ex: self._log( "Error rendering %s %s template: %s", self._script.name, key, ex, level=logging.ERROR, ) raise _StopScript from ex
def _handle_coordinator_update(self) -> None: """Handle update of the data.""" self._process_data() if self._delay_cancel: self._delay_cancel() self._delay_cancel = None if self._auto_off_cancel: self._auto_off_cancel() self._auto_off_cancel = None self._auto_off_time = None if not self.available: self.async_write_ha_state() return raw = self._rendered.get(CONF_STATE) state = template.result_as_boolean(raw) key = CONF_DELAY_ON if state else CONF_DELAY_OFF delay = self._rendered.get(key) or self._config.get(key) # state without delay. None means rendering failed. if self._state == state or state is None or delay is None: self._set_state(state) return if not isinstance(delay, timedelta): try: delay = cv.positive_time_period(delay) except vol.Invalid as err: logging.getLogger(__name__).warning( "Error rendering %s template: %s", key, err ) return # state with delay. Cancelled if new trigger received self._delay_cancel = async_call_later( self.hass, delay.total_seconds(), partial(self._set_state, state) )
def state_automation_listener(event: Event): """Listen for state changes and calls action.""" entity: str = event.data["entity_id"] from_s: State | None = event.data.get("old_state") to_s: State | None = event.data.get("new_state") if from_s is None: old_value = None elif attribute is None: old_value = from_s.state else: old_value = from_s.attributes.get(attribute) if to_s is None: new_value = None elif attribute is None: new_value = to_s.state else: new_value = to_s.attributes.get(attribute) # When we listen for state changes with `match_all`, we # will trigger even if just an attribute changes. When # we listen to just an attribute, we should ignore all # other attribute changes. if attribute is not None and old_value == new_value: return if (not match_from_state(old_value) or not match_to_state(new_value) or (not match_all and old_value == new_value)): return @callback def call_action(): """Call action with right context.""" hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period[entity], "attribute": attribute, "description": f"state of {entity}", } }, event.context, ) if not time_delta: call_action() return trigger_info = { "trigger": { "platform": "state", "entity_id": entity, "from_state": from_s, "to_state": to_s, } } variables = {**_variables, **trigger_info} try: period[entity] = cv.positive_time_period( template.render_complex(time_delta, variables)) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error("Error rendering '%s' for template: %s", automation_info["name"], ex) return def _check_same_state(_, _2, new_st: State | None) -> bool: if new_st is None: return False cur_value: str | None if attribute is None: cur_value = new_st.state else: cur_value = new_st.attributes.get(attribute) if CONF_FROM in config and CONF_TO not in config: return cur_value != old_value return cur_value == new_value unsub_track_same[entity] = async_track_same_state( hass, period[entity], call_action, _check_same_state, entity_ids=entity, )
def state_automation_listener(event): """Listen for state changes and calls action.""" entity_id = event.data.get("entity_id") from_s = event.data.get("old_state") to_s = event.data.get("new_state") @callback def call_action(): """Call action with right context.""" hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": platform_type, "entity_id": entity_id, "below": below, "above": above, "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period[entity_id], "description": f"numeric state of {entity_id}", } }, to_s.context, ) @callback def check_numeric_state_no_raise(entity_id, from_s, to_s): """Return True if the criteria are now met, False otherwise.""" try: return check_numeric_state(entity_id, from_s, to_s) except exceptions.ConditionError: # This is an internal same-state listener so we just drop the # error. The same error will be reached and logged by the # primary async_track_state_change_event() listener. return False try: matching = check_numeric_state(entity_id, from_s, to_s) except exceptions.ConditionError as ex: _LOGGER.warning("Error in '%s' trigger: %s", trigger_info["name"], ex) return if not matching: armed_entities.add(entity_id) elif entity_id in armed_entities: armed_entities.discard(entity_id) if time_delta: try: period[entity_id] = cv.positive_time_period( template.render_complex(time_delta, variables(entity_id)) ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", trigger_info["name"], ex, ) return unsub_track_same[entity_id] = async_track_same_state( hass, period[entity_id], call_action, entity_ids=entity_id, async_check_same_func=check_numeric_state_no_raise, ) else: call_action()
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 hass.async_run_hass_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(hass, period.seconds, call_action)
self._delay_off_raw = config.get(CONF_DELAY_OFF) async def async_added_to_hass(self): """Restore state and register callbacks.""" if ((self._delay_on_raw is not None or self._delay_off_raw is not None) and (last_state := await self.async_get_last_state()) is not None and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)): self._state = last_state.state == STATE_ON self.add_template_attribute("_state", self._template, None, self._update_state) if self._delay_on_raw is not None: try: self._delay_on = cv.positive_time_period(self._delay_on_raw) except vol.Invalid: self.add_template_attribute("_delay_on", self._delay_on_raw, cv.positive_time_period) if self._delay_off_raw is not None: try: self._delay_off = cv.positive_time_period(self._delay_off_raw) except vol.Invalid: self.add_template_attribute("_delay_off", self._delay_off_raw, cv.positive_time_period) await super().async_added_to_hass() @callback def _update_state(self, result):