async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, action: TriggerActionType, trigger_info: TriggerInfo, *, platform_type: str = "state", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_ids = config[CONF_ENTITY_ID] if (from_state := config.get(CONF_FROM)) is not None: match_from_state = process_state_match(from_state)
platform_type: str = "state", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_ids = config[CONF_ENTITY_ID] if (from_state := config.get(CONF_FROM)) is None: from_state = MATCH_ALL if (to_state := config.get(CONF_TO)) is None: to_state = MATCH_ALL time_delta = config.get(CONF_FOR) template.attach(hass, time_delta) # If neither CONF_FROM or CONF_TO are specified, # fire on all changes to the state or an attribute match_all = CONF_FROM not in config and CONF_TO not in config unsub_track_same = {} period: dict[str, timedelta] = {} match_from_state = process_state_match(from_state) match_to_state = process_state_match(to_state) attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) trigger_data = automation_info["trigger_data"] _variables = automation_info["variables"] or {} @callback 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:
async def async_attach_trigger( hass: HomeAssistant, config, action, automation_info, *, platform_type: str = "state", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) to_state = config.get(CONF_TO, MATCH_ALL) time_delta = config.get(CONF_FOR) template.attach(hass, time_delta) match_all = from_state == MATCH_ALL and to_state == MATCH_ALL unsub_track_same = {} period: dict[str, timedelta] = {} match_from_state = process_state_match(from_state) match_to_state = process_state_match(to_state) attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} _variables = {} if automation_info: _variables = automation_info.get("variables") or {} @callback 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): if new_st is None: return False 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, ) unsub = async_track_state_change_event(hass, entity_id, state_automation_listener) @callback def async_remove(): """Remove state listeners async.""" unsub() for async_remove in unsub_track_same.values(): async_remove() unsub_track_same.clear() return async_remove
async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, action: TriggerActionType, trigger_info: TriggerInfo, *, platform_type: str = "state", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_ids = config[CONF_ENTITY_ID] if (from_state := config.get(CONF_FROM)) is not None: match_from_state = process_state_match(from_state) elif (not_from_state := config.get(CONF_NOT_FROM)) is not None: match_from_state = process_state_match(not_from_state, invert=True) else: match_from_state = process_state_match(MATCH_ALL) if (to_state := config.get(CONF_TO)) is not None: match_to_state = process_state_match(to_state) elif (not_to_state := config.get(CONF_NOT_TO)) is not None: match_to_state = process_state_match(not_to_state, invert=True) else: match_to_state = process_state_match(MATCH_ALL) time_delta = config.get(CONF_FOR) template.attach(hass, time_delta) # If neither CONF_FROM or CONF_TO are specified, # fire on all changes to the state or an attribute match_all = all(item not in config for item in (CONF_FROM, CONF_NOT_FROM,
async def async_attach_trigger( hass: HomeAssistant, config, action, automation_info, *, platform_type: str = "state", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) to_state = config.get(CONF_TO, MATCH_ALL) time_delta = config.get(CONF_FOR) template.attach(hass, time_delta) match_all = from_state == MATCH_ALL and to_state == MATCH_ALL unsub_track_same = {} period: Dict[str, timedelta] = {} match_from_state = process_state_match(from_state) match_to_state = process_state_match(to_state) @callback def state_automation_listener(event: Event): """Listen for state changes and calls action.""" entity: str = event.data["entity_id"] if entity not in entity_id: return from_s = event.data.get("old_state") to_s = event.data.get("new_state") old_state = getattr(from_s, "state", None) new_state = getattr(to_s, "state", None) if (not match_from_state(old_state) or not match_to_state(new_state) or (not match_all and old_state == new_state)): return @callback def call_action(): """Call action with right context.""" hass.async_run_job( action( { "trigger": { "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period[entity], } }, context=event.context, )) if not time_delta: call_action() return variables = { "trigger": { "platform": "state", "entity_id": entity, "from_state": from_s, "to_state": to_s, } } try: if isinstance(time_delta, template.Template): period[entity] = vol.All( cv.time_period, cv.positive_timedelta)(time_delta.async_render(variables)) elif isinstance(time_delta, dict): time_delta_data = {} time_delta_data.update( template.render_complex(time_delta, variables)) period[entity] = vol.All( cv.time_period, cv.positive_timedelta)(time_delta_data) else: period[entity] = time_delta 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): if new_st is None: return False return new_st.state == to_s.state unsub_track_same[entity] = async_track_same_state( hass, period[entity], call_action, _check_same_state, entity_ids=entity, ) unsub = async_track_state_change_event(hass, entity_id, state_automation_listener) @callback def async_remove(): """Remove state listeners async.""" unsub() for async_remove in unsub_track_same.values(): async_remove() unsub_track_same.clear() return async_remove