- 5 descriptors -> unit: shorthand (cmd.calibrate.volume/level, set.inflow/ outflow/demand). - setInflow/setOutflow: drop the hand-rolled scalar-vs-object parsing — the registry now normalises every shape to a number in the descriptor unit; the handlers become guarded one-liners (matching setDemand). - Regenerate wiki topic-contract + command-envelope note (msg.origin). 143/143 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
Reference — Contracts
Note
Full topic contract, configuration schema, and child-registration filters for
pumpingStation. The topic-contract and data-model sections are regenerated bynpm run wiki:all— do not hand-edit between theBEGIN AUTOGEN/END AUTOGENmarkers. Source of truth for everything on this page: the node'ssrc/commands/index.js,src/specificClass.jsconfigure(), and the schema atgeneralFunctions/src/configs/pumpingStation.json.For an intuitive overview, return to the Home.
Topic contract
The Unit column reflects each descriptor's declared unit (via the unit: 'm3/h' shorthand or the legacy units: { measure, default }; the measure is derived from the unit). The default unit is what the commandRegistry coerces incoming values to before the handler runs.
Command envelope (all EVOLV nodes). Every command shares one envelope on top of msg.topic:
- Value + unit — send
msg.payloadas a number (with optional siblingmsg.unit) or as{ value, unit }. The registry always converts the value to the descriptor's unit before the handler; numeric strings are converted too. A missing unit assumes the descriptor default. msg.origin— the control authority that issued the command:parent(automation/parent controller, the default),GUI(SCADA/HMI operator), orfysical(physical buttons). On nodes with a control mode, the mode'sallowedSourcesdecides which origins are accepted; releasing control is done by changing the mode.
| Canonical topic | Aliases | Payload | Unit | Effect |
|---|---|---|---|---|
set.mode |
changemode |
string |
— | Switch the station between auto / manual control modes. |
child.register |
registerChild |
string |
— | Register a child node (machine group, measurement, …) with this station. |
cmd.calibrate.volume |
calibratePredictedVolume |
any | volume (default m3) |
Calibrate the predicted-volume integrator to a known basin volume. |
cmd.calibrate.level |
calibratePredictedLevel |
any | length (default m) |
Calibrate the predicted-volume integrator to a known basin level. |
set.inflow |
q_in |
any | volumeFlowRate (default m3/h) |
Push a measured inflow value into the basin balance. |
set.outflow |
q_out |
any | volumeFlowRate (default m3/h) |
Push a measured outflow value into the basin balance. |
set.demand |
Qd |
any | volumeFlowRate (default m3/h) |
Operator outflow demand setpoint for the station. |
Input message examples
One worked msg per accepted topic. Send these into Port 0. For unit-bearing
topics the commandRegistry converts msg.unit (or a { value, unit } payload) to
the default unit before the handler runs — so the unit is optional and any
compatible unit is accepted.
// 1. set.mode — switch control strategy
msg = { topic: 'set.mode', payload: 'manual' }; // manual | levelbased | flowbased | none
// 2. child.register — register a child (usually arrives on Port 2 from the child;
// this is the manual form). payload = the child node's Node-RED id.
msg = { topic: 'child.register', payload: 'a1b2c3d4.ef567', positionVsParent: 'upstream' };
// positionVsParent: upstream | downstream | atequipment (or in | out for predicted-flow children)
// 3. cmd.calibrate.volume — seed the predicted-volume integrator (default m³)
msg = { topic: 'cmd.calibrate.volume', payload: 12.5 }; // 12.5 m³
msg = { topic: 'cmd.calibrate.volume', payload: 12500, unit: 'L' }; // 12 500 L → auto-converted to 12.5 m³
// 4. cmd.calibrate.level — seed the predicted level (default m)
msg = { topic: 'cmd.calibrate.level', payload: 1.8 }; // 1.8 m
// 5. set.inflow — push a measured inflow (default m³/h)
msg = { topic: 'set.inflow', payload: 45 }; // 45 m³/h
msg = { topic: 'set.inflow', payload: 12.5, unit: 'L/s' }; // 12.5 L/s → 45 m³/h
msg = { topic: 'set.inflow', payload: { value: 45, unit: 'm3/h' }, timestamp: 1716998400000 };
// 6. set.outflow — push a measured/forced outflow (default m³/h)
msg = { topic: 'set.outflow', payload: 30 }; // 30 m³/h drawn from the basin
// 7. set.demand — operator outflow setpoint (default m³/h); ignored unless mode === 'manual'
msg = { topic: 'set.demand', payload: 120 }; // 120 m³/h
// Built-in (every EVOLV node): query.units — ask which units each topic accepts.
// Replies on Port 0 with { topic:'query.units', payload:{ node, units } }.
msg = { topic: 'query.units', payload: null };
Deprecated aliases behave identically and log a one-time warning, e.g.
{ topic: 'q_in', payload: 45 }≡set.inflow,{ topic: 'Qd', payload: 120 }≡set.demand.
Data model — getOutput() shape
Keys composed each tick by specificClass.getOutput() and emitted via outputUtils.formatMsg on Port 0. Delta-compressed: consumers see only the keys that changed.
| Key | Type | Unit | Sample |
|---|---|---|---|
direction |
string | — | "steady" |
dryRunLevel |
number | — | 0.20400000000000001 |
dryRunSafetyVol |
number | — | 2.55 |
flowSource |
null | — | null |
heightBasin |
number | m | 4 |
highVolumeSafetyLevel |
number | — | 3.7239999999999998 |
highVolumeSafetyVol |
number | — | 46.55 |
inflowLevel |
number | m | 1.5 |
inletPipeDiameter |
number | — | 0.4 |
manualDemand |
null | — | null |
maxVol |
number | m3 | 50 |
maxVolAtOverflow |
number | m3 | 47.5 |
minHeightBasedOn |
string | — | "outlet" |
minVol |
number | m3 | 2.5 |
minVolAtInflow |
number | m3 | 18.75 |
minVolAtOutflow |
number | m3 | 2.5 |
mode |
string | — | "levelbased" |
outflowLevel |
number | m | 0.2 |
outletPipeDiameter |
number | — | 0.4 |
overflowLevel |
number | m | 3.8 |
percControl |
number | % | 0 |
predictedOverflowRate |
number | — | 0 |
predictedOverflowVolume |
number | — | 0 |
predictedUnderflowVolume |
number | — | 0 |
surfaceArea |
number | m2 | 12.5 |
timeleft |
null | s | null |
volEmptyBasin |
number | m3 | 50 |
volume.predicted.atequipment.wikigen-pumpingstation-id |
number | m3 | 2.5 |
Sample values come from a stub instantiation in wikiGen — in a live deployment the volume key is shaped volume.<variant>.<position>.<childId> per the standard Measurement Key Shape.
Note
Two control-state keys carry the live operating mode rather than a measurement:
mode— string, the active control strategy (levelbased/manual/flowbased/none). Echoes the most recentset.modeinput.manualDemand— number (m³/h) ornull. The operator outflow setpoint last accepted viaset.demand;nulloutsidemanualmode.
Output message examples
The node emits on three ports every tick (outputUtils.formatMsg). Port 0 / Port 1
fire only when at least one field changed (delta-compression); Port 2 fires once at
startup. topic is the station's configured name (here "PS-Influent-01").
// Port 0 — process data. payload = only the keys that changed this tick.
msg = {
topic: 'PS-Influent-01',
payload: {
mode: 'levelbased',
direction: 'filling',
percControl: 25,
'level.predicted.atequipment.default': 3.25, // m
'volume.predicted.atequipment.default': 32.5, // m³
timeleft: 400, // s, or null when steady
manualDemand: null // m³/h, or null outside manual mode
}
};
// Port 1 — InfluxDB telemetry. Same changed fields, wrapped for the InfluxDB node.
msg = {
topic: 'PS-Influent-01',
payload: {
measurement: 'PS-Influent-01',
fields: { percControl: 25, 'volume.predicted.atequipment.default': 32.5 },
tags: { id: 'a1b2c3d4.ef567', softwareType: 'pumpingstation', type: 'pumpingStation' },
timestamp: '2026-05-29T10:00:00.000Z' // Date
}
};
// Port 2 — registration handshake, sent once at startup to the upstream parent.
msg = {
topic: 'child.register',
payload: 'a1b2c3d4.ef567', // this node's id
positionVsParent: 'atEquipment',
distance: null
};
Child-facing events are not Port messages — they fire on
source.measurements.emitteras<type>.<variant>.<position>, e.g. eventvolume.predicted.atequipmentwith payload{ value: 32.5, unit: 'm3', timestamp }. Parents subscribe by event name.
Configuration schema — editor form to config keys
Source of truth: generalFunctions/src/configs/pumpingStation.json.
Basin geometry (config.basin)
| Form field | Config key | Default | Unit | Notes |
|---|---|---|---|---|
| Basin Volume | basin.volume |
1 |
m3 | Total geometric storage from floor to rim |
| Basin Height | basin.height |
1 |
m | Floor-to-rim wall height |
| Inlet Elevation | basin.inflowLevel |
2 |
m | Bottom of incoming pipe, from floor |
| Outlet Elevation | basin.outflowLevel |
0.2 |
m | Top of pump-suction pipe, from floor |
| Inlet Pipe Diameter | basin.inletPipeDiameter |
0.4 |
m | For future hydraulic upgrades |
| Outlet Pipe Diameter | basin.outletPipeDiameter |
0.4 |
m | For future hydraulic upgrades |
| Overflow Level | basin.overflowLevel |
2.5 |
m | Physical overflow weir crest |
Safety thresholds (config.safety)
| Form field | Config key | Default | Notes |
|---|---|---|---|
| High-Volume Safety % | safety.highVolumeSafetyThresholdPercent |
98 |
Trigger high-volume safety at this fill % |
| Dry-Run Safety Level | safety.dryRunLevel |
0.2 |
Below this level all pumps stop |
| Enable High-Volume Safety | safety.enableHighVolumeSafety |
true |
Master switch |
Warning
Earlier versions used
enableOverfillProtectionandoverfillThresholdPercent. Those names are deprecated. The current canonical names areenableHighVolumeSafetyandhighVolumeSafetyThresholdPercent. See.claude/refactor/OPEN_QUESTIONS.mdfor the alias-removal timeline.
Control mode (config.control)
| Form field | Config key | Default | Notes |
|---|---|---|---|
| Mode | control.mode |
"levelbased" |
One of levelbased, manual, flowbased, pressureBased, percentageBased, powerBased, hybrid*. Asterisked modes are placeholders in code. |
| Level Curve Type | control.levelbased.curveType |
"linear" |
linear or log |
| Log Curve Factor | control.levelbased.logCurveFactor |
0.5 |
Slope tuning for log curve |
| Min Level | control.levelbased.minLevel |
0.3 |
Demand hard-zero below this |
| Start Level | control.levelbased.startLevel |
0.5 |
Falling-ramp returns to 0 % here |
| Stop Level | control.levelbased.stopLevel |
0.4 |
Schmitt-trigger lower bound for pump-count keep-alive |
| Max Level | control.levelbased.maxLevel |
2.3 |
Demand saturates at 100 % here |
| Enable Shifted Ramp | control.levelbased.enableShiftedRamp |
true |
Hysteresis-armed shift between rising / falling ramps |
| Manual Flow Setpoint | control.manual.flowSetpoint |
0 |
Honoured in manual mode |
General (config.general)
| Form field | Config key | Default | Notes |
|---|---|---|---|
| Time-left full / empty threshold | general.timeleftToFullOrEmptyThresholdSeconds |
120 |
ETA below this triggers warning state |
| Flow dead-band | general.flowThreshold |
1e-4 m³/s |
Net-flow below this is treated as steady |
Child registration
Source: nodes/pumpingStation/src/specificClass.js configure(), lines 107–116.
| Software type | Filter | Wired to | Side-effect |
|---|---|---|---|
measurement |
any | _subscribeMeasurement |
Subscribes to the measurement's emitter; updates basin balance |
machine |
only if no machinegroup parent is present |
direct dispatch | Bypassed when an MGC is the predicted-flow source |
machinegroup |
any | _subscribePredictedFlow |
Reads aggregated predicted flow from the MGC |
pumpingstation |
any | _subscribePredictedFlow |
Cascaded PS — reads predicted outflow of upstream station |
The router only subscribes to the highest-level aggregator for predicted flow. If an MGC is present, direct machine children are not double-counted.
Unit policy
Source: nodes/pumpingStation/src/specificClass.js lines 21–30.
| Quantity | Canonical (internal) | Output (rendered) |
|---|---|---|
| Flow | m3/s |
m3/s (also netFlowRate) |
| Level | m |
m |
| Volume | m3 |
m3 |
| Pressure | Pa |
(not surfaced) |
| Power | W |
(not surfaced) |
| Temperature | K |
(not surfaced) |
overflowVolume and underflowVolume are explicitly listed in the policy output so the MeasurementContainer keeps the integrator's m3 unit on those streams (FlowAggregator writes spill / underflow per tick).
Related pages
| Page | Why |
|---|---|
| Home | Intuitive overview |
| Reference — Architecture | Code map, state chart, lifecycle |
| Reference — Examples | Shipped example flows |
| Reference — Limitations | Known limitations and open questions |
| EVOLV — Topic Conventions | Platform-wide topic rules |
| EVOLV — Telemetry | Port 0 / 1 / 2 InfluxDB layout |