Manual components
Sometimes the field layout comes from config (e.g. YAML) rather than a
typed class, so there is no Component subclass to declare. Use a
ManualComponent for this case. It is the imperative twin of
Component: you add() targets by
key at runtime, and it pools them into as few reads as possible, mixing all
four tables (holding, input, coils, discrete inputs) in one update.
from modbus_connection.model import ManualComponent, gauge, uint32, coil, discrete_input
mc = ManualComponent(unit, max_gap=16)mc.add("flow_temp", gauge(40, 0.1)) # holding (default)mc.add("energy", uint32(2), space="input") # input registersmc.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": 100000, ...}mc.get("flow_temp") # 21.5await mc.write("relay", True) # per-key write (holding / coils only)How it differs from Component
Section titled “How it differs from Component”Component |
ManualComponent |
|
|---|---|---|
| Layout | class attributes, known statically | add()ed by key at runtime |
| Value access | typed attribute (meter.voltage) |
get(key) / the dict from async_update() |
| Addressing | index / stride / base_offset |
absolute address, no index/stride |
It reuses the same planning, write (validator / force_fc16), bit, and
repeating-group machinery as Component. It has no class to hang typed
descriptors on, so values come out by key.
Adding and removing targets
Section titled “Adding and removing targets”mc.add(key, target, *, space=None)mc.remove(key)- A register target takes its
spaceonadd():"holding"(default) or"input". - A bit target’s space is fixed by the helper (
coil→ FC01,discrete_input→ FC02). Passingspacefor a bit raises. - A
repeating_groupcan beadd()ed like any other target. Its instances come out viaget(key)as alistof sub-components, sized at poll time for a register count. - The field
addressis absolute — there is noindex/stride.
add() and remove() invalidate the cached plan, so it re-plans on the next
update. Adding a key that already exists replaces the previous target.
Reading values
Section titled “Reading values”mc.get("flow_temp") # the decoded value for one key (None if not yet read)mc.values # a copy of every decoded value as a dictasync_update() returns the same dict it stores, so you can use either the
return value or get() afterwards.
Writing
Section titled “Writing”await mc.write("relay", True)Writes go by key and share Component.write’s behaviour: the writable
validator, and FC06 / FC16 selection with force_fc16. Only holding registers
and coils are writable. A discrete input or input register is read-only, so
writing one raises.
Readable ranges
Section titled “Readable ranges”Ranges are per-table keyword arguments on the constructor:
holding_ranges / input_ranges / coil_ranges / discrete_ranges. Any
table left unset falls back to gap-based planning:
ManualComponent(unit, holding_ranges=((0, 40),), input_ranges=((500, 520),))