Repository navigation
feat(connection): add typed exceptions, fast send reconnect, and API hardening - #132
Merged
GreenGrassBlueOcean merged 2 commits intoOct 20, 2026
Merged
Conversation
…paths and preserve discovery patch compatibility
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
fedem95
approved these changes
Oct 20, 2026
fedem95
left a comment
Contributor
There was a problem hiding this comment.
Tested merged with current master (a25024b): no conflicts, 1780 passed locally, CI green including the MyHOME suite. Also tried it on my MH201 (fw 3.6.27) with this branch's OWNd loaded from a temp dir, read-only frames only:
*#13**16##→ reply in 0.02 s; after 200 s of silencessshowed the socket in CLOSE-WAIT (the MH201 closes idle command sessions after ~2.5-3 min); the next*#13**16##hit the dead socket, reconnected with a single attempt and got the reply in 0.07 s;*#0##withraise_on_error=True→OWNNackError,last_errorset.
Against a local fake gateway (not the MH201), per send():
- EOF during negotiation: master 13.3 s / 5 TCP connects, this branch 0.05 s / 1 connect, and a second
send()within 2 s makes no connect at all; same numbers when the fake sends*#*1##before closing; - reset during negotiation: master waited 60 s between attempts (I cut each send at 120 s, 3 queued sends still blocked after 200 s); this branch fails at once with one connect for all three.
So the lock stall is gone. Approving, with notes:
- The flip side, on an MH201 with exhausted session slots (it accepts TCP and closes during negotiation): a command used to get ~13 s of retries, now it is dropped after one attempt. Does MyHOME's worker retry a
None? - The description says
set_datetime()/set_time()reject naive inputs, but neither function exists in OWNd; the diff only changesset_datetime_to_now,set_date_to_todayandset_time_to_now. _resolve_discovery_fningateway.pyexists only so tests can keep patchingOWNd.connection.find_gateways/get_gateway/get_port; patchingOWNd.connection.gateway.*in the tests would keep that out of the runtime path.tools/golden/MASSI_OUTREACH.mdandMCP_NOTES.mdare deleted without a mention in the description.
Contributor
Author
|
Thanks for the thorough review @fedem95! Here is how each point has been addressed:
|
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.
What is the problem
OWNdSession & Gateway APIs (OWNd/connection/):connect(),test_connection(),test_gateway(),send(),send_to_gateway(), andconnect_to_gateway()had no structured exception hierarchy (OWNError,OWNConnectionError,OWNTimeoutError,OWNAuthError,OWNNackError,OWNProtocolError) to distinguish socket connection failures, timeouts, OPEN/HMAC authentication rejections, and gateway*#*0##(NACK) responses, nor asession.last_errorattribute when using default return values.OWNEventSession.get_next()reconnect loops, runtime password/auth failures could not be surfaced to a dedicated callback (on_auth_error) or raised (raise_on_auth_error=True).OWNCommandSession._locked_send():_locked_send()ran a full multi-attempt exponential backoff reconnect while holding_send_lock, stalling every queued caller behind it._ConnectionModuleProxy Overhead & Obsolete Golden Scratch Docs:OWNTransport.send,AsyncTcpTransport.send, andAsyncSerialTransport.sendtypedmessageasstronly instead ofstr | OWNMessage.OWNGatewayCommand.set_datetime_to_now(),set_date_to_today(), andset_time_to_now()rejecteddatetime.tzinfoinstances (such asZoneInfo("Europe/Rome")ordatetime.timezone.utc), accepting onlystr.OWNd/connection/__init__.pyinstalled a custom_ConnectionModulesys.modulesproxy instead of clean static re-exports, and_locked_send()logged routine command send successes atINFOinstead ofDEBUG.tools/golden/MASSI_OUTREACH.mdandtools/golden/MCP_NOTES.md) were still tracked intools/golden/and referenced intests/golden/README.md.How is it solved
raise_on_error(OWNd/exceptions.py,OWNd/__init__.py,OWNd/connection/):OWNError,OWNConnectionError,OWNTimeoutError,OWNAuthError,OWNNackError, andOWNProtocolErrorinOWNd.exceptions(re-exported fromOWNdandOWNd.connection).session.last_error: OWNError | Noneand opt-inraise_on_error: bool = Falseacrossconnect(),test_connection(),test_gateway(),send(),send_to_gateway(), andconnect_to_gateway().on_auth_error: Callable[[OWNAuthError], Any] | Noneandraise_on_auth_error: bool = FalseonOWNEventSession/get_next()._locked_send()(OWNd/connection/command_session.py):send()reconnect insideOWNCommandSession._locked_send()to a single fast attempt (max_attempts=1) guarded bySEND_RECONNECT_COOLDOWN = 2.0seconds so queued callers fail fast instead of blocking behind exponential backoff._locked_send()fromINFOtoDEBUG.OWNd/connection/,OWNd/message/gateway.py,tools/golden/,tests/golden/README.md):OWNTransport.send,AsyncTcpTransport.send, andAsyncSerialTransport.sendsignatures tomessage: str | OWNMessage.OWNGatewayCommand.set_datetime_to_now(),set_date_to_today(), andset_time_to_now()to acceptdatetime.tzinfo | strvia_resolve_tzinfo()._ConnectionModuleinOWNd/connection/__init__.pywith standard static re-exports.tools/golden/MASSI_OUTREACH.mdandtools/golden/MCP_NOTES.mdand updatedtests/golden/README.md.How is it tested
OWNdtest suite (pytest tests -q) passing across Python 3.11–3.14 with 100.0% coverage onOWNd/exceptions.py,OWNd/connection/, andOWNd/message/gateway.py.mypy OWNd(0 issues),ruff check, andscripts/sync_documentation.py --checkall green.State Diagram
stateDiagram-v2 [*] --> Disconnected state "Connected Command Session" as Connected state "Single-Attempt Fast Reconnect (max_attempts=1)" as FastReconnect state "Cooldown Active (SEND_RECONNECT_COOLDOWN = 2.0s)" as Cooldown state "Raise OWNError / Record session.last_error" as ErrorOutcome Disconnected --> Connected : connect() -> ACK (*#*1##) Disconnected --> ErrorOutcome : connect() fails -> OWNAuthError / OWNConnectionError / OWNTimeoutError Connected --> Connected : send(cmd) -> ACK (*#*1##) / replies Connected --> ErrorOutcome : send(cmd) -> NACK (*#*0##) -> OWNNackError Connected --> FastReconnect : Socket closed & cooldown elapsed FastReconnect --> Connected : Reconnect succeeds -> write frame FastReconnect --> Cooldown : Reconnect fails -> stamp _last_reconnect_failure Cooldown --> ErrorOutcome : Subsequent queued send() within 2.0s fails fast (OWNConnectionError)Where are the traces
tests/golden/corpus.json,tests/golden/firmware_oracle.json):tests/golden/corpus.json(multi-subsystem OpenWebNet frame corpus including 2026-10-18 live physical BTicinoF4612.0.11captures).tests/golden/firmware_oracle.json(6,500 empirical command-sessionACK/NACK/timeout verdicts across 12 emulated gateway firmwares).OpenWebNet-HA/MyHOME:MyHOME/tests/fixtures/traces/f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.jsonandf461_who14_live_2026-10-18/myhome_trace_F461_who14_live_2026-10-18.json(liveF4612.0.11HMAC handshake, WHO=13 clock/timezone dimensions0&22, and unpopulated-address2.05 sbus timeoutNACKcaptures).What does the Oracle / Encyclopedia say
own-firmware-oracle/tests/golden/firmware_oracle.json):F450,F453,F453AV,F454,F459,F460,F461,H4684,MH200,MH200N,MH202,MyHomeServer1), command-session negotiations enforce either OPEN password challenge (*#98#...##) or HMAC-SHA256 (*#99*9##*98*2##), answering*#*0##(OWNAuthError) on invalid credentials and*#*0##(OWNNackError) on unsupported WHO/dimension frames or unpopulated bus addresses (~2.05 sbus timeout onF460/F461/MyHomeServer1).MH200,MH200N,F453,F453AV,H4684) drop idle command sockets aggressively; bounding in-send()reconnect to a single attempt with a2.0 scooldown prevents cascading lock contention when a gateway is unreachable.functional/who-13-gateway-management/dimensions.md):DIMENSION 0(*#13**#0*H*M*S*TZ##),DIMENSION 1(*#13**#1*0W*D*M*Y##), andDIMENSION 22(*#13**#22*H*M*S*TZ*DOW*D*M*Y##) encode explicit signed UTC offset tokens (000..014,101..112). Acceptingdatetime.tzinfo | strinset_datetime_to_now(),set_date_to_today(), andset_time_to_now()allows callers (such as Home Assistant'sdt_util.get_time_zone()) to passtzinfoinstances directly alongside IANA timezone strings.