⚠️ This is an investigation, not a fresh confirmed reproduction. I read the code path and matched it against an existing in-the-wild traceback. Filing as a tracking issue since the in-flight fix lives in #720 alongside several unrelated changes and there is no single issue covering it.
HA core home-assistant/core#168432 (April 2026, SLZB-06M over the network, Python 3.14, bellows 0.49.0) includes:
File "/usr/local/lib/python3.14/site-packages/bellows/ezsp/__init__.py", line 224, in disconnect
await self._gw.disconnect()
TypeError: 'NoneType' object can't be awaited
bellows/ezsp/__init__.py:221-225 already guards if self._gw: before the await:
async def disconnect(self):
self.stop_ezsp()
if self._gw:
await self._gw.disconnect()
self._gw = None
So self._gw itself is not the None. The None is the return value of self._gw.disconnect(). _gw is a ThreadsafeProxy, so the call routes through ThreadsafeProxy.__getattr__ (bellows/thread.py:81-110):
def func_wrapper(*args, **kwargs):
loop = self._obj_loop
curr_loop = asyncio.get_running_loop()
call = functools.partial(func, *args, **kwargs)
if loop == curr_loop:
return call()
if loop.is_closed():
# Disconnected
LOGGER.warning("Attempted to use a closed event loop")
return # ← silent None
if asyncio.iscoroutinefunction(func):
future = asyncio.run_coroutine_threadsafe(call(), loop)
return asyncio.wrap_future(future, loop=curr_loop)
else:
...
If the proxied EventLoopThread's loop is closed (i.e. the worker thread has already exited or its loop has been closed) at the moment of the call, the proxy logs a warning and returns plain None. An async caller doing await proxy.some_async_method() then awaits None and gets TypeError: 'NoneType' object can't be awaited. The error propagates up _async_initialize → shutdown → disconnect and leaves ZHA in an indefinite retry loop, since the cleanup itself fails.
There is also a small race between the loop.is_closed() check and run_coroutine_threadsafe(...) (the loop can become closed in between), which would today raise from run_coroutine_threadsafe and is otherwise survivable, but worth addressing in the same place.
Already-in-flight fix
@silenthooligan's open PR #720 fixes this as part of a larger bundle (Python 3.14 deprecation cleanup, EZSP startup-reset race for TCP serial bridges, and tolerance for _gw is None in _startup_reset / disconnect). The relevant change raises ConnectionError instead of returning None, and guards the loop-closed-mid-dispatch race.
Filing this mainly to give the bug a tracking point that is decoupled from the bundled PR. Happy to close as duplicate if maintainers prefer to track entirely from #720, or to split #720 into smaller PRs by topic.
What I have not verified
- I have not produced a fresh reproducer log. The traceback is from the original #168432 report.
- On a current Python 3.14.4 install I tested,
asyncio.iscoroutinefunction works (it emits a DeprecationWarning but still returns the right boolean), so the proxy's coroutine-detection path itself is functioning. The closed-loop branch is the path most likely firing in the reported case, triggered by an ESPHome serial-bridge connection blip on the SLZB-06M.
HA core home-assistant/core#168432 (April 2026, SLZB-06M over the network, Python 3.14, bellows 0.49.0) includes:
bellows/ezsp/__init__.py:221-225already guardsif self._gw:before the await:So
self._gwitself is not theNone. TheNoneis the return value ofself._gw.disconnect()._gwis aThreadsafeProxy, so the call routes throughThreadsafeProxy.__getattr__(bellows/thread.py:81-110):If the proxied
EventLoopThread's loop is closed (i.e. the worker thread has already exited or its loop has been closed) at the moment of the call, the proxy logs awarningand returns plainNone. An async caller doingawait proxy.some_async_method()then awaitsNoneand getsTypeError: 'NoneType' object can't be awaited. The error propagates up_async_initialize→shutdown→disconnectand leaves ZHA in an indefinite retry loop, since the cleanup itself fails.There is also a small race between the
loop.is_closed()check andrun_coroutine_threadsafe(...)(the loop can become closed in between), which would today raise fromrun_coroutine_threadsafeand is otherwise survivable, but worth addressing in the same place.Already-in-flight fix
@silenthooligan's open PR #720 fixes this as part of a larger bundle (Python 3.14 deprecation cleanup, EZSP startup-reset race for TCP serial bridges, and tolerance for
_gw is Nonein_startup_reset/disconnect). The relevant change raisesConnectionErrorinstead of returningNone, and guards the loop-closed-mid-dispatch race.Filing this mainly to give the bug a tracking point that is decoupled from the bundled PR. Happy to close as duplicate if maintainers prefer to track entirely from #720, or to split #720 into smaller PRs by topic.
What I have not verified
asyncio.iscoroutinefunctionworks (it emits aDeprecationWarningbut still returns the right boolean), so the proxy's coroutine-detection path itself is functioning. The closed-loop branch is the path most likely firing in the reported case, triggered by an ESPHome serial-bridge connection blip on the SLZB-06M.