23 KiB
title, node, updated, status
| title | node | updated | status |
|---|---|---|---|
| pumpingStation — Functional Description | pumpingStation | 2026-04-22 | 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. For the 3-tier code layout see EVOLV — Node Architecture; for the atomic pump model see the rotatingMachine wiki.
Diagrams on this page are editable. Sources live in
diagrams/— open the.drawiofile in draw.io, export to SVG, commit. Seediagrams/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
- 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). - 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).
- 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 storage volume from basin floor to rim. |
| Basin Height (m) | 1 |
Physical wall height from floor to rim. |
| Inlet Elevation (m) | 2 |
Bottom/invert of the incoming sewer pipe, measured from the basin floor. This is the level where backing up into the inlet starts to matter hydraulically. |
| Outlet Elevation (m) | 0.2 |
Top of the pump-suction/outlet pipe, measured from the basin floor. This is the practical lower hydraulic reference for pump protection. |
| Inlet Pipe Diameter (m) | 0.4 |
Nominal incoming sewer pipe diameter. Used with inflowLevel to distinguish pipe bottom, centre, and crown in future hydraulic upgrades. |
| Outlet Pipe Diameter (m) | 0.4 |
Nominal pump-suction/outlet pipe diameter. Used with outflowLevel to distinguish pipe top, centre, and invert in future hydraulic upgrades. |
| Overflow Level (m) | 2.5 |
Physical overflow-weir crest, measured from the floor. At or above this level the basin is actually spilling. |
Constant cross-section is assumed: surfaceArea = volume / height. All derived volumes (minVolAtOutflow, minVolAtInflow, maxVolAtOverflow, maxVol) are computed once in initBasinProperties() and kept on station.basin.
The current runtime still uses the level fields directly for its volume math. Pipe diameters are part of the basin model contract so later hydraulic logic can reason about pipe invert/crown and not silently treat every pipe elevation as a centreline.
Hydraulics (section hydraulics)
| Field | Default | Meaning |
|---|---|---|
| Minimum Height Based On | outlet |
outlet → minVol = outflowLevel × area (includes the buffer). inlet → minVol = inflowLevel × 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. |
| minLevel (m) | 1 |
Below this level → unconditional MGC shutdown. |
| startLevel (m) | 1 |
Mode-specific threshold. In levelbased, this is the bottom of the linear scaling range (0 % demand). It is not part of the generic basin model because other modes can define a different start policy. |
| maxLevel (m) | 4 |
Upper normal operating/storage level used by the active mode. In levelbased, this is where demand reaches 100 %. |
| 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 |
Safety margin above the configured minimum volume: dryRunSafetyVol = minVol × (1 + pct/100). This creates dryRunLevel; it is derived, not a separately entered basin height. |
| Enable Overfill Protection | true |
If on, upstream inflows are shut down once volume climbs above the high-volume safety point while filling. |
| High Volume Threshold (%) | 98 |
Safety margin below physical overflow: highVolumeSafetyVol = maxVolAtOverflow × pct/100. Actual overflowing is still the boolean condition level >= overflowLevel. |
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
overflowLevelat 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 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
{ "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
{ "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
{ "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
{ "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
{ "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 <type>.<variant>.<position>.<childId> 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.<position>.<childId> |
Raw level sensor reading (m). |
volumePercent.predicted.atequipment.default |
(vol - minVol) / (maxVolAtOverflow - minVol) × 100 (%). |
flow.predicted.in.<childId> |
Inflow contribution from a registered child (m³/s internally; editor unit on output). |
flow.predicted.out.<childId> |
Outflow contribution from a registered child. |
flow.measured.<position>.<childId> |
Flow sensor reading. |
netFlowRate.<variant>.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, inflowLevel, overflowLevel, maxVol, maxVolAtOverflow, minVol, minVolAtInflow, minVolAtOutflow, 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.
Port 2 — parent
{ topic: "registerChild", payload: <this-node-id>, 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, with every level measured upward from the basin floor.
Editable source: diagrams/basin-model.drawio.svg (drag into draw.io; the SVG embeds the editable source). See diagrams/README.md for the edit-and-export workflow.
Generic basin ordering (bottom → top): outflowLevel ≤ dryRunLevel ≤ minLevel < inflowLevel < maxLevel ≤ highVolumeSafetyLevel < overflowLevel ≤ basinHeight.
startLevel is deliberately not part of this generic basin diagram. It belongs to a control mode. For the current level-linear mode, see diagrams/modes/basin-mode-level-linear.drawio.svg.
The pipe labels are intentional:
inflowLevelis the bottom/invert of the incoming sewer pipe.outflowLevelis the top of the pump-suction/outlet pipe.
This avoids hiding hydraulic consequences behind ambiguous pipe-centre elevations. Pipe diameters are part of the model contract so later versions can derive pipe centre/crown/invert where needed.
dryRunLevel and highVolumeSafetyLevel are derived safety points. They provide margin before the two hard physical conditions:
- Actual dry-run risk is at or below the pumpable lower hydraulic reference.
- Actual overflowing is the boolean condition
level >= overflowLevel.
The high-volume safety point exists so the station can still react before the basin is physically spilling. Once overflowLevel is reached, the model should report overflowing rather than treating that point as a controllable threshold.
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:
● maxVolAtOverflow ● maxVolAtOverflow
│ │
● inflowLevel ● inflowLevel ─── minVol
│ │
● outflowLevel ──── minVol ● outflowLevel
│ │
● floor ● floor
Buffer counts as usable stock. Buffer reserved; 0% fill
starts at the inlet.
The rectangular approximation is acceptable for this node's first basin model because operational level is always in metres from the basin floor, while calculated m³ can tolerate small shape errors. A later upgrade can replace volume = level × surfaceArea with a level-volume curve for benching, sumps, sediment/dead zones, and irregular wet-well geometry.
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:
flowPositions = { inflow: ['in', 'upstream'], outflow: ['out', 'downstream'] }
Control logic
The pumpingStation supports multiple control modes. Each mode is a policy that maps basin state to demand (0-100 %). levelbased uses minLevel, startLevel, and maxLevel; other modes may use different thresholds or compute them dynamically.
The basin model owns the shared physical and safety references: pipe elevations, dryRunLevel, highVolumeSafetyLevel, and overflowLevel. startLevel is mode-specific and is documented with the mode diagrams, not the generic basin drawing.
Every mode gets its own page under modes/ 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 |
manual |
✅ implemented (via Qd topic) |
— |
flowbased, pressureBased, percentageBased, powerBased, hybrid |
🚧 placeholder in code | — |
See 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, high-volume protection tries to preserve distance to actual overflow.
During high-volume or overflow conditions, level-based control naturally commands >=100 % on the downstream MGC because the level is above maxLevel.
⚠️ 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 at the high-volume safety point is to alarm early and keep downstream pumps at maximum demand. If
level >= overflowLevel, the station should report actual overflowing as a boolean and, later, estimate/log spill over the weir for compliance reporting. The current code firesexecSequence: shutdownon 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/. It wires three rotatingMachine pumps beneath an MGC beneath a pumpingStation, with the dashboard layout rule set (see the EVOLV flow-layout rules) — a useful template for any new station.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
fill % exceeds 100 % or is negative |
Basin geometry inconsistent — e.g. overflowLevel > basinHeight, or outflowLevel > inflowLevel. |
Cross-check 0 < outflowLevel < inflowLevel < overflowLevel <= basinHeight in the editor. |
Pumps never start in levelbased |
Level is stuck in the DEAD ZONE between minLevel and startLevel, or startLevel == maxLevel so the scaling range collapses. |
Widen the mode control band. In sewer-gravity cases, startLevel is normally below inflowLevel so the station starts draining before the incoming sewer pipe is hydraulically affected. |
| "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
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
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 — atomic pump model beneath pumpingStation / MGC.
- measurement wiki — sensor conditioning for inflow, outflow, level, and pressure inputs.
- machineGroupControl wiki — how MGC coordinates multiple pumps.
- EVOLV — Node Architecture — the entry → nodeClass → specificClass pattern.
- EVOLV — Group Optimization — pump-group scheduling theory.
- EVOLV — flow-layout rules — the lane / group / channel layout rules used by the demo flows.