--- title: pumpingStation — Functional Description node: pumpingStation updated: 2026-04-22 status: draft --- # pumpingStation — Functional Description The `pumpingStation` node models an S88 **Process Cell**: a wet-well basin with inflow and outflow, wrapped around one or more pump controllers. Every second it recomputes the basin's water balance, picks the most trustworthy net-flow source, runs its safety interlocks, and finally commands its children (individual pumps, `machineGroupControl`, or nested pumping stations) so the level stays inside the safe operating band. This page is the operator-facing reference, derived from [`src/specificClass.js`](../src/specificClass.js). For the 3-tier code layout see [EVOLV — Node Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/wiki/architecture/node-architecture.md); for the atomic pump model see the [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki). > **Diagrams on this page are editable.** Sources live in [`diagrams/`](diagrams/) — open the `.drawio` file in [draw.io](https://app.diagrams.net/), export to SVG, commit. See [`diagrams/README.md`](diagrams/README.md). ## At a glance | Item | Value | |---|---| | Node category | EVOLV | | S88 level | Process Cell (`#0c99d9`, lane L5) | | Inputs | 1 (message-driven) | | Outputs | 3 — `process` / `dbase` / `parent` | | Tick period | 1 s | | Basin model | Rectangular prismatic — `volume = level × surfaceArea` | | Canonical units (internal) | Pa, m³/s, W, K, m, m³ | | Control modes implemented | `levelbased`, `manual` (placeholders for `flowbased`, `pressureBased`, `percentageBased`, `powerBased`, `hybrid`) | | Default flow dead-band | `1e-4 m³/s` (≈ 0.36 m³/h) | ## Lifecycle 1. **Construct.** The node merges the user's editor config over the schema defaults, creates the measurement store, and seeds the predicted volume at the basin's operational floor (`minVol`). 2. **Register children.** Sensors, pumps, machine groups, and nested stations register via the Port-2 handshake. The station subscribes only to the *highest-level aggregator* for predicted flow to avoid double-counting (MGC if present, otherwise the individual pump). 3. **Tick loop (1 s).** `_updatePredictedVolume → _selectBestNetFlow → _safetyController → _controlLogic → state snapshot → output`. ## Editor configuration Every field on the pumpingStation editor maps directly to the config schema in `generalFunctions/src/configs/pumpingStation.json`. ### Basin geometry (section `basin`) | Field | Default | Meaning | |---|---|---| | **Basin Volume (m³)** | `1` | Total geometric volume of the empty basin (floor to rim). | | **Basin Height (m)** | `1` | Physical wall height from floor to rim. | | **Inlet Elevation (m)** | `2` | Centre of the inlet pipe, measured from the floor. | | **Outlet Elevation (m)** | `0.2` | Centre of the pump-suction pipe, measured from the floor. | | **Overflow Level (m)** | `2.5` | Overflow-weir crest, measured from the floor. Above this → overfill safety. | Constant cross-section is assumed: `surfaceArea = volume / height`. All derived volumes (`minVolOut`, `minVolIn`, `maxVolOverflow`, `maxVol`) are computed once in `initBasinProperties()` and kept on `station.basin`. ### Hydraulics (section `hydraulics`) | Field | Default | Meaning | |---|---|---| | **Minimum Height Based On** | `outlet` | `outlet` → `minVol = heightOutlet × area` (includes the buffer). `inlet` → `minVol = heightInlet × area` (buffer treated as unavailable). | | **Reference Height** | `NAP` | Vertical datum: `NAP` / `EVRF` / `EGM2008`. Metadata only — not used in math today. | | **Basin Bottom (m Refheight)** | `0` | Absolute elevation of the basin floor, for cross-basin comparisons. | ### Control (section `control`) | Field | Default | Meaning | |---|---|---| | **Control mode** | `levelbased` | Active control strategy. Schema enumerates seven modes; today `levelbased` is fully implemented, `manual` forwards demand via `Qd`, others are placeholders. | | **startLevel (m)** | `1` | At or below this level, the station is in the DEAD ZONE — pumps stay in their last state. | | **stopLevel (m)** | `1` | Below this level → unconditional MGC shutdown. | | **Min flow (m)** | `1` | Bottom of the linear scaling range (0 % demand). Should equal `startLevel`. | | **Max flow (m)** | `4` | Top of the linear scaling range (100 % demand). Typically ≈ `heightOverflow`. | | **Flow setpoint** | `0` | Flow-based target (m³/h). Placeholder until `flowbased` is wired. | | **Deadband** | `0` | Flow-based deadband (m³/h). Placeholder. | ### Safety (section `safety`) | Field | Default | Meaning | |---|---|---| | **Time To Empty/Full (s)** | `0` | If > 0, triggers safety when predicted time-to-overflow or time-to-empty falls below this value. `0` disables time-based protection. | | **Enable Dry-Run Protection** | `true` | If on, pumps are shut down once volume drops below the dry-run threshold while draining. | | **Low Volume Threshold (%)** | `2` | Dry-run trigger: `triggerLowVol = minVol × (1 + pct/100)`. | | **Enable Overfill Protection** | `true` | If on, upstream inflows are shut down once volume climbs above the overfill threshold while filling. | | **High Volume Threshold (%)** | `98` | Overfill trigger: `triggerHighVol = maxVolOverflow × pct/100`. | ### Output formats - **Process Output** — format for Port 0 (`process` / `json` / `csv`). - **Database Output** — format for Port 1 (`influxdb` / `json` / `csv`). > **Tip — always configure every field.** The pumpingStation mixes geometry and control thresholds freely. Leaving `heightOverflow` at the schema default of 2.5 m while sizing the basin for 10 m walls produces nonsensical fill-percentages and spurious safety events. See the [EVOLV flow-layout rules §9](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/.claude/rules/node-red-flow-layout.md) for the completeness rule. ## Input topics All commands enter on the single input port. `msg.topic` selects the handler; `msg.payload` carries the argument. ### `changemode` ```json { "topic": "changemode", "payload": "manual" } ``` Switches the active control strategy. The new mode must be in `config.control.allowedModes` — unknown values are rejected with a warning. Typical transitions: `levelbased ⇄ manual` for operator override during maintenance. ### `calibratePredictedVolume` ```json { "topic": "calibratePredictedVolume", "payload": 3.4 } ``` Hard-reset the predicted volume time-series to the supplied value (m³). Also rewrites the predicted level (derived from the constant-area geometry) and resets the internal flow-integrator state. Use this when a trustworthy measured level becomes available. ### `calibratePredictedLevel` ```json { "topic": "calibratePredictedLevel", "payload": 1.8 } ``` Same as above, but caller supplies a level (m). The predicted volume is recomputed via `volume = level × surfaceArea`. ### `q_in` ```json { "topic": "q_in", "payload": 300, "unit": "l/s" } ``` Inject a **manual inflow** into the basin. Registered as a predicted flow under the synthetic child `manual-qin` at position `in`. Useful when no physical inflow sensor is wired but the inflow is known externally (e.g. fed from a sewer model). ### `Qd` ```json { "topic": "Qd", "payload": 75 } ``` Forward a manual demand to every child aggregator (MGC first, then any direct pumps). **Only honoured when `config.control.mode === 'manual'`** — in any other mode the command is logged and discarded. Mirrors how `rotatingMachine` gates commands behind its mode field. The interpretation of the number depends on the child's scaling (`absolute` = m³/h, `normalized` = 0–100 %). ### `registerChild` Internal. Child nodes (measurements, rotatingMachines, machineGroupControls, nested pumpingStations) emit this on their Port 2 a few hundred ms after deploy. The station resolves the Node-RED node id back to the source object and registers it via `childRegistrationUtils`. ## Output ports ### Port 0 — process data Delta-compressed payload (only changed fields per tick). Keys follow the standard 4-segment format `...` plus a handful of top-level state fields merged in by `getOutput()`: | Key | Meaning | |---|---| | `volume.predicted.atequipment.default` | Running predicted volume from the flow integrator (m³). | | `volume.measured.atequipment.default` | Volume derived from a `measured` level sensor (m³). | | `level.predicted.atequipment.default` | Predicted level = `volume / area` (m). | | `level.measured..` | Raw level sensor reading (m). | | `volumePercent.predicted.atequipment.default` | `(vol - minVol) / (maxVolOverflow - minVol) × 100` (%). | | `flow.predicted.in.` | Inflow contribution from a registered child (m³/s internally; editor unit on output). | | `flow.predicted.out.` | Outflow contribution from a registered child. | | `flow.measured..` | Flow sensor reading. | | `netFlowRate..atequipment.default` | Net flow used for control (inflow − outflow). | | `direction` | `filling` / `draining` / `steady` / `unknown`. | | `flowSource` | Which variant drove the current control cycle (`measured`, `predicted`, `level:predicted`, `null`). | | `timeleft` | Predicted seconds to overflow (while filling) or to dry-run (while draining). | | `volEmptyBasin`, `heightInlet`, `heightOverflow`, `maxVol`, `maxVolOverflow`, `minVol`, `minVolIn`, `minVolOut`, `minHeightBasedOn` | Echoes of the basin geometry for dashboards. | | `percControl` | Last demand (0–100+ %) forwarded to the machine group during level-based control. | Consumers must cache and merge deltas — the example dashboard flows include a reusable function node that does exactly this. ### Port 1 — dbase (InfluxDB) Line-protocol payload for the `telemetry` bucket. Tags stay low-cardinality (station name, asset type); fields carry the numeric state. See [EVOLV — InfluxDB Schema Design](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/wiki/concepts/influxdb-schema-design.md). ### Port 2 — parent `{ topic: "registerChild", payload: , positionVsParent, distance }` — fired once ~100 ms after deploy so an upstream cascade can discover this station. Nested stations use this to register with an outer `pumpingStation` parent. ## Basin model The basin is modelled as a rectangular prism with constant cross-section. Everything derives from `volume = level × surfaceArea`. ![Basin model — physical layout with control thresholds](diagrams/basin-model.drawio.svg) *Editable source: [`diagrams/basin-model.drawio`](diagrams/basin-model.drawio). See [`diagrams/README.md`](diagrams/README.md) for the edit-and-export workflow.* **Typical ordering** (bottom → top): `stopLevel < heightInlet < startLevel = minFlowLevel < maxFlowLevel ≤ heightOverflow`. > ⚠️ The comment block in `specificClass.js` currently says `startLevel ≤ heightInlet` (inlet above startLevel). The physical convention is the opposite: pumps start *before* the water reaches the gravity inlet, so `heightInlet < startLevel`. Worth fixing in the code comment next time that file is touched. **minHeightBasedOn** — which pipe defines `minVol`, the operational floor used for the initial seed, the dry-run trigger, and the 0 % point of the fill percentage: ``` outlet (default): inlet: ● maxVolOverflow ● maxVolOverflow │ │ ● heightInlet ● heightInlet ─── minVol │ │ ● heightOutlet ──── minVol ● heightOutlet │ │ ● floor ● floor Buffer counts as usable stock. Buffer reserved; 0% fill starts at the inlet. ``` ## Net-flow selection Every tick, `_selectBestNetFlow()` walks a priority ladder and returns the first net flow that clears the dead-band (`|flow| ≥ flowThreshold`): ``` priority source note 1 ────● measured.flow real sensors on inflow/outflow │ 2 ────● predicted.flow manual q_in + pump-curve outputs │ 3 ────● level:measured dL/dt × surfaceArea │ 4 ────● level:predicted dL/dt of the integrator │ 5 ────● steady (fallback) warn, return { value: 0, source: null } ``` Both **measured** and **predicted** variants are always computed and stored, regardless of which one drives control. The active source surfaces on Port 0 as `flowSource`, so operators can watch sensor drift (measured diverges from predicted), validate the volume integrator, and diagnose "which source was active when X happened?". The inflow / outflow alias map is deliberately wide so measurements (`upstream`/`downstream`) and predicted-flow subscriptions (`in`/`out`) both feed the same aggregator: ```js flowPositions = { inflow: ['in', 'upstream'], outflow: ['out', 'downstream'] } ``` ## Control logic The `pumpingStation` supports multiple control modes. Each mode is a **policy that sets the three control thresholds (`minLevel`, `startLevel`, `maxLevel`) and produces a demand (0 – 100 %)** — the two safety thresholds (`dryRunLevel`, `overflowLevel`) are mode-independent and handled by the safety layer below. Every mode gets its own page under [`modes/`](modes/README.md) with a consistent layout (inputs, threshold policy, demand formula, edge cases) so they can be compared side-by-side. Currently: | Mode | Status | Page | |---|---|---| | `levelbased` | ✅ implemented | [modes/levelbased.md](modes/levelbased.md) | | `manual` | ✅ implemented (via `Qd` topic) | — | | `flowbased`, `pressureBased`, `percentageBased`, `powerBased`, `hybrid` | 🚧 placeholder in code | — | See [`modes/README.md`](modes/README.md) for the index and page template. ## Safety controller `_safetyController` runs **before** `_controlLogic` every tick. Two rules, deliberately asymmetric — *dry-run protects the pumps from running themselves into air*, *overfill protects the basin from spilling*. ![Safety rules — dry-run vs overfill](diagrams/safety-rules.drawio.svg) During overfill, level-based control naturally commands ≥100 % on the downstream MGC because the level is above `maxFlowLevel`. > ⚠️ **Known limitation — gravity-sewer context.** The "upstream STOP" action only makes sense in a **cascaded** station layout where the upstream equipment is an EVOLV-controllable pump or station. In a conventional wastewater wet-well the inflow is gravity-fed from the municipal sewer and **cannot be stopped** — attempting to would back up toilets. For that case the correct response to an overfill event is to **measure and log the spill over the weir** (for compliance reporting) and raise an alarm, while keeping downstream pumps at maximum demand. The current code fires `execSequence: shutdown` on upstream children regardless of what they are; that should be gated on "is the upstream actually controllable?" and supplemented with overflow-rate tracking. Tracked as follow-up work. A missing volume reading is treated as a hard fault: every direct machine is sent `execSequence: shutdown` and `safetyControllerActive` latches. Calibrate predicted volume (`calibratePredictedVolume`) or wire a level measurement to recover. ## Registration — which children count as flow? `_registerPredictedFlowChild` subscribes only to the *highest-level aggregator* to prevent double-counting. ``` Without MGC: With MGC: [ PumpingStation ] [ PumpingStation ] │ │ │ │ │ │ │ [ MGC ] │ │ │ │ │ │ ● ● ● ● ● ● (each pump subscribed (only MGC is subscribed; directly) MGC aggregates its pumps) N flow subscriptions. 1 flow subscription. Risk: double-count if an Pumps' flow is already MGC is added later. inside the MGC total. ``` Measurement children register separately via `_registerMeasurementChild` and feed the `measured` variant — they never collide with the predicted-flow subscription. Nested `pumpingStation` children are always subscribed and expose their net flow at the parent's position. ## Node status badge Updated every second by `_updateNodeStatus` in `nodeClass.js`: ``` ⬆️ 42.3% | V=4.57 / 10.80 m³ | net: 180 m³/h | t≈12 min ``` | Symbol | Direction | Badge colour | |---|---|---| | ⬆️ | `filling` | blue | | ⬇️ | `draining` | orange | | ⏸️ | `steady` | green | | ❔ | `unknown` / missing measurements | grey | ## Example flow The canonical end-to-end demo lives in the EVOLV superproject at [`examples/pumpingstation-3pumps-dashboard/`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/examples/pumpingstation-3pumps-dashboard). It wires three `rotatingMachine` pumps beneath an MGC beneath a `pumpingStation`, with the dashboard layout rule set (see the [EVOLV flow-layout rules](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/.claude/rules/node-red-flow-layout.md)) — a useful template for any new station. ## Troubleshooting | Symptom | Likely cause | Fix | |---|---|---| | `fill %` exceeds 100 % or is negative | Basin geometry inconsistent — e.g. `heightOverflow > heightBasin`, or `heightOutlet > heightInlet`. | Cross-check `0 < heightOutlet < heightInlet < heightOverflow ≤ heightBasin` in the editor. | | Pumps never start in `levelbased` | Level is stuck in the DEAD ZONE between `stopLevel` and `startLevel`, or `minFlowLevel == maxFlowLevel` so scaling collapses. | Widen the control band: move `startLevel` above `stopLevel` and set `maxFlowLevel ≈ heightOverflow`. | | "No volume data available to safe guard system; shutting down all machines." in logs | No measured level, predicted volume not calibrated, and no inflow/outflow samples yet. | Issue `calibratePredictedVolume` (or `calibratePredictedLevel`) once at startup, or wire a level sensor. | | `flowSource: null` and `direction: 'steady'` forever | Every flow / level signal falls inside the dead-band (default `1e-4 m³/s`). | Confirm flows are non-zero, or lower `config.general.flowThreshold` for a small-scale demo. | | `Qd` ignored | Station is not in `manual` mode. | Send `{ topic: 'changemode', payload: 'manual' }` first, or fall back to level-based control. | | Pumps keep running during overfill | Intended — overfill safety only stops **upstream** equipment; downstream pumps must drain. | To override, switch to `manual` and set `Qd = 0`, or issue an emergency-stop at the MGC. | | Predicted volume drifts away from measured | Flow integrator has no reference — flows might have the wrong sign, or `unit` is mis-declared. | Call `calibratePredictedVolume` periodically from a measured level. | ## Running it locally ```bash git clone --recurse-submodules https://gitea.wbd-rd.nl/RnD/EVOLV.git cd EVOLV docker compose up -d # Node-RED: http://localhost:1880 InfluxDB: :8086 Grafana: :3000 ``` Then in Node-RED: **Import ▸ Examples ▸ EVOLV ▸ pumpingStation** (or open `examples/pumpingstation-3pumps-dashboard/flow.json`). ## Testing ```bash cd nodes/pumpingStation npm test ``` Unit tests live in `test/specificClass.test.js` — construction, basin derivation, measurement registration, net-flow selection, safety interlocks, and calibration. ## Related - [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki) — atomic pump model beneath pumpingStation / MGC. - [measurement wiki](https://gitea.wbd-rd.nl/RnD/measurement/wiki) — sensor conditioning for inflow, outflow, level, and pressure inputs. - [machineGroupControl wiki](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki) — how MGC coordinates multiple pumps. - [EVOLV — Node Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/wiki/architecture/node-architecture.md) — the entry → nodeClass → specificClass pattern. - [EVOLV — Group Optimization](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/wiki/architecture/group-optimization.md) — pump-group scheduling theory. - [EVOLV — flow-layout rules](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/.claude/rules/node-red-flow-layout.md) — the lane / group / channel layout rules used by the demo flows.