767 lines
24 KiB
JavaScript
767 lines
24 KiB
JavaScript
const EventEmitter = require('events');
|
|
const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation} = require('generalFunctions');
|
|
class PumpingStation {
|
|
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('netFlowRate', 'm3/s');
|
|
this.measurements.setPreferredUnit('level', 'm');
|
|
this.measurements.setPreferredUnit('volume', 'm3');
|
|
this.childRegistrationUtils = new childRegistrationUtils(this);
|
|
this.machines = {};
|
|
this.stations = {};
|
|
|
|
|
|
//variants in determining what gets priority
|
|
this.flowVariants = ['measured', 'predicted'];
|
|
this.levelVariants = ['measured', 'predicted'];
|
|
this.volVariants = ['measured', 'predicted'];
|
|
this.flowPositions = { inflow: ['in', 'upstream'], outflow: ['out', 'downstream'] };
|
|
this.predictedFlowChildren = new Map(); // childId -> { in: 0, out: 0 }
|
|
|
|
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' || softwareType === 'machineGroupController') {
|
|
this._registerPredictedFlowChild(child);
|
|
return;
|
|
}
|
|
|
|
this.logger.warn(`Unsupported child software type: ${softwareType}`);
|
|
}
|
|
|
|
_safeGuardSystem(snapshot,remainingTime){
|
|
let vol = null;
|
|
|
|
for (const variant of this.volVariants){
|
|
const volsnap = snapshot.vols[variant];
|
|
//go through with variants until we find one that exists
|
|
if (!volsnap.samples.exists){ continue};
|
|
|
|
const vol = volsnap.samples.current?.value ?? null;
|
|
}
|
|
|
|
if(vol == null){
|
|
//if we cant get a volume, we must force whole system off.
|
|
|
|
};
|
|
/*
|
|
if(remainingTime < timeThreshhold || vol > maxVolume || vol < minVolume){}
|
|
*/
|
|
|
|
}
|
|
|
|
tick() {
|
|
const snapshot = this._takeMeasurementSnapshot();
|
|
|
|
this._updatePredictedVolume(snapshot);
|
|
|
|
|
|
const netFlow = this._selectBestNetFlow(snapshot);
|
|
const remaining = this._computeRemainingTime(snapshot, netFlow);
|
|
|
|
this._safeGuardSystem(snapshot,remaining.seconds);
|
|
|
|
this.state = {
|
|
direction: netFlow.direction,
|
|
netFlow: netFlow.value,
|
|
flowSource: netFlow.source,
|
|
seconds: remaining.seconds,
|
|
remainingSource: remaining.source
|
|
};
|
|
|
|
this.logger.debug(`netflow = ${JSON.stringify(netFlow)}`);
|
|
this.logger.debug(`Height : ${this.measurements.type('level').variant('predicted').position('atequipment').getCurrentValue('m') } m`);
|
|
}
|
|
|
|
|
|
_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);
|
|
});
|
|
}
|
|
|
|
//register machines or pumping stations that can provide predicted flow data
|
|
_registerPredictedFlowChild(child) {
|
|
const position = child.config.functionality.positionVsParent;
|
|
const childName = child.config.general.name;
|
|
const childId = child.config.general.id ?? childName;
|
|
|
|
const posKey =
|
|
position === 'downstream' || position === 'out' || position === 'atequipment'
|
|
? 'out'
|
|
: position === 'upstream' || position === 'in'
|
|
? 'in'
|
|
: null;
|
|
|
|
if (!posKey) {
|
|
this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`);
|
|
return;
|
|
}
|
|
|
|
if (!this.predictedFlowChildren.has(childId)) {
|
|
this.predictedFlowChildren.set(childId, { in: 0, out: 0 });
|
|
}
|
|
|
|
const handler = (eventData = {}) => {
|
|
const value = Number.isFinite(eventData.value) ? eventData.value : 0;
|
|
const timestamp = eventData.timestamp ?? Date.now();
|
|
const unit = eventData.unit ?? 'm3/s';
|
|
|
|
this.logger.debug(
|
|
`Predicted flow update from ${childName} (${childId}, ${posKey}) -> ${value} ${unit}`
|
|
);
|
|
|
|
this.predictedFlowChildren.get(childId)[posKey] = value;
|
|
this._refreshAggregatedPredictedFlow(posKey, timestamp, unit);
|
|
};
|
|
|
|
const eventNames =
|
|
posKey === 'in'
|
|
? ['flow.predicted.downstream', 'flow.predicted.upstream']
|
|
: ['flow.predicted.downstream'];
|
|
|
|
for (const eventName of eventNames) {
|
|
child.measurements.emitter.on(eventName, handler);
|
|
}
|
|
}
|
|
|
|
_refreshAggregatedPredictedFlow(direction, timestamp = Date.now(), unit = 'm3/s') {
|
|
const sum = Array.from(this.predictedFlowChildren.values())
|
|
.map((entry) => (Number.isFinite(entry[direction]) ? entry[direction] : 0))
|
|
.reduce((acc, val) => acc + val, 0);
|
|
|
|
this.measurements
|
|
.type('flow')
|
|
.variant('predicted')
|
|
.position(direction)
|
|
.value(sum, timestamp, unit);
|
|
}
|
|
|
|
_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('volumePercent')
|
|
.variant('measured')
|
|
.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: {},
|
|
vols:{},
|
|
};
|
|
|
|
for (const variant of this.flowVariants) {
|
|
snapshot.flows[variant] = this._snapshotFlowsForVariant(variant);
|
|
}
|
|
|
|
for (const variant of this.volVariants){
|
|
snapshot.vols[variant] = this._snapshotVolsForVariant(variant);
|
|
}
|
|
|
|
for (const variant of this.levelVariants) {
|
|
snapshot.levels[variant] = this._snapshotLevelForVariant(variant);
|
|
snapshot.levelRates[variant] = this._estimateLevelRate(snapshot.levels[variant]);
|
|
}
|
|
|
|
return snapshot;
|
|
}
|
|
|
|
_snapshotVolsForVariant(variant) {
|
|
const volumeSeries = this._locateSeries('volume', variant, ['atequipment']);
|
|
|
|
return {variant,samples: this._seriesSamples(volumeSeries)};
|
|
}
|
|
|
|
_snapshotFlowsForVariant(variant) {
|
|
const inflowSeries = this._locateSeries('flow', variant, this.flowPositions.inflow);
|
|
const outflowSeries = this._locateSeries('flow', variant, this.flowPositions.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, measurement: null, current: null, previous: null };
|
|
}
|
|
|
|
try {
|
|
const current = seriesInfo.measurement.getLaggedSample(0); // newest
|
|
const previous = seriesInfo.measurement.getLaggedSample(1); // previous
|
|
return {
|
|
exists: Boolean(current),
|
|
measurement: seriesInfo.measurement,
|
|
current,
|
|
previous
|
|
};
|
|
} catch (err) {
|
|
this.logger.debug(
|
|
`Failed to read samples for ${seriesInfo.type}.${seriesInfo.variant}.${seriesInfo.position}: ${err.message}`
|
|
);
|
|
return { exists: false, measurement: seriesInfo.measurement, current: null, previous: null };
|
|
}
|
|
}
|
|
|
|
_locateSeries(type, variant, positions) {
|
|
for (const position of positions) {
|
|
try {
|
|
const chain = this.measurements.type(type).variant(variant).position(position);
|
|
if (!chain.exists({ requireValues: true })) continue;
|
|
|
|
const measurement = chain.get();
|
|
if (!measurement) continue;
|
|
|
|
return { type, variant, position, measurement };
|
|
} 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 this.flowVariants) {
|
|
const flow = snapshot.flows[variant];
|
|
|
|
if (!flow.inflow.exists && !flow.outflow.exists) continue;
|
|
|
|
const inflow = flow.inflow.current?.value ?? 0;
|
|
const outflow = flow.outflow.current?.value ?? 0;
|
|
const net = inflow - outflow; // positive => filling
|
|
|
|
this.measurements.type('netFlowRate').variant(variant).position('atequipment').value(net).unit('m3/s');
|
|
this.logger.debug(`inflow : ${inflow} - outflow : ${outflow}`);
|
|
|
|
return { value: net,source: variant,direction: this._deriveDirection(net) };
|
|
}
|
|
|
|
// fallback using level trend
|
|
for (const variant of this.levelVariants) {
|
|
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 this.levelVariants) {
|
|
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 now = Date.now();
|
|
const inflowSample = predicted.inflow.current ?? predicted.inflow.previous ?? null;
|
|
const outflowSample = predicted.outflow.current ?? predicted.outflow.previous ?? null;
|
|
|
|
if (!this._predictedFlowState) {
|
|
this._predictedFlowState = {
|
|
inflow: inflowSample?.value ?? 0,
|
|
outflow: outflowSample?.value ?? 0,
|
|
lastTimestamp: inflowSample?.timestamp ?? outflowSample?.timestamp ?? now
|
|
};
|
|
}
|
|
|
|
if (inflowSample) this._predictedFlowState.inflow = inflowSample.value;
|
|
if (outflowSample) this._predictedFlowState.outflow = outflowSample.value;
|
|
|
|
const latestObservedTimestamp =
|
|
inflowSample?.timestamp ?? outflowSample?.timestamp ?? this._predictedFlowState.lastTimestamp;
|
|
|
|
const timestampPrev = this._predictedFlowState.lastTimestamp ?? latestObservedTimestamp;
|
|
|
|
let timestampNow = latestObservedTimestamp;
|
|
if (!Number.isFinite(timestampNow) || timestampNow <= timestampPrev) {
|
|
timestampNow = now;
|
|
}
|
|
|
|
let deltaSeconds = (timestampNow - timestampPrev) / 1000;
|
|
if (!Number.isFinite(deltaSeconds) || deltaSeconds <= 0) {
|
|
deltaSeconds = 0;
|
|
}
|
|
|
|
let netVolumeChange = 0;
|
|
if (deltaSeconds > 0) {
|
|
const avgInflow = inflowSample ? inflowSample.value : this._predictedFlowState.inflow;
|
|
const avgOutflow = outflowSample ? outflowSample.value : this._predictedFlowState.outflow;
|
|
netVolumeChange = (avgInflow - avgOutflow) * deltaSeconds;
|
|
}
|
|
|
|
const writeTimestamp = timestampPrev + Math.max(deltaSeconds, 0) * 1000;
|
|
|
|
const volumeSeries = this.measurements.type('volume').variant('predicted').position('atEquipment');
|
|
|
|
const currentVolume = volumeSeries.getCurrentValue('m3') ?? this.basin.minVol;
|
|
|
|
const nextVolume = currentVolume + netVolumeChange;
|
|
|
|
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');
|
|
|
|
//calc how full this is in procen using minVol vs maxVolOverflow
|
|
const percent = this.interpolate.interpolate_lin_single_point(
|
|
currentVolume,
|
|
this.basin.minVol,
|
|
this.basin.maxVolOverflow,
|
|
0,
|
|
100
|
|
);
|
|
|
|
//store this percent value
|
|
this.measurements
|
|
.type('volumePercent')
|
|
.variant('predicted')
|
|
.position('atequipment')
|
|
.value(percent);
|
|
|
|
this._predictedFlowState.lastTimestamp = writeTimestamp;
|
|
}
|
|
|
|
_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(maxVolOverflow)
|
|
.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.direction = this.state.direction;
|
|
output.flowSource = this.state.flowSource;
|
|
output.timeleft = this.state.seconds;
|
|
output.volEmptyBasin = this.basin.volEmptyBasin;
|
|
output.heightInlet = this.basin.heightInlet;
|
|
output.heightOverflow = this.basin.heightOverflow;
|
|
output.maxVol = this.basin.maxVol;
|
|
output.minVol = this.basin.minVol;
|
|
output.maxVolOverflow = this.basin.maxVolOverflow;
|
|
output.minVolOut = this.basin.minVolOut;
|
|
return output;
|
|
}
|
|
}
|
|
|
|
module.exports = PumpingStation;
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 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: 10,
|
|
heightInlet: 3,
|
|
heightOutlet: 0.2,
|
|
heightOverflow: 3.2
|
|
},
|
|
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,position) {
|
|
return {
|
|
general: {
|
|
name,
|
|
logging: { enabled: false, logLevel: 'debug' }
|
|
},
|
|
functionality: {
|
|
softwareType: "machine",
|
|
positionVsParent: position
|
|
},
|
|
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 PumpingStation(createPumpingStationConfig('PumpingStationDemo'));
|
|
const pump1 = new RotatingMachine(createMachineConfig('Pump1','downstream'), createMachineStateConfig());
|
|
const pump2 = new RotatingMachine(createMachineConfig('Pump2','upstream'), 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(pump1, 'machine');
|
|
station.childRegistrationUtils.registerChild(pump2, '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));
|
|
|
|
console.log('Initial state:', station.state);
|
|
|
|
await pump1.handleInput('parent', 'execSequence', 'startup');
|
|
await pump1.handleInput('parent', 'execMovement', 10);
|
|
|
|
await pump2.handleInput('parent', 'execSequence', 'startup');
|
|
await pump2.handleInput('parent', 'execMovement', 10);
|
|
|
|
|
|
console.log('Station state:', station.state);
|
|
console.log('Station output:', station.getOutput());
|
|
})().catch((err) => {
|
|
console.error('Demo failed:', err);
|
|
});
|
|
}
|
|
//*/
|