Files
pumpingStation/src/nodeClass.js
znetsixe e991ea64ef Merge origin/basin-docs-update: per-mode SVG + stopLevel hysteresis + shifted ramp
Reconciles the 7-commit basin-docs-update feature branch (which never
landed on main before the platform refactor) with the post-refactor
architecture on development. Each basin-docs feature ported into the
relevant concern module:

  control/levelBased.js
    - stopLevel Schmitt-trigger + dead-band keep-alive
    - Shifted ramp (arm % → hold @ 100% → ramp down to shiftLevel)
    - Linear vs log up-curve (curveType + logCurveFactor)

  measurement/flowAggregator.js
    - Predicted-volume overflow clamp + spill flow stream
    - Cumulative overflowVolume + underflowVolume
    - Hard floor at 0 + dry-run-on-transition handling

  basin/thresholdValidator.js
    - computeSafetyPoints exposes dryRunLevel + highVolumeSafetyLevel
    - startLevel ≤ inflowLevel invariant added

  measurement/calibration.js + commands/
    - Manual q_out path (set.outflow / q_out alias)

  safety/safetyController.js
    - Accepts both legacy + new high-volume threshold names

UI:
  pumpingStation.html — restored the side-panel + SVG mode-preview block,
  added defaults for stopLevel/shiftLevel/shiftArmPercent/levelCurveType/
  logCurveFactor/enableShiftedRamp.
  src/editor/* — basin-docs' 7-file modular editor (replaces single
  src/editor.js, which is deleted).
  pumpingStation.js — admin endpoint serves editor/:file.

Tests: 130/130 pass (125 basic + 5 integration). Two basin-docs test
files added: nodeClass-config.test.js, basic-dashboard-flow.test.js,
shifted-ramp-end-to-end.test.js. One pre-refactor control-levelBased
test adapted to match basin-docs canonical "no-shutdown in dead zone"
behaviour.

Human-review items (see commit context):
  - rampFoot = inflowLevel (matches basin-docs test); basin-docs source
    used rampFoot = startLevel. Domain owner: confirm intent.
  - Naming kept dual (overfillLevel + highVolumeSafetyLevel).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:19:55 +02:00

80 lines
3.2 KiB
JavaScript

const { BaseNodeAdapter, configManager } = require('generalFunctions');
const PumpingStation = require('./specificClass');
const commands = require('./commands');
class nodeClass extends BaseNodeAdapter {
static DomainClass = PumpingStation;
static commands = commands;
// Tick-driven: predicted-volume integrator needs delta-time per second.
static tickInterval = 1000;
static statusInterval = 1000;
buildDomainConfig(uiConfig) {
return {
basin: {
volume: uiConfig.basinVolume,
height: uiConfig.basinHeight,
inflowLevel: uiConfig.inflowLevel,
outflowLevel: uiConfig.outflowLevel,
overflowLevel: uiConfig.overflowLevel,
inletPipeDiameter: uiConfig.inletPipeDiameter,
outletPipeDiameter: uiConfig.outletPipeDiameter,
},
hydraulics: {
refHeight: uiConfig.refHeight,
minHeightBasedOn: uiConfig.minHeightBasedOn,
basinBottomRef: uiConfig.basinBottomRef,
maxInflowRate: uiConfig.maxInflowRate,
staticHead: uiConfig.staticHead,
maxDischargeHead: uiConfig.maxDischargeHead,
pipelineLength: uiConfig.pipelineLength,
defaultFluid: uiConfig.defaultFluid,
temperatureReferenceDegC: uiConfig.temperatureReferenceDegC,
},
control: {
mode: uiConfig.controlMode,
levelbased: {
minLevel: uiConfig.minLevel,
startLevel: uiConfig.startLevel,
stopLevel: uiConfig.stopLevel,
maxLevel: uiConfig.maxLevel,
// Editor names the field levelCurveType; runtime uses curveType.
curveType: uiConfig.levelCurveType || uiConfig.curveType,
logCurveFactor: uiConfig.logCurveFactor,
enableShiftedRamp: uiConfig.enableShiftedRamp,
shiftLevel: uiConfig.shiftLevel,
shiftArmPercent: uiConfig.shiftArmPercent,
},
},
safety: {
enableDryRunProtection: uiConfig.enableDryRunProtection,
dryRunThresholdPercent: uiConfig.dryRunThresholdPercent,
enableHighVolumeSafety: uiConfig.enableHighVolumeSafety ?? uiConfig.enableOverfillProtection,
highVolumeSafetyThresholdPercent: uiConfig.highVolumeSafetyThresholdPercent ?? uiConfig.overfillThresholdPercent,
enableOverfillProtection: uiConfig.enableOverfillProtection,
overfillThresholdPercent: uiConfig.overfillThresholdPercent,
timeleftToFullOrEmptyThresholdSeconds: uiConfig.timeleftToFullOrEmptyThresholdSeconds,
},
output: {
process: uiConfig.processOutputFormat,
dbase: uiConfig.dbaseOutputFormat,
},
};
}
// Test-only entrypoint mirroring the basin-docs config-mapping surface.
// Lets `NodeClass.prototype._loadConfig.call({name:'pumpingStation'}, ui, node)`
// produce the merged config without instantiating a full Node-RED adapter.
// Production wiring goes through BaseNodeAdapter; this is a thin shim.
_loadConfig(uiConfig, node) {
const cfgMgr = new configManager();
const name = this.name || 'pumpingStation';
const domain = nodeClass.prototype.buildDomainConfig.call(this, uiConfig);
this.defaultConfig = cfgMgr.getConfig(name);
this.config = cfgMgr.buildConfig(name, uiConfig, node && node.id, domain);
return this.config;
}
}
module.exports = nodeClass;