Files
pumpingStation/src/measurement/measurementRouter.js
znetsixe 7afcd6e54a P2 wave 1: extract concerns from pumpingStation specificClass
Splits pumpingStation/src/ into focused concern modules. specificClass.js
will be slimmed to an orchestrator in P2.9 (integration); for now both
the inlined logic AND the new modules coexist so tests stay green
throughout.

  src/basin/         BasinGeometry + thresholdValidator (pure)
  src/measurement/   flowAggregator + measurementRouter + calibration
  src/control/       levelBased + flowBased(stub) + manual + index dispatcher
  src/safety/        safetyController split into dryRun + overfill rules
  src/commands/      registry array + handlers (canonical names from start)
  src/editor.js      260 lines of SVG basin-diagram redraw, was inline in .html
  examples/standalone-demo.js  was if(require.main===module) at bottom of specificClass.js
  CONTRACT.md        canonical inputs + outputs + emitted events

Modified:
  src/specificClass.js  removed the 170-line standalone demo block
  pumpingStation.html   oneditprepare/oneditsave delegate to editor.{init,save}
  pumpingStation.js     added admin endpoint serving src/editor.js

102 basic tests pass (60 new + 42 existing).
specificClass.js itself is unchanged in behaviour — integration is P2.9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:18:49 +02:00

83 lines
3.1 KiB
JavaScript

// MeasurementRouter — dispatches incoming measurement updates by type and
// derives downstream measurements (volume from level, predicted level from
// pressure). Pure domain over a context bag; no Node-RED dependency.
const { coolprop, interpolation } = require('generalFunctions');
const G = 9.80665;
const ASSUMED_TEMPERATURE_C = 15;
const ATMOSPHERIC_PRESSURE_PA = 101325;
class MeasurementRouter {
constructor(ctx = {}) {
if (!ctx.measurements) throw new Error('MeasurementRouter: ctx.measurements is required');
if (!ctx.basin) throw new Error('MeasurementRouter: ctx.basin is required');
this.measurements = ctx.measurements;
this.basin = ctx.basin;
this.logger = ctx.logger || null;
this._interp = ctx.interpolation || new interpolation();
}
route(measurementType, value, position, eventData = {}) {
switch (measurementType) {
case 'level':
this.onLevelMeasurement(position, value, eventData);
return true;
case 'pressure':
this.onPressureMeasurement(position, value, eventData);
return true;
default:
return false;
}
}
onLevelMeasurement(position, value, context = {}) {
this.measurements.type('level').variant('measured').position(position)
.value(value).unit(context.unit);
const series = this.measurements.type('level').variant('measured').position(position);
const levelMeters = series.getCurrentValue('m');
if (levelMeters == null) return;
const surfaceArea = this.basin.surfaceArea;
const volume = surfaceArea > 0 ? Math.max(levelMeters, 0) * surfaceArea : 0;
const percent = this._interp.interpolate_lin_single_point(
volume, this.basin.minVol, this.basin.maxVolAtOverflow, 0, 100
);
this.measurements.type('volume').variant('measured').position('atequipment')
.value(volume, context.timestamp, 'm3');
this.measurements.type('volumePercent').variant('measured').position('atequipment')
.value(percent, context.timestamp, '%');
}
onPressureMeasurement(position, value, context = {}) {
let kelvin = this.measurements
.type('temperature').variant('measured').position('atequipment')
.getCurrentValue('K') ?? null;
if (kelvin === null) {
if (this.logger) {
this.logger.warn('No temperature measurement; assuming 15C for pressure to level conversion.');
}
this.measurements.type('temperature').variant('assumed').position('atequipment')
.value(ASSUMED_TEMPERATURE_C, Date.now(), 'C');
kelvin = this.measurements.type('temperature').variant('assumed').position('atequipment')
.getCurrentValue('K');
}
if (kelvin == null) return;
const density = coolprop.PropsSI('D', 'T', kelvin, 'P', ATMOSPHERIC_PRESSURE_PA, 'Water');
const pressurePa = this.measurements.type('pressure').variant('measured').position(position)
.getCurrentValue('Pa');
if (!Number.isFinite(pressurePa) || !Number.isFinite(density)) return;
const level = pressurePa / (density * G);
this.measurements.type('level').variant('predicted').position(position)
.value(level, context.timestamp, 'm');
}
}
module.exports = MeasurementRouter;