Files
pumpingStation/src/commands/handlers.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

88 lines
2.7 KiB
JavaScript

'use strict';
// Handler functions for pumpingStation commands. Each handler receives:
// source: the domain (specificClass) instance — has the public methods
// (changeMode, calibratePredicted*, setManualInflow, ...).
// msg: the Node-RED input message.
// ctx: { node, RED, send, logger } — provided by BaseNodeAdapter.
//
// Handlers are pure functions: they don't keep state. Validation that goes
// beyond the registry's typeof-check ladder lives here.
function _logger(source, ctx) {
return ctx?.logger || source?.logger || null;
}
exports.setMode = (source, msg) => {
source.changeMode(msg.payload);
};
exports.registerChild = (source, msg, ctx) => {
const log = _logger(source, ctx);
const childId = msg.payload;
const childObj = ctx?.RED?.nodes?.getNode?.(childId);
if (!childObj || !childObj.source) {
log?.warn?.(`registerChild: child '${childId}' not found or has no .source`);
return;
}
source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent);
};
exports.calibrateVolume = (source, msg, ctx) => {
const log = _logger(source, ctx);
const v = parseFloat(msg.payload);
if (!Number.isFinite(v)) {
log?.warn?.(`cmd.calibrate.volume: non-numeric payload '${msg.payload}'`);
return;
}
source.calibratePredictedVolume(v);
};
exports.calibrateLevel = (source, msg, ctx) => {
const log = _logger(source, ctx);
const v = parseFloat(msg.payload);
if (!Number.isFinite(v)) {
log?.warn?.(`cmd.calibrate.level: non-numeric payload '${msg.payload}'`);
return;
}
source.calibratePredictedLevel(v);
};
exports.setInflow = (source, msg) => {
// Payload is either a number (legacy q_in shape) or
// { value, unit, timestamp } (richer object form).
const p = msg.payload;
let value;
let unit;
let timestamp;
if (p !== null && typeof p === 'object') {
value = Number(p.value);
unit = p.unit;
timestamp = p.timestamp || Date.now();
} else {
value = Number(p);
unit = msg?.unit;
timestamp = msg?.timestamp || Date.now();
}
source.setManualInflow(value, timestamp, unit);
};
exports.setDemand = (source, msg, ctx) => {
const log = _logger(source, ctx);
const demand = Number(msg.payload);
if (!Number.isFinite(demand)) {
log?.warn?.(`set.demand: invalid Qd value '${msg.payload}'`);
return;
}
if (source.mode !== 'manual') {
log?.debug?.(
`set.demand ignored in '${source.mode}' mode; switch to manual to use the demand slider`
);
return;
}
// forwardDemandToChildren returns a promise — surface failures via logger.
Promise.resolve(source.forwardDemandToChildren(demand)).catch((err) => {
log?.error?.(`set.demand: failed to forward demand: ${err && err.message}`);
});
};