Auto-generated topic-contract + data-model sections via shared wikiGen script. Hand-written Mermaid diagrams for position-in-platform, code map, child registration, lifecycle, configuration, state chart (where applicable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 KiB
machineGroupControl
Reflects code as of
afc304b· regenerated2026-05-11vianpm run wiki:allIf this banner is stale, the page may be out of date. Treat as informative, not authoritative.
1. What this node is
machineGroupControl (MGC) is an S88 Unit orchestrator that coordinates multiple rotatingMachine children sharing a common header. It receives a demand setpoint, evaluates valid pump combinations against the group's totals and curves, picks the best operating point (BEP-Gravitation or NCog), and dispatches per-machine flow setpoints + start/stop commands.
2. Position in the platform
flowchart LR
parent[pumpingStation<br/>Process Cell]:::pc -->|set.demand| mgc[machineGroupControl<br/>Unit]:::unit
header[measurement<br/>header pressure]:::ctrl -.data.-> mgc
mgc -->|flowmovement / execsequence| m_a[rotatingMachine A]:::equip
mgc -->|flowmovement / execsequence| m_b[rotatingMachine B]:::equip
mgc -->|flowmovement / execsequence| m_c[rotatingMachine C]:::equip
mgc -->|child.register| parent
m_a -->|child.register| mgc
m_b -->|child.register| mgc
m_c -->|child.register| mgc
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000
S88 colours: Process Cell #0c99d9, Unit #50a8d9, Equipment #86bbdd, Control Module #a9daee. Source of truth: .claude/rules/node-red-flow-layout.md.
3. Capability matrix
| Capability | Status | Notes |
|---|---|---|
| Aggregate group flow / power totals | ✅ | TotalsCalculator — absolute and dynamic. |
| Valid-combination enumeration | ✅ | combinatorics/pumpCombinations. |
| Best-combination optimiser (BEP-Gravitation) | ✅ | Directional or symmetric variant. |
| Best-combination optimiser (NCog) | ✅ | Normalised cost-of-goods score. |
| Priority / equal-flow control | ✅ | mode='prioritycontrol'. |
| Priority percentage control | ✅ | Requires scaling='normalized'. |
| Optimal control | ✅ | mode='optimalcontrol'. |
| Group efficiency + BEP distance | ✅ | GroupEfficiency. |
| Header-pressure equalisation | ✅ | operatingPoint.equalize(). |
| Demand serialisation (latest-wins) | ✅ | Inline gate; deferred call drains on completion. |
Forced shutdown on Qd ≤ 0 |
✅ | turnOffAllMachines(). |
4. Code map
flowchart TB
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
nc["buildDomainConfig()<br/>static DomainClass, commands"]
end
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
sc["MachineGroup.configure()<br/>ChildRouter rules<br/>handleInput() dispatch gate"]
end
subgraph concerns["src/ concern modules"]
groupOps["groupOps/<br/>GroupOperatingPoint + curves"]
totals["totals/<br/>TotalsCalculator"]
combi["combinatorics/<br/>validPumpCombinations"]
opt["optimizer/<br/>BEP-Grav / NCog selectors"]
efficiency["efficiency/<br/>GroupEfficiency + BEP dist"]
dispatch["control/<br/>strategies (equalFlow / prioPct)"]
io["io/<br/>output + status"]
commands["commands/<br/>topic registry + handlers"]
end
nc --> sc
sc --> groupOps
sc --> totals
sc --> combi
sc --> opt
sc --> efficiency
sc --> dispatch
sc --> io
nc --> commands
| Module | Owns | Read first if you're changing… |
|---|---|---|
groupOps/ |
Group operating point + child read helpers | Header pressure handling, child measurement plumbing. |
totals/ |
Absolute + dynamic flow/power totals | Demand clamping, totals math. |
combinatorics/ |
Enumeration of valid pump subsets | Which combinations are considered eligible. |
optimizer/ |
Best-combination selectors | Optimiser selection method, scoring math. |
efficiency/ |
Group efficiency, BEP distance | BEP gravitation tuning, peak math. |
control/strategies.js |
Per-mode dispatch (priority, prioPct) | Mode behaviour, priorityList usage. |
dispatch/ |
Demand fan-out helpers (legacy alongside inline gate) | Serialisation, mid-flight overrides. |
commands/ |
Input-topic registry and handlers | New input topics, payload validation. |
io/ |
getOutput, getStatusBadge |
Output shape, dashboard badge. |
5. Topic contract
Auto-generated from
src/commands/index.js. Do NOT hand-edit between the markers. Re-runnpm run wiki:contract.
| Canonical topic | Aliases | Payload | Effect |
|---|---|---|---|
set.mode |
setMode |
string |
Replaces the named state value with the supplied payload. |
set.scaling |
setScaling |
string |
Replaces the named state value with the supplied payload. |
child.register |
registerChild |
string |
Parent/child plumbing — registers or unregisters a child node. |
set.demand |
Qd |
any |
Replaces the named state value with the supplied payload. |
6. Child registration
ChildRouter declarations in specificClass.js → configure().
flowchart LR
subgraph kids["accepted children (softwareType)"]
mach["machine<br/>(rotatingMachine)"]:::equip
m["measurement<br/>(header pressure)"]:::ctrl
end
mach -->|"pressure.measured.downstream<br/>pressure.measured.differential<br/>flow.predicted.downstream"| eq[operatingPoint.equalize<br/>+ totals refresh]
m -->|"<type>.measured.<position>"| mirror[mirror into own<br/>MeasurementContainer]
mirror -->|"if type === 'pressure'"| eq
eq --> emit[notifyOutputChanged]
classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000
| softwareType | filter / subscribed events | Side-effect |
|---|---|---|
machine |
onRegister stores in this.machines[id]; subscribes to pressure.measured.downstream, pressure.measured.differential, flow.predicted.downstream |
handlePressureChange() — equalise + recompute totals + recompute group efficiency. |
measurement |
onRegister attaches listener for <asset.type>.measured.<positionVsParent> |
Mirror value into MGC's own MeasurementContainer; pressure also triggers handlePressureChange(). |
7. Lifecycle — what one event does
sequenceDiagram
participant parent as pumpingStation
participant mgc as MGC
participant op as GroupOperatingPoint
participant tot as TotalsCalculator
participant opt as optimizer
participant kids as rotatingMachine[]
parent->>mgc: set.demand (Qd)
Note over mgc: dispatch gate — latest-wins
mgc->>mgc: abortActiveMovements('new demand')
mgc->>tot: calcDynamicTotals()
mgc->>mgc: clamp Qd to [minFlow, maxFlow]
alt mode=optimalcontrol
mgc->>mgc: validPumpCombinations(Qd)
mgc->>opt: pick best (BEP-Grav | NCog)
opt-->>mgc: bestCombination + bestFlow/Power
mgc->>kids: flowmovement (per-pump flow)
mgc->>kids: execsequence (startup / shutdown)
else mode=prioritycontrol
mgc->>mgc: equalFlowControl(Qd, powerCap, priorityList)
end
mgc->>op: writeOwn flow/power predicted (AT_EQUIPMENT + DOWNSTREAM)
mgc->>mgc: notifyOutputChanged()
8. Data model — getOutput()
What lands on Port 0. Composed in io/output.js → getOutput(this) and delta-compressed by outputUtils.formatMsg.
| Key | Type | Unit | Sample |
|---|---|---|---|
absDistFromPeak |
number | — | 0 |
mode |
string | — | "optimalcontrol" |
relDistFromPeak |
number | — | 0 |
scaling |
string | — | "normalized" |
Concrete sample (excerpt — see live test output for the canonical shape):
{
"mode": "optimalcontrol",
"scaling": "normalized",
"flow.predicted.atequipment.<nodeId>": 0.0125,
"flow.predicted.downstream.<nodeId>": 0.0125,
"power.predicted.atequipment.<nodeId>": 1800,
"efficiency.predicted.atequipment.<nodeId>": 0.65,
"absDistFromPeak": 0.02,
"relDistFromPeak": 0.10
}
The <nodeId> segment is the Node-RED node id assigned at deploy time.
9. Configuration — editor form ↔ config keys
flowchart TB
subgraph editor["Node-RED editor form"]
f1[Control mode dropdown]
f2[Scaling dropdown]
f3[Optimisation method]
f4[Output unit (flow)]
f5[Position vs parent]
f6[Allowed sources / actions per mode]
end
subgraph cfg["Domain config slice"]
c1[mode.current]
c2[scaling.current]
c3[optimization.method]
c4[general.unit]
c5[functionality.positionVsParent]
c6[mode.allowedSources<br/>mode.allowedActions]
end
f1 --> c1
f2 --> c2
f3 --> c3
f4 --> c4
f5 --> c5
f6 --> c6
| Form field | Config key | Default | Range | Where used |
|---|---|---|---|---|
| Control mode | mode.current |
optimalControl |
enum (prioritycontrol, prioritypercentagecontrol, optimalcontrol) |
dispatch switch in _runDispatch |
| Scaling | scaling.current |
normalized |
enum (absolute, normalized) |
demand mapping in _runDispatch |
| Optimisation method | optimization.method |
BEP-Gravitation-Directional |
enum (NCog, BEP-Gravitation, BEP-Gravitation-Directional) |
_optimalControl selector |
| Output unit (flow) | general.unit |
m3/h |
unit string | unit policy output.flow |
| Position vs parent | functionality.positionVsParent |
atEquipment |
enum | event suffix for parent subscription |
10. State chart
MGC is event-driven and stateless with respect to operating modes — there is no FSM. The closest thing to "state" is the dispatch gate. Diagram for that single state vector:
stateDiagram-v2
[*] --> idle_disp: configure()
idle_disp --> dispatching: handleInput(Qd)
dispatching --> idle_disp: dispatch complete
dispatching --> dispatching: handleInput(Qd) — deferred and re-fired on completion
dispatching --> turning_off: Qd <= 0
turning_off --> idle_disp: all machines acknowledged shutdown
While dispatching, additional handleInput calls overwrite _delayedCall (latest-wins); the gate drains the latest one on completion. turnOffAllMachines() clears _delayedCall to make turn-off the final intent.
11. Examples
| Tier | File | What it shows | Status |
|---|---|---|---|
| Basic | examples/basic.flow.json |
Single MGC + 2 pumps, manual setDemand | ⚠️ legacy shape, pre-refactor |
| Integration | examples/integration.flow.json |
MGC wired under pumpingStation | ⚠️ legacy shape, pre-refactor |
| Edge | examples/edge.flow.json |
Mid-flight demand override + abort | ⚠️ legacy shape, pre-refactor |
Tier 1/2/3 visual-first example flows are still TODO (see MEMORY.md "TODO: Example Flows"). Screenshots will land under wiki/_partial-screenshots/machineGroupControl/ when the new flows ship.
12. Debug recipes
| Symptom | First thing to check | Where to look |
|---|---|---|
| No combination selected | Demand outside [dynamicTotals.flow.min, max] — clamped on entry; _optimalControl returns early if combinations empty. |
validPumpCombinations + warn log. |
| Group flow stuck at zero | Machines never reach an ACTIVE_STATE — check per-pump startup logs. |
isMachineActive. |
| Priority-percentage mode warns and exits | Mode requires scaling='normalized'. Set both. |
_runDispatch switch. |
| Stale flow setpoints on chained calls | Dispatch gate may have collapsed multiple calls — confirm _delayedCall was honoured. |
handleInput finally block. |
| Header pressure not equalising | Pressure children must register with asset.type='pressure' and a matching position. |
operatingPoint.equalize. |
| Optimiser picks unexpected combo | Verify optimization.method and per-method scoring (NCog vs BEP-Grav). |
optimizer/. |
Never ship
enableLog: 'debug'in a demo — fills the container log within seconds and obscures real errors.
13. When you would NOT use this node
- Don't use MGC for a single pump — wire
rotatingMachinedirectly. MGC's combinatorics + totals add no value below N=2. - Don't use MGC for valves — use
valveGroupControl. MGC's optimiser assumes a flow-vs-pressure characteristic curve. - Don't use MGC when the pumps live behind independent headers — combinations assume a shared discharge / suction pressure.
14. Known limitations / current issues
| # | Issue | Tracked in |
|---|---|---|
| 1 | optimalControl requires every machine to expose a curve — null-curve members silently exclude themselves from combinations. |
combinatorics/pumpCombinations. |
| 2 | Mid-flight setpoint overrides on accelerating / decelerating rely on abortActiveMovements per dispatch — a sequence with no awaitable abortMovement will warn but proceed. |
abortActiveMovements. |
| 3 | Power-cap parameter exposed but not surfaced as a topic input — only programmatic via handleInput(source, demand, powerCap). |
commands/index.js — no canonical topic. |
| 4 | Tier 1/2/3 visual-first example flows not yet written. | P9 follow-up. |