Files
machineGroupControl/wiki/Home.md
znetsixe 7d19fc1db0 P11.6 wiki regen + Phase 10 private-test rewrites where applicable
For all 11 nodes with auto-gen markers: wiki/Home.md sections 5 (topic
contract) and 9 (data model) regenerated via npm run wiki:all. New
Unit column shows '<measure> (default <unit>)' for declared topics,
'—' otherwise. Effect column now uses descriptor.description (P11.2
field) overriding the generic per-prefix fallback.

For rotatingMachine + reactor: Phase 10 test rewrites — 3 + 8 files
moved off private nodeClass internals (_attachInputHandler, _commands,
_pendingExtras, _registerChild, _tick, etc.) to the public
BaseNodeAdapter surface (node.handlers.input, node.source.*).
+6 / +7 net new tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:44:03 +02:00

13 KiB

machineGroupControl

Reflects code as of afc304b · 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

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-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().

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 -->|"&lt;type&gt;.measured.&lt;position&gt;"| 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 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 Tier 1/2/3 visual-first example flows not yet written. P9 follow-up.