Device modelling overview
modbus_connection.model is an optional, backend-neutral framework. It maps a
device’s registers and coils to typed Python attributes, then reads the whole
device — or one sub-system — in as few Modbus calls as possible. It talks only
to a ModbusUnit, so it runs over any backend and over the
mock.
A first component
Section titled “A first component”A Component is a device sub-system. Declare its registers and coils as class
attributes using the built-in fields:
from modbus_connection.model import Component, gauge, uint32, coil
class Meter(Component): voltage = gauge(0, 0.1, unit="V") # scaled 16-bit """Grid voltage."""
current = gauge(1, 0.1, unit="A") """Grid current."""
energy = uint32(2, unit="Wh") # 32-bit over two registers """Lifetime energy."""
relay = coil(0, writable=True) """Load relay."""
meter = Meter(unit)await meter.async_update() # one block read of registers 0-3, one of coil 0meter.voltage # float | Noneawait meter.write("relay", True)The string under each field is an attribute docstring. It is optional, but
editors show it when hovering meter.voltage anywhere in the codebase.
async_update() reads every field, decodes it, and stores the result. Reading
an attribute returns the decoded value, or None for a field that has not been
read yet or a device sentinel that decodes to “no value”. A component reads
only its own registers, so it can refresh independently.
The update is not one request per field. Neighbouring addresses are pooled into block reads, bounded by what the device is willing to serve. See Reading a device for the pooling knobs, the readable ranges, and what a refused block does to an update.
Register spaces: holding vs input
Section titled “Register spaces: holding vs input”A component’s register fields default to the holding space (FC03). For a
read-only sub-system whose data lives in input registers (FC04), set
register_space = "input". The field declarations are unchanged:
class Sensors(Component): register_space = "input" flow_temp = gauge(5, 0.1, unit="°C") # read with FC04Input and holding are separate address spaces (input 507 ≠ holding 507), so the
planner never merges them into one read. Input registers are physically
read-only, so writing an "input" field raises.
Bit spaces: coils vs discrete inputs
Section titled “Bit spaces: coils vs discrete inputs”Bits work the same way over their own pair of spaces:
from modbus_connection.model import Component, coil, discrete_input
class IO(Component): relay = coil(0, writable=True) # FC01, read/write fault = discrete_input(0) # FC02, read-only — distinct from coil 0coil fields are read and written via FC01. discrete_input fields are read
from FC02 and are read-only. A single component may declare both. Coil 12 and
discrete input 12 are different addresses, so they are planned and read
separately.
Writing
Section titled “Writing”Component.write(field, value) writes a register or coil by attribute name:
await meter.write("relay", True)The field must be marked
writable,
optionally with a validator that vets the value before it reaches the device.
Override write() in a subclass for any device-specific write sequencing.
Listeners
Section titled “Listeners”Each component has its own update listeners, fired after each update:
unsubscribe = meter.add_update_listener(lambda: print("updated", meter.voltage))await meter.async_update() # printsunsubscribe()Pass async_update(notify=False) to read without firing the listeners, for a
caller that notifies them itself.
Where to next
Section titled “Where to next”Building a device library? Read The device object once you know the basics here — it shows how components combine into a full library.
The rest of this section, in reading order:
- Built-in fields — every generic field type.
- Reading a device — block pooling, readable ranges, failed blocks, and the raw register map.
- Placing a component — read the same
layout at another address with
index/strideorbase_offset. - Repeating groups — one sub-unit modelled once, the list sized from a fixed or device-reported count.
- Restricting fields — narrow a component to the subset of a layout a device actually serves.
- Component groups — refresh several components in one pooled read.
- Manual components — build the layout at runtime from config.
- SunSpec — the SunSpec point types.
- Field reference and Component reference — every class, method, and field of the modelling layer.