Bump modbus-connection to 4.3.0 and adopt its new APIs - #15
Closed
balloobbot wants to merge 7 commits into
Closed
balloobbot wants to merge 7 commits into
balloobbot wants to merge 7 commits into
Conversation
4.0.0 rebuilds connection management, but nothing in this library owns a connection, so the migration is small: every component declares absolute addresses at base_offset 0, so the breaking readable-range shift is a no-op, and all twelve components share one range map, so ComponentGroup's new range merge collapses to a single map. - script/query.py builds its connection from ModbusTcpParams / ModbusSerialParams instead of the connect_* factories, which 4.0 demotes. Construction performs no I/O; the script still connects eagerly so a connect failure stays distinguishable from a device that refuses a read. - Component.declared_fields (new in 4.0.0a2) replaces reaching into Component._register_fields / _bit_fields in metadata_for() and across the tests. The two metadata audits and the canonical-parity sweep now cover every declared field instead of only those the default model reads. - The read-layout guard watched four cache attributes that no longer exist in 4.0; only _read_items and _plan remain. - Cover the connection lifetime 4.0 guarantees: a dropped link heals on the next update over the same handles, and close() is permanent (ClientClosedError), both now modelled by the mock backend. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WYw6yc9R1sgG2qybAp6p5j
- Move connect() out of the read try/except in query.py and drop the explanatory comment. A failed connect returns before there is anything to close, so the read path keeps its own try/finally. - Revert the README paragraph on connection lifetime. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WYw6yc9R1sgG2qybAp6p5j
Follow-up cleanup on the 4.0.0a3 bump, all of it possible with the library as it stands today: - _register_field_is_readable reimplemented Component._scale_address verbatim, down to the scale_in_block branch. Call the library's helper so the two cannot drift apart. - test_ranges_must_be_configured_before_read_layout still branched on hasattr(type(functions), "register_items"). That attribute went away in 4.0, so the branch was dead. - Eight tests reached through a component for the unit to assert on (trovis.rk1._unit). The trovis fixture is built from mock_modbus_unit, so a `unit` fixture hands them the same object by a public route. - test_full_update_never_reads_across_an_unreadable_gap wrapped the unit to capture block spans. async_read_raw() reports every address a pooled read touched, which is what the test actually asserts on. - test_consolidated_reads_decode_correctly wrapped the unit without ever reading the counts. _CountingUnit stays for test_full_update_consolidates_reads, which needs block boundaries the library does not expose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WYw6yc9R1sgG2qybAp6p5j
a4 is a small release for this library. Its headline items — sunspec.boolean, float-typed repeating_group counts, the scan() lookup helper — are SunSpec, which TROVIS is not, and the newly exported field classes (NumberField, FloatField, RawField, StringField) are ones nothing here constructs. The model-layer changes are type widening and exports, with no behaviour change. What does reach us: - The default request timeout rose from 3s to 10s, which script/query.py inherits. Better suited to an RTU-over-TCP gateway than the old 3s. - Import PymodbusConnection under its own name rather than aliasing the backend's ModbusConnection, now that the backend names are canonical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WYw6yc9R1sgG2qybAp6p5j
The pin drops the prerelease suffix and goes back to the repo's usual >=MAJOR.MINOR form, which also stops it opting in to prereleases. Nothing new to adopt since 4.0.0a4: notify=False on Component.async_update and the _verify_read hook it enables are for callers that fire their own listeners or check a model header after a read, and this library does neither. NumberField's per-field warning dedup applies to the enum() fields here with no change on our side. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WYw6yc9R1sgG2qybAp6p5j
MockModbusUnit now logs every dispatched read as a ReadEvent(register_type, address, count), which is what _CountingUnit existed to do. The release notes name this case: several libraries built on modbus-connection carried a near-identical wrapper. Removing it makes the two tests that used it stronger, not just shorter: - The unreadable-gap test went back to asserting per block rather than per address, and no longer reaches through Trovis557x._group for async_read_raw. It also covers coil blocks now, which it never did — 18 of the 35 blocks a full update issues are coils, and COIL_RANGES has gaps of its own. - Both tests take the trovis fixture instead of rebuilding a device with the same stores, and neither needs a type: ignore for a unit that is not a ModbusUnit. 4.1.0's other additions do not apply: TROVIS models every boolean as a real coil, so the new generic boolean() register field has no candidate here, and there are no flags() fields for the CLI helper's new rendering to reach. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WYw6yc9R1sgG2qybAp6p5j
4.2.0's mock upgrades close two coverage gaps that were previously untestable here. WriteEvent now carries the function code. Clock.write deliberately sends the year and the packed date as two FC06 writes, year first, instead of one FC16 across both words — the sequence the 55Pro uses. Asserting the resulting register values cannot tell the two apart, so deleting that override passed the whole suite and would have failed on hardware. Verified: with the override removed, 644 tests still pass and only the new one fails. fail_requests() models a device that answers nothing, without a test having to guess which address its caller reaches first. That covers the four TrovisWriteAccessError wrappers around HR40145, which had no tests at all. It is armed with GatewayTargetError — one of the new typed exception-response subclasses, and the realistic failure for a TROVIS behind an RTU-over-TCP bridge. Also cover disconnect(), 4.2.0's headline addition: the owner recycling a stuck link keeps the same unit handles and components, and does not fire the on_connection_lost callbacks. Not applicable: TROVIS declares no repeating_group, no flags(), and no multi-register numeric fields, and every boolean is a real coil. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WYw6yc9R1sgG2qybAp6p5j
Owner
|
Thanks, appreciate the ongoing efforts! :) I integrated the changes manually into develop_tom, which already contains additional ongoing development. I also updated the dependency to modbus-connection 4.2 and verified the combined state with the full test suite plus live TCP + RTU tests on TROVIS 5578 and 5579. Closing this PR without merge. |
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.
Bumps
modbus-connectionfrom>=3.6to>=4.3and adopts what 4.0.0 through 4.3.0 offer. The pin keeps the repo's usual>=MAJOR.MINORform, which also stops it opting in to prereleases.Why the migration is small
4.0.0 is deliberately breaking, but the breaks are about owning a connection, and this library never owns one — the caller injects a
ModbusUnit. The two model-layer breaks miss us too:base_offsetand the per-instance shift. Every Trovis component declares absolute addresses at the defaultbase_offset=0(circuit_heating.pydocuments using a field-levelstriderather thanrepeating_group/base_offset), so the shift is a no-op.ComponentGroupnow merges its members' resolved ranges and raises when they conflict.Trovis557x.__init__hands the sameranges_for_model(model)map to all twelve components, so the merge collapses to one map.The pin bump alone was green before any source change.
What changed
script/query.pybuilds its connection from params. 4.0 demotesconnect_tcp/connect_serial— a connection that carries its own params is what lets it re-establish the link on its own._open()becomes_connection(), constructsModbusConnection(ModbusTcpParams(...))/ModbusSerialParams(...), and performs no I/O. The script still callsconnect()eagerly, which 4.0 retains for exactly this: a one-shot dump gains nothing from connect-on-demand, and connecting up front keepsCould not connect:distinguishable fromError reading device:. The CLI surface is unchanged.Component.declared_fields(new in 4.0.0a2) replaces private access. The release notes call out this exact case: "a consumer had to reach intoComponent._register_fields".TrovisComponent.metadata_fordid precisely that, twice, to cover registers and bits —declared_fieldsis that union, in one lookup. The same swap removes_register_fields/_bit_fieldsfrom five test modules.Two of those are audits (
test_all_writable_numbers_have_complete_ranges,test_all_writable_temporal_values_have_complete_ranges) and one is the canonical-parity sweep. They iterated the instance layout, so they only covered fields the default model happens to read. Ondeclared_fieldsthey cover every declared field — parity picks up 4 more cases, and all pass.Sensors.detected_sensor_namesandClock.writekeep using the narrowed instance set: they specifically mean "fields this model serves", which is not whatdeclared_fieldsdescribes.The read-layout guard was watching dead names.
_ensure_read_layout_is_configurablechecked forregister_items,bit_items,_register_blocksand_bit_blocks. None of those exist in 4.0; only_read_itemsand_plando.Two tests for the connection lifetime 4.0 guarantees, which a2 taught the mock to model: a dropped link fires
on_connection_lostand then heals on the next update over the same unit handles, andclose()is permanent and raisesClientClosedError. Worth pinning here — an integration built on this library needs to know a drop does not require rebuildingTrovis557x.4.1.0: the mock logs reads
MockModbusUnitrecords every dispatched read as aReadEvent(register_type, address, count). That is exactly what_CountingUnitintest_device.pyexisted to do — and the release notes name the case: "Several libraries built on this one carried a near-identical hand-rolled wrapper for exactly this."Dropping it made the two tests that used it stronger, not just shorter:
Trovis557x._groupforasync_read_raw(). It also covers coil blocks, which it never did — 18 of the 35 blocks a full update issues are coils, andCOIL_RANGEShas gaps of its own. Verified the new assertion has teeth: against a deliberately wrong coil map, all 18 blocks fail it.trovisfixture instead of rebuilding a device over the same stores, and neither needs a# type: ignore[arg-type]for a unit that isn't aModbusUnit.Net −25 lines in
test_device.pywith wider coverage.The rest of 4.1.0 has no candidate here. TROVIS models every boolean as a real coil — the one
value_kind="boolean"in the tree is thecoil()helper itself — so the new genericboolean()register field finds nothing, and there are noflags()fields for the CLI helper's new name-rendering to reach.field_rowsdropping the unit from an unread field does changescript/query.pyoutput (— °Cbecomes—), which is the intended improvement and needs nothing on our side.4.2.0: two write paths that were untestable before
WriteEventnow carries the function code, which pins a sequence this library was silently exposed on.Clock.writedeliberately sends the year and the packed date as two FC06 writes, year first, rather than one FC16 across both words — the order the 55Pro uses. Asserting the resulting register values cannot tell the two apart, since a single FC16 leaves the same two words behind.So the override was unprotected. Verified by removing it: 644 tests still pass and only the new one fails, with the mock reporting
(100, [201, 2027], 16)— one FC16 — instead of the two FC06s. This is precisely the hazard the release notes describe forforce_fc16.fail_requests()models a device that answers nothing, without a test having to guess which address its caller happens to reach first. That closes a real gap: the fourTrovisWriteAccessErrorwrappers around HR40145 had no tests at all. It is armed withGatewayTargetError()— one of the new typed exception-response subclasses, constructed with its code implied, and the realistic failure for a TROVIS behind an RTU-over-TCP bridge.disconnect(), the release's headline, is covered alongside the existing drop test: the owner recycling a stuck link keeps the same unit handles and components, and — unlike a real drop — fires noon_connection_lostcallbacks.Nothing else in 4.2.0 has a candidate here: TROVIS declares no
repeating_group(so the newprint_componentrendering and the clash message have nothing to reach), noflags()fields, and no multi-register numeric fields for the newnan=to apply to. The mock now starting disconnected breaks nothing — this suite makes noconnectedassertions.4.3.0: one error taxonomy
4.3.0 folds
BlockReadErrorinto the typed exception model — an aborted component update now raises the class its exception code names, carrying the refused block as data on.block. Nothing here referencedBlockReadError, so there is no migration: the whole suite passes against 4.3.0 untouched.What it makes testable is a gap. This library's entire readable-range machinery exists so a pooled block never covers an address the controller refuses — and there was no test for what a caller sees when one is refused anyway. Arming
fail_readmid-block (VF1 at 12, inside the pooled 9..28 sensor read rather than at its start) now yieldsIllegalDataAddressErrorwith.blocknaming the span, and the update applies nothing.Verified against 4.2.0 as well, where the test fails exactly as it should — the planner there raises
BlockReadError, which is not anIllegalDataAddressError.Cleanup that needed nothing from the library
While checking what still reaches into
modbus-connectioninternals, five things turned out to be avoidable with the library exactly as it is:_register_field_is_readablereimplementedComponent._scale_addressverbatim, down to thescale_in_blockbranch. It now calls the library's helper, so the two cannot drift apart — the old copy would have silently misjudged readability if the library ever changed how a scale register resolves.test_ranges_must_be_configured_before_read_layoutstill branched onhasattr(type(functions), "register_items"). That attribute is gone in 4.0, so the branch was dead.trovis.rk1._unit). Thetrovisfixture is built frommock_modbus_unit, so aunitfixture hands them the same object by a public route.test_full_update_never_reads_across_an_unreadable_gapwrapped the unit to capture block spans. The publicasync_read_raw()reports every address a pooled read touched, which is exactly what the test asserts on.test_consolidated_reads_decode_correctlywrapped the unit and never read the counts._CountingUnitstays fortest_full_update_consolidates_reads, which needs block boundaries the library does not expose.What the later 4.0 releases added
The alphas after a1 layered on
Component.declared_fields, the mock's connect-on-demand modelling, multi-valuenan, and a batch of SunSpec ergonomics. Only the first two apply here — TROVIS is not SunSpec, and every sentinel in its register map is0x7FFFviaNAN_INT16, so there is no second "no value" code for multi-valuenanto catch.Two smaller things do reach us:
script/query.pyinherits it (verified: the constructed connection reportstimeout=10), which suits an RTU-over-TCP gateway better than the old 3s. Passtimeout=3to any factory to keep the old behaviour if that turns out to be wrong for this device.PymodbusConnectionunder its own name instead of aliasing the backend'sModbusConnection— the backend names became canonical in the same 4.0 rework.Nothing in the final release needs action either:
notify=FalseonComponent.async_updateand the_verify_readhook it enables are for callers that fire their own listeners or check a model header after a read, and this library does neither.NumberField's per-field warning dedup applies to theenum(...)fields here with no change on our side.NumberField(convert=...)was worth a look for the packed HHMM/DDMM fields, butconvertis decode-only and those fields need a customencodewith domain validation, which is why they subclassRegisterField— a documented extension point. Everyfrom modbus_connectionimport in this repo already used a public namespace, so the newly exported field classes change nothing.Considered, not adopted
Component.restrict_fields()as a replacement forconfigure_readable_ranges/configure_readable_fields. It reshapes the component's readable ranges around the dropped fields, and those narrowed maps then conflict insideComponentGroup:It does work if the device-wide map is restored right after the call — I prototyped that and got a byte-identical read plan (35 blocks) and identical per-component field sets. But "call it, then undo one of its side effects" is not obviously better than the current code, and it would change behaviour: reconfiguring after the first read would stop raising, and already-read values for dropped fields would be cleared. Left alone deliberately.
Multi-value
nan(4.0.0a3). Every Trovis sentinel is0x7FFFviaNAN_INT16; there is no second "no value" code in the register map to catch. Available if one ever turns up.Verification
pytest: 648 passed, 17 skipped (638 before — the widened audits and the new 4.2.0/4.3.0 tests account for the difference)ruff format --check .,ruff check .,compileall,python -m build: all cleanpip install -e .resolves4.3.0in a clean 3.13 venv — this is the path CI takesscript/query.py --helpand both params paths construct; the connect-failure path printsCould not connect: could not connect to 127.0.0.1:1Targeting
develop:enforce-develop.ymlrejects PRs tomainthat do not come from the localdevelopbranch, and CI only runs on PRs todevelop.🤖 Generated with Claude Code
https://claude.ai/code/session_01WYw6yc9R1sgG2qybAp6p5j