Beispiel #1
0
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
Beispiel #2
0
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)
Beispiel #3
0
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)
Beispiel #4
0
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,
            )
Beispiel #5
0
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,
            )
Beispiel #6
0
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)
Beispiel #7
0
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
Beispiel #8
0
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)
Beispiel #9
0
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
Beispiel #10
0
 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)