Repeating groups
Devices that expose several identical sub-units (heating circuits, channels,
phases, MPPT modules) repeat the same registers at a fixed step. A
repeating_group models one instance as a Component and gives the parent a
typed list of them. The count is a fixed int, or a register the device
reports at poll time. For example, a SunSpec multiple-MPPT model (160) carries
an N point saying how many modules follow.
from modbus_connection.model import Component, integer, repeating_groupfrom 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 at register 8
inv = Inverter(unit)await inv.async_update()inv.modules # list[MPPTModule]inv.modules[0].dc_w # typed per-instance accessawait inv.modules[2].write("dc_w", ...) # writes go through the instanceFor a layout a group cannot express, such as a sub-unit whose registers are
interleaved by type across the map, place the instances by hand with
index and a per-field stride.
Reading the count
Section titled “Reading the count”count is a RegisterField (read each poll) or a fixed int. Instance i
has every address of its declared layout shifted by i * stride on top of the
parent’s own placement. stride is therefore the block length.
- A fixed
intcount is static, so its instances fold into the component’s normal read. No extra pass is needed. - A
RegisterFieldcount needs a second pass. The count is read first, then the sized-out instances, pooled among themselves. The count must be known before the instances it sizes can be planned. A float-typed count field (a SunSpecuint16Npoint) is accepted, and the decoded count is truncated.
An unimplemented or unreadable count yields no instances. A component with a
repeating_group can refresh on its own or be pooled in a
ComponentGroup. The group
reads the counts in its pooled read, then refreshes each member’s groups.
The sub-unit’s readable ranges
Section titled “The sub-unit’s readable ranges”A sub-unit that declares readable ranges constrains the reads of every instance. Each instance’s map resolves like its fields, shifted by its own place in the repeat. The maps are merged into the plan its instances are read from.
class Channel(Component): register_ranges = ((0, 1), (4, 5)) # 2-3 unreadable inside a channel a = integer(0) b = integer(4)
class Meter(Component): channels = repeating_group(2, Channel, stride=10)
# Reads 0, 4, 10 and 14, and never across the gaps the channel declares unreadable.Nesting
Section titled “Nesting”A repeating_group’s component_class is itself a Component, so it may
declare its own repeating_group: a sub-unit that repeats within each instance
(channels within each module, cells within each string). Nesting works in any
combination of fixed and register counts, to any depth. Each instance’s
addresses shift by its parent’s stride, and the shifts compose additively
down the levels.
class Cell(Component): voltage = uint16(0)
class String(Component): cells = repeating_group(uint16(1), Cell, stride=1) # per-string cell count
class Battery(Component): strings = repeating_group(uint16(0), String, stride=100) # string countA register count at any level adds a read pass for the level below it, because
the count must be read before the instances it sizes can be planned. A two-deep
tree with register counts at both levels therefore polls in three passes: the
outer count, then the inner counts, then the leaves. Fixed int counts add no
pass at any level. They fold into the enclosing read.
The address of a nested count
Section titled “The address of a nested count”A nested group’s register count shifts with the enclosing instance by default:
string i above reads its cell count at 1 + i * 100. Pass
count_in_block=False when the count is a point of the outermost layout
instead, as a SunSpec NPt point in the model’s fixed block is. Every instance
then reads it at the same address.
class Curve(Component): # NPt is at model offset 5, whatever curve this is points = repeating_group(uint16(5), Point, stride=2, count_in_block=False)
class VoltVar(Component): n_crv = uint16(4) n_pt = uint16(5) curves = repeating_group(uint16(4), Curve, stride=30)Without the flag, curve 1 would read its count at 35. A device that answers
0 there silently gives it no points.
Placing a block the device sizes
Section titled “Placing a block the device sizes”stride may be a callable instead of an int, for a block whose width is only
known once the device has been read. The callable receives the component that
owns the outermost block, after its fixed block has been read.
In SunSpec model 705 each curve is a ten-register header followed by NPt
points, and NPt is a point of the model. The sub-components declare their
fields where the first instance has them, as any repeated block does:
class VoltVarPt(Component): v = int16(25) # the first point follows the first curve's header var = int16(26)
class VoltVarCrv(Component): act_pt = uint16(15) # the first curve follows the fixed block pt = repeating_group(uint16(5), VoltVarPt, stride=2, count_in_block=False)
class VoltVar(Component): n_pt = uint16(5) n_crv = uint16(6) crv = repeating_group(uint16(6), VoltVarCrv, stride=lambda m: 10 + 2 * m.n_pt)A group with a callable stride is read in the second pass, like a
register-counted group, even with a fixed int count. The trip models
(707–710) use that: each curve holds three same-shaped regions of 1 + 3 * NPt
registers, so they are one repeating_group(3, TripRegion, stride=_region),
with a property per region for the spec’s names. Such a group is empty until
the first update. The callable runs on every poll. If its result changes, the
instances are rebuilt where it now puts them. On a
ManualComponent the
callable receives the ManualComponent. Read the values it needs with get().
Scale factors inside the block
Section titled “Scale factors inside the block”By default a scaled field’s scale_register stays put across instances. It
names a shared scale factor in the parent’s fixed block. A sub-unit that
carries its own scale factor per repeat sets the scale_in_block class
attribute. Each instance’s scale registers then shift with it:
from modbus_connection.model import Component, integer, repeating_group
class Channel(Component): scale_in_block = True # each channel carries its own scale factor a = integer(0, scale_register=1)
class Meter(Component): channels = repeating_group(integer(4, signed=False), Channel, stride=2)Without scale_in_block, every channel would read its scale factor from the
one address relative to the block start. With it, channel i’s
scale_register shifts by i * stride like the rest of its block.