Releases: home-assistant-libs/modbus-connection
Release list
3.4.1
3.4.0
What's Changed
- Clarify WordOrder re-export in package documentation by @balloob in #33
- cli_helper: query-helper building blocks for CLIs by @balloob in #35
- Add Astro Starlight documentation website by @balloob in #34
- cli_helper: auto-detect the installed backend for connect_from_args by @balloob in #36
- docs: query through an ESPHome serial proxy via esphome:// URLs by @balloob in #37
- README: trim to one component example, point to the website by @balloob in #38
- docs: add README example to introduction page by @balloob in #39
- Add per-unit message spacing by @balloob in #40
Full Changelog: 3.3.0...3.4.0
3.3.0
3.2.0
A feature release: new TLS / UDP / ASCII transports, inter-request pacing, and a
batch of device-model features.
New features
New transports: UDP, TLS, ASCII (#7, #25)
New connect functions on the pymodbus backend, with framer spanning
socket / rtu / ascii across TCP and UDP:
from modbus_connection.pymodbus import connect_udp, connect_tls, connect_serial
udp = await connect_udp("192.168.1.50", port=502)
ascii_serial = await connect_serial("/dev/ttyUSB0", framer="ascii")connect_tls (new) verifies the server certificate by default — system trust
store plus hostname check. Opt out, point at a private CA, or present a client
certificate for mutual TLS via verify / check_hostname / client_cert:
await connect_tls("device.local") # verify (default)
await connect_tls("192.168.1.50", verify=False) # self-signed device
await connect_tls("device.local", verify="/etc/ssl/my-ca.pem") # private CA (file or dir)
await connect_tls("192.168.1.50", check_hostname=False) # verify cert, skip hostnamePass a fully configured ssl.SSLContext via sslctx for complete control. tmodbus
has no UDP / TLS / ASCII-over-TCP transport; those raise NotImplementedError — use
the pymodbus backend.
Inter-request pacing: message_spacing (#10)
For devices that need a pause between frames, pass message_spacing (seconds) to
any connect function. Every request across every unit sharing the link waits until
that gap has elapsed since the previous one finished.
conn = await connect_serial("/dev/ttyUSB0", message_spacing=0.05)Device-model framework (modbus_connection.model)
Runtime-built groups — ManualComponent (#8). For layouts that come from config
(e.g. YAML) rather than a typed Component subclass. Add targets by key at runtime;
it pools them across all four tables into as few reads as possible.
mc = ManualComponent(unit, max_gap=16)
mc.add("flow_temp", gauge(40, 0.1)) # holding (default)
mc.add("energy", uint32(2), space="input") # input registers
mc.add("relay", coil(5, writable=True)) # coils (FC01)
mc.add("alarm", discrete_input(9)) # discrete inputs (FC02)
data = await mc.async_update() # {"flow_temp": 21.5, "energy": ...}
await mc.write("relay", True)Repeated sub-units — repeating_group (#20, #22). Model one sub-unit once and
get back a typed list of instances with pooled reads. The count is either a
fixed int or a count register read at poll time (e.g. a SunSpec
multiple-MPPT model that advertises how many modules follow).
from modbus_connection.model import Component, integer, repeating_group
from modbus_connection.model.sunspec import uint16
class MPPTModule(Component): # one module, at instance 0's addresses
dc_w = integer(11, scale_register=2)
dc_v = integer(10, scale_register=1)
class Inverter(Component):
modules = repeating_group(uint16(8), MPPTModule, stride=20) # N read from register 8
# or a fixed count:
# modules = repeating_group(3, MPPTModule, stride=20)
inv = Inverter(unit)
await inv.async_update()
inv.modules[0].dc_w # typed per-instance accessPer-instance address shift — base_offset (#19). For contiguous repeating blocks
where every field shares one step, instead of repeating stride on each field.
modules = [MPPTModule(unit, base_offset=i * 20) for i in range(n)]Discrete-input fields (#18). Read-only bits over the FC02 space, a distinct
address space from coils.
class IO(Component):
relay = coil(0, writable=True) # FC01, read/write
fault = discrete_input(0) # FC02, read-only — distinct from coil 0Write a single register via FC16 — force_fc16 (#14). For devices that honour
only FC16, even for one register.
class Inverter(Component):
limit = integer(0, writable=True, force_fc16=True)Affine offset on scaled fields (#15). Numeric fields decode as raw * scale + offset; writable fields invert it.
temp = gauge(0, 0.1, offset=-100) # value = raw * 0.1 - 100Validator callable on writable fields (#13). Pass a callable instead of
writable=True to both mark the field writable and vet the value before each write
(it returns the value to write, or raises to reject).
def in_range(value: int) -> int:
if not 0 <= value <= 100:
raise ValueError(f"{value} out of range")
return value
class Boiler(Component):
setpoint = integer(0, writable=in_range)(We don't ship validators; for ready-made ones, see
probatio.)
Improvements
- Readable ranges are now validated (#28) — overlapping or reversed
register_ranges/coil_ranges/ … raiseValueErrorat declaration instead of
silently mis-planning reads (both were always config bugs). - tmodbus 0.4.0 and its new file-record API (#30). The
[tmodbus]extra now
requires tmodbus ≥ 0.4.0. on_connection_lostfires at most once per connection (#29) — a single drop no
longer triggers multiple owner reloads.- Connect/close errors map to the neutral hierarchy (#21) — a failed connect or
close now raisesModbusConnectionError(etc.), consistent with every other call. - Register and coil block reads share one internal loop (#17); FC43/14 device
identification is covered on both backends (#27); pre-release cleanup and docstring
trims (#23).
Docs & CI
3.1.0
Added
- Configurable block-planning limits.
Component.max_gapand
Component.max_spanare now overridable per device (class attribute) or
instance, instead of fixed module constants. AComponentGroupvalidates its
components agree on both.max_gap(default 16, was 8): in gap-based planning (no
register_ranges), fields within this many addresses share one read. Higher
means fewer round-trips but more over-reading; lower is safer for devices
that reject reads of unmapped registers. Ignored whenregister_rangesis set.max_span(default 125, the Modbus FC03/FC04 per-request ceiling): the
widest a single block read may be — lower it for a gateway that caps reads
shorter.
Changed
- The default
max_gapincreased from 8 to 16, so devices without declared
register_rangesissue fewer, wider reads by default.
3.0.0
Breaking changes
- Removed
scaled_sum/MagnitudeField(anddecode_scaled_sum). Summing
magnitude registers is value-composition that belongs in the consumer — declare
the registers as fields and combine them in a@property. - Removed write-unlock coil support (the
level_coilfield option). A coil
that must be released before a write is device-specific write sequencing; do it
by overridingComponent.write()in your subclass.
Other
- The
stride/indexmechanism for repeated sub-units (e.g. heating
circuits) is now documented in the README and theComponentdocstring,
including the interleaved-by-register-type case where a sub-unit's fields each
carry their own stride. - Require
tmodbus[async-serial]>=0.3.1(communication-lock fix).
2.0.1
Patch release: dependency relaxation and pymodbus 3.13 support.
- Support the latest pymodbus (3.13) and drop the
<3.12upper cap — the floor
stays>=3.11(the backend works across the range; CI tests the latest). - Pull serialx via
tmodbus[async-serial]instead of pinning it directly,
mirroringpymodbus[serial]. - Fix a stale
Deviceimport in the README's model example (renamed to
ComponentGroup).
No code/API changes to the library itself.
2.0.0
Breaking changes
- The typed register helpers (
read_uint16,read_int16,read_uint32,
read_float32,read_string,write_uint16,write_float32) have been
removed from theModbusUnitprotocol and every backend. The connection
layer now exposes raw register/coil I/O plus the full Modbus function-code set
only; datatype encoding/decoding moves one layer up (see below). Replace
await unit.read_uint32(addr)with
decode_uint32(await unit.read_holding_registers(addr, 2)).
New
modbus_connection.decode/modbus_connection.encode— a pure,
backend-neutral register codec covering the SunSpec point-type set: int/uint
16·32·64, float32/64, strings, IPv4/IPv6/EUI-48, and weighted sums, in either
word order.modbus_connection.model— an optional device-modelling framework. Map a
device's registers and coils to typed Python attributes and read a whole device
(or one sub-system) in as few Modbus calls as possible:Component/ComponentGroupwith pooled, range-aware, cached block-read
planning.- Generic field factories:
gauge,integer,uint32/64,int32/64,
float32/64,string,raw_register,scaled_sum,enum/flags
(nativeIntEnum/IntFlagmapping, incl. signed codes), andcoil. modbus_connection.model.sunspec— SunSpec point types pre-wired with their
per-point "unimplemented" sentinels, plus address types and dynamic scale
factors (sunssf).- Holding (FC03) and input (FC04) register spaces, planned and read separately.
The backends, the in-memory mock, and the pytest plugin are otherwise unchanged.