Repository navigation
Conversation
Since Python 3.12, asyncio.Server.wait_closed() waits for every accepted connection to finish, not only for the listening socket. handle_line checks shutdown_flag only between reads and otherwise blocks in reader.read(), so async_stop() returned only after every connected panel had sent its next message, and never if a connection had gone silent. The TCP server now keeps the writers of its open connections, and async_stop() closes them before awaiting wait_closed().
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SIAClientTCP.async_stop()callsserver.close()and thenawait server.wait_closed(). Since Python 3.12,wait_closed()also waits for every accepted connection to finish (docs, "Changed in version 3.12").handle_lineonly checksshutdown_flagbetween reads and otherwise blocks inreader.read(1000), soasync_stop()returns only after every connected panel has sent its next message, and never if a connection has gone silent.In Home Assistant this shows up as a slow shutdown: the
siaintegration callsasync_stop()from itsEVENT_HOMEASSISTANT_STOPlistener. My Ajax hub keeps two connections open (Ethernet and Wi-Fi) and sends a heartbeat on each every 60 s, so every HA stop waited up to a minute on SIA, and several times all three shutdown stages timed out (Timed out waiting for integrations to stop … SIAHub.async_shutdown()) and the stop took about 3.5 minutes.This PR keeps the writers of open TCP connections in
SIAServerTCPand closes them inasync_stop()beforewait_closed(). Closing the transport makes the pendingreader.read()return, sohandle_lineleaves through its existingfinally. It works on every supported Python;Server.close_clients()would do the same but only exists from 3.13.Test plan
test_aio_tcp_stop_with_idle_connection: opens a connection that sends nothing and expectsasync_stop()to return within 2 s. Fails onmasterwithTimeoutError, passes with the fix.master).pydocstyle src/pysiaalarm/aio/clean.Note: with the current unpinned test extras (
pytest9.1.1,pytest-cases3.10.1) the suite doesn't start (TypeError: IdMaker.__init__() takes 7 positional arguments but 8 were giveninpytest_cases). I ran it withpytest<9.Notes for the reviewer
Validated on a live Home Assistant 2026.9.3 install (pysiaalarm 3.2.2, Python 3.14) with an Ajax Hub 2 Plus: with the same change applied, reloading the SIA config entry, which goes through the same
async_stop(), took 0.03 s, and the hub reconnected on both channels within 4 s. Before, it waited for the next heartbeat. I also checked stopping while the event callback is still running and with a peer that never reads:async_stop()returns at once in both cases, and the callback finishes on its own.