fix(thread-safety): marshal pyhon MQTT push updates onto the event loop

PR #43 (cherry-picked in the previous commit) only swaps
async_write_ha_state() for schedule_update_ha_state() inside entity
command methods, which run on the event loop anyway. It does NOT address
the actual root cause of mmalolepszy/hon-revived#44.

pyhon-revived delivers MQTT push notifications from the awscrt network
thread: MQTTClient._on_publish_received -> Hon.notify ->
self._notify_function(None), with no loop marshalling. The integration
registered coordinator.async_set_updated_data directly as that
subscriber, so the loop-affine @callback (which fans out to listeners and
arms loop timers via _schedule_refresh) was being invoked from a foreign
thread. HA Core 2026.5.x enforces thread safety and raises on this,
breaking live state updates on 2026.5.4 even with PR #43 applied.

Wrap the subscriber in a @callback that re-dispatches via
hass.loop.call_soon_threadsafe, so the coordinator update always runs on
the event loop. The None payload is preserved (entities read state from
self._device, not coordinator.data, so it is only a listener trigger).

Refs: https://developers.home-assistant.io/docs/asyncio_thread_safety/
Fixes mmalolepszy/hon-revived#44

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Alexander Tihoniuk 2026-06-12 10:54:56 +03:00
parent 047800ccbf
commit ca8ed250c2

View file

@ -6,7 +6,7 @@ import voluptuous as vol # type: ignore[import-untyped]
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers import config_validation as cv, aiohttp_client
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from pyhon import Hon
@ -48,7 +48,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator: DataUpdateCoordinator[dict[str, Any]] = DataUpdateCoordinator(
hass, _LOGGER, name=DOMAIN
)
hon.subscribe_updates(coordinator.async_set_updated_data)
@callback
def _push_update(data: Any) -> None:
"""Apply a pyhon push update on the event loop.
pyhon-revived delivers MQTT push notifications from the awscrt
network thread and invokes this subscriber synchronously (see
pyhon.connection.mqtt.MQTTClient._on_publish_received ->
Hon.notify). Calling the loop-affine ``async_set_updated_data``
directly from that foreign thread raises a thread-safety error on
HA Core 2026.5.x, so we marshal it onto the event loop.
"""
hass.loop.call_soon_threadsafe(coordinator.async_set_updated_data, data)
hon.subscribe_updates(_push_update)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.unique_id] = {"hon": hon, "coordinator": coordinator}