Files
valve/wiki/Home.md
znetsixe 95ccc77b25 docs(wiki): rewrite Home.md — correct FSM states + config keys for Section 9/10
Section 10 (State chart): replace invented opening/closing/closed states with
the real shared FSM states (accelerating/decelerating for moves; idle/starting/
warmingup/operational/stopping/coolingdown/emergencystop/off/maintenance for
lifecycle). Show all valid transitions from stateConfig.json allowedTransitions.
Document protected transitions (warmingup, coolingdown) and valve-specific
pre-shutdown ramp-to-zero behaviour.

Section 9 (Config): add missing editor fields from nodeClass.buildDomainConfig
(startup/warmup/shutdown/cooldown times, speed, serviceType, fluidDensity,
fluidTemperatureK, gasChokedRatioLimit). Correct config paths to match actual
stateConfig / runtimeOptions split.

Section 7 (Lifecycle): add FSM state labels to sequence diagram; show
accelerating → operational final step.

Sections 2/6/12/14: minor precision improvements (Port-2 note, abort-deadlock
recipe, execSequence Phase-7 removal warning).

Re-ran npm run wiki:all; AUTOGEN blocks intact.

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

16 KiB
Raw Blame History

valve

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

valve models a single actuated throttling valve at the S88 Equipment Module level. It loads a supplier Kv-vs-position characteristic curve, drives an FSM for open/close move sequences (using accelerating/decelerating states shared with rotatingMachine), and recomputes pressure drop from flow + Kv via a hydraulic model. Used standalone or as a child of valveGroupControl.

2. Position in the platform

flowchart LR
    vgc[valveGroupControl<br/>Unit]:::unit -->|set.position| this[valve<br/>Equipment]:::equip
    src["machine / MGC / PS<br/>(upstream source)"]:::unit -->|child.register| this
    meas[measurement<br/>type=pressure / flow]:::ctrl -.->|data| this
    this -->|child.register Port 2| vgc
    this -->|evt.deltaPChange| vgc
    this -->|evt.fluidCompatibilityChange| vgc
    classDef unit fill:#50a8d9,color:#000
    classDef equip fill:#86bbdd,color:#000
    classDef ctrl fill:#a9daee,color:#000

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

3. Capability matrix

Capability Status Notes
Predicts deltaP from flow + Kv Hydraulic model picks liquid vs gas formula per serviceType.
Loads supplier curve by model name asset.model resolved through loadModel; inline valveCurve override supported.
Position move FSM accelerating / decelerating states with interruptible setpoints; moveTo uses shared state machine.
Startup / shutdown sequences Pre-shutdown ramps valve to position 0 before executing stop sequence.
Emergency-stop sequence cmd.estopemergencystop → off sequence.
Fluid-contract aggregation Tracks upstream service type via registered sources through FluidCompatibility.
Gas-choke detection ⚠️ Hard cap at gasChokedRatioLimit; surfaced in hydraulicDiagnostics.
Multi-parent registration ⚠️ Allowed but not exercised in production tests.

4. Code map

flowchart TB
    subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
        nc["buildDomainConfig()<br/>static DomainClass=Valve, commands<br/>tickInterval=null (event-driven)"]
    end
    subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
        sc["Valve.configure()<br/>wires concern modules<br/>overrides registerChild → FluidCompatibility"]
    end
    subgraph concerns["src/ concern modules"]
        state["state/<br/>stateBindings → positionChange"]
        fluid["fluid/<br/>FluidCompatibility"]
        curve["curve/<br/>SupplierCurvePredictor"]
        meas["measurement/<br/>MeasurementRouter + FORMULA_UNITS"]
        flow["flow/<br/>FlowController (setpoint, sequences)"]
        io["io/<br/>buildOutput + buildStatusBadge"]
        hyd["hydraulicModel.js<br/>ValveHydraulicModel"]
    end
    nc --> sc
    sc --> state
    sc --> fluid
    sc --> curve
    sc --> meas
    sc --> flow
    sc --> io
    sc --> hyd
Module Owns Read first if you're changing…
state/ Bindings from state-machine positionChangeupdatePosition() Move-finished triggers, position callbacks.
fluid/ Service-type compatibility, contract aggregation Gas-vs-liquid mismatch warnings, upstream fluid tracking.
curve/ Supplier Kv curve load + interpolation Curve fitting, model selection, density keys.
measurement/ Pressure/flow routing + deltaP recompute What triggers a recalc, measurement container writes.
flow/ Sequence + setpoint execution, mode validation Startup / shutdown / move semantics, allowed-source checks.
io/ Port-0 output shape + status badge What lands on the wire each tick.
hydraulicModel.js Liquid + gas deltaP formulas, choke detection Hydraulic calculation errors, choke ratio behaviour.

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 between auto / manual control modes.
cmd.startup (none) any Initiate the valve startup sequence.
cmd.shutdown (none) any Initiate the valve shutdown sequence.
cmd.estop emergencystop, emergencyStop any Trigger an emergency stop on the valve.
execSequence (none) object Legacy umbrella that demuxes payload.action to startup / shutdown / estop.
set.position execMovement object Move the valve to a control-% position via execMovement.
data.flow updateFlow object Push a measured flow into the valve (variant + position + unit).
query.curve showcurve any Return the valve characteristic curve on the reply port.
child.register registerChild string Register a child measurement with this valve.

6. Child registration

valve overrides BaseDomain.registerChild with FluidCompatibility.registerChild. Upstream sources feed the fluid-contract aggregator; measurement children attach through the standard measurement handshake and land in MeasurementRouter.

flowchart LR
    subgraph kids["accepted children (softwareType)"]
        src["machine / rotatingmachine<br/>machinegroup / pumpingstation<br/>valvegroupcontrol"]:::unit
        m["measurement<br/>type=pressure or flow"]:::ctrl
    end
    src -->|getFluidContract| fluid[FluidCompatibility<br/>aggregates serviceType]
    m   -->|"&lt;type&gt;.measured.&lt;position&gt;"| router[MeasurementRouter<br/>updatePressure / updateFlow]
    router --> deltaP[updateDeltaP<br/>writes pressure.predicted.delta]
    fluid --> evt1["evt.fluidCompatibilityChange<br/>evt.fluidContractChange"]
    deltaP --> evt2[evt.deltaPChange]
    classDef unit fill:#50a8d9,color:#000
    classDef ctrl fill:#a9daee,color:#000
softwareType onRegister side-effect Subscribed events
machine / rotatingmachine Stored as upstream source; reads getFluidContract() or defaults to liquid. fluidContractChange
machinegroup / machinegroupcontrol Same; recomputes aggregate service type. fluidContractChange
pumpingstation Same. fluidContractChange
valvegroupcontrol Same. fluidContractChange
measurement Routed via measurement handshake; values land in MeasurementContainer. <type>.measured.<position>

7. Lifecycle — what one event does

sequenceDiagram
    participant parent as valveGroupControl
    participant valve as valve
    participant fsm as state FSM
    participant hyd as hydraulicModel
    participant out as Port-0

    parent->>valve: set.position { setpoint: 60 }
    valve->>fsm: moveTo(60)
    fsm-->>fsm: operational → accelerating
    fsm-->>valve: positionChange ticks (stateBindings)
    valve->>valve: predictKv(position)
    valve->>hyd: calculateDeltaPMbar(q, kv, downP, rho, T)
    hyd-->>valve: { deltaPMbar, diagnostics }
    valve->>valve: write pressure.predicted.delta
    valve->>parent: emitter.emit('deltaPChange', deltaP)
    valve->>out: msg { topic, payload (delta-compressed) }
    fsm-->>fsm: accelerating → operational (setpoint reached)

8. Data model — getOutput()

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

Key Type Unit Sample
state string "operational"
percentageOpen number % 0
moveTimeleft number s 0
mode string "auto"
downstream_predicted_flow number m3/h 0
downstream_measured_flow number m3/h (emitted when measurement child present)
downstream_predicted_pressure number mbar (emitted when upstream pressure present)
downstream_measured_pressure number mbar (emitted when measurement child present)
delta_predicted_pressure number mbar 0

Measurement keys follow the legacy <position>_<variant>_<type> shape (e.g. downstream_predicted_flow, delta_predicted_pressure). Only keys with finite values are emitted — consumers must cache and merge (delta-compression is active).

9. Configuration — editor form ↔ config keys

flowchart TB
    subgraph editor["Node-RED editor form"]
        f1[Reaction Speed]
        f2[Asset model / supplier / category]
        f3[Service type]
        f4[Fluid density / temperature K]
        f5[Gas choke ratio limit]
        f6[Startup / warmup / shutdown / cooldown times]
        f7[Log level / enableLog]
        f8[positionVsParent]
    end
    subgraph config["Domain config slice"]
        c1["movement.speed (stateConfig)"]
        c2[asset.model]
        c3["runtimeOptions.serviceType → hydraulicModel"]
        c4["runtimeOptions.fluidDensity / fluidTemperatureK"]
        c5["runtimeOptions.gasChokedRatioLimit"]
        c6["stateConfig.time.starting / warmingup / stopping / coolingdown"]
        c7["general.logging.enabled / logLevel"]
        c8["functionality.positionVsParent → Port-2 registration"]
    end
    f1 --> c1
    f2 --> c2
    f3 --> c3
    f4 --> c4
    f5 --> c5
    f6 --> c6
    f7 --> c7
    f8 --> c8
Form field Config path Default Range / type Where used
Reaction Speed movement.speed (stateConfig) 1 > 0 (%/s) MovementManager — sets rate of position change
Asset model asset.model 'Unknown' string SupplierCurvePredictor — selects Kv curve dataset
Service type runtimeOptions.serviceType null (from asset) 'gas' / 'liquid' ValveHydraulicModel formula selection
Fluid density runtimeOptions.fluidDensity model default > 0 (kg/m³) liquid hydraulic formula
Fluid temperature runtimeOptions.fluidTemperatureK model default > 0 (K) gas hydraulic formula
Gas choke limit runtimeOptions.gasChokedRatioLimit per asset 01 gas choke cap in ValveHydraulicModel
Startup time stateConfig.time.starting 10 s > 0 (s) StateManager transition timer
Warmup time stateConfig.time.warmingup 5 s > 0 (s) StateManager protected transition
Shutdown time stateConfig.time.stopping 5 s > 0 (s) StateManager transition timer
Cooldown time stateConfig.time.coolingdown 10 s > 0 (s) StateManager transition timer
Mode mode.current 'auto' auto / virtualControl / fysicalControl / maintenance FlowController.isValidSourceForMode
Log level general.logging.logLevel 'info' enum structured logger
positionVsParent functionality.positionVsParent 'atEquipment' enum Port-2 registration message to parent

10. State chart

stateDiagram-v2
    [*] --> off
    off --> idle : cmd.startup (boot sequence)
    off --> emergencystop : cmd.estop
    off --> maintenance : set.mode=maintenance

    idle --> starting : cmd.startup
    idle --> off : (direct transition)
    idle --> emergencystop : cmd.estop
    idle --> maintenance : set.mode=maintenance

    starting --> warmingup : timed (starting duration)
    starting --> emergencystop : cmd.estop

    warmingup --> operational : timed (warmup duration) [protected — cannot abort]
    warmingup --> emergencystop : cmd.estop

    operational --> accelerating : set.position > current
    operational --> decelerating : set.position < current
    operational --> stopping : cmd.shutdown
    operational --> emergencystop : cmd.estop

    accelerating --> operational : setpoint reached
    accelerating --> emergencystop : cmd.estop

    decelerating --> operational : setpoint reached
    decelerating --> emergencystop : cmd.estop

    stopping --> coolingdown : timed (stopping duration)
    stopping --> idle : (direct)
    stopping --> emergencystop : cmd.estop

    coolingdown --> idle : timed (cooldown duration) [protected — cannot abort]
    coolingdown --> off : (direct)
    coolingdown --> emergencystop : cmd.estop

    emergencystop --> idle : cmd.reset / sequence
    emergencystop --> off : cmd.reset / sequence
    emergencystop --> maintenance : (allowed)

    maintenance --> idle : manual reset
    maintenance --> off : manual reset

Key valve-specific behaviours:

  • accelerating = position moving up; decelerating = position moving down. Both fire positionChange ticks. The valve's stateBindings hooks these to updatePosition() → Kv lookup → deltaP recompute.
  • warmingup and coolingdown are protected — the abort signal is disabled; these phases cannot be interrupted.
  • cmd.shutdown from operational first ramps the valve to position 0 (via FlowController.executeSequence('shutdown')), then transitions stopping → coolingdown → idle.
  • cmd.estop triggers emergencystop → off regardless of current state (except from within protected transitions).
  • Default sequences: startup = [starting, warmingup, operational]; boot = [idle, starting, warmingup, operational]; emergencystop = [emergencystop, off].

11. Examples

Tier File What it shows Mandatory?
Basic examples/basic.flow.json Inject set.position + minimal wiring, no parent
Integration examples/integration.flow.json valve + VGC + upstream measurement source
Edge examples/edge.flow.json Edge-case inputs (gas, choke, estop, bad setpoints) optional

Renamed example files (01-Basic.flow.json, 02-Integration.flow.json, 03-Dashboard.flow.json) will replace the above when produced. Screenshots under wiki/_partial-screenshots/valve/. Docker compose snippet under examples/README.md.

12. Debug recipes

Symptom First thing to check Where to look
Status badge shows ⚠ no input Did any pressure / flow measurement register? Watch Port 2. Debug tap on Port 2
delta_predicted_pressure stuck at 0 Is kv > 0? FSM may be in off / idle — valve is closed. state.getCurrentState(), percentageOpen
Gas mismatch warning on status badge fluidCompatibility.status is mismatch or conflict. getFluidCompatibility()
query.curve returns empty curve asset.model not found by loadModel; check SupplierCurvePredictor.snapshot(). SupplierCurvePredictor.snapshot()
deltaP non-finite Downstream gauge pressure absolute term ≤ 0, or choked ratio reached. hydraulicDiagnostics in output
set.position has no effect Check currentMode — source may not be in mode.allowedSources[mode]. FlowController.isValidSourceForMode
FSM stuck in accelerating / decelerating Movement was aborted but _returnToOperationalOnAbort was false. Send a new set.position. state.js abort logic

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 for a throttling actuator with a known Kv curve. For a fixed-restriction orifice (no actuator, no curve), model the deltaP externally.
  • Don't use valve to model a non-return / check valve — no position control or FSM-driven actuation is exposed.
  • Skip valve when an upstream source already provides flow directly and no pressure-drop estimate is needed — wire the source straight to the parent without inserting a valve.

14. Known limitations / current issues

# Issue Tracked in
1 Gas-choke detection is a hard cap, not a smooth transition — chart traces show a step at the choked-ratio limit. hydraulicModel.js
2 Multi-parent registration is allowed but not exercised in production tests. CONTRACT.md — Children registered by this node
3 set.position move sequences are interruptible but tests cover happy-path only; abort-deadlock edge case documented separately. state.js abort logic + test/integration/
4 execSequence (legacy umbrella topic) will be removed in Phase 7 — callers must migrate to cmd.startup / cmd.shutdown / cmd.estop. CONTRACT.md — execSequence demux