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
def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: """Send signal and data. This method must be run in the event loop. """ target_list: dict[Callable[..., Any], HassJob[..., None | Coroutine[Any, Any, None]] | None] = hass.data.get(DATA_DISPATCHER, {}).get(signal, {}) run: list[HassJob[..., None | Coroutine[Any, Any, None]]] = [] for target, job in target_list.items(): if job is None: job = _generate_job(signal, target) target_list[target] = job # Run the jobs all at the end # to ensure no jobs add more dispatchers # which can result in the target_list # changing size during iteration run.append(job) for job in run: hass.async_run_hass_job(job, *args)
def async_track_point_in_utc_time( hass: HomeAssistant, action: HassJob | Callable[..., Awaitable[None] | 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. cancel_callback: asyncio.TimerHandle | None = None @callback def run_action(job: HassJob) -> None: """Call the action.""" nonlocal cancel_callback now = time_tracker_utcnow() # Depending on the available clock support (including timer hardware # and the OS kernel) it can happen that we fire a little bit too early # as measured by utcnow(). That is bad when callbacks have assumptions # about the current time. Thus, we rearm the timer for the remaining # time. if (delta := (utc_point_in_time - now).total_seconds()) > 0: _LOGGER.debug("Called %f seconds too early, rearming", delta) cancel_callback = hass.loop.call_later(delta, run_action, job) return hass.async_run_hass_job(job, utc_point_in_time)
async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, action: TriggerActionType, trigger_info: TriggerInfo, *, platform_type: str = "zone", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = trigger_info["trigger_data"] entity_id: list[str] = config[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", trigger_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: 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, )
def _async_dispatch_domain_event(hass: HomeAssistant, event: Event, callbacks: dict[str, list]) -> None: domain = split_entity_id(event.data["entity_id"])[0] if domain not in callbacks and MATCH_ALL not in callbacks: return listeners = callbacks.get(domain, []) + callbacks.get(MATCH_ALL, []) for job in listeners: try: hass.async_run_hass_job(job, event) except Exception: # pylint: disable=broad-except _LOGGER.exception("Error while processing event %s for domain %s", event, domain)
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) job = HassJob(action) if event == EVENT_SHUTDOWN: @callback def hass_shutdown(event): """Execute when Home Assistant is shutting down.""" hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": "homeassistant", "event": event, "description": "Home Assistant stopping", } }, event.context, ) return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_shutdown) # Automation are enabled while hass is starting up, fire right away # Check state because a config reload shouldn't trigger it. if trigger_info["home_assistant_start"]: hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": "homeassistant", "event": event, "description": "Home Assistant starting", } }, ) return lambda: None
def async_at_start( hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Awaitable[None] | None] ) -> None: """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 async def _matched_event(event: Event) -> None: """Call the callback when Home Assistant started.""" hass.async_run_hass_job(at_start_job, hass) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event)
def async_listen_platform( hass: core.HomeAssistant, component: str, callback: Callable[[str, dict[str, Any] | None], Any], ) -> None: """Register a platform loader listener. This method must be run in the event loop. """ service = EVENT_LOAD_PLATFORM.format(component) job = core.HassJob(callback) async def discovery_platform_listener(discovered: DiscoveryDict) -> None: """Listen for platform discovery events.""" if not (platform := discovered["platform"]): return task = hass.async_run_hass_job(job, platform, discovered.get("discovered")) if task: await task
def async_run(self, hass: HomeAssistant, context: Context | None = None) -> None: """Run all turn on triggers.""" for job, variables in self._actions.values(): hass.async_run_hass_job(job, variables, context)