# machineGroupControl > **Reflects code as of `7d19fc1` · regenerated `2026-05-11` via `npm run wiki:all`** > If 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 ```mermaid flowchart LR parent[pumpingStation
Process Cell]:::pc -->|set.demand| mgc[machineGroupControl
Unit]:::unit header[measurement
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) | ✅ | `DemandDispatcher` / `LatestWinsGate.fireAndWait`. | | Forced shutdown on `Qd ≤ 0` | ✅ | `turnOffAllMachines()`. | ## 4. Code map ```mermaid flowchart TB subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"] nc["buildDomainConfig()
static DomainClass, commands"] end subgraph domain["specificClass.js — orchestrator (BaseDomain)"] sc["MachineGroup.configure()
ChildRouter rules
handleInput() dispatch gate"] end subgraph concerns["src/ concern modules"] groupOps["groupOps/
GroupOperatingPoint + curves"] totals["totals/
TotalsCalculator"] combi["combinatorics/
validPumpCombinations"] opt["optimizer/
BEP-Grav / NCog selectors"] efficiency["efficiency/
GroupEfficiency + BEP dist"] ctrl["control/
strategies (equalFlow / prioPct)"] dispatch["dispatch/
DemandDispatcher (LatestWinsGate)"] io["io/
output + status"] commands["commands/
topic registry + handlers"] end nc --> sc sc --> groupOps sc --> totals sc --> combi sc --> opt sc --> efficiency sc --> ctrl 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/` | `DemandDispatcher` wrapping `LatestWinsGate.fireAndWait` | Demand 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-run `npm run wiki:contract`. | Canonical topic | Aliases | Payload | Unit | Effect | |---|---|---|---|---| | `set.mode` | `setMode` | `string` | — | Switch the machine group between auto / manual modes. | | `set.scaling` | `setScaling` | `string` | — | Select the group scaling strategy. | | `child.register` | `registerChild` | `string` | — | Register a child machine with this group. | | `set.demand` | `Qd` | `any` | `volumeFlowRate` (default `m3/h`) | Operator demand setpoint dispatched to the child machines. | ## 6. Child registration `ChildRouter` declarations in `specificClass.js → configure()`. ```mermaid flowchart LR subgraph kids["accepted children (softwareType)"] mach["machine
(rotatingMachine)"]:::equip m["measurement
(header pressure)"]:::ctrl end mach -->|"pressure.measured.downstream
pressure.measured.differential
flow.predicted.downstream"| eq[operatingPoint.equalize
+ totals refresh] m -->|"<type>.measured.<position>"| mirror[mirror into own
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 `.measured.` | Mirror value into MGC's own MeasurementContainer; pressure also triggers `handlePressureChange()`. | ## 7. Lifecycle — what one event does ```mermaid 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): ~~~json { "mode": "optimalcontrol", "scaling": "normalized", "atEquipment_predicted_flow": 42.5, "downstream_predicted_flow": 42.5, "atEquipment_predicted_power": 18.0, "atEquipment_predicted_efficiency": 0.65, "atEquipment_predicted_Ncog": 1.23, "absDistFromPeak": 0.02, "relDistFromPeak": 0.10 } ~~~ Key format from `io/output.js`: `__` (e.g. `atEquipment_predicted_flow`). Output units: flow in `m3/h`, power in `kW`, pressure in `mbar`. ## 9. Configuration — editor form ↔ config keys ```mermaid 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
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: ```mermaid 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 are absorbed by `DemandDispatcher` (latest-wins). A superseded call resolves with `{ superseded: true }`. `turnOffAllMachines()` calls `cancelPending()` so turn-off is always the final intent. ## 11. Examples | Tier | File | What it shows | |---|---|---| | 1 | `examples/01-Basic.json` | One MGC + three `rotatingMachine` pumps driven by inject buttons. Setup auto-fires `virtualControl` + `cmd.startup` on all three pumps; numbered driver groups for mode / scaling / demand. | | 2 | `examples/02-Dashboard.json` | Same command surface driven by a FlowFuse Dashboard 2.0 page — Mode + Scaling buttons, Demand slider, live Status rows (mode / scaling / total flow / total power / capacity / active machines / BEP %), three trend charts, and a raw-output table. | See [`examples/README.md`](https://gitea.wbd-rd.nl/RnD/machineGroupControl/src/branch/development/examples/README.md) for the canonical command surface table and step-by-step "what to try" recipes. > [!IMPORTANT] > **Screenshots needed.** Capture both flows in the editor + the rendered dashboard. Save under `wiki/_partial-screenshots/machineGroupControl/` as `01-basic-flow.png`, `02-dashboard-editor.png`, `03-dashboard-rendered.png`. Replace this callout with the image links. ## 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 superseded intermediate calls — callers should check `result.superseded`. | `DemandDispatcher` / `LatestWinsGate`. | | 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 `rotatingMachine` directly. 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 | Per-pump fan-out for dashboard charts (per-machine flow / power series) not surfaced from MGC's Port 0 — only group aggregates appear. Subscribe to each rotatingMachine's Port 0 if you need per-pump trends. | `io/output.js` aggregates only. | | 5 | **`maxEfficiency` naming bug** — `GroupEfficiency.calcGroupEfficiency` returns `{ maxEfficiency, lowestEfficiency }` but `maxEfficiency` is actually the **mean cog** across all machines (not the maximum). The name is deliberately preserved for behavioural parity; callers using it as "the peak" will over-estimate the BEP target. | `efficiency/groupEfficiency.js` comment + `OPEN_QUESTIONS.md` 2026-05-10. | | 6 | **`calcAbsoluteTotals` implicit pressure-key coupling** — iterates `machine.predictFlow.inputCurve` and re-uses the same pressure key to index `machine.predictPower.inputCurve[pressure]`. If the two curves were sampled at different pressures the lookup is `undefined` and the call throws. Enforcement or defensive skip deferred to P5 (rotatingMachine curveLoader). | `totals/totalsCalculator.js` + `OPEN_QUESTIONS.md` 2026-05-10. |