Thread safety
Which API to call when your code runs outside the event loop, and how to fix the thread safety errors Home Assistant reports.
Nearly everything in asyncio is not thread safe, and neither are the core objects of Home Assistant. Calling an async_ method from another thread can corrupt the internal state of the event loop. The result is not a clean error, but random misbehavior and restarts that are very hard to trace back.
So Home Assistant checks. Many async_ APIs verify they are called from the event loop thread. If not, Home Assistant reports the call with a stack trace, and blocks it to keep the system stable. If you got here from such a report: the stack trace points at the code that made the call.
- In the event loop, in a coroutine or a
@callback: call theasync_version. - In another thread, like an executor job or a callback from a sync library: call the version without
async_, or schedule the work on the event loop.
Helpers that take a function decide how to run it by looking at it. A coroutine function or a function decorated with @callback runs in the event loop. Anything else is assumed to be blocking and runs in the executor.
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import Event, EventStateChangedData, callback
from homeassistant.helpers.event import async_track_state_change_event
async def async_added_to_hass(self) -> None:
"""Start tracking the other light."""
self.async_on_remove(
async_track_state_change_event(
self.hass, ["light.other"], self._async_light_changed
)
)
@callback
def _async_light_changed(
self, event: Event[EventStateChangedData]
) -> None:
"""Follow the state of the other light."""
new_state = event.data["new_state"]
if new_state is None or new_state.state in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
return
self.async_write_ha_state()Without @callback, _async_light_changed runs in a thread, and async_write_ha_state is called from the wrong thread. The fix is one line.
Some APIs have no thread safe version at all:
- The area, category, device, entity, floor, and label registries, including
async_get_or_create,async_update_entity,async_update_device, andasync_remove. hass.config_entries.async_update_entryandhass.config_entries.async_update_subentry.- Rendering templates, like
async_render_to_info. - Ignoring an issue with
issue_registry.async_ignore_issue.
For those, schedule a function on the event loop that makes the call. hass.add_job does that from any thread:
from homeassistant.core import callback
@callback
def _async_update_title(entry: ConfigEntry, title: str) -> None:
"""Update the title of the config entry."""
hass.config_entries.async_update_entry(entry, title=title)
# In a thread:
hass.add_job(_async_update_title, entry, "New title")Better still: ask yourself why the code runs in a thread. Often it is a sync library that could be wrapped once, at the edge, so the rest of your integration lives in the event loop.
Turn on debug mode while you develop, with debug: true under homeassistant: in configuration.yaml. Home Assistant then checks more calls, and reports them right away. See async in Home Assistant.