dev-Rene #1
@@ -85,7 +85,6 @@ class pumpingStation {
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
case("upstream"):
|
||||
//check for predicted outgoing flow at the connected child pumpingsation
|
||||
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
||||
@@ -152,8 +151,8 @@ class pumpingStation {
|
||||
const prevFlow = series.getLaggedValue(1, "m3/s"); // { value, timestamp, unit }
|
||||
|
||||
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
|
||||
const deltaT = currFLow.timestamp - prevFlow.timestamp;
|
||||
@@ -222,7 +221,7 @@ class pumpingStation {
|
||||
//this._calcNetFlow();
|
||||
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(){
|
||||
@@ -624,7 +623,7 @@ const PumpingStation = require("./specificClass");
|
||||
const RotatingMachine = require("../../rotatingMachine/src/specificClass");
|
||||
const Measurement = require("../../measurement/src/specificClass");
|
||||
|
||||
/** Helpers ******************************************************************/
|
||||
//Helpers
|
||||
function createPumpingStationConfig(name) {
|
||||
return {
|
||||
general: {
|
||||
@@ -754,10 +753,11 @@ function pushSample(measurement, type, value, unit) {
|
||||
.value(value, Date.now(), unit);
|
||||
}
|
||||
|
||||
/** Demo *********************************************************************/
|
||||
// Demo
|
||||
(async function demoStationWithPump() {
|
||||
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 upstreamFlow = new Measurement(createFlowMeasurementConfig("InfluentFlow", "upstream"));
|
||||
@@ -765,15 +765,16 @@ function pushSample(measurement, type, value, unit) {
|
||||
|
||||
|
||||
// station uses the sensors
|
||||
/*
|
||||
|
||||
station.childRegistrationUtils.registerChild(levelSensor, levelSensor.config.functionality.softwareType);
|
||||
station.childRegistrationUtils.registerChild(upstreamFlow, upstreamFlow.config.functionality.softwareType);
|
||||
station.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.softwareType);
|
||||
*/
|
||||
|
||||
|
||||
// pump owns the downstream flow sensor
|
||||
pump.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.positionVsParent);
|
||||
station.childRegistrationUtils.registerChild(pump,"downstream");
|
||||
//pump.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.positionVsParent);
|
||||
station.childRegistrationUtils.registerChild(pump1,"downstream");
|
||||
station.childRegistrationUtils.registerChild(pump2,"upstream");
|
||||
|
||||
setInterval(() => station.tick(), 1000);
|
||||
|
||||
@@ -782,7 +783,7 @@ function pushSample(measurement, type, value, unit) {
|
||||
pushSample(levelSensor, "level", 1.8, "m");
|
||||
pushSample(upstreamFlow, "flow", 0.35, "m3/s");
|
||||
pushSample(downstreamFlow, "flow", 0.20, "m3/s");
|
||||
*/
|
||||
//*/
|
||||
|
||||
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(upstreamFlow, "flow", 0.40, "m3/s");
|
||||
pushSample(levelSensor, "level", 1.85, "m");
|
||||
*/
|
||||
//*/
|
||||
|
||||
console.log("Station output:", station.getOutput());
|
||||
await pump.handleInput("parent", "execSequence", "startup");
|
||||
await pump.handleInput("parent", "execMovement", 50);
|
||||
await pump1.handleInput("parent", "execSequence", "startup");
|
||||
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 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