Re: alexa_media calls hass.bus.async_fire from a thread other than the event loop

I’m new to the inner python world of HA integrations and I’m perplexed about how this error should be solved: alexa_media calls hass.bus.async_fire from a thread other than the event loop

sensor.py has been throwing the error and I’ve figured out it’s when an echo alarm is dismissed/snoozed and I’m not sure if the code should be decorated with @callback so it runs in the event loop or if self.hass.bus.async_fire should be changed to self.hass.bus.fire and left to run in the executor.

Event loop using @callback and hass.bus.async_fire:

    def _process_raw_notifications(self):
        self._all = (
            list(map(self._fix_alarm_date_time, self._n_dict.items()))
            if self._n_dict
            else []
        )
        self._all = list(map(self._update_recurring_alarm, self._all))
        self._all = sorted(self._all, key=lambda x: x[1][self._sensor_property])
        self._prior_value = self._next if self._active else None
        self._active = (
            list(filter(lambda x: x[1]["status"] in ("ON", "SNOOZED"), self._all))
            if self._all
            else []
        )
        self._next = self._active[0][1] if self._active else None
        alarm = next(
            (alarm[1] for alarm in self._all if alarm[1].get("id") == self._amz_id),
            None,
        )

        if alarm_just_dismissed(alarm, self._status, self._version):
            self._dismissed = dt.now().isoformat()
        self._attr_native_value = self._process_state(self._next)
        self._status = self._next.get("status", "OFF") if self._next else "OFF"
        self._version = self._next.get("version", "0") if self._next else None
        self._amz_id = self._next.get("id") if self._next else None

        if self._attr_native_value is None 
        if self._attr_native_value is None or self._next != self._prior_value:
            # cancel any event triggers
            if self._tracker:
                _LOGGER.debug(
                    "%s: Cancelling old event",
                    self,
                )
                self._tracker()
            if self._attr_native_value is not None and self._status != "SNOOZED":
                _LOGGER.debug(
                    "%s: Scheduling event in %s",
                    self,
                    dt.as_utc(self._attr_native_value) - dt.utcnow(),
                )
                self._tracker = async_track_point_in_utc_time(
                    self.hass,
                    self._trigger_event,                   < = = = = = 
                    dt.as_utc(self._attr_native_value),
                )

    @callback                                              <<<<<<<<<<<   
    def _trigger_event(self, time_date) -> None:
        _LOGGER.debug(
            "%s:Firing %s at %s",
            self,
            "alexa_media_notification_event",
            dt.as_local(time_date),
        )
        self.hass.bus.async_fire(                          < = = = = = 
            "alexa_media_notification_event",
            event_data={
                "email": hide_email(self._account),
                "device": {"name": self.name, "entity_id": self.entity_id},
                "event": self._active[0],
            },
        )

Executor with hass.bus.fire:

    def _process_raw_notifications(self):
        self._all = (
            list(map(self._fix_alarm_date_time, self._n_dict.items()))
            if self._n_dict
            else []
        )
        self._all = list(map(self._update_recurring_alarm, self._all))
        self._all = sorted(self._all, key=lambda x: x[1][self._sensor_property])
        self._prior_value = self._next if self._active else None
        self._active = (
            list(filter(lambda x: x[1]["status"] in ("ON", "SNOOZED"), self._all))
            if self._all
            else []
        )
        self._next = self._active[0][1] if self._active else None
        alarm = next(
            (alarm[1] for alarm in self._all if alarm[1].get("id") == self._amz_id),
            None,
        )

        if alarm_just_dismissed(alarm, self._status, self._version):
            self._dismissed = dt.now().isoformat()
        self._attr_native_value = self._process_state(self._next)
        self._status = self._next.get("status", "OFF") if self._next else "OFF"
        self._version = self._next.get("version", "0") if self._next else None
        self._amz_id = self._next.get("id") if self._next else None

        if self._attr_native_value is None 
        if self._attr_native_value is None or self._next != self._prior_value:
            # cancel any event triggers
            if self._tracker:
                _LOGGER.debug(
                    "%s: Cancelling old event",
                    self,
                )
                self._tracker()
            if self._attr_native_value is not None and self._status != "SNOOZED":
                _LOGGER.debug(
                    "%s: Scheduling event in %s",
                    self,
                    dt.as_utc(self._attr_native_value) - dt.utcnow(),
                )
                self._tracker = async_track_point_in_utc_time(
                    self.hass,
                    self._trigger_event,               < = = = = = 
                    dt.as_utc(self._attr_native_value),
                )
                                                       < = = = = =
    def _trigger_event(self, time_date) -> None:  
        _LOGGER.debug(
            "%s:Firing %s at %s",
            self,
            "alexa_media_notification_event",
            dt.as_local(time_date),
        )
        hass.bus.fire(                                 <<<<<<<<<<< 
            "alexa_media_notification_event",
            event_data={
                "email": hide_email(self._account),
                "device": {"name": self.name, "entity_id": self.entity_id},
                "event": self._active[0],
            },
        )

They both eliminate the error for me so, which one is correct in this particular case
Or does it really not matter since _trigger_event is only being called at one point in the code?

Sorry for the late answer. But…

@callback basically tells HA that a non coroutine is safe to run in the event loop and is non blocking. If this isnt true, then you will get an error that your function is blocking the loop.

Running it without @callback will run it in another thread (hence the error you see).

async_fire checks if the call is coming from the main HA event loop and errors if not.

fire runs in an executor on the HA event loop.

So, in answer to your question, either is fine in this situation as the code in the method is non blocking, so long as you know the call is coming from the main event loop.

fire is safer in the fact that whatever thread or loop it is coming from, it will be run on the main HA loop and obviously if there was more code in the method that was blocking you couldn’t use @callback.

1 Like