Replaces the prior stub/partial wiki with a Home + Reference-{Architecture,
Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and
data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool.
Source-vs-spec contradictions surfaced and flagged inline (not silently
fixed). Pending-review notes mark sections that need a full node review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.9 KiB
valveGroupControl
A valveGroupControl (VGC) coordinates a group of valve children that share a common manifold — selector-valve banks, dosing-valve trains, mixing manifolds. It accepts a group-level total flow target, splits the flow proportional to each valve's Kv rating, runs a residual-reconciliation pass against what every valve actually accepted, and aggregates max delta-P across the group. It also reconciles upstream-source fluid-contract advertisements (liquid / gas) into one group-level service-type view that downstream consumers can read.
Note
Pending full node review (2026-05). Content reflects
CONTRACT.mdand current source only.
At a glance
| Thing | Value |
|---|---|
| What it represents | A parallel valve manifold — 2 + valves sharing a header, distributing one total flow target |
| S88 level | Unit |
| Use it when | You have 2 + parallel valves that should share an upstream flow target proportional to their Kv |
| Don't use it for | A single valve (wire valve directly), series valves (the Kv-share solver assumes parallel branches), or a manifold whose upstream already publishes per-branch setpoints |
| Children it accepts | valve (group members) + upstream sources (machine / rotatingmachine / machinegroup / machinegroupcontrol / pumpingstation / valvegroupcontrol) |
| Parents it talks to | Any upstream node — typically pumpingStation, reactor, or another VGC; VGC registers via Port 2 |
How it fits
flowchart LR
src[machine / MGC /<br/>pumpingStation<br/>upstream source]:::unit -.flow.predicted.*.-> vgc[valveGroupControl<br/>Unit]:::unit
vgc -->|updateFlow predicted<br/>downstream| v1[valve A]:::equip
vgc -->|updateFlow predicted<br/>downstream| v2[valve B]:::equip
v1 -->|positionChange<br/>deltaPChange| vgc
v2 -->|positionChange<br/>deltaPChange| vgc
vgc -->|child.register| parent[upstream parent]:::pc
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
S88 colours are anchored in .claude/rules/node-red-flow-layout.md.
Try it — 3-minute demo
Import the basic example flow, deploy, and drive a 2-valve group with an injected total-flow setpoint.
curl -X POST -H 'Content-Type: application/json' \
--data @nodes/valveGroupControl/examples/basic.flow.json \
http://localhost:1880/flow
What to click after deploy (the inject buttons map to canonical topics in Reference — Contracts):
child.registerfor eachvalve— or rely on Port-2 wiring to auto-register.set.mode = auto— lets the parent source drive the group.data.totalFlow = 80(withunit: 'm3/h') — VGC splits 80 m³/h across the available valves by Kv share; runs up tomaxPasses: 2residual passes; writes backatEquipment_predicted_flow= sum of accepted per-valve flows.cmd.execSequencewith{action: "startup"}— runs the group-wide startup sequence throughexecuteSequence, transitioning the group state machine through each step.cmd.emergencyStop— runs theemergencystopsequence on all valves ([emergencystop, off]).set.reconcileInterval = 2— re-tunes the periodic tick to 2 s (reconcileIntervalChangeevent triggers the adapter to restart its tick loop; minimum 100 ms).
Important
GIF needed. Demo recording of steps 1–6 with the live status panel. Save as
wiki/_partial-gifs/valveGroupControl/01-basic-demo.gif, target ≤ 1 MB aftergifsicle -O3 --lossy=80.
The seven things you'll send
| Topic | Aliases | Payload | What it does |
|---|---|---|---|
set.mode |
setMode |
"auto" | "virtualControl" | "fysicalControl" | "maintenance" |
Switch operational mode. Each mode has its own allow-list of sources (mode.allowedSources). |
set.position |
setpoint |
any | No-op pending Phase 7. Reserved for future per-valve positional override. Debug-logged only. |
child.register |
registerChild |
string (child node id) |
Manually register a child via RED.nodes.getNode; Port 2 wiring does this automatically in most flows. |
cmd.execSequence |
execSequence |
{ source, action, parameter } |
Forward to source.handleInput(source, action, parameter) — runs a group-wide sequence (startup / shutdown / emergencystop / boot). |
data.totalFlow |
totalFlowChange |
number, { value, position?, variant?, unit? }, or { source, action, ... } |
Update the total measured/predicted flow at the configured position; triggers calcValveFlows to re-distribute across valves. |
cmd.emergencyStop |
emergencyStop, emergencystop |
optional { source } |
Run the emergencystop sequence on all valves. |
set.reconcileInterval |
setReconcileInterval |
number — seconds (> 0) | Re-tune the periodic flow-reconciliation interval. Min clamp 100 ms. |
Aliases log a one-time deprecation warning the first time they fire.
What you'll see come out
Sample Port 0 message (delta-compressed, after a data.totalFlow = 80 split across two valves):
{
"topic": "valveGroupControl#VGC1",
"payload": {
"mode": "auto",
"maxDeltaP": 1450,
"atEquipment_measured_flow": 80,
"atEquipment_predicted_flow": 80,
"deltaMax_predicted_pressure": 1450
}
}
Key shape: <position>_<variant>_<type> — same as MGC's key shape (inverse of rotatingMachine's per-measurement form). The output reflects the group aggregate, not per-valve snapshots; per-valve detail comes off each valve's own Port 0.
| Field | Meaning |
|---|---|
mode |
Current operational mode (auto / virtualControl / fysicalControl / maintenance). |
maxDeltaP |
Max delta-P across registered valves — refreshed whenever a child emits deltaPChange. Also surfaced as deltaMax_predicted_pressure via the measurement container. |
atEquipment_measured_flow |
Total measured flow at the group inlet (from an upstream source's flow.measured.* event). |
atEquipment_predicted_flow |
Sum of per-valve accepted flows after the Kv-share + residual pass. |
deltaMax_predicted_pressure |
Max delta-P across the group, written via the measurement container at position delta / variant predicted / type pressure. |
Flow-distribution loop — what one event does
When a data.totalFlow arrives (or an upstream source publishes flow.predicted.* / flow.measured.*), VGC re-distributes by Kv share:
flowchart LR
src[data.totalFlow /<br/>upstream source event] --> upd[updateFlow<br/>predicted/measured atEquipment]
upd --> avail[getAvailableValves<br/>state ≠ off/maintenance, kv > 0]
avail --> solve[solveFlowDistribution<br/>share by Kv / totalKv]
solve --> push[valve.updateFlow predicted<br/>downstream]
push --> readback[read accepted from<br/>flow.predicted.downstream]
readback --> residual[residual = target − sum(accepted)]
residual -->|residual > tol & passes < max| solve
residual --> writeback[write flow.predicted.atEquipment<br/>= sum(accepted)]
writeback --> dp[calcMaxDeltaP]
dp --> emit[notifyOutputChanged]
Reconciliation defaults (flowReconciliation):
| Field | Default | Notes |
|---|---|---|
maxPasses |
2 |
Max iterations of the residual-correction loop. |
residualTolerance |
0.001 |
Stops the loop when ` |
A valve is available if: state.getCurrentState() !== 'off' and !== 'maintenance', currentMode !== 'maintenance', and kv > 0. Unavailable valves are skipped and receive updateFlow('predicted', 0, 'downstream').
VGC has no FSM of its own — state semantics belong to the child valves. specificClass instantiates a state object internally and stamps it operational at boot for sequence dispatch; the group's only coordination loop is the Kv-share solver above.
Need more?
| Page | What you'll find |
|---|---|
| Reference — Contracts | Topic registry, config schema, child-registration filters |
| Reference — Architecture | Code map, flow-distribution loop, source aggregation, output ports |
| Reference — Examples | Shipped flows, debug recipes |
| Reference — Limitations | When not to use, known issues, open questions |