Repository navigation
feat(modbus): let the connection be kept between polls, off by default - #188
Merged
Merged
Conversation
Dialling a fresh connection for every read is the unusual choice for modbus TCP; one connection held open is the normal one. datalogger.persistent_ connection does that, and is off by default because it is a trade rather than an improvement: this datalogger accepts one connection at a time, so holding ours keeps everything else -- Solis Cloud included -- off the stick, where hanging up leaves a gap in every poll cycle where something else can get in. The client is now the app's rather than the poll's. ensure_connected is the only thing that dials, drop_connection the only thing that throws one away, and release_connection at the end of a good poll is the single place the setting is read. That also means a read that raised redials here instead of leaving pymodbus to do it out of sight, and a poll that could not read the whole span drops the connection so the next one starts fresh -- which is what every poll did before, so turning the setting on cannot make a bad morning worse than it already was. ensure_connected checks whether the client still holds a socket before reusing it. pymodbus closes its own socket when the peer hangs up mid-read (_handle_abrupt_socket_close) and its next request dials again silently, which is precisely the case being measured here: it would have counted as zero reconnects, on a socket carrying none of the keepalive set below. Keepalive is set on every connection: SO_KEEPALIVE, 30 seconds idle, then three probes five seconds apart, so a datalogger that vanished while the app slept is declared dead by the kernel inside 45 seconds instead of by the next read timing out CHUNK_ATTEMPTS times over. The option names are Linux's and the container is Alpine, but the tests run on macOS, so each is applied only where the platform has it. Every connection logs one numbered line saying why the previous one ended, at info when the setting is on and debug when it is off -- 2880 lines a day otherwise, saying only that the app is working as configured. That line is the point of shipping the setting at all: these sticks are widely reported to hang up on an idle connection after a minute or two, and at a 30 second poll interval that would mean reconnecting nearly every poll and gaining nothing. It cannot be settled from here, only measured. Turn it on, leave it a day, count the lines. Then the setting and the losing branch both go, and that removal is the breaking change worth a 4.0.0. The online transition needed no change: #181 already moved it off the handshake and onto the first chunk of registers that arrives, which is the only thing a reused connection could have said it from anyway. A poll whose reads are all refused still counts against the retry budget, kept connection or not, and a test pins it. The stub datalogger and the query/polls fixtures moved from test_modbus_read to conftest so the new connection lifetime tests poll the same stub. Closes #172 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BizgAhMgYcmMXjHFX7pbbs
ahinko
force-pushed
the
feat/persistent-connection
branch
from
August 20, 2026 18:30
513cb77 to
c343d39
Compare
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.
Closes #172.
Adds
datalogger.persistent_connection, off by default, which keeps the modbus connection open between polls instead of dialling a new one every time.Why off by default
The datalogger accepts one connection at a time, so holding ours keeps everything else — Solis Cloud included — off the stick. Hanging up after each poll leaves a gap in every cycle where something else can get in. That is a trade, not an improvement, so it has to be asked for.
The client is now the app's, not the poll's
ensure_connected()drop_connection(reason)release_connection()enable_keepalive()/keepalive_options()SO_KEEPALIVE, 30s idle, 5s probes, 3 probesread_chunktakes the client per attempt, so a read that raised drops the connection and the next attempt dials a fresh one — the redial is the app's now, not one pymodbus does out of sight.ensure_connectedchecks whether the client still holds a socket before reusing it. This matters more than it looks: pymodbus's_handle_abrupt_socket_closecloses its own socket when the peer hangs up mid-read, and its next request dials again silently — which is exactly the idle-hangup case being measured. It would have counted as zero reconnects, on a socket carrying none of the keepalive.TCP keepalive
On every connection, kept or not. 30 seconds idle plus three probes five seconds apart means a datalogger that vanished while the app slept is declared dead by the kernel inside 45 seconds, instead of by the next read timing out
CHUNK_ATTEMPTStimes over. The option names are Linux's and the container is Alpine, but the tests run on macOS, so each option is applied only where the platform has it.One line per connection
Info when the setting is on, debug when it is off — 2880 lines a day otherwise, saying only that the app is working as configured.
This is the point of shipping the setting at all. These sticks are widely reported to hang up on an idle connection after a minute or two; at a 30 second poll interval that would mean reconnecting nearly every poll and gaining nothing. It cannot be settled from here, only measured: turn it on, leave it a day, count the lines. Then the setting and the losing branch both go, and that removal is the breaking change worth a 4.0.0.
The behaviour change #172 flagged
None needed. #181 already moved the online transition off the handshake and onto the first chunk of registers that arrives — which is the only thing a reused connection could have said it from anyway. A poll whose reads are all refused still counts against the retry budget, kept connection or not, and
test_a_reused_connection_is_not_evidence_the_datalogger_is_therepins that.Tests
New
tests/test_persistent_connection.py— connection lifetime both ways, drop on a failed poll, mid-poll redial, the silent-hangup case, the log lines, and keepalive (including a socket that refuses the options). The stub datalogger and thequery/pollsfixtures moved toconftest.pyso both test files poll the same stub;test_modbus_read.pyimports them and is otherwise untouched.174 passed;
ruff format --checkandruff checkclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01BizgAhMgYcmMXjHFX7pbbs