Files
valveGroupControl/wiki/Home.md
znetsixe 618ad27e03 docs(wiki): rewrite Home.md to 14-section visual-first template
Fixes section ordering (11-8-9-10 scramble + duplicate §11), updates
banner hash to ef34c82, corrects §2 diagram (set.position → updateFlow,
evt.deltaPChange → positionChange/deltaPChange), upgrades capability
matrix to ⚠️ for set.position and cascaded VGC, rewrites §9 to reflect
the actual editor form fields (name/format/position/logger), and
replaces the orphaned "Distribution loop" duplicate-§11 with a proper
§10. AUTOGEN markers verified intact via npm run wiki:all.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:05:17 +02:00

13 KiB
Raw Blame History

valveGroupControl

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

valveGroupControl (VGC) is an S88 Unit coordinator for a group of valve children. It distributes a group-level flow target across registered valves proportional to their Kv rating, runs a residual-reconciliation pass to absorb per-valve acceptance limits, aggregates group max delta-P, and reconciles upstream fluid-contract advertisements into a single group-level service-type view.

2. Position in the platform

flowchart LR
    src[machine / MGC / PS<br/>upstream source]:::unit -.flow.predicted.-> vgc[valveGroupControl<br/>Unit]:::unit
    vgc -->|updateFlow predicted| v1[valve A]:::equip
    vgc -->|updateFlow predicted| v2[valve B]:::equip
    v1 -->|positionChange| vgc
    v2 -->|positionChange| vgc
    v1 -->|deltaPChange| vgc
    v2 -->|deltaPChange| vgc
    vgc -->|child.register| parent[upstream parent]:::unit
    classDef unit fill:#50a8d9,color:#000
    classDef equip fill:#86bbdd,color:#000

S88 colours: Unit #50a8d9, Equipment #86bbdd. Source of truth: .claude/rules/node-red-flow-layout.md.

3. Capability matrix

Capability Status Notes
Proportional flow distribution by Kv groupOps/flowDistribution.js.
Two-pass residual reconciliation Default maxPasses: 2, residualTolerance: 0.001.
Periodic tick re-balance tick() calls calcValveFlows(); set.reconcileInterval re-tunes interval.
Group maxDeltaP aggregation Recomputed on any child deltaPChange event.
Upstream fluid-contract aggregation sources/fluidContract.js; emits fluidContractChange.
Group-wide sequence dispatch cmd.execSequenceexecuteSequence → per-state transitionToState.
Emergency stop cmd.emergencyStopemergencystop sequence on all valves.
Per-valve positional override ⚠️ set.position is a debug-logged no-op pending Phase 7. See §14.
Multi-source aggregation Multiple machines / MGCs / PSs may register as upstream sources.
Cascaded VGC as upstream source ⚠️ Accepted by router but not exercised in production; test coverage absent.

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["ValveGroupControl.configure()<br/>router.onRegister: valve + sources<br/>tick() → calcValveFlows()"]
    end
    subgraph concerns["src/ concern modules"]
        gops["groupOps/<br/>flowDistribution + calcMaxDeltaP"]
        srcs["sources/<br/>fluidContract registration"]
        cmds["commands/<br/>topic registry + handlers"]
        io["io/<br/>getOutput + getStatusBadge"]
    end
    nc --> sc
    sc --> gops
    sc --> srcs
    sc --> io
    nc --> cmds
Module Owns Read first if you're changing…
groupOps/ Kv-share solver, residual pass, max-deltaP aggregation How the group divides flow between valves.
sources/ Upstream-source registration, flow-event subscription, fluid-contract reconciliation Service-type aggregation, source event wiring.
commands/ Input-topic registry + per-topic handlers New input topics or payload validation rules.
io/ Port-0 output shape + status badge composition What lands on the wire, badge text.

5. Topic contract

Auto-generated from src/commands/index.js. Do NOT hand-edit between the markers. Re-run npm run wiki:contract.

The Unit column reflects the descriptor's units: { measure, default } declaration, rendered as <measure> (default <unit>). Topics without a units field show . The Effect column is sourced from the descriptor's description field.

Canonical topic Aliases Payload Unit Effect
set.mode setMode string Switch the valve group between auto / manual control modes.
set.position setpoint any Set the group-level valve position (currently a no-op pending Phase 7).
child.register registerChild string Register a child valve with this group.
cmd.execSequence execSequence object Run a group-wide sequence (startup / shutdown / emergencystop).
data.totalFlow totalFlowChange any Notify the group that the total flow setpoint has changed.
cmd.emergencyStop emergencyStop, emergencystop any Trigger an emergency stop across all valves in the group.
set.reconcileInterval setReconcileInterval any Update the reconciliation interval (seconds).

6. Child registration

flowchart LR
    subgraph kids["accepted children (softwareType)"]
        v["valve"]:::equip
        src["machine / rotatingmachine /<br/>machinegroup / pumpingstation /<br/>valvegroupcontrol"]:::unit
    end
    v   -->|positionChange| handler1[onPositionChange<br/>→ calcValveFlows]
    v   -->|deltaPChange|   handler2[onDeltaPChange<br/>→ calcMaxDeltaP]
    src -->|flow.predicted.*<br/>flow.measured.*| handler3[updateFlow<br/>atEquipment]
    src -->|fluidContractChange| handler4[sources.refresh<br/>aggregate contract]
    classDef equip fill:#86bbdd,color:#000
    classDef unit fill:#50a8d9,color:#000
softwareType onRegister side-effect Subscribed events
valve Stored in this.valves[id]; binds positionChange (via child.state.emitter) + deltaPChange (via child.emitter); triggers calcValveFlows + calcMaxDeltaP. positionChange, deltaPChange.
machine / rotatingmachine Stored as upstream source; reads getFluidContract() or infers liquid by default. flow.predicted.*, flow.measured.*, fluidContractChange.
machinegroup / machinegroupcontrol Same as machine. Same as machine.
pumpingstation Same as machine. Same as machine.
valvegroupcontrol Cascaded VGC; accepted by router. Not exercised in production. Same as machine.

7. Lifecycle — what one tick / event does

sequenceDiagram
    participant src as upstream source
    participant vgc as VGC
    participant v1 as valve A
    participant v2 as valve B
    participant out as Port-0

    src->>vgc: flow.predicted.downstream (m³/h)
    vgc->>vgc: updateFlow('predicted', value, 'atEquipment')
    vgc->>vgc: calcValveFlows()
    Note over vgc: solveFlowDistribution<br/>by Kv share (≤ maxPasses)
    vgc->>v1: updateFlow('predicted', shareA, 'downstream')
    vgc->>v2: updateFlow('predicted', shareB, 'downstream')
    v1-->>vgc: positionChange / deltaPChange
    v2-->>vgc: positionChange / deltaPChange
    vgc->>vgc: calcMaxDeltaP
    vgc->>vgc: notifyOutputChanged()
    vgc->>out: msg{topic, payload (delta-compressed)}

8. Data model — getOutput()

What lands on Port 0. Composed in io/output.getOutput(), then delta-compressed by outputUtils.formatMsg.

Key Type Unit Sample
maxDeltaP number 0
mode string "auto"

Measurement-derived keys follow the <position>_<variant>_<type> shape and are emitted only when the container holds a finite value.

Example key Unit Description
atEquipment_predicted_flow m³/h Total group predicted flow (sum of per-valve assigned flows).
atEquipment_measured_flow m³/h Total group measured flow (from upstream source).
deltaMax_predicted_pressure mbar Max delta-P across registered valves.

Delta compression: only changed fields are sent per tick. Consumers must cache and merge. See outputUtils.formatMsg.

9. Configuration — editor form ↔ config keys

flowchart TB
    subgraph editor["Node-RED editor form (vgc.html)"]
        f1[Name]
        f2[Process output format]
        f3[Database output format]
        f4[Position vs parent]
        f5[Logger / log level]
    end
    subgraph config["Domain config (runtime / schema)"]
        c1[general.name]
        c2[output.process]
        c3[output.dbase]
        c4[functionality.positionVsParent]
        c5[logging.enableLog / logLevel]
    end
    f1 --> c1
    f2 --> c2
    f3 --> c3
    f4 --> c4
    f5 --> c5
Form field Config key Default Where used
Name general.name "" Node label, InfluxDB tag, status badge.
Process output format output.process "process" outputUtils.formatMsg Port-0 formatter.
Database output format output.dbase "influxdb" outputUtils.formatMsg Port-1 formatter.
Position vs parent functionality.positionVsParent "" Child registration Port-2 message; read by upstream parent.
Enable log / log level logging.enableLog, logging.logLevel false / "error" Logger verbosity. Never ship debug in demos.

Domain-level config (mode.current, sequences, mode.allowedSources, flowReconciliation) is loaded from the node's schema defaults and updated at runtime via set.mode / set.reconcileInterval. These fields are not exposed in the editor form.

10. Flow-distribution loop (replaces state chart)

VGC has no FSM of its own — state semantics belong to the child valves. The core coordination loop is the Kv-share solver described below.

sequenceDiagram
    participant tick as adapter tick / incoming event
    participant vgc as VGC
    participant solver as solveFlowDistribution
    participant valves as valve children

    tick->>vgc: calcValveFlows()
    vgc->>vgc: read flow.measured (or predicted) .atEquipment
    vgc->>solver: target=totalFlow, entries=availableValves
    loop ≤ maxPasses while |residual| > tolerance
        solver->>valves: updateFlow share by (Kv / totalKv)
        valves-->>solver: accepted flow (read back from child.measurements)
        solver->>solver: residual = target  sum(accepted)
    end
    solver-->>vgc: { flowsById, residual, passes }
    vgc->>vgc: write flow.predicted.atEquipment = assignedTotal
    vgc->>vgc: calcMaxDeltaP()
    vgc->>vgc: notifyOutputChanged()

A valve is available if: state ≠ off|maintenance, mode ≠ maintenance, and kv > 0. Unavailable valves receive updateFlow('predicted', 0) and are excluded from the solver.

11. Examples

Tier File What it shows Mandatory?
Basic examples/basic.flow.json Inject data.totalFlow + 2 valves (no parent)
Integration examples/integration.flow.json VGC + valves + upstream rotatingMachine
Edge examples/edge.flow.json Edge cases: no valves, residual non-convergence, cascaded VGC

Tier-1 / Tier-2 / Tier-3 naming scheme pending when existing flows are validated on live Node-RED. Screenshots under wiki/_partial-screenshots/valveGroupControl/ when produced.

12. Debug recipes

Symptom First thing to check Where to look
All valves receive assigned flow = 0 getAvailableValves() returns empty list; check Kv > 0 and child state ≠ off / maintenance. groupOps/flowDistribution.isValveAvailable
Residual never converges flowReconciliation.maxPasses too low for valve curve shape; check lastFlowSolve.residual. groupOps/flowDistribution.js
Group maxDeltaP stale Child deltaPChange not subscribed; valve emitter not exposed via child.emitter. specificClass._bindValveEvents
Service-type stays unknown No upstream source registered, or all advertise unknown type. sources/fluidContract.refreshFluidContract
data.totalFlow silently ignored Mode rejects the source id; check mode.allowedSources for the current mode. specificClass.isValidSourceForMode
set.position has no effect Known limitation — handler is a no-op. See §14. commands/handlers.setPosition

Never ship enableLog: 'debug' in a demo — fills the container log within seconds and obscures real errors. Use only for live debugging.

13. When you would NOT use this node

  • Use valve directly when you have a single valve under an upstream parent. VGC adds coordination overhead for no benefit with one child.
  • Do NOT use VGC to coordinate series valves — the Kv-share solver assumes parallel branches sharing a common header pressure.
  • Skip VGC when the upstream source already publishes per-branch flow setpoints; route those directly to each valve instead.

14. Known limitations / current issues

# Issue Tracked in
1 set.position is a debug-logged no-op — per-valve positional override is reserved, pending Phase 7 topic standardisation of valve setpoint payloads. The handler in commands/handlers.setPosition intentionally does nothing. CONTRACT.md §Inputs; OPEN_QUESTIONS.md Phase 7
2 Residual solver assumes Kv share is a valid first estimate. Pathological valve curves (e.g. very non-linear Kv vs position) may need more passes than the default maxPasses: 2 to reach residualTolerance. groupOps/flowDistribution.js
3 Cascaded valvegroupcontrol registration (VGC as upstream source of another VGC) is accepted by the router but has no test coverage and is not exercised in production. CONTRACT.md §Children