testing codex
This commit is contained in:
@@ -85,7 +85,6 @@ class pumpingStation {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case("upstream"):
|
case("upstream"):
|
||||||
//check for predicted outgoing flow at the connected child pumpingsation
|
//check for predicted outgoing flow at the connected child pumpingsation
|
||||||
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
||||||
@@ -152,8 +151,8 @@ class pumpingStation {
|
|||||||
const prevFlow = series.getLaggedValue(1, "m3/s"); // { value, timestamp, unit }
|
const prevFlow = series.getLaggedValue(1, "m3/s"); // { value, timestamp, unit }
|
||||||
|
|
||||||
if (!currFLow || !prevFlow) return;
|
if (!currFLow || !prevFlow) return;
|
||||||
|
|
||||||
this.logger.debug(`currDownflow = ${currFLow.value} , prevDownFlow = ${prevFlow.value}`);
|
this.logger.debug(`Flowdir ${flowDir} => currFlow ${currFLow.value} , prevflow = ${prevFlow.value}`);
|
||||||
|
|
||||||
// calc difference in time
|
// calc difference in time
|
||||||
const deltaT = currFLow.timestamp - prevFlow.timestamp;
|
const deltaT = currFLow.timestamp - prevFlow.timestamp;
|
||||||
@@ -222,7 +221,7 @@ class pumpingStation {
|
|||||||
//this._calcNetFlow();
|
//this._calcNetFlow();
|
||||||
const {time:timeleft, source:variant} = this._calcTimeRemaining();
|
const {time:timeleft, source:variant} = this._calcTimeRemaining();
|
||||||
|
|
||||||
this.logger.debug(`Remaining time ${Math.round(timeleft/60/60*100)/100} h, based on variant ${variant} `);
|
this.logger.debug(`Remaining time ~${Math.round(timeleft/60/60*10)/10} h, based on variant ${variant} `);
|
||||||
}
|
}
|
||||||
|
|
||||||
_calcTimeRemaining(){
|
_calcTimeRemaining(){
|
||||||
@@ -624,7 +623,7 @@ const PumpingStation = require("./specificClass");
|
|||||||
const RotatingMachine = require("../../rotatingMachine/src/specificClass");
|
const RotatingMachine = require("../../rotatingMachine/src/specificClass");
|
||||||
const Measurement = require("../../measurement/src/specificClass");
|
const Measurement = require("../../measurement/src/specificClass");
|
||||||
|
|
||||||
/** Helpers ******************************************************************/
|
//Helpers
|
||||||
function createPumpingStationConfig(name) {
|
function createPumpingStationConfig(name) {
|
||||||
return {
|
return {
|
||||||
general: {
|
general: {
|
||||||
@@ -754,10 +753,11 @@ function pushSample(measurement, type, value, unit) {
|
|||||||
.value(value, Date.now(), unit);
|
.value(value, Date.now(), unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Demo *********************************************************************/
|
// Demo
|
||||||
(async function demoStationWithPump() {
|
(async function demoStationWithPump() {
|
||||||
const station = new PumpingStation(createPumpingStationConfig("PumpingStationDemo"));
|
const station = new PumpingStation(createPumpingStationConfig("PumpingStationDemo"));
|
||||||
const pump = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig());
|
const pump1 = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig());
|
||||||
|
const pump2 = new RotatingMachine(createMachineConfig("Pump2"), createMachineStateConfig());
|
||||||
|
|
||||||
const levelSensor = new Measurement(createLevelMeasurementConfig("WetWellLevel"));
|
const levelSensor = new Measurement(createLevelMeasurementConfig("WetWellLevel"));
|
||||||
const upstreamFlow = new Measurement(createFlowMeasurementConfig("InfluentFlow", "upstream"));
|
const upstreamFlow = new Measurement(createFlowMeasurementConfig("InfluentFlow", "upstream"));
|
||||||
@@ -765,15 +765,16 @@ function pushSample(measurement, type, value, unit) {
|
|||||||
|
|
||||||
|
|
||||||
// station uses the sensors
|
// station uses the sensors
|
||||||
/*
|
|
||||||
station.childRegistrationUtils.registerChild(levelSensor, levelSensor.config.functionality.softwareType);
|
station.childRegistrationUtils.registerChild(levelSensor, levelSensor.config.functionality.softwareType);
|
||||||
station.childRegistrationUtils.registerChild(upstreamFlow, upstreamFlow.config.functionality.softwareType);
|
station.childRegistrationUtils.registerChild(upstreamFlow, upstreamFlow.config.functionality.softwareType);
|
||||||
station.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.softwareType);
|
station.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.softwareType);
|
||||||
*/
|
|
||||||
|
|
||||||
// pump owns the downstream flow sensor
|
// pump owns the downstream flow sensor
|
||||||
pump.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.positionVsParent);
|
//pump.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.positionVsParent);
|
||||||
station.childRegistrationUtils.registerChild(pump,"downstream");
|
station.childRegistrationUtils.registerChild(pump1,"downstream");
|
||||||
|
station.childRegistrationUtils.registerChild(pump2,"upstream");
|
||||||
|
|
||||||
setInterval(() => station.tick(), 1000);
|
setInterval(() => station.tick(), 1000);
|
||||||
|
|
||||||
@@ -782,7 +783,7 @@ function pushSample(measurement, type, value, unit) {
|
|||||||
pushSample(levelSensor, "level", 1.8, "m");
|
pushSample(levelSensor, "level", 1.8, "m");
|
||||||
pushSample(upstreamFlow, "flow", 0.35, "m3/s");
|
pushSample(upstreamFlow, "flow", 0.35, "m3/s");
|
||||||
pushSample(downstreamFlow, "flow", 0.20, "m3/s");
|
pushSample(downstreamFlow, "flow", 0.20, "m3/s");
|
||||||
*/
|
//*/
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 20));
|
await new Promise(resolve => setTimeout(resolve, 20));
|
||||||
|
|
||||||
@@ -791,13 +792,17 @@ function pushSample(measurement, type, value, unit) {
|
|||||||
pushSample(downstreamFlow, "flow", 0.28, "m3/s");
|
pushSample(downstreamFlow, "flow", 0.28, "m3/s");
|
||||||
pushSample(upstreamFlow, "flow", 0.40, "m3/s");
|
pushSample(upstreamFlow, "flow", 0.40, "m3/s");
|
||||||
pushSample(levelSensor, "level", 1.85, "m");
|
pushSample(levelSensor, "level", 1.85, "m");
|
||||||
*/
|
//*/
|
||||||
|
|
||||||
console.log("Station output:", station.getOutput());
|
console.log("Station output:", station.getOutput());
|
||||||
await pump.handleInput("parent", "execSequence", "startup");
|
await pump1.handleInput("parent", "execSequence", "startup");
|
||||||
await pump.handleInput("parent", "execMovement", 50);
|
await pump2.handleInput("parent", "execSequence", "startup");
|
||||||
|
await pump1.handleInput("parent", "execMovement", 5);
|
||||||
|
await pump2.handleInput("parent", "execMovement", 5);
|
||||||
console.log("Station state:", station.state);
|
console.log("Station state:", station.state);
|
||||||
console.log("Station output:", station.getOutput());
|
console.log("Station output:", station.getOutput());
|
||||||
console.log("Pump state:", pump.state.getCurrentState());
|
console.log("Pump state:", pump1.state.getCurrentState());
|
||||||
|
console.log("Pump state:", pump2.state.getCurrentState());
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
681
src/specificClass2.js
Normal file
681
src/specificClass2.js
Normal file
@@ -0,0 +1,681 @@
|
|||||||
|
const EventEmitter = require('events');
|
||||||
|
const {
|
||||||
|
logger,
|
||||||
|
configUtils,
|
||||||
|
configManager,
|
||||||
|
childRegistrationUtils,
|
||||||
|
MeasurementContainer,
|
||||||
|
coolprop,
|
||||||
|
interpolation
|
||||||
|
} = require('generalFunctions');
|
||||||
|
|
||||||
|
const FLOW_VARIANTS = ['measured', 'predicted'];
|
||||||
|
const LEVEL_VARIANTS = ['measured', 'predicted'];
|
||||||
|
const FLOW_POSITIONS = {
|
||||||
|
inflow: ['in', 'upstream'],
|
||||||
|
outflow: ['out', 'downstream']
|
||||||
|
};
|
||||||
|
|
||||||
|
class PumpingStationV2 {
|
||||||
|
constructor(config = {}) {
|
||||||
|
this.emitter = new EventEmitter();
|
||||||
|
this.configManager = new configManager();
|
||||||
|
this.defaultConfig = this.configManager.getConfig('pumpingStation');
|
||||||
|
this.configUtils = new configUtils(this.defaultConfig);
|
||||||
|
this.config = this.configUtils.initConfig(config);
|
||||||
|
this.interpolate = new interpolation();
|
||||||
|
|
||||||
|
this.logger = new logger(
|
||||||
|
this.config.general.logging.enabled,
|
||||||
|
this.config.general.logging.logLevel,
|
||||||
|
this.config.general.name
|
||||||
|
);
|
||||||
|
|
||||||
|
this.measurements = new MeasurementContainer({ autoConvert: true });
|
||||||
|
this.measurements.setPreferredUnit('flow', 'm3/s');
|
||||||
|
this.measurements.setPreferredUnit('level', 'm');
|
||||||
|
this.measurements.setPreferredUnit('volume', 'm3');
|
||||||
|
this.childRegistrationUtils = new childRegistrationUtils(this);
|
||||||
|
this.machines = {};
|
||||||
|
this.stations = {};
|
||||||
|
|
||||||
|
this.basin = {};
|
||||||
|
this.state = {
|
||||||
|
direction: 'steady',
|
||||||
|
netFlow: 0,
|
||||||
|
flowSource: null,
|
||||||
|
seconds: null,
|
||||||
|
remainingSource: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const thresholdFromConfig = Number(this.config.general?.flowThreshold);
|
||||||
|
this.flowThreshold = Number.isFinite(thresholdFromConfig) ? thresholdFromConfig : 1e-4;
|
||||||
|
|
||||||
|
this.initBasinProperties();
|
||||||
|
this.logger.debug('PumpingStationV2 initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
registerChild(child, softwareType) {
|
||||||
|
this.logger.debug(`Registering child (${softwareType}) "${child.config.general.name}"`);
|
||||||
|
|
||||||
|
if (softwareType === 'measurement') {
|
||||||
|
this._registerMeasurementChild(child);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (softwareType === 'machine' || softwareType === 'pumpingStation') {
|
||||||
|
this._registerPredictedFlowChild(child);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn(`Unsupported child software type: ${softwareType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
tick() {
|
||||||
|
const snapshot = this._takeMeasurementSnapshot();
|
||||||
|
|
||||||
|
this._updatePredictedVolume(snapshot);
|
||||||
|
|
||||||
|
const netFlow = this._selectBestNetFlow(snapshot);
|
||||||
|
const remaining = this._computeRemainingTime(snapshot, netFlow);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
direction: netFlow.direction,
|
||||||
|
netFlow: netFlow.value,
|
||||||
|
flowSource: netFlow.source,
|
||||||
|
seconds: remaining.seconds,
|
||||||
|
remainingSource: remaining.source
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Remaining time (${remaining.source ?? 'n/a'}): ${
|
||||||
|
remaining.seconds != null ? `${Math.round((remaining.seconds / 60 / 60) * 10) / 10} h` : 'n/a'
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Helpers */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
_registerMeasurementChild(child) {
|
||||||
|
const position = child.config.functionality.positionVsParent;
|
||||||
|
const measurementType = child.config.asset.type;
|
||||||
|
const eventName = `${measurementType}.measured.${position}`;
|
||||||
|
|
||||||
|
child.measurements.emitter.on(eventName, (eventData) => {
|
||||||
|
this.logger.debug(
|
||||||
|
`Measurement update ${eventName} <- ${eventData.childName}: ${eventData.value} ${eventData.unit}`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.measurements
|
||||||
|
.type(measurementType)
|
||||||
|
.variant('measured')
|
||||||
|
.position(position)
|
||||||
|
.value(eventData.value, eventData.timestamp, eventData.unit);
|
||||||
|
|
||||||
|
this._handleMeasurement(measurementType, eventData.value, position, eventData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_registerPredictedFlowChild(child) {
|
||||||
|
const position = child.config.functionality.positionVsParent;
|
||||||
|
const childName = child.config.general.name;
|
||||||
|
|
||||||
|
const listener = (eventName, posKey) => {
|
||||||
|
child.measurements.emitter.on(eventName, (eventData) => {
|
||||||
|
this.logger.debug(
|
||||||
|
`Predicted flow update from ${childName} (${position}) -> ${eventData.value} ${eventData.unit}`
|
||||||
|
);
|
||||||
|
this.measurements
|
||||||
|
.type('flow')
|
||||||
|
.variant('predicted')
|
||||||
|
.position(posKey)
|
||||||
|
.value(eventData.value, eventData.timestamp, eventData.unit);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (position === 'downstream' || position === 'atEquipment' || position === 'out') {
|
||||||
|
listener('flow.predicted.downstream', 'out');
|
||||||
|
} else if (position === 'upstream' || position === 'in') {
|
||||||
|
listener('flow.predicted.downstream', 'in');
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleMeasurement(measurementType, value, position, context) {
|
||||||
|
switch (measurementType) {
|
||||||
|
case 'level':
|
||||||
|
this._onLevelMeasurement(position, value, context);
|
||||||
|
break;
|
||||||
|
case 'pressure':
|
||||||
|
this._onPressureMeasurement(position, value, context);
|
||||||
|
break;
|
||||||
|
case 'flow':
|
||||||
|
// Additional flow-specific logic could go here if needed
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.logger.debug(`Unhandled measurement type "${measurementType}", storing only.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onLevelMeasurement(position, value, context = {}) {
|
||||||
|
const levelSeries = this.measurements.type('level').variant('measured').position(position);
|
||||||
|
const levelMeters = levelSeries.getCurrentValue('m');
|
||||||
|
if (levelMeters == null) return;
|
||||||
|
|
||||||
|
const volume = this._calcVolumeFromLevel(levelMeters);
|
||||||
|
const percent = this.interpolate.interpolate_lin_single_point(
|
||||||
|
volume,
|
||||||
|
this.basin.minVol,
|
||||||
|
this.basin.maxVolOverflow,
|
||||||
|
0,
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
this.measurements
|
||||||
|
.type('volume')
|
||||||
|
.variant('measured')
|
||||||
|
.position('atEquipment')
|
||||||
|
.value(volume, context.timestamp, 'm3');
|
||||||
|
|
||||||
|
this.measurements
|
||||||
|
.type('volume')
|
||||||
|
.variant('percent')
|
||||||
|
.position('atEquipment')
|
||||||
|
.value(percent, context.timestamp, '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
_onPressureMeasurement(position, value, context = {}) {
|
||||||
|
let kelvinTemp =
|
||||||
|
this.measurements
|
||||||
|
.type('temperature')
|
||||||
|
.variant('measured')
|
||||||
|
.position('atEquipment')
|
||||||
|
.getCurrentValue('K') ?? null;
|
||||||
|
|
||||||
|
if (kelvinTemp === null) {
|
||||||
|
this.logger.warn('No temperature measurement; assuming 15C for pressure to level conversion.');
|
||||||
|
this.measurements
|
||||||
|
.type('temperature')
|
||||||
|
.variant('assumed')
|
||||||
|
.position('atEquipment')
|
||||||
|
.value(15, Date.now(), 'C');
|
||||||
|
kelvinTemp = this.measurements
|
||||||
|
.type('temperature')
|
||||||
|
.variant('assumed')
|
||||||
|
.position('atEquipment')
|
||||||
|
.getCurrentValue('K');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kelvinTemp == null) return;
|
||||||
|
|
||||||
|
const density = coolprop.PropsSI('D', 'T', kelvinTemp, 'P', 101325, 'Water');
|
||||||
|
const pressurePa = this.measurements
|
||||||
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.position(position)
|
||||||
|
.getCurrentValue('Pa');
|
||||||
|
|
||||||
|
if (!Number.isFinite(pressurePa) || !Number.isFinite(density)) return;
|
||||||
|
|
||||||
|
const g = 9.80665;
|
||||||
|
const level = pressurePa / (density * g);
|
||||||
|
|
||||||
|
this.measurements.type('level').variant('predicted').position(position).value(level, context.timestamp, 'm');
|
||||||
|
}
|
||||||
|
|
||||||
|
_takeMeasurementSnapshot() {
|
||||||
|
const snapshot = {
|
||||||
|
flows: {},
|
||||||
|
levels: {},
|
||||||
|
levelRates: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const variant of FLOW_VARIANTS) {
|
||||||
|
snapshot.flows[variant] = this._snapshotFlowsForVariant(variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const variant of LEVEL_VARIANTS) {
|
||||||
|
snapshot.levels[variant] = this._snapshotLevelForVariant(variant);
|
||||||
|
snapshot.levelRates[variant] = this._estimateLevelRate(snapshot.levels[variant]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
_snapshotFlowsForVariant(variant) {
|
||||||
|
const inflowSeries = this._locateSeries('flow', variant, FLOW_POSITIONS.inflow);
|
||||||
|
const outflowSeries = this._locateSeries('flow', variant, FLOW_POSITIONS.outflow);
|
||||||
|
|
||||||
|
return {
|
||||||
|
variant,
|
||||||
|
inflow: this._seriesSamples(inflowSeries),
|
||||||
|
outflow: this._seriesSamples(outflowSeries)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_snapshotLevelForVariant(variant) {
|
||||||
|
const levelSeries = this._locateSeries('level', variant, ['atEquipment']);
|
||||||
|
return {
|
||||||
|
variant,
|
||||||
|
samples: this._seriesSamples(levelSeries)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_seriesSamples(seriesInfo) {
|
||||||
|
if (!seriesInfo) {
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
series: null,
|
||||||
|
current: null,
|
||||||
|
previous: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const current = seriesInfo.series.getLaggedSample(0);
|
||||||
|
const previous = seriesInfo.series.getLaggedSample(1);
|
||||||
|
return {
|
||||||
|
exists: Boolean(current),
|
||||||
|
series: seriesInfo.series,
|
||||||
|
current,
|
||||||
|
previous
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Failed to read samples for ${seriesInfo.type}.${seriesInfo.variant}.${seriesInfo.position}: ${err.message}`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
series: seriesInfo.series,
|
||||||
|
current: null,
|
||||||
|
previous: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_locateSeries(type, variant, positions) {
|
||||||
|
for (const position of positions) {
|
||||||
|
try {
|
||||||
|
this.measurements.type(type).variant(variant);
|
||||||
|
const exists = this.measurements.exists({ position, requireValues: true });
|
||||||
|
if (!exists) continue;
|
||||||
|
const series = this.measurements.type(type).variant(variant).position(position);
|
||||||
|
return { type, variant, position, series };
|
||||||
|
} catch (err) {
|
||||||
|
// ignore missing combinations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_estimateLevelRate(levelSnapshot) {
|
||||||
|
if (!levelSnapshot.samples.exists) return null;
|
||||||
|
const { current, previous } = levelSnapshot.samples;
|
||||||
|
if (!current || !previous || previous.timestamp == null) return null;
|
||||||
|
|
||||||
|
const deltaT = (current.timestamp - previous.timestamp) / 1000;
|
||||||
|
if (!Number.isFinite(deltaT) || deltaT <= 0) return null;
|
||||||
|
|
||||||
|
const deltaLevel = current.value - previous.value;
|
||||||
|
return deltaLevel / deltaT;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectBestNetFlow(snapshot) {
|
||||||
|
for (const variant of FLOW_VARIANTS) {
|
||||||
|
const flow = snapshot.flows[variant];
|
||||||
|
if (!flow.inflow.exists || !flow.outflow.exists) continue;
|
||||||
|
|
||||||
|
const inflow = flow.inflow.current?.value ?? null;
|
||||||
|
const outflow = flow.outflow.current?.value ?? null;
|
||||||
|
if (!Number.isFinite(inflow) || !Number.isFinite(outflow)) continue;
|
||||||
|
|
||||||
|
const net = inflow - outflow; // positive => filling
|
||||||
|
if (!Number.isFinite(net)) continue;
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: net,
|
||||||
|
source: variant,
|
||||||
|
direction: this._deriveDirection(net)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback using level trend
|
||||||
|
for (const variant of LEVEL_VARIANTS) {
|
||||||
|
const levelRate = snapshot.levelRates[variant];
|
||||||
|
if (!Number.isFinite(levelRate)) continue;
|
||||||
|
|
||||||
|
const netFlow = levelRate * this.basin.surfaceArea;
|
||||||
|
return {
|
||||||
|
value: netFlow,
|
||||||
|
source: `level:${variant}`,
|
||||||
|
direction: this._deriveDirection(netFlow)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn('No usable measurements to compute net flow; assuming steady.');
|
||||||
|
return { value: 0, source: null, direction: 'steady' };
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeRemainingTime(snapshot, netFlow) {
|
||||||
|
if (!netFlow || Math.abs(netFlow.value) < this.flowThreshold) {
|
||||||
|
return { seconds: null, source: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { heightOverflow, heightOutlet, surfaceArea } = this.basin;
|
||||||
|
if (!Number.isFinite(surfaceArea) || surfaceArea <= 0) {
|
||||||
|
this.logger.warn('Invalid basin surface area.');
|
||||||
|
return { seconds: null, source: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const variant of LEVEL_VARIANTS) {
|
||||||
|
const levelSnap = snapshot.levels[variant];
|
||||||
|
const current = levelSnap.samples.current?.value ?? null;
|
||||||
|
if (!Number.isFinite(current)) continue;
|
||||||
|
|
||||||
|
const remainingHeight =
|
||||||
|
netFlow.value > 0
|
||||||
|
? Math.max(heightOverflow - current, 0)
|
||||||
|
: Math.max(current - heightOutlet, 0);
|
||||||
|
|
||||||
|
const seconds = (remainingHeight * surfaceArea) / Math.abs(netFlow.value);
|
||||||
|
if (!Number.isFinite(seconds)) continue;
|
||||||
|
|
||||||
|
return { seconds, source: `${netFlow.source}/${variant}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn('No level data available to compute remaining time.');
|
||||||
|
return { seconds: null, source: netFlow.source };
|
||||||
|
}
|
||||||
|
|
||||||
|
_updatePredictedVolume(snapshot) {
|
||||||
|
const predicted = snapshot.flows.predicted;
|
||||||
|
if (!predicted) return;
|
||||||
|
|
||||||
|
const inflowCur = predicted.inflow.current;
|
||||||
|
const inflowPrev = predicted.inflow.previous ?? inflowCur;
|
||||||
|
const outflowCur = predicted.outflow.current;
|
||||||
|
const outflowPrev = predicted.outflow.previous ?? outflowCur;
|
||||||
|
|
||||||
|
const timestampNow =
|
||||||
|
inflowCur?.timestamp ?? outflowCur?.timestamp ?? inflowPrev?.timestamp ?? outflowPrev?.timestamp;
|
||||||
|
const timestampPrev = inflowPrev?.timestamp ?? outflowPrev?.timestamp ?? timestampNow;
|
||||||
|
|
||||||
|
if (!Number.isFinite(timestampNow) || !Number.isFinite(timestampPrev)) return;
|
||||||
|
|
||||||
|
const deltaSeconds = (timestampNow - timestampPrev) / 1000;
|
||||||
|
if (!Number.isFinite(deltaSeconds) || deltaSeconds <= 0) return;
|
||||||
|
|
||||||
|
const avgInflow = this._averageSampleValues(inflowCur, inflowPrev);
|
||||||
|
const avgOutflow = this._averageSampleValues(outflowCur, outflowPrev);
|
||||||
|
|
||||||
|
const netVolumeChange = (avgInflow - avgOutflow) * deltaSeconds;
|
||||||
|
if (!Number.isFinite(netVolumeChange) || netVolumeChange === 0) return;
|
||||||
|
|
||||||
|
const volumeSeries = this.measurements
|
||||||
|
.type('volume')
|
||||||
|
.variant('predicted')
|
||||||
|
.position('atEquipment');
|
||||||
|
|
||||||
|
const currentVolume = volumeSeries.getCurrentValue('m3') ?? this.basin.minVol;
|
||||||
|
const nextVolume = currentVolume + netVolumeChange;
|
||||||
|
|
||||||
|
const writeTimestamp = Number.isFinite(timestampNow) ? timestampNow : Date.now();
|
||||||
|
|
||||||
|
volumeSeries.value(nextVolume, writeTimestamp, 'm3').unit('m3');
|
||||||
|
|
||||||
|
const nextLevel = this._calcLevelFromVolume(nextVolume);
|
||||||
|
this.measurements
|
||||||
|
.type('level')
|
||||||
|
.variant('predicted')
|
||||||
|
.position('atEquipment')
|
||||||
|
.value(nextLevel, writeTimestamp, 'm')
|
||||||
|
.unit('m');
|
||||||
|
}
|
||||||
|
|
||||||
|
_averageSampleValues(sampleA, sampleB) {
|
||||||
|
const values = [sampleA?.value, sampleB?.value].filter((v) => Number.isFinite(v));
|
||||||
|
if (!values.length) return 0;
|
||||||
|
return values.reduce((acc, val) => acc + val, 0) / values.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
_deriveDirection(netFlow) {
|
||||||
|
if (netFlow > this.flowThreshold) return 'filling';
|
||||||
|
if (netFlow < -this.flowThreshold) return 'draining';
|
||||||
|
return 'steady';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Basin Calculations */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
initBasinProperties() {
|
||||||
|
const volEmptyBasin = this.config.basin.volume;
|
||||||
|
const heightBasin = this.config.basin.height;
|
||||||
|
const heightInlet = this.config.basin.heightInlet;
|
||||||
|
const heightOutlet = this.config.basin.heightOutlet;
|
||||||
|
const heightOverflow = this.config.basin.heightOverflow;
|
||||||
|
|
||||||
|
const surfaceArea = volEmptyBasin / heightBasin;
|
||||||
|
const maxVol = heightBasin * surfaceArea;
|
||||||
|
const maxVolOverflow = heightOverflow * surfaceArea;
|
||||||
|
const minVol = heightOutlet * surfaceArea;
|
||||||
|
const minVolOut = heightInlet * surfaceArea;
|
||||||
|
|
||||||
|
this.basin = {
|
||||||
|
volEmptyBasin,
|
||||||
|
heightBasin,
|
||||||
|
heightInlet,
|
||||||
|
heightOutlet,
|
||||||
|
heightOverflow,
|
||||||
|
surfaceArea,
|
||||||
|
maxVol,
|
||||||
|
maxVolOverflow,
|
||||||
|
minVol,
|
||||||
|
minVolOut
|
||||||
|
};
|
||||||
|
|
||||||
|
this.measurements
|
||||||
|
.type('volume')
|
||||||
|
.variant('predicted')
|
||||||
|
.position('atEquipment')
|
||||||
|
.value(minVol)
|
||||||
|
.unit('m3');
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Basin initialized | area=${surfaceArea.toFixed(2)} m2, max=${maxVol.toFixed(2)} m3, overflow=${maxVolOverflow.toFixed(2)} m3`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_calcVolumeFromLevel(level) {
|
||||||
|
return Math.max(level, 0) * this.basin.surfaceArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
_calcLevelFromVolume(volume) {
|
||||||
|
return Math.max(volume, 0) / this.basin.surfaceArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Output */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
getOutput() {
|
||||||
|
const output = {};
|
||||||
|
Object.entries(this.measurements.measurements).forEach(([type, variants]) => {
|
||||||
|
Object.entries(variants).forEach(([variant, positions]) => {
|
||||||
|
Object.entries(positions).forEach(([position, measurement]) => {
|
||||||
|
output[`${type}.${variant}.${position}`] = measurement.getCurrentValue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
output.state = this.state;
|
||||||
|
output.basin = this.basin;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PumpingStationV2;
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Example usage */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
const Measurement = require('../../measurement/src/specificClass');
|
||||||
|
const RotatingMachine = require('../../rotatingMachine/src/specificClass');
|
||||||
|
|
||||||
|
function createPumpingStationConfig(name) {
|
||||||
|
return {
|
||||||
|
general: {
|
||||||
|
logging: { enabled: true, logLevel: 'debug' },
|
||||||
|
name,
|
||||||
|
id: `${name}-${Date.now()}`,
|
||||||
|
flowThreshold: 1e-4
|
||||||
|
},
|
||||||
|
functionality: {
|
||||||
|
softwareType: 'pumpingStation',
|
||||||
|
role: 'stationcontroller'
|
||||||
|
},
|
||||||
|
basin: {
|
||||||
|
volume: 43.75,
|
||||||
|
height: 3.5,
|
||||||
|
heightInlet: 0.3,
|
||||||
|
heightOutlet: 0.2,
|
||||||
|
heightOverflow: 3.0
|
||||||
|
},
|
||||||
|
hydraulics: {
|
||||||
|
refHeight: 'NAP',
|
||||||
|
basinBottomRef: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLevelMeasurementConfig(name) {
|
||||||
|
return {
|
||||||
|
general: {
|
||||||
|
logging: { enabled: true, logLevel: 'debug' },
|
||||||
|
name,
|
||||||
|
id: `${name}-${Date.now()}`,
|
||||||
|
unit: 'm'
|
||||||
|
},
|
||||||
|
functionality: {
|
||||||
|
softwareType: 'measurement',
|
||||||
|
role: 'sensor',
|
||||||
|
positionVsParent: 'atEquipment'
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
category: 'sensor',
|
||||||
|
type: 'level',
|
||||||
|
model: 'demo-level',
|
||||||
|
supplier: 'demoCo',
|
||||||
|
unit: 'm'
|
||||||
|
},
|
||||||
|
scaling: { enabled: false },
|
||||||
|
smoothing: { smoothWindow: 5, smoothMethod: 'none' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFlowMeasurementConfig(name, position) {
|
||||||
|
return {
|
||||||
|
general: {
|
||||||
|
logging: { enabled: true, logLevel: 'debug' },
|
||||||
|
name,
|
||||||
|
id: `${name}-${Date.now()}`,
|
||||||
|
unit: 'm3/s'
|
||||||
|
},
|
||||||
|
functionality: {
|
||||||
|
softwareType: 'measurement',
|
||||||
|
role: 'sensor',
|
||||||
|
positionVsParent: position
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
category: 'sensor',
|
||||||
|
type: 'flow',
|
||||||
|
model: 'demo-flow',
|
||||||
|
supplier: 'demoCo',
|
||||||
|
unit: 'm3/s'
|
||||||
|
},
|
||||||
|
scaling: { enabled: false },
|
||||||
|
smoothing: { smoothWindow: 5, smoothMethod: 'none' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMachineConfig(name) {
|
||||||
|
return {
|
||||||
|
general: {
|
||||||
|
name,
|
||||||
|
logging: { enabled: true, logLevel: 'debug' }
|
||||||
|
},
|
||||||
|
functionality: {
|
||||||
|
positionVsParent: 'downstream'
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
supplier: 'Hydrostal',
|
||||||
|
type: 'pump',
|
||||||
|
category: 'centrifugal',
|
||||||
|
model: 'hidrostal-H05K-S03R'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMachineStateConfig() {
|
||||||
|
return {
|
||||||
|
general: {
|
||||||
|
logging: {
|
||||||
|
enabled: true,
|
||||||
|
logLevel: 'debug'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
movement: { speed: 1 },
|
||||||
|
time: {
|
||||||
|
starting: 2,
|
||||||
|
warmingup: 3,
|
||||||
|
stopping: 2,
|
||||||
|
coolingdown: 3
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function seedSample(measurement, type, value, unit) {
|
||||||
|
const pos = measurement.config.functionality.positionVsParent;
|
||||||
|
measurement.measurements.type(type).variant('measured').position(pos).value(value, Date.now(), unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function demo() {
|
||||||
|
const station = new PumpingStationV2(createPumpingStationConfig('PumpingStationDemo'));
|
||||||
|
const pump = new RotatingMachine(createMachineConfig('Pump1'), createMachineStateConfig());
|
||||||
|
|
||||||
|
const levelSensor = new Measurement(createLevelMeasurementConfig('WetWellLevel'));
|
||||||
|
const inflowSensor = new Measurement(createFlowMeasurementConfig('InfluentFlow', 'in'));
|
||||||
|
const outflowSensor = new Measurement(createFlowMeasurementConfig('PumpDischargeFlow', 'out'));
|
||||||
|
|
||||||
|
station.childRegistrationUtils.registerChild(levelSensor, levelSensor.config.functionality.softwareType);
|
||||||
|
station.childRegistrationUtils.registerChild(inflowSensor, inflowSensor.config.functionality.softwareType);
|
||||||
|
station.childRegistrationUtils.registerChild(outflowSensor, outflowSensor.config.functionality.softwareType);
|
||||||
|
station.childRegistrationUtils.registerChild(pump, 'machine');
|
||||||
|
|
||||||
|
// Seed initial measurements
|
||||||
|
seedSample(levelSensor, 'level', 1.8, 'm');
|
||||||
|
seedSample(inflowSensor, 'flow', 0.35, 'm3/s');
|
||||||
|
seedSample(outflowSensor, 'flow', 0.20, 'm3/s');
|
||||||
|
|
||||||
|
setInterval(() => station.tick(), 1000);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
|
||||||
|
station.tick();
|
||||||
|
console.log('Initial state:', station.state);
|
||||||
|
|
||||||
|
await pump.handleInput('parent', 'execSequence', 'startup');
|
||||||
|
await pump.handleInput('parent', 'execMovement', 50);
|
||||||
|
|
||||||
|
console.log('Station state:', station.state);
|
||||||
|
console.log('Station output:', station.getOutput());
|
||||||
|
})().catch((err) => {
|
||||||
|
console.error('Demo failed:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user