Skip to content

feat(connection): add OWNSession.is_open and reset is_connected on close - #35

Merged
xtimmy86x merged 3 commits into
OpenWebNet-HA:masterfrom
GreenGrassBlueOcean:feat/session-is-open
Sep 16, 2026
Merged

xtimmy86x merged 3 commits into
OpenWebNet-HA:masterfrom
GreenGrassBlueOcean:feat/session-is-open

Conversation

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor

Summary

This PR addresses and closes #34 by providing a truthful transport view of the command session's socket state and aligning close() semantics:

  1. OWNSession.is_open:

    • Exposes True iff both _stream_reader and _stream_writer are set and active.
    • Allows integrations (such as MyHOME) to verify whether the command session has an open socket without reaching into private stream attributes.
  2. OWNSession.close() resets is_connected:

    • Explicit close() now drops the streams and clears _connected, notifying on_state_change on the transition so consumers don't see is_connected == True after teardown.
    • Internal stream recycles (_reconnect() and retry loops) use _close_streams(), preserving the no-flap behavior for transient reconnects.
    • OWNEventSession._close_streams() ensures keepalive tasks are cleanly canceled during teardown and recycles.

Closes #34.

GreenGrassBlueOcean and others added 2 commits September 15, 2026 12:06
… view

MyHOME's _session_is_open() helper (OpenWebNet-HA/MyHOME#318) had to read
_stream_reader / _stream_writer because is_connected reports the negotiated
state, which close() does not reset: after an idle close it still reads True
while send() would reopen the session based on the streams.

is_open exposes exactly that transport view - True iff both streams are set -
so the integration can drop its dependency on private names.

Refs OpenWebNet-HA#34

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…close_streams()

close() used to drop the streams but leave the negotiated flag as connect()
last set it, so a consumer that closed a session still saw is_connected True.
close() is now the explicit teardown: it releases the socket and flips the
flag, notifying on_state_change on the transition.

The no-flap rule for routine reconnects is preserved by construction: the
stream teardown moves to _close_streams(), which _reconnect() and the retry
loop in connect() call instead of close(), so a recycle that recovers never
signals False to the consumer. connect() keeps owning the flag on its give-up
and fatal-negotiation branches, as before.

OWNEventSession overrides _close_streams() rather than close(), so the
keepalive task is stopped on both the explicit close and an internal recycle.

Closes OpenWebNet-HA#34

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Sep 16, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 99.15966% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
OWNd/connection.py 99.15% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@Interstellar0verdrive Interstellar0verdrive left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the one thing that came out of the #318 finding, not the whole diff.

It does what it should: is_open reads the two streams, which is what send() actually looks at before reopening, and it's documented as distinct from is_connected. close() now releases the socket and clears the connected flag, while the internal recycling paths (_reconnect() and the retry loop inside connect()) go through the new _close_streams(), so a routine reconnect still doesn't flap availability. The command session's override moved to _close_streams() too, so the keepalive can't outlive the socket on either path. The three new tests cover exactly those cases, including the one that matters most: closed, flag False; recycled, flag untouched.

One inconsistency, and I'd rather ask than assert since the library isn't my ground: inside _locked_send the failure paths still call self.close() — the timeout, the connection-reset retry, the cancellation, the "session unavailable" branch. By the rationale in your own docstring those look like internal recycling, so _close_streams() would be the match; as written, a single command timeout now flips is_connected and notifies the consumer, where before the flag stayed put. It happens to be harmless for MyHOME today, because the command session is built without an on_state_change callback there (only the event session has one), so nothing downstream sees it. But it's a behaviour change that's invisible until someone wires that callback up, and then a busy bus would look like a gateway going offline and back.

Everything beyond this point in the diff needs someone who knows the library; I'd defer to @fedem95 or @xtimmy86x for the rest.

@xtimmy86x

Copy link
Copy Markdown
Contributor

Thanks for this improvement. I reviewed 1fe9d81 and can confirm @Interstellar0verdrive’s concern.

In an isolated simulation using the PR’s actual methods, a status request encounters a connection reset, retries successfully, and returns success—but on_state_change receives [False, True]. This happens because _locked_send() still calls close() before retrying, which now clears the connected flag.

Please use _close_streams() for recoverable retries, preserving the connected state when recovery succeeds. Definitive failures and explicit teardown should still report disconnection; I would avoid replacing every close() call indiscriminately.

A regression test should verify both cases: no offline/online transition during a successful retry, and a disconnection notification when recovery fails.

The distinction between is_open and is_connected is useful, and CI is green. I would like this remaining inconsistency addressed before approving. My verification was an isolated simulation, without hardware testing.

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor Author

@xtimmy86x , @Interstellar0verdrive addressing this as we speak

Review of 1fe9d81 (OpenWebNet-HA#35, @xtimmy86x): _locked_send() still called close()
before retrying after a transport error, and close() now clears the
connected flag - so a status request that lost its socket and recovered
on the retry told the consumer [False, True] for a session that was never
really gone.

The recoverable path (status request, or a command not yet written)
now releases the streams only, like _reconnect() does. The definitive
loss - no retry left, or a written command whose acknowledgement was
lost - reports disconnection as before. Three tests pin both halves:
no transition on a successful retry; exactly one False when both
attempts fail; exactly one False for a written command.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor Author

@xtimmy86x — confirmed and fixed in 54f61e9. Your simulation was right: _locked_send() closed the session on a transport error before deciding whether it would retry, and after 1fe9d81 that close carries the flag.

What changed — one path only, as you suggested, not a blanket replacement:

  • ConnectionResetError / IncompleteReadError / LimitOverrunError / OSError in _locked_send(): now _close_streams() first, then the existing retry decision. If it retries (status request, or a command not yet written) the reconnect is an internal recycle and the consumer hears nothing. If it does not retry, _set_connected(False) — the loss is definitive and reported once.
  • Every other close() in _locked_send() stays: connect() giving up, the response timeout, cancellation, the crash path. None of those retries, so each is a real loss.

Regression tests (tests/test_connection_extended.py, TestOWNCommandSession), covering both halves you asked for and the third case in between:

case on_state_change sees
status request, reset, retry succeeds [True] — nothing new
status request, reset on both attempts [True, False]
command already written, ack lost (never replayed) [True, False]

The first one fails on 1fe9d81 with exactly your observation ([True, False, True]). OWNd: 630 passed, mypy and ruff clean. MyHOME's full suite against this branch: 1599 passed.

#36 is rebased on top (9c122ec), no changes of its own. I'll keep the two landing in that order: #35 first, on its own.

@xtimmy86x xtimmy86x left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing this in 54f61e9. I reviewed the fix and reran the isolated simulation: a successful retry now completes without an offline/online notification.

The change preserves disconnection reporting for definitive failures and avoids replaying commands already written. The three regression tests cover the requested cases, and CI is green across Python 3.11–3.14, with 630 tests passing.

My previous concern is resolved. No further blocking concerns from my review—approved.

@xtimmy86x
xtimmy86x merged commit 6508634 into OpenWebNet-HA:master Sep 16, 2026
6 checks passed
GreenGrassBlueOcean added a commit to GreenGrassBlueOcean/OWNd that referenced this pull request Sep 16, 2026
…e, test)

master stored five files with CRLF - .gitignore, LICENSE, __main__.py,
connection.py, message.py - next to an LF majority, and connection.py
mixed the two within the file. Any edit from a differently configured
editor then rewrote the whole file: OpenWebNet-HA#35 rendered as +248/-218 for a
+38/-8 change, OpenWebNet-HA#36 as +691/-615 for +324/-248, and a reviewer passed
on both for that reason.

`* text=auto eol=lf`, `git add --renormalize .`, and
tests/test_line_endings.py so a CR that bypasses the clean filter fails
the suite instead of the next reviewer. That test found one such path
straight away: validate_corpus.py rewrote tests/golden/corpus.json with
the platform newline on every run, so any Windows checkout that ran the
suite had a CRLF corpus.json waiting to be committed. It now writes LF.

Every byte other than line endings is unchanged: `git diff
--ignore-cr-at-eol` shows only the two new files and the one-line
newline argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GreenGrassBlueOcean added a commit to GreenGrassBlueOcean/OWNd that referenced this pull request Sep 16, 2026
…e, test)

master stored five files with CRLF - .gitignore, LICENSE, __main__.py,
connection.py, message.py - next to an LF majority, and connection.py
mixed the two within the file. Any edit from a differently configured
editor then rewrote the whole file: OpenWebNet-HA#35 rendered as +248/-218 for a
+38/-8 change, OpenWebNet-HA#36 as +691/-615 for +324/-248, and a reviewer passed
on both for that reason.

`* text=auto eol=lf`, `git add --renormalize .`, and
tests/test_line_endings.py so a CR that bypasses the clean filter fails
the suite instead of the next reviewer. That test found one such path
straight away: validate_corpus.py rewrote tests/golden/corpus.json with
the platform newline on every run, so any Windows checkout that ran the
suite had a CRLF corpus.json waiting to be committed. It now writes LF.

Every byte other than line endings is unchanged: `git diff
--ignore-cr-at-eol` shows only the two new files and the one-line
newline argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GreenGrassBlueOcean added a commit to GreenGrassBlueOcean/OWNd that referenced this pull request Sep 16, 2026
Two jobs run on push and pull request, with this checkout installed in
place of the release MyHOME pins:

  corpus - MyHOME's golden corpus and smoke gates (parser parity,
           platform imports, mock-gateway loopback). Seconds.
  suite  - MyHOME's full test suite on core 2026.x / Python 3.14.
           Minutes. This is the run that showed OpenWebNet-HA#35 breaking one
           MyHOME test and the retry flap xtimmy86x found in review.

workflow_dispatch takes a MyHOME ref, so a release candidate can be
proven against a MyHOME branch before either side tags.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose a public 'socket is open' property on OWNSession (MyHOME reads _stream_reader/_stream_writer)

4 participants