dev-Rene #1
1297
src/specificClass.js
1297
src/specificClass.js
File diff suppressed because it is too large
Load Diff
@@ -1,681 +0,0 @@
|
|||||||
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