Device modelling overview
modbus_connection.model is an optional, backend-neutral framework. It maps a
device’s registers and coils to typed Python attributes. It 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. It returns None for a field that has
not been read yet, or for a device sentinel that decodes to “no value”. A
component reads only its own registers, so it can refresh independently.
The update does not issue one request per field. Neighbouring addresses are pooled into block reads, bounded by what the device serves. See Reading a device for the pooling settings, the readable ranges, and what a refused block does to an update.
Register spaces: holding and input
Section titled “Register spaces: holding and 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 is a different
register from holding 507), so the planner never merges them into one read.
Input registers are read-only, so writing an "input" field raises.
Bit spaces: coils and discrete inputs
Section titled “Bit spaces: coils and 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.
Next steps
Section titled “Next steps”To build a device library, read The device object after this page. 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, refused 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 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.