Skip to content

Releases: home-assistant-libs/modbus-connection

3.4.1

Choose a tag to compare

@balloob balloob released this 08 Jul 16:18
ca053a3

What's Changed

Full Changelog: 3.4.0...3.4.1

3.4.0

Choose a tag to compare

@balloob balloob released this 08 Jul 15:23
d5c47ee

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

Choose a tag to compare

@balloob balloob released this 03 Jul 11:21
9344dca

What's Changed

  • tmodbus: implement connect_tls (Modbus/TLS) by @balloob in #31
  • tmodbus: bump to v0.4.1 and fire on_connection_lost from the native hook by @balloob in #32

Full Changelog: 3.2.0...3.3.0

3.2.0

Choose a tag to compare

@balloob balloob released this 02 Jul 18:03

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 hostname

Pass 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 access

Per-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 0

Write 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 - 100

Validator 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 / … raise ValueError at 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_lost fires 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 raises ModbusConnectionError (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

  • Recommend probatio for write validators (#12); README Home Assistant-reference
    cleanup (#16); GitHub Actions bumped to Node 24 (#6).

3.1.0

Choose a tag to compare

@balloob balloob released this 26 Jun 02:57
d37755d

Added

  • Configurable block-planning limits. Component.max_gap and
    Component.max_span are now overridable per device (class attribute) or
    instance, instead of fixed module constants. A ComponentGroup validates 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 when register_ranges is 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_gap increased from 8 to 16, so devices without declared
    register_ranges issue fewer, wider reads by default.

3.0.0

Choose a tag to compare

@balloob balloob released this 25 Jun 10:51
6ea445b

Breaking changes

  • Removed scaled_sum / MagnitudeField (and decode_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_coil field option). A coil
    that must be released before a write is device-specific write sequencing; do it
    by overriding Component.write() in your subclass.

Other

  • The stride / index mechanism for repeated sub-units (e.g. heating
    circuits) is now documented in the README and the Component docstring,
    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

Choose a tag to compare

@balloob balloob released this 25 Jun 00:20
30c8a1f

Patch release: dependency relaxation and pymodbus 3.13 support.

  • Support the latest pymodbus (3.13) and drop the <3.12 upper 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,
    mirroring pymodbus[serial].
  • Fix a stale Device import in the README's model example (renamed to
    ComponentGroup).

No code/API changes to the library itself.

2.0.0

Choose a tag to compare

@balloob balloob released this 24 Jun 23:00
ae17586

Breaking changes

  • The typed register helpers (read_uint16, read_int16, read_uint32,
    read_float32, read_string, write_uint16, write_float32) have been
    removed from the ModbusUnit protocol 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 / ComponentGroup with 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
      (native IntEnum / IntFlag mapping, incl. signed codes), and coil.
    • 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.

1.0.0

Choose a tag to compare

@balloob balloob released this 24 Jun 01:41

Initial release.