Component groups
A ComponentGroup stands in for its members at update time. It takes a list
of components instead of declaring fields, and offers the same
async_update() / async_read_raw() / notify() surface over one pooled set
of block reads spanning every member. Fields, writes and listeners stay on the
members themselves.
A physical device is usually several sub-systems on one unit, such as a water heater, three heating circuits, and a set of sensors. Polling each one separately means many small Modbus reads where a few larger ones would do. A group fetches adjacent registers from different components in the same Modbus call, and each component’s listeners still fire after the update.
from modbus_connection.model import ComponentGroup
group = ComponentGroup(unit, [water_heater, circuit_1, circuit_2, circuit_3])await group.async_update() # one pooled set of reads; each component notifiedPlanning
Section titled “Planning”The ComponentGroup builds its pooled read plan from the components’ static
layout and reuses it on every later poll. Reshaping a member through
restrict_fields is
supported at any time. It invalidates the pooled plan, which is rebuilt from
the narrowed layout on the next update. The member list itself is fixed at
construction. Assigning to a member’s layout attributes directly (its fields or
range tuples) bypasses that invalidation and is not supported. Build a new
ComponentGroup (or Component) for those.
Because reads across a group are pooled, read_holding_registers is called
once per contiguous block spanning whichever components fall in it, rather than
once per component. For a device with dozens of scattered fields this typically
collapses tens of reads into a handful.
Pooling never widens a read beyond what the members already allow. A member that declared no readable ranges merges exactly as it would if it refreshed alone, and it shares a block with another member only where their blocks meet. Where a member did declare ranges, that map applies to the pooled read as it does to a solo one: inside a range the planner still merges freely over registers no field claims.
Shared configuration
Section titled “Shared configuration”The readable address ranges and planning limits come from the components, because they describe one device’s address map. Components in a group must therefore agree:
- Readable ranges apply per address space, and the group merges what its
members declare for each one. Ranges are resolved to the addresses the
members read, so a member placed with
base_offsetcontributes its shifted map. Members at different offsets each describe their own part of the device. - Two members whose resolved ranges overlap without matching describe the
same addresses two different ways. That raises
ValueError. - Every component must share
max_span, which caps a pooled block’s width.
The range rules are a guard. A group is one device, so its members cannot disagree about that device’s map.
class Base(Component): register_ranges = ((0, 6), (9, 40)) coil_ranges = ((0, 15),)
class WaterHeater(Base): ...
class Circuit(Base): ...
# All share the same ranges, so the group accepts them.group = ComponentGroup(unit, [WaterHeater(unit), Circuit(unit, index=1)])await group.async_update()A refused block read
Section titled “A refused block read”Group updates fail the same way individual ones do. If the device answers one
of the pooled block reads with a Modbus exception response, async_update()
raises the
typed exception
for its code. This holds for any block across the pooled members. See
A refused block read
for what the exception carries.
Choosing between them
Section titled “Choosing between them”- One sub-system, or sub-systems polled on different schedules: individual
Component.async_update(). - Several sub-systems of one device polled together: a
ComponentGroup. - A layout not known until runtime (from config): a
ManualComponent.