# valve
  
A `valve` models a single actuated throttling valve. It loads a supplier Kv-vs-position characteristic curve, drives a position FSM (`accelerating` / `decelerating` through `operational`), and recomputes pressure drop from flow + Kv via a hydraulic model that picks a liquid or gas formula by `serviceType`. Used standalone, or as a child of `valveGroupControl`, downstream of a `rotatingMachine` / `machineGroupControl` / `pumpingStation`.
> [!NOTE]
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
---
## At a glance
| Thing | Value |
|:---|:---|
| What it represents | One actuated throttling valve — supplier Kv curve, position FSM, deltaP estimate |
| S88 level | Equipment Module |
| Use it when | You need a position-controlled valve whose deltaP depends on flow, Kv(position), and service-type (gas / liquid) |
| Don't use it for | Fixed-restriction orifices, non-return / check valves, or curveless throttling devices (no fallback model) |
| Children it accepts | Upstream sources (`rotatingmachine`, `machinegroup` / `machinegroupcontrol`, `pumpingstation`, `valvegroupcontrol`) for fluid-contract tracking; `measurement` for pressure / flow |
| Parents it talks to | `valveGroupControl` (typical) or any node that issues `set.position` / `cmd.startup` / `cmd.shutdown` |
---
## How it fits
```mermaid
flowchart LR
parent[valveGroupControl]:::unit -->|set.position
cmd.startup / shutdown| v[valve
Equipment]:::equip
src["rotatingMachine /
MGC / pumpingStation"]:::unit -->|child.register
(fluid contract)| v
m_p[measurement
pressure]:::ctrl -.measured.-> v
m_f[measurement
flow]:::ctrl -.measured.-> v
v -->|child.register| parent
v -.->|evt.deltaPChange
evt.fluidCompatibilityChange
evt.fluidContractChange| parent
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,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 single valve through a position move.
```bash
curl -X POST -H 'Content-Type: application/json' \
--data @nodes/valve/examples/basic.flow.json \
http://localhost:1880/flow
```
> [!NOTE]
> The shipped `examples/{basic,integration,edge}.flow.json` files are minimal stubs (one inject → valve → debug). A tiered `01 - Basic Manual Control.json` / `02 - Integration with Valve Group.json` / `03 - Dashboard Visualization.json` set, matching the `rotatingMachine` template, is on the backlog. Until then, drive the node directly with injects.
What to send after deploy (the topics map one-to-one to entries in [Reference — Contracts](Reference-Contracts#topic-contract)):
1. `set.mode = virtualControl` — lets the GUI source drive the valve (parent path is for grouped use).
2. `cmd.startup` — FSM runs `idle → starting → warmingup → operational`.
3. `set.position = {setpoint: 60}` (position %) — valve ramps from 0 to 60; state goes `operational → accelerating → operational`. Each position tick fires a Kv lookup + deltaP recompute.
4. `data.flow = {variant: 'measured', value: 25, position: 'downstream', unit: 'm3/h'}` — push flow so the hydraulic model has something to chew on. `delta_predicted_pressure` updates and `evt.deltaPChange` fires upward.
5. `cmd.shutdown` — if currently `operational`, the controller first ramps to position 0, then transitions `stopping → coolingdown → idle`.
> [!IMPORTANT]
> **GIF needed.** Demo recording of steps 1–5 with the live status badge. Save as `wiki/_partial-gifs/valve/01-basic-demo.gif`, target ≤ 1 MB after `gifsicle -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. Source allow-list per mode (defaults from `valve.json`). |
| `cmd.startup` | — | `{ source?: string }` | Run the configured `startup` sequence (default `[starting, warmingup, operational]`). |
| `cmd.shutdown` | — | `{ source?: string }` | Run `shutdown`. If currently `operational`, first ramps the valve to position 0, then transitions `stopping → coolingdown → idle`. |
| `cmd.estop` | `emergencystop`, `emergencyStop` | `{ source?: string, action?: string }` | Trigger an emergency stop — runs the `emergencystop` sequence (default `[emergencystop, off]`). |
| `set.position` | `execMovement` | `{ source?: string, action?: string, setpoint: number }` | Move the valve to a position (control-%, `0..100`). Setpoint is coerced to `Number`. |
| `data.flow` | `updateFlow` | `{ variant, value, position, unit? }` — `variant ∈ {'measured','predicted'}` | Push a flow measurement; triggers a Kv lookup + deltaP recompute via the hydraulic model. |
| `query.curve` | `showcurve` | any | Reply on Port 0 with `{ topic: 'Showing curve', payload: }`. |
Plus the registration topic emitted upward at startup and accepted from real `measurement` children:
| Topic | Aliases | Payload |
|:---|:---|:---|
| `child.register` | `registerChild` | child Node-RED id (string); `msg.positionVsParent` carries the position label |
The legacy umbrella `execSequence` (`{action: 'startup' \| 'shutdown' \| 'emergencystop'}`) is still accepted — it forwards to the canonical `cmd.*` handler and logs a one-time deprecation warning. Scheduled for removal in Phase 7.
---
## What you'll see come out
Sample Port 0 message (delta-compressed, while operational at ~60 % open with a 25 m³/h flow):
```json
{
"topic": "valve#valve_a",
"payload": {
"state": "operational",
"percentageOpen": 60,
"moveTimeleft": 0,
"mode": "auto",
"downstream_measured_flow": 25,
"downstream_predicted_flow": 0,
"delta_predicted_pressure": 84
}
}
```
Key shape: **`__`** — the legacy three-segment shape. Position labels are lowercase (`downstream`, `delta`, `upstream`). `valve` does **not** use the four-segment `...` shape that `rotatingMachine` emits.
| Field | Meaning |
|:---|:---|
| `state` | Current FSM state. See [Architecture — FSM](Reference-Architecture#fsm). |
| `percentageOpen` | Current position (`0..100`). 0 = closed, 100 = fully open. |
| `moveTimeleft` | Seconds remaining on the current position move (0 when stationary). |
| `mode` | One of `auto` / `virtualControl` / `fysicalControl` / `maintenance`. |
| `delta_predicted_pressure` | Predicted deltaP across the valve (output unit `mbar`). |
| `downstream_predicted_flow` / `_measured_flow` | Last flow pushed via `data.flow` (output unit `m3/h`). |
| `downstream_measured_pressure` / `_predicted_pressure` | Pressure measurements pushed via the `MeasurementRouter`. |
---
## Need more?
| Page | What you'll find |
|:---|:---|
| [Reference — Contracts](Reference-Contracts) | Full topic contract, config schema, child registration filters |
| [Reference — Architecture](Reference-Architecture) | Code map, FSM, hydraulic model, lifecycle |
| [Reference — Examples](Reference-Examples) | Shipped example flows + debug recipes |
| [Reference — Limitations](Reference-Limitations) | When not to use, known limitations, open questions |
[EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) · [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) · [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)