def async_track_time_interval( hass: HomeAssistant, action: Callable[..., None | Awaitable], interval: timedelta, ) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" remove = None interval_listener_job = None job = HassJob(action) def next_interval() -> datetime: """Return the next interval.""" return dt_util.utcnow() + interval @callback def interval_listener(now: datetime) -> None: """Handle elapsed intervals.""" nonlocal remove nonlocal interval_listener_job remove = async_track_point_in_utc_time( hass, interval_listener_job, next_interval() # type: ignore ) hass.async_run_hass_job(job, now) interval_listener_job = HassJob(interval_listener) remove = async_track_point_in_utc_time(hass, interval_listener_job, next_interval()) def remove_listener() -> None: """Remove interval listener.""" remove() # type: ignore return remove_listener
async def async_attach_trigger(hass, config, action, automation_info): """Listen for tag_scanned events based on configuration.""" tag_ids = set(config[TAG_ID]) device_ids = set(config[DEVICE_ID]) if DEVICE_ID in config else None job = HassJob(action) async def handle_event(event): """Listen for tag scan events and calls the action when data matches.""" if event.data.get(TAG_ID) not in tag_ids or ( device_ids is not None and event.data.get(DEVICE_ID) not in device_ids): return task = hass.async_run_hass_job( job, { "trigger": { "platform": DOMAIN, "event": event, "description": "Tag scanned", } }, event.context, ) if task: await task return hass.bus.async_listen(EVENT_TAG_SCANNED, handle_event)
def _attach_trigger( hass: HomeAssistant, config: ConfigType, action: TriggerActionType, event_type, trigger_info: TriggerInfo, ): trigger_data = trigger_info["trigger_data"] job = HassJob(action) @callback def _handle_event(event: Event): if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]: hass.async_run_hass_job( job, { "trigger": { **trigger_data, **config, "description": event_type } }, event.context, ) return hass.bus.async_listen(event_type, _handle_event)
def async_track_point_in_utc_time( hass: HomeAssistant, action: Union[HassJob, Callable[..., None]], point_in_time: datetime, ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in UTC time.""" # Ensure point_in_time is UTC utc_point_in_time = dt_util.as_utc(point_in_time) # Since this is called once, we accept a HassJob so we can avoid # having to figure out how to call the action every time its called. job = action if isinstance(action, HassJob) else HassJob(action) cancel_callback = hass.loop.call_at( hass.loop.time() + point_in_time.timestamp() - time.time(), hass.async_run_hass_job, job, utc_point_in_time, ) @callback def unsub_point_in_time_listener() -> None: """Cancel the call_later.""" cancel_callback.cancel() return unsub_point_in_time_listener
def __init__( self, hass: HomeAssistant, logger: Logger, *, cooldown: float, immediate: bool, function: Callable[..., Awaitable[Any]] | None = None, ): """Initialize debounce. immediate: indicate if the function needs to be called right away and wait <cooldown> until executing next invocation. function: optional and can be instantiated later. """ self.hass = hass self.logger = logger self._function = function self.cooldown = cooldown self.immediate = immediate self._timer_task: asyncio.TimerHandle | None = None self._execute_at_end_of_timer: bool = False self._execute_lock = asyncio.Lock() self._job: HassJob | None = None if function is None else HassJob( function)
async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, action: AutomationActionType, automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} job = HassJob(action) if config[CONF_TYPE] == "turn_on": entity_id = config[CONF_ENTITY_ID] @callback def _handle_event(event: Event): if event.data[ATTR_ENTITY_ID] == entity_id: hass.async_run_hass_job( job, { "trigger": { **trigger_data, **config, "description": f"{DOMAIN} - {entity_id}", } }, event.context, ) return hass.bus.async_listen(EVENT_TURN_ON, _handle_event) return lambda: None
def _attach_trigger( hass: HomeAssistant, config: ConfigType, action: AutomationActionType, event_type, automation_info: dict, ): trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} job = HassJob(action) @callback def _handle_event(event: Event): if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]: hass.async_run_hass_job( job, { "trigger": { **trigger_data, **config, "description": event_type } }, event.context, ) return hass.bus.async_listen(event_type, _handle_event)
def async_at_start( hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Execute something when Home Assistant is started. Will execute it now if Home Assistant is already started. """ at_start_job = HassJob(at_start_cb) if hass.is_running: hass.async_run_hass_job(at_start_job, hass) return lambda: None unsub: None | CALLBACK_TYPE = None @callback def _matched_event(event: Event) -> None: """Call the callback when Home Assistant started.""" hass.async_run_hass_job(at_start_job, hass) nonlocal unsub unsub = None @callback def cancel() -> None: if unsub: unsub() unsub = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) return cancel
async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] hours = config.get(CONF_HOURS) minutes = config.get(CONF_MINUTES) seconds = config.get(CONF_SECONDS) job = HassJob(action) # If larger units are specified, default the smaller units to zero if minutes is None and hours is not None: minutes = 0 if seconds is None and minutes is not None: seconds = 0 @callback def time_automation_listener(now): """Listen for time changes and calls action.""" hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": "time_pattern", "now": now, "description": "time pattern", } }, ) return async_track_time_change(hass, time_automation_listener, hour=hours, minute=minutes, second=seconds)
def async_track_state_change_event( hass: HomeAssistant, entity_ids: Union[str, Iterable[str]], action: Callable[[Event], Any], ) -> Callable[[], None]: """Track specific state change events indexed by entity_id. Unlike async_track_state_change, async_track_state_change_event passes the full event to the callback. In order to avoid having to iterate a long list of EVENT_STATE_CHANGED and fire and create a job for each one, we keep a dict of entity ids that care about the state change events so we can do a fast dict lookup to route events. """ entity_ids = _async_string_to_lower_list(entity_ids) if not entity_ids: return _remove_empty_listener entity_callbacks = hass.data.setdefault(TRACK_STATE_CHANGE_CALLBACKS, {}) if TRACK_STATE_CHANGE_LISTENER not in hass.data: @callback def _async_state_change_dispatcher(event: Event) -> None: """Dispatch state changes by entity_id.""" entity_id = event.data.get("entity_id") if entity_id not in entity_callbacks: return for job in entity_callbacks[entity_id][:]: try: hass.async_run_hass_job(job, event) except Exception: # pylint: disable=broad-except _LOGGER.exception( "Error while processing state changed for %s", entity_id) hass.data[TRACK_STATE_CHANGE_LISTENER] = hass.bus.async_listen( EVENT_STATE_CHANGED, _async_state_change_dispatcher) job = HassJob(action) for entity_id in entity_ids: entity_callbacks.setdefault(entity_id, []).append(job) @callback def remove_listener() -> None: """Remove state change listener.""" _async_remove_indexed_listeners( hass, TRACK_STATE_CHANGE_CALLBACKS, TRACK_STATE_CHANGE_LISTENER, entity_ids, job, ) return remove_listener
def __init__(self, device: 'MerossDevice', id: object): super().__init__(device, id, DEVICE_CLASS_SHUTTER) self._position_native = None # as reported by the device self._signalOpen: int = 30000 # msec to fully open (config'd on device) self._signalClose: int = 30000 # msec to fully close (config'd on device) self._position_timed: int = 50 # estimated based on timings self._position_start = None # set when when we're controlling a timed position self._position_starttime = None # epoch of transition start self._position_endtime = None # epoch of 'target position reached' self._transition_unsub = None self._transition_job = HassJob( self._transition_callback) # job to follow transition self._stop_unsub = None self._stop_job = HassJob( self._stop_callback) # job to terminate transition self._attr_extra_state_attributes = dict()
async def async_attach_trigger( hass, config, action, automation_info, *, platform_type: str = "zone" ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) event = config.get(CONF_EVENT) job = HassJob(action) @callback def zone_automation_listener(zone_event): """Listen for state changes and calls action.""" entity = zone_event.data.get("entity_id") from_s = zone_event.data.get("old_state") to_s = zone_event.data.get("new_state") if ( from_s and not location.has_location(from_s) or not location.has_location(to_s) ): return if not (zone_state := hass.states.get(zone_entity_id)): _LOGGER.warning( "Automation '%s' is referencing non-existing zone '%s' in a zone trigger", automation_info["name"], zone_entity_id, ) return from_match = condition.zone(hass, zone_state, from_s) if from_s else False to_match = condition.zone(hass, zone_state, to_s) if to_s else False if ( event == EVENT_ENTER and not from_match and to_match or event == EVENT_LEAVE and from_match and not to_match ): description = f"{entity} {_EVENT_DESCRIPTION[event]} {zone_state.attributes[ATTR_FRIENDLY_NAME]}" hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, "zone": zone_state, "event": event, "description": description, } }, to_s.context, )
async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) description = event if offset: description = f"{description} with offset" job = HassJob(action) @callback def call_action(): """Call action with right context.""" hass.async_run_hass_job( job, { "trigger": { "platform": "sun", "event": event, "offset": offset, "description": description, } }, ) if event == SUN_EVENT_SUNRISE: return async_track_sunrise(hass, call_action, offset) return async_track_sunset(hass, call_action, offset)
def async_subscribe_connection_status( hass: HomeAssistant, connection_status_callback: ConnectionStatusCallback ) -> Callable[[], None]: """Subscribe to MQTT connection changes.""" connection_status_callback_job = HassJob(connection_status_callback) async def connected(): task = hass.async_run_hass_job(connection_status_callback_job, True) if task: await task async def disconnected(): task = hass.async_run_hass_job(connection_status_callback_job, False) if task: await task subscriptions = { "connect": async_dispatcher_connect(hass, MQTT_CONNECTED, connected), "disconnect": async_dispatcher_connect(hass, MQTT_DISCONNECTED, disconnected), } @callback def unsubscribe(): subscriptions["connect"]() subscriptions["disconnect"]() return unsubscribe
async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, action: TriggerActionType, trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" trigger_data = trigger_info["trigger_data"] job = HassJob(action) if config[CONF_TYPE] == "turn_on": entity_id = config[CONF_ENTITY_ID] @callback def _handle_event(event: Event): if event.data[ATTR_ENTITY_ID] == entity_id: hass.async_run_hass_job( job, { "trigger": { **trigger_data, # type: ignore[arg-type] # https://github.com/python/mypy/issues/9117 **config, "description": f"{DOMAIN} - {entity_id}", } }, event.context, ) return hass.bus.async_listen(EVENT_TURN_ON, _handle_event) return lambda: None
async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" topic = config[CONF_TOPIC] payload = config.get(CONF_PAYLOAD) encoding = config[CONF_ENCODING] or None qos = config[CONF_QOS] job = HassJob(action) @callback def mqtt_automation_listener(mqttmsg): """Listen for MQTT messages.""" if payload is None or payload == mqttmsg.payload: data = { "platform": "mqtt", "topic": mqttmsg.topic, "payload": mqttmsg.payload, "qos": mqttmsg.qos, "description": f"mqtt topic {mqttmsg.topic}", } try: data["payload_json"] = json.loads(mqttmsg.payload) except ValueError: pass hass.async_run_hass_job(job, {"trigger": data}) remove = await mqtt.async_subscribe( hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos ) return remove
async def async_attach_trigger( self, config: ConfigType, action: TriggerActionType, trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" trigger_data = trigger_info["trigger_data"] job = HassJob(action) @callback def event_handler(char): if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[ char["value"]]: return self._hass.async_run_hass_job( job, {"trigger": { **trigger_data, **config }}) trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] iid = trigger["characteristic"] await self._connection.add_watchable_characteristics([(self._aid, iid)] ) self._callbacks.setdefault(iid, []).append(event_handler) def async_remove_handler(): if iid in self._callbacks: self._callbacks[iid].remove(event_handler) return async_remove_handler
def async_dispatcher_connect(hass: HomeAssistantType, signal: str, target: Callable[..., Any]) -> Callable[[], None]: """Connect a callable function to a signal. This method must be run in the event loop. """ if DATA_DISPATCHER not in hass.data: hass.data[DATA_DISPATCHER] = {} job = HassJob( catch_log_exception( target, lambda *args: "Exception in {} when dispatching '{}': {}".format( # Functions wrapped in partial do not have a __name__ getattr(target, "__name__", None) or str(target), signal, args, ), )) hass.data[DATA_DISPATCHER].setdefault(signal, []).append(job) @callback def async_remove_dispatcher() -> None: """Remove signal listener.""" try: hass.data[DATA_DISPATCHER][signal].remove(job) except (KeyError, ValueError): # KeyError is key target listener did not exist # ValueError if listener did not exist within signal _LOGGER.warning("Unable to remove unknown dispatcher %s", target) return async_remove_dispatcher
def async_track_sunset( hass: HomeAssistant, action: Callable[..., None], offset: timedelta | None = None ) -> CALLBACK_TYPE: """Add a listener that will fire a specified offset from sunset daily.""" listener = SunListener(hass, HassJob(action), SUN_EVENT_SUNSET, offset) listener.async_attach() return listener.async_detach
async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, action: TriggerActionType, trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for events based on configuration.""" trigger_data = trigger_info["trigger_data"] event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) description = event if offset: description = f"{description} with offset" job = HassJob(action) @callback def call_action(): """Call action with right context.""" hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": "sun", "event": event, "offset": offset, "description": description, } }, ) if event == SUN_EVENT_SUNRISE: return async_track_sunrise(hass, call_action, offset) return async_track_sunset(hass, call_action, offset)
def async_track_utc_time_change( hass: HomeAssistant, action: None, hour: Optional[Any] = None, minute: Optional[Any] = None, second: Optional[Any] = None, tz: Optional[Any] = None, ) -> CALLBACK_TYPE: """Add a listener that will fire if time matches a pattern.""" # This is function is modifies to support timezones. # We do not have to wrap the function with time pattern matching logic # if no pattern given if all(val is None for val in (hour, minute, second)): # Previously this relied on EVENT_TIME_FIRED # which meant it would not fire right away because # the caller would always be misaligned with the call # time vs the fire time by < 1s. To preserve this # misalignment we use async_track_time_interval here return async_track_time_interval(hass, action, timedelta(seconds=1)) job = HassJob(action) matching_seconds = dt_util.parse_time_expression(second, 0, 59) matching_minutes = dt_util.parse_time_expression(minute, 0, 59) matching_hours = dt_util.parse_time_expression(hour, 0, 23) def calculate_next(now: datetime) -> datetime: """Calculate and set the next time the trigger should fire.""" ts_now = now.astimezone(tz) if tz else now return dt_util.find_next_time_expression_time( ts_now, matching_seconds, matching_minutes, matching_hours ) time_listener: CALLBACK_TYPE | None = None @callback def pattern_time_change_listener(_: datetime) -> None: """Listen for matching time_changed events.""" nonlocal time_listener now = time_tracker_utcnow() hass.async_run_hass_job(job, now.astimezone(tz) if tz else now) time_listener = async_track_point_in_utc_time( hass, pattern_time_change_listener, calculate_next(now + timedelta(seconds=1)), ) time_listener = async_track_point_in_utc_time( hass, pattern_time_change_listener, calculate_next(dt_util.utcnow()) ) @callback def unsub_pattern_time_change_listener() -> None: """Cancel the time listener.""" assert time_listener is not None time_listener() return unsub_pattern_time_change_listener
def async_track_utc_time_change( hass: HomeAssistant, action: Callable[..., None], hour: Optional[Any] = None, minute: Optional[Any] = None, second: Optional[Any] = None, local: bool = False, ) -> CALLBACK_TYPE: """Add a listener that will fire if time matches a pattern.""" job = HassJob(action) # We do not have to wrap the function with time pattern matching logic # if no pattern given if all(val is None for val in (hour, minute, second)): @callback def time_change_listener(event: Event) -> None: """Fire every time event that comes in.""" hass.async_run_hass_job(job, event.data[ATTR_NOW]) return hass.bus.async_listen(EVENT_TIME_CHANGED, time_change_listener) matching_seconds = dt_util.parse_time_expression(second, 0, 59) matching_minutes = dt_util.parse_time_expression(minute, 0, 59) matching_hours = dt_util.parse_time_expression(hour, 0, 23) def calculate_next(now: datetime) -> datetime: """Calculate and set the next time the trigger should fire.""" localized_now = dt_util.as_local(now) if local else now return dt_util.find_next_time_expression_time(localized_now, matching_seconds, matching_minutes, matching_hours) time_listener: Optional[CALLBACK_TYPE] = None @callback def pattern_time_change_listener(_: datetime) -> None: """Listen for matching time_changed events.""" nonlocal time_listener now = time_tracker_utcnow() hass.async_run_hass_job(job, dt_util.as_local(now) if local else now) time_listener = async_track_point_in_utc_time( hass, pattern_time_change_listener, calculate_next(now + timedelta(seconds=1)), ) time_listener = async_track_point_in_utc_time( hass, pattern_time_change_listener, calculate_next(dt_util.utcnow())) @callback def unsub_pattern_time_change_listener() -> None: """Cancel the time listener.""" assert time_listener is not None time_listener() return unsub_pattern_time_change_listener
async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, action: TriggerActionType, trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Trigger based on incoming webhooks.""" webhook_id: str = config[CONF_WEBHOOK_ID] job = HassJob(action) triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault( WEBHOOK_TRIGGERS, {}) if webhook_id not in triggers: async_register( hass, trigger_info["domain"], trigger_info["name"], webhook_id, _handle_webhook, ) triggers[webhook_id] = [] trigger_instance = TriggerInstance(trigger_info, job) triggers[webhook_id].append(trigger_instance) @callback def unregister(): """Unregister webhook.""" triggers[webhook_id].remove(trigger_instance) if not triggers[webhook_id]: async_unregister(hass, webhook_id) triggers.pop(webhook_id) return unregister
def change_listener(self, change_listener: Callable[..., Any]) -> None: """Update the change_listener.""" self._change_listener = change_listener if ( self._change_listener_job is None or change_listener != self._change_listener_job.target ): self._change_listener_job = HassJob(change_listener)
async def async_attach_trigger(hass, config, action, automation_info, *, platform_type="event"): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) event_data_schema = None if config.get(CONF_EVENT_DATA): event_data_schema = vol.Schema( { vol.Required(key): value for key, value in config.get(CONF_EVENT_DATA).items() }, extra=vol.ALLOW_EXTRA, ) event_context_schema = None if config.get(CONF_EVENT_CONTEXT): event_context_schema = vol.Schema( { vol.Required(key): _schema_value(value) for key, value in config.get(CONF_EVENT_CONTEXT).items() }, extra=vol.ALLOW_EXTRA, ) job = HassJob(action) @callback def handle_event(event): """Listen for events and calls the action when data matches.""" try: # Check that the event data and context match the configured # schema if one was provided if event_data_schema: event_data_schema(event.data) if event_context_schema: event_context_schema(event.context.as_dict()) except vol.Invalid: # If event doesn't match, skip event return hass.async_run_hass_job( job, { "trigger": { "platform": platform_type, "event": event, "description": f"event '{event.event_type}'", } }, event.context, ) return hass.bus.async_listen(event_type, handle_event)
def async_track_entity_registry_updated_event( hass: HomeAssistant, entity_ids: Union[str, Iterable[str]], action: Callable[[Event], Any], ) -> Callable[[], None]: """Track specific entity registry updated events indexed by entity_id. Similar to async_track_state_change_event. """ entity_ids = _async_string_to_lower_list(entity_ids) if not entity_ids: return _remove_empty_listener entity_callbacks = hass.data.setdefault( TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS, {}) if TRACK_ENTITY_REGISTRY_UPDATED_LISTENER not in hass.data: @callback def _async_entity_registry_updated_dispatcher(event: Event) -> None: """Dispatch entity registry updates by entity_id.""" entity_id = event.data.get("old_entity_id", event.data["entity_id"]) if entity_id not in entity_callbacks: return for job in entity_callbacks[entity_id][:]: try: hass.async_run_hass_job(job, event) except Exception: # pylint: disable=broad-except _LOGGER.exception( "Error while processing entity registry update for %s", entity_id, ) hass.data[ TRACK_ENTITY_REGISTRY_UPDATED_LISTENER] = hass.bus.async_listen( EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_registry_updated_dispatcher) job = HassJob(action) for entity_id in entity_ids: entity_callbacks.setdefault(entity_id, []).append(job) @callback def remove_listener() -> None: """Remove state change listener.""" _async_remove_indexed_listeners( hass, TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS, TRACK_ENTITY_REGISTRY_UPDATED_LISTENER, entity_ids, job, ) return remove_listener
async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, action: AutomationActionType, automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] source: str = config[CONF_SOURCE].lower() zone_entity_id = config.get(CONF_ZONE) trigger_event = config.get(CONF_EVENT) job = HassJob(action) @callback def state_change_listener(event): """Handle specific state changes.""" # Skip if the event's source does not match the trigger's source. from_state = event.data.get("old_state") to_state = event.data.get("new_state") if not source_match(from_state, source) and not source_match( to_state, source): return if (zone_state := hass.states.get(zone_entity_id)) is None: _LOGGER.warning( "Unable to execute automation %s: Zone %s not found", automation_info["name"], zone_entity_id, ) return from_match = (condition.zone(hass, zone_state, from_state) if from_state else False) to_match = condition.zone(hass, zone_state, to_state) if to_state else False if (trigger_event == EVENT_ENTER and not from_match and to_match or trigger_event == EVENT_LEAVE and from_match and not to_match): hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": "geo_location", "source": source, "entity_id": event.data.get("entity_id"), "from_state": from_state, "to_state": to_state, "zone": zone_state, "event": trigger_event, "description": f"geo_location - {source}", } }, event.context, )
async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] source = config.get(CONF_SOURCE).lower() zone_entity_id = config.get(CONF_ZONE) trigger_event = config.get(CONF_EVENT) job = HassJob(action) @callback def state_change_listener(event): """Handle specific state changes.""" # Skip if the event's source does not match the trigger's source. from_state = event.data.get("old_state") to_state = event.data.get("new_state") if not source_match(from_state, source) and not source_match( to_state, source): return zone_state = hass.states.get(zone_entity_id) if zone_state is None: _LOGGER.warning( "Unable to execute automation %s: Zone %s not found", automation_info["name"], zone_entity_id, ) return from_match = (condition.zone(hass, zone_state, from_state) if from_state else False) to_match = condition.zone(hass, zone_state, to_state) if to_state else False if (trigger_event == EVENT_ENTER and not from_match and to_match or trigger_event == EVENT_LEAVE and from_match and not to_match): hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": "geo_location", "source": source, "entity_id": event.data.get("entity_id"), "from_state": from_state, "to_state": to_state, "zone": zone_state, "event": trigger_event, "description": f"geo_location - {source}", } }, event.context, ) return async_track_state_change_filtered( hass, TrackStates(False, set(), {DOMAIN}), state_change_listener).async_remove
async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, action: AutomationActionType, automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] entities: dict[str, CALLBACK_TYPE] = {} removes = [] job = HassJob(action) @callback def time_automation_listener(description, now, *, entity_id=None): """Listen for time changes and calls action.""" hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": "time", "now": now, "description": description, "entity_id": entity_id, } }, ) @callback def update_entity_trigger_event(event): """update_entity_trigger from the event.""" return update_entity_trigger(event.data["entity_id"], event.data["new_state"]) @callback def update_entity_trigger(entity_id, new_state=None): """Update the entity trigger for the entity_id.""" # If a listener was already set up for entity, remove it. if remove := entities.pop(entity_id, None): remove() remove = None if not new_state: return # Check state of entity. If valid, set up a listener. if new_state.domain == "input_datetime": if has_date := new_state.attributes["has_date"]: year = new_state.attributes["year"] month = new_state.attributes["month"] day = new_state.attributes["day"] if has_time := new_state.attributes["has_time"]: hour = new_state.attributes["hour"] minute = new_state.attributes["minute"] second = new_state.attributes["second"]
def __init__(self, hass: HomeAssistant, user_id: str, token: str): self._hass = hass self._user_id = user_id self._token = token self._property_entities = self._get_property_entities() self._session = async_create_clientsession(hass) self._unsub_pending: CALLBACK_TYPE | None = None self._pending = deque() self._report_states_job = HassJob(self._report_states)