Clone
3
Home
vps1_gitea_admin edited this page 2026-05-11 18:30:15 +00:00

valveGroupControl

Reflects code as of e02cd1a · 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 that distributes a group-level flow target across registered valve children proportional to their current Kv, aggregates the per-valve accepted flow back into a group total, and surfaces the maximum delta-P across the group. It also reconciles each child's fluid-contract advertisement 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 -->|set.position| v1[valve A]:::equip
    vgc -->|set.position| v2[valve B]:::equip
    v1 -->|evt.deltaPChange| vgc
    v2 -->|evt.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 maxPasses: 2, residualTolerance: 0.001.
Periodic tick re-balance Runs each adapter tick; set.reconcileInterval re-tunes.
Group maxDeltaP aggregation Recomputed on any child deltaPChange.
Upstream fluid-contract aggregation sources/fluidContract.js.
Sequence dispatch to all valves cmd.execSequence → per-valve handleInput.
Per-valve positional override set.position reserved; no-op in current build.
Multi-source aggregation Multiple machines / MGCs may register as sources.

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/>registerChild dispatch<br/>tick → calcValveFlows"]
    end
    subgraph concerns["src/ concern modules"]
        gops["groupOps/<br/>flowDistribution"]
        srcs["sources/<br/>fluidContract"]
        cmds["commands/<br/>topic registry + handlers"]
        io["io/<br/>output + status badge"]
    end
    nc --> sc
    sc --> gops
    sc --> srcs
    sc --> io
    nc --> cmds
Module Owns Read first if you're changing…
groupOps/ Flow distribution, residual solver, max-deltaP aggregation How the group divides flow.
sources/ Upstream-source registration, fluid-contract reconciliation Service-type aggregation, source-event subscriptions.
commands/ Input-topic registry + per-topic handlers New input topics, payload validation.
io/ Port-0 output shape + status badge What lands on the wire.

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 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.*| handler3[updateFlow<br/>at 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 + deltaPChange; triggers calcValveFlows + calcMaxDeltaP. positionChange, deltaPChange.
machine / rotatingmachine Stored as upstream source; reads getFluidContract() (or default liquid). flow.predicted.*, flow.measured.*, fluidContractChange.
machinegroup / machinegroupcontrol Same as machine. Same as machine.
pumpingstation Same as machine. Same as machine.
valvegroupcontrol Allowed for cascaded VGCs; not exercised in production. Same.

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 (m3/s)
    vgc->>vgc: _write flow.predicted.atEquipment
    vgc->>vgc: calcValveFlows()
    Note over vgc: solveFlowDistribution<br/>(by Kv share)
    vgc->>v1: updateFlow('predicted', shareA, 'downstream')
    vgc->>v2: updateFlow('predicted', shareB, 'downstream')
    v1-->>vgc: accepted flow + deltaPChange
    v2-->>vgc: accepted flow + deltaPChange
    vgc->>vgc: residual pass (up to maxPasses)
    vgc->>vgc: calcMaxDeltaP
    vgc->>out: msg{topic, payload (delta-compressed)}

11. Distribution loop — flow-share solver

VGC has no FSM. The loop below replaces the state chart for this section.

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

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

8. Data model — getOutput()

What lands on Port 0. Composed in io/output, 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 (e.g. atEquipment_predicted_flow) and are emitted only when the container holds a finite value.

9. Configuration — editor form ↔ config keys

flowchart TB
    subgraph editor["Node-RED editor form"]
        f1[Mode]
        f2[Sequences]
        f3[Allowed sources per mode]
        f4[Reconciliation interval]
    end
    subgraph config["Domain config slice"]
        c1[mode.current]
        c2[sequences.<name>]
        c3[mode.allowedSources.<mode>]
        c4[flowReconciliation (runtime)]
    end
    f1 --> c1
    f2 --> c2
    f3 --> c3
    f4 --> c4
Form field Config key Default Range Where used
Mode mode.current per schema enum setMode
Sequences sequences.<name> per schema Set of state names executeSequence
Allowed sources mode.allowedSources.<mode> per schema Set of source ids isValidSourceForMode
Reconcile maxPasses flowReconciliation.maxPasses 2 ≥ 1 solveFlowDistribution
Reconcile tolerance flowReconciliation.residualTolerance 0.001 ≥ 0 solveFlowDistribution
Reconcile interval (runtime, via set.reconcileInterval) adapter default ≥ 100 ms setReconcileIntervalSeconds

10. State chart

Skipped — VGC has no FSM of its own. State semantics belong to the child valves; VGC is a coordinator. See section 11 for the flow-distribution loop.

11. Examples

Tier File What it shows Mandatory?
Basic examples/01-Basic.flow.json Inject data.totalFlow + 2 valves + dashboard
Integration examples/02-Integration.flow.json VGC + valves + upstream rotatingMachine
Dashboard examples/03-Dashboard.flow.json Live FlowFuse charts (per-valve flow, max ΔP)

Screenshots under wiki/_partial-screenshots/valveGroupControl/ when produced. Docker compose snippet under examples/README.md.

12. Debug recipes

Symptom First thing to check Where to look
All valves show assigned flow = 0 getAvailableValves() empty; check Kv > 0 and state ≠ off / maintenance. groupOps/flowDistribution.isValveAvailable
Residual never converges flowReconciliation.maxPasses too low for the valve curve shape. lastFlowSolve.residual
Group maxDeltaP stale Child deltaPChange not subscribed; valve emitter not exposed. _bindValveEvents
Service-type stays unknown No source registered yet or all sources advertise unknown. sources.refreshFluidContract
data.totalFlow ignored Mode rejects the source; check mode.allowedSources. isValidSourceForMode

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 VGC for 2+ parallel valves sharing a header. For a single valve, wire valve directly under the upstream parent.
  • Don't use VGC to coordinate series valves — the flow-distribution model assumes parallel branches with a shared upstream pressure.
  • Skip VGC when the upstream source already publishes per-branch flow setpoints; route those straight to each valve.

14. Known limitations / current issues

# Issue Tracked in
1 set.position is reserved but currently a no-op — per-valve override is pending Phase 7. CONTRACT.md ## Inputs
2 Residual solver assumes Kv share is a good first estimate; pathological curves can need more passes than the default 2. groupOps/flowDistribution.js
3 Cascaded valvegroupcontrol registration accepted but not test-covered. CONTRACT.md ## Children