# pumpingStation > **Reflects code as of `d2384b1` · regenerated `` 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 **pumpingStation** is an S88 Process Cell that owns a wet-well basin and orchestrates the pumps that drain it. It tracks measured + predicted volume, evaluates safety interlocks (dry-run, overfill), and dispatches a control strategy that hands a demand setpoint to one or more downstream machine groups or individual pumps. ## 2. Position in the platform ```mermaid flowchart LR ps[pumpingStation
Process Cell]:::pc meas_lvl[measurement
type=level
position=atequipment]:::ctrl meas_in[measurement
type=flow
position=upstream]:::ctrl mgc[machineGroupControl
Unit]:::unit meas_lvl -.data.-> ps meas_in -.data.-> ps ps -->|set.demand| mgc mgc -.evt.flow-predicted.-> ps mgc -->|child.register| ps classDef pc fill:#0c99d9,color:#fff classDef unit fill:#50a8d9,color:#000 classDef ctrl fill:#a9daee,color:#000 ``` S88 colours: Process Cell `#0c99d9`, Unit `#50a8d9`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md`. ## 3. Capability matrix | Capability | Status | Notes | |---|---|---| | Predicts basin volume from net flow | ✅ | Integrator seeded from `basin.minVol`; recomputes level. | | Accepts measured level / volume / pressure | ✅ | Routed via `measurementRouter` on child registration. | | Level-based control strategy | ✅ | Linear or log ramp between `minLevel` and `maxLevel`. | | Flow-based control strategy | ✅ | PID against `flowSetpoint`. | | Manual demand passthrough | ✅ | `set.demand` only honoured in `manual` mode. | | Dry-run safety interlock | ✅ | Stops downstream pumps when volume < `minVol` while draining. | | Overfill safety interlock | ✅ | Stops upstream equipment when volume crosses overfill threshold. | | Cascaded children (sub-stations) | ⚠️ | Accepted via `pumpingstation` softwareType but not exercised in production. | ## 4. Code map ```mermaid flowchart TB subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"] nc["buildDomainConfig()
static DomainClass, commands
static tickInterval = 1000ms"] end subgraph domain["specificClass.js — orchestrator (BaseDomain)"] sc["PumpingStation.configure()
declares ChildRouter rules
tick() → safety → control"] end subgraph concerns["src/ concern modules"] basin["basin/
BasinGeometry + thresholdValidator"] measurement["measurement/
flowAggregator + router + calibration"] control["control/
levelbased / flowbased / manual"] safety["safety/
SafetyController"] commands["commands/
topic registry + handlers"] end nc --> sc sc --> basin sc --> measurement sc --> control sc --> safety nc --> commands ``` | Module | Owns | Read first if you're changing… | |---|---|---| | `basin/` | Geometry, volume↔level conversion, threshold ordering | Capacity, level↔volume math, fill %. | | `measurement/` | Net-flow aggregation, predicted-volume integrator, calibration | Predicted volume / time-to-full. | | `control/` | Control strategy dispatch (`levelbased`, `flowbased`, `manual`) | Demand calculation, mode behaviour. | | `safety/` | Dry-run + overfill rules, pump-shutdown side-effects | Safety envelope, alarm reactions. | | `commands/` | Input-topic registry and handlers | New input topics, payload validation. | ## 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 | Effect | |---|---|---|---| | `set.mode` | `changemode` | `string` | Replaces the named state value with the supplied payload. | | `child.register` | `registerChild` | `string` | Parent/child plumbing — registers or unregisters a child node. | | `cmd.calibrate.volume` | `calibratePredictedVolume` | `any` | Triggers an action / sequence — not idempotent. | | `cmd.calibrate.level` | `calibratePredictedLevel` | `any` | Triggers an action / sequence — not idempotent. | | `set.inflow` | `q_in` | `any` | Replaces the named state value with the supplied payload. | | `set.demand` | `Qd` | `any` | Replaces the named state value with the supplied payload. | ## 6. Child registration Mirrors the `ChildRouter` declarations in `specificClass.js → configure()`. ```mermaid flowchart LR subgraph kids["accepted children (softwareType)"] m["measurement"]:::ctrl mach["machine
(rotatingMachine)"]:::equip mgc["machinegroup"]:::unit sub["pumpingstation
(sub-station)"]:::pc end m -->|"<type>.measured.<position>"| route1[_subscribeMeasurement
routes to measurementRouter] mach -->|flow.predicted.<in or out>| route2[_subscribePredictedFlow
+ flowAggregator] mgc -->|flow.predicted.<in or out>| route2 sub -->|flow.predicted.<in or out>| route2 route1 --> tick[tick] route2 --> tick classDef ctrl fill:#a9daee,color:#000 classDef equip fill:#86bbdd,color:#000 classDef unit fill:#50a8d9,color:#000 classDef pc fill:#0c99d9,color:#fff ``` | softwareType | onRegister side-effect | Subscribed events | |---|---|---| | `measurement` | `_subscribeMeasurement(child)` — registers in MeasurementContainer. | `.measured.` for any type (pressure, level, flow, …). | | `machine` | Stored in `this.machines[id]`. **Skipped when a machineGroup parent is present** to avoid double-counting. | `flow.predicted.` per the child's `positionVsParent`. | | `machinegroup` | Stored in `this.machineGroups[id]`. | `flow.predicted.`. | | `pumpingstation` | Stored in `this.stations[id]`. | `flow.predicted.`. | ## 7. Lifecycle — what one tick does ```mermaid sequenceDiagram participant child as measurement / pump child participant ps as pumpingStation participant fa as flowAggregator participant sf as safetyController participant ctl as control strategy participant out as Port-0 output child->>ps: data event (measured.level / flow.predicted.out) ps->>ps: ChildRouter dispatches to handler Note over ps: every 1000 ms (static tickInterval) ps->>fa: tick() — net flow, ETA, predicted volume ps->>sf: evaluate({direction, secondsRemaining}) alt safety blocked sf-->>ps: blocked=true, reason Note over ctl: skipped this tick else safety clear ps->>ctl: dispatch(mode, ctx, controlState) ctl-->>ps: percControl updated end ps->>ps: notifyOutputChanged() ps->>out: msg{topic, payload (delta-compressed)} ``` ## 8. Data model — `getOutput()` What lands on Port 0. Built in `getOutput()`, then delta-compressed by `outputUtils.formatMsg`. | Key | Type | Unit | Sample | |---|---|---|---| | `direction` | string | — | `"steady"` | | `flowSource` | null | — | `null` | | `heightBasin` | number | m | `1` | | `inflowLevel` | number | m | `2` | | `maxVol` | number | m3 | `1` | | `maxVolAtOverflow` | number | m3 | `2.5` | | `minHeightBasedOn` | string | — | `"outlet"` | | `minVol` | number | m3 | `0.2` | | `minVolAtInflow` | number | m3 | `2` | | `minVolAtOutflow` | number | m3 | `0.2` | | `outflowLevel` | number | m | `0.2` | | `overflowLevel` | number | m | `2.5` | | `percControl` | number | % | `0` | | `surfaceArea` | number | m2 | `1` | | `timeleft` | null | s | `null` | | `volEmptyBasin` | number | m3 | `1` | | `volume.predicted.atequipment.wikigen-pumpingstation-id` | number | m3 | `0.2` | The `` segment of the MeasurementContainer key is the Node-RED node id assigned at deploy time; auto-gen substitutes a placeholder stub. ## 9. Configuration — editor form ↔ config keys ```mermaid flowchart TB subgraph editor["Node-RED editor form"] f1[Basin: volume / height] f2[Levels: inflow / outflow / overflow] f3[Control mode] f4[Level setpoints: min / start / max] f5[Safety: dry-run % / overfill %] end subgraph config["Domain config slice"] c1[basin.volume
basin.height] c2[basin.inflowLevel
basin.outflowLevel
basin.overflowLevel] c3[control.mode] c4[control.levelbased.minLevel
control.levelbased.startLevel
control.levelbased.maxLevel] c5[safety.dryRunThresholdPercent
safety.overfillThresholdPercent] end f1 --> c1 f2 --> c2 f3 --> c3 f4 --> c4 f5 --> c5 ``` | Form field | Config key | Default | Range | Where used | |---|---|---|---|---| | `basinVolume` | `basin.volume` | `1` | > 0 (m³) | `BasinGeometry` | | `basinHeight` | `basin.height` | `1` | > 0 (m) | `BasinGeometry` | | `inflowLevel` | `basin.inflowLevel` | `2` | ≥ 0 (m) | threshold validator, control | | `outflowLevel` | `basin.outflowLevel` | `0.2` | ≥ 0 (m) | dead-volume floor | | `overflowLevel` | `basin.overflowLevel` | `2.5` | > 0 (m) | overfill safety | | `controlMode` | `control.mode` | `levelbased` | enum | `control/dispatch` | | `minLevel` | `control.levelbased.minLevel` | `1` | ≥ 0 (m) | `levelBased.run` | | `startLevel` | `control.levelbased.startLevel` | `1` | ≥ minLevel | ramp foot | | `maxLevel` | `control.levelbased.maxLevel` | `4` | ≤ overflowLevel | ramp top | | `enableDryRunProtection` | `safety.enableDryRunProtection` | `true` | bool | `SafetyController` | | `dryRunThresholdPercent` | `safety.dryRunThresholdPercent` | `2` | 0–100 % | dry-run trip | | `enableOverfillProtection` | `safety.enableOverfillProtection` | `true` | bool | overfill safety | | `overfillThresholdPercent` | `safety.overfillThresholdPercent` | `98` | 0–100 % | overfill trip | ## 10. State chart Two orthogonal state vectors: **control mode** (operator-driven) and **safety state** (data-driven). The diagram shows them together — most transitions are independent. ```mermaid stateDiagram-v2 state ControlMode { [*] --> none none --> levelbased: set.mode levelbased --> flowbased: set.mode flowbased --> manual: set.mode manual --> levelbased: set.mode levelbased --> none: set.mode } state SafetyState { [*] --> nominal nominal --> dryRun: vol < minVol AND draining nominal --> overfill: vol > overfillThreshold AND filling dryRun --> nominal: vol ≥ minVol overfill --> nominal: vol ≤ overfillThreshold } ``` While the safety state is `dryRun`, control dispatch is **skipped** entirely. While `overfill`, control still runs (pumps must keep draining) but upstream equipment is shut down. ## 11. Examples Example flows live under `examples/` in the repo. The structured tier-1/2/3 flows for this node are still in progress; until they land, the standalone simulator demo is the only runnable artefact. | Tier | File | What it shows | Status | |---|---|---|---| | Basic | `examples/01-Basic.flow.json` | Inject + dashboard, single basin, no parent | ⏳ TBD | | Integration | `examples/02-Integration.flow.json` | pumpingStation + MGC + 2 pumps + measurement children | ⏳ TBD | | Dashboard | `examples/03-Dashboard.flow.json` | Live FlowFuse charts (level, net flow, ETA) | ⏳ TBD | | Headless | `examples/standalone-demo.js` | Node.js-only simulator, no Node-RED | ✅ in repo | ## 12. Debug recipes | Symptom | First thing to check | Where to look | |---|---|---| | Status badge stuck on `❔ 0.0%` | Did any volume / level measurement register? Watch Port 2 + first-child event. | Editor debug tap on Port 2 + `_subscribeMeasurement` log line. | | `direction` always `steady` | Net flow inside `general.flowThreshold` dead-band (default 0.0001 m³/s). | `flowAggregator.deriveDirection`. | | `set.demand` ignored | Mode isn't `manual`. Check `set.mode` history. | `handlers.setDemand` debug log. | | Predicted volume drifts off measured | Calibration needed — fire `cmd.calibrate.volume` with a known reading. | `measurement/calibration.js`. | | Pumps don't stop on dry-run | `safety.enableDryRunProtection` must be `true` AND the orchestrator must see `direction='draining'`. | `SafetyController.evaluate`. | | Threshold-ordering warnings on startup | `validateThresholdOrdering` printed `inflowLevel < overflowLevel` style violations. | `basin/thresholdValidator.js`. | > 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 pumpingStation for a **wet-well basin** that needs orchestrated drainage. For a single pump with no basin model, use `rotatingMachine` directly. - Don't use pumpingStation to schedule a fixed pump rota — its modes are reactive (level / flow / manual). Use an external scheduler if you need a calendar-driven schedule. - Skip pumpingStation if you don't need predicted volume / time-to-full. A bare `machineGroupControl` is lighter when the upstream basin is modelled elsewhere. ## 14. Known limitations / current issues | # | Issue | Tracked in | |---|---|---| | 1 | Cascaded `pumpingstation` children accepted but not exercised in production — semantics of nested stations are not test-covered. | TBD | | 2 | `pressureBased`, `percentageBased`, `powerBased`, and `hybrid` are in the config enum but not implemented as control strategies. | `control/index.js` — only `levelbased` / `flowbased` / `manual` dispatched. | | 3 | Predicted-volume integrator can drift over long horizons without a measured-level calibration source. | `cmd.calibrate.volume` is operator-triggered, not automatic. | | 4 | Tier 1/2/3 example flows not yet written — current `examples/` only contains the standalone simulator. | P2.14 (Docker E2E) + P9 wiki cleanup. |