|
|
|
|
@@ -1,5 +1,5 @@
|
|
|
|
|
const EventEmitter = require('events');
|
|
|
|
|
const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop} = require('generalFunctions');
|
|
|
|
|
const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation} = require('generalFunctions');
|
|
|
|
|
|
|
|
|
|
class pumpingStation {
|
|
|
|
|
constructor(config={}) {
|
|
|
|
|
@@ -9,6 +9,7 @@ class pumpingStation {
|
|
|
|
|
this.defaultConfig = this.configManager.getConfig('pumpingStation');
|
|
|
|
|
this.configUtils = new configUtils(this.defaultConfig);
|
|
|
|
|
this.config = this.configUtils.initConfig(config);
|
|
|
|
|
this.interpolate = new interpolation();
|
|
|
|
|
|
|
|
|
|
// Init after config is set
|
|
|
|
|
this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name);
|
|
|
|
|
@@ -24,8 +25,10 @@ class pumpingStation {
|
|
|
|
|
|
|
|
|
|
// Initialize basin-specific properties and calculate used parameters
|
|
|
|
|
this.initBasinProperties();
|
|
|
|
|
|
|
|
|
|
this.parent = {}; // object to hold parent information for when we follow flow directions.
|
|
|
|
|
this.child = {}; // object to hold child information so we know on what to subscribe
|
|
|
|
|
this.machines = {}; // object to hold child machine information
|
|
|
|
|
this.stations = {}; // object to hold station information
|
|
|
|
|
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
|
|
|
|
|
|
|
|
|
|
this.logger.debug('pumpstation Initialized with all helpers');
|
|
|
|
|
@@ -35,6 +38,7 @@ class pumpingStation {
|
|
|
|
|
registerChild(child, softwareType) {
|
|
|
|
|
this.logger.debug('Setting up child event for softwaretype ' + softwareType);
|
|
|
|
|
|
|
|
|
|
//define what to do with measurements
|
|
|
|
|
if(softwareType === "measurement"){
|
|
|
|
|
const position = child.config.functionality.positionVsParent;
|
|
|
|
|
const distance = child.config.functionality.distanceVsParent || 0;
|
|
|
|
|
@@ -50,18 +54,170 @@ class pumpingStation {
|
|
|
|
|
|
|
|
|
|
this.logger.debug(` Emitting... ${eventName} with data:`);
|
|
|
|
|
// Store directly in parent's measurement container
|
|
|
|
|
this.measurements
|
|
|
|
|
.type(measurementType)
|
|
|
|
|
.variant("measured")
|
|
|
|
|
.position(position)
|
|
|
|
|
.value(eventData.value, eventData.timestamp, eventData.unit);
|
|
|
|
|
this.measurements.type(measurementType).variant("measured").position(position).value(eventData.value, eventData.timestamp, eventData.unit);
|
|
|
|
|
|
|
|
|
|
// Call the appropriate handler
|
|
|
|
|
this._callMeasurementHandler(measurementType, eventData.value, position, eventData);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//define what to do when machines are connected
|
|
|
|
|
if(softwareType == "machine"){
|
|
|
|
|
// Check if the machine is already registered
|
|
|
|
|
this.machines[child.config.general.id] === undefined ? this.machines[child.config.general.id] = child : this.logger.warn(`Machine ${child.config.general.id} is already registered.`);
|
|
|
|
|
|
|
|
|
|
//listen for machine pressure changes
|
|
|
|
|
this.logger.debug(`Listening for flow changes from machine ${child.config.general.id}`);
|
|
|
|
|
|
|
|
|
|
switch(child.config.functionality.positionVsParent){
|
|
|
|
|
case("downstream"):
|
|
|
|
|
case("atequipment"): //in case of atequipment we also assume downstream seeing as it is registered at this pumpingstation as part of it.
|
|
|
|
|
//for now lets focus on handling downstream predicted flow
|
|
|
|
|
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
|
|
|
|
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
|
|
|
|
this.measurements.type('flow').variant('predicted').position('out').value(eventData.value,eventData.timestamp,eventData.unit);
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
case("upstream"):
|
|
|
|
|
//check for predicted outgoing flow at the connected child pumpingsation
|
|
|
|
|
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
|
|
|
|
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
|
|
|
|
//register this then as upstream flow that arrives at the station
|
|
|
|
|
this.measurements.type('flow').variant('predicted').position('in').value(eventData.value,eventData.timestamp,eventData.unit);
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
this.logger.warn(`nu such position ${child.config.functionality.positionVsParent}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// add one for group later
|
|
|
|
|
if( softwareType == "machineGroup" ){
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// add one for pumping station
|
|
|
|
|
if ( softwareType == "pumpingStation"){
|
|
|
|
|
// Check if the machine is already registered
|
|
|
|
|
this.stations[child.config.general.id] === undefined ? this.machistationsnes[child.config.general.id] = child : this.logger.warn(`Machine ${child.config.general.id} is already registered.`);
|
|
|
|
|
|
|
|
|
|
//listen for machine pressure changes
|
|
|
|
|
this.logger.debug(`Listening for flow changes from machine ${child.config.general.id}`);
|
|
|
|
|
|
|
|
|
|
switch(child.config.functionality.positionVsParent){
|
|
|
|
|
case("downstream"):
|
|
|
|
|
//check for predicted outgoing flow at the connected child pumpingsation
|
|
|
|
|
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
|
|
|
|
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
|
|
|
|
//register this then as upstream flow that arrives at the station
|
|
|
|
|
this.measurements.type('flow').variant('predicted').position('out').value(eventData.value,eventData.timestamp,eventData.unit);
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case("upstream"):
|
|
|
|
|
//check for predicted outgoing flow at the connected child pumpingsation
|
|
|
|
|
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
|
|
|
|
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
|
|
|
|
//register this then as upstream flow that arrives at the station
|
|
|
|
|
this.measurements.type('flow').variant('predicted').position('in').value(eventData.value,eventData.timestamp,eventData.unit);
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// there is no such thing as atequipment from 1 pumpingstation to another....
|
|
|
|
|
this.logger.warn(`nu such position ${child.config.functionality.positionVsParent} for pumping station`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//in or outgoing flow = direction
|
|
|
|
|
_updateVolumePrediction(flowDir){
|
|
|
|
|
|
|
|
|
|
//get downflow
|
|
|
|
|
const seriesExists = this.measurements.type("flow").variant("predicted").position(flowDir).exists();
|
|
|
|
|
if(!seriesExists){return};
|
|
|
|
|
|
|
|
|
|
const series = this.measurements.type("flow").variant("predicted").position(flowDir);
|
|
|
|
|
const currFLow = series.getLaggedValue(0, "m3/s"); // { value, timestamp, unit }
|
|
|
|
|
const prevFlow = series.getLaggedValue(1, "m3/s"); // { value, timestamp, unit }
|
|
|
|
|
|
|
|
|
|
if (!currFLow || !prevFlow) return;
|
|
|
|
|
|
|
|
|
|
this.logger.debug(`currDownflow = ${currFLow.value} , prevDownFlow = ${prevFlow.value}`);
|
|
|
|
|
|
|
|
|
|
// calc difference in time
|
|
|
|
|
const deltaT = currFLow.timestamp - prevFlow.timestamp;
|
|
|
|
|
const deltaSeconds = deltaT / 1000;
|
|
|
|
|
|
|
|
|
|
if (deltaSeconds <= 0) {
|
|
|
|
|
this.logger.warn(`Flow integration aborted; invalid Δt=${deltaSeconds}s.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const avgFlow = (currFLow.value + prevFlow.value) / 2;
|
|
|
|
|
const calcVol = avgFlow * deltaSeconds;
|
|
|
|
|
|
|
|
|
|
//substract seeing as this is downstream and is being pulled away from the pumpingstaion and keep track of status
|
|
|
|
|
const currVolume = this.measurements.type('volume').variant('predicted').position('atEquipment').getCurrentValue('m3');
|
|
|
|
|
let newVol = currVolume;
|
|
|
|
|
|
|
|
|
|
switch(flowDir){
|
|
|
|
|
case("out"):
|
|
|
|
|
newVol = currVolume - calcVol;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case("in"):
|
|
|
|
|
newVol = currVolume + calcVol;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
this.logger.error('Flow must come in or out of the station!');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.measurements.type('volume').variant('predicted').position('atEquipment').value(newVol).unit('m3');
|
|
|
|
|
//convert to a predicted level
|
|
|
|
|
const newLevel = this._calcLevelFromVolume(newVol);
|
|
|
|
|
|
|
|
|
|
this.measurements.type('level').variant('predicted').position('atEquipment').value(newLevel).unit('m');
|
|
|
|
|
|
|
|
|
|
this.logger.debug(`new predicted volume : ${newVol} new predicted level: ${newLevel} `);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//trigger shutdown when level is too low and trigger no start flag for childs ?
|
|
|
|
|
safetyVolCheck(){
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//update measured temperature to adjust density of liquid
|
|
|
|
|
updateMeasuredTemperature(){
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//update measured flow and recalc
|
|
|
|
|
updateMeasuredFlow(){
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//keep updating the volume / level when the flow is still active from a machine or machinegroup or incoming from another source
|
|
|
|
|
tick(){
|
|
|
|
|
//go through all the functions that require time based checks or updates
|
|
|
|
|
this._updateVolumePrediction("out"); //check for changes in outgoing flow
|
|
|
|
|
this._updateVolumePrediction("in"); // check for changes in incomming flow
|
|
|
|
|
//calc the most important values back to determine state and net up or downstream flow
|
|
|
|
|
this._calcNetFlow();
|
|
|
|
|
this._calcTimeRemaining();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_callMeasurementHandler(measurementType, value, position, context) {
|
|
|
|
|
switch (measurementType) {
|
|
|
|
|
case 'pressure':
|
|
|
|
|
@@ -86,7 +242,7 @@ class pumpingStation {
|
|
|
|
|
this.updatePosition();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// context handler for pressure updates
|
|
|
|
|
updateMeasuredPressure(value, position, context = {}) {
|
|
|
|
|
@@ -108,6 +264,7 @@ class pumpingStation {
|
|
|
|
|
this.logger.warn(`No temperature measurement available, defaulting to 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');
|
|
|
|
|
this.logger.debug(`Temperature is : ${kelvinTemp}`);
|
|
|
|
|
} else {
|
|
|
|
|
kelvinTemp = mTemp;
|
|
|
|
|
}
|
|
|
|
|
@@ -118,11 +275,10 @@ class pumpingStation {
|
|
|
|
|
const level = pressure_Pa / density * g;
|
|
|
|
|
|
|
|
|
|
this.measurements.type("level").variant("predicted").position(position).value(level);
|
|
|
|
|
//updatePredictedLevel(); ??
|
|
|
|
|
//updatePredictedLevel(); ?? OLIFANT!
|
|
|
|
|
|
|
|
|
|
//calculate how muc flow went in or out based on pressure difference
|
|
|
|
|
this.logger.debug(`Using pressure: ${value} for calculations`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -134,111 +290,89 @@ class pumpingStation {
|
|
|
|
|
const level = this.measurements.type("level").variant("measured").position(position).getCurrentValue('m');
|
|
|
|
|
//calc vol in m3
|
|
|
|
|
const volume = this._calcVolumeFromLevel(level);
|
|
|
|
|
this.measurements.type("volume").variant("measured").position("atEquipment").value(volume).unit('m3');
|
|
|
|
|
this.logger.debug(`basin minvol : ${this.basin.minVol}, cur volume : ${volume} / ${this.basin.maxVolOverflow}`);
|
|
|
|
|
|
|
|
|
|
//calc the most important values back to determine state and net up or downstream flow
|
|
|
|
|
this._calcNetFlow();
|
|
|
|
|
const proc = this.interpolate.interpolate_lin_single_point(volume,this.basin.minVol,this.basin.maxVolOverflow,0,100);
|
|
|
|
|
this.logger.debug(`PROC volume : ${proc}`);
|
|
|
|
|
this.measurements.type("volume").variant("measured").position("atEquipment").value(volume).unit('m3');
|
|
|
|
|
this.measurements.type("volume").variant("procent").position("atEquipment").value(proc);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_calcNetFlow() {
|
|
|
|
|
let netFlow = null;
|
|
|
|
|
|
|
|
|
|
const netFlow_FlowSensor = Math.abs(this.measurements.type("flow").variant("measured").difference({ from: "downstream", to: "upstream", unit: "m3/s" }));
|
|
|
|
|
const netFlow_LevelSensor = this._calcNetFlowFromLevelDiff();
|
|
|
|
|
const netFlow_PredictedFlow = Math.abs(this.measurements.type('flow').variant('predicted').difference({ from: "in", to: "out", unit: "m3/s" }));
|
|
|
|
|
|
|
|
|
|
switch (true){
|
|
|
|
|
//prefer flowsensor netflow
|
|
|
|
|
case (netFlow_FlowSensor!=null):
|
|
|
|
|
return netFlow_FlowSensor;
|
|
|
|
|
//try using level difference if possible to infer netflow
|
|
|
|
|
case (netFlow_LevelSensor!= null):
|
|
|
|
|
return netFlow_LevelSensor;
|
|
|
|
|
case (netFlow_PredictedFlow != null):
|
|
|
|
|
return netFlow_PredictedFlow;
|
|
|
|
|
default:
|
|
|
|
|
this.logger.warn(`Can't calculate netflow without the proper measurements or predictions`);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_calcRemainingTime(level,variant){
|
|
|
|
|
|
|
|
|
|
const { heightOverflow, heightOutlet, surfaceArea } = this.basin;
|
|
|
|
|
const flowDiff = this.measurements.type("flow").variant(variant).difference({ from: "downstream", to: "upstream", unit: "m3/s" });
|
|
|
|
|
|
|
|
|
|
const flowBased = this._calcNetFlowFromMeasurements({
|
|
|
|
|
heightOverflow,
|
|
|
|
|
heightOutlet,
|
|
|
|
|
surfaceArea
|
|
|
|
|
});
|
|
|
|
|
switch(true){
|
|
|
|
|
case(flowDiff>0):
|
|
|
|
|
remainingHeight = Math.max(heightOverflow - level, 0);
|
|
|
|
|
this.state.seconds = remainingHeight * surfaceArea / flowDiff;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
const levelBased = this._calcNetFlowFromLevel({
|
|
|
|
|
heightOverflow,
|
|
|
|
|
heightOutlet,
|
|
|
|
|
surfaceArea
|
|
|
|
|
});
|
|
|
|
|
case(flowDiff<0):
|
|
|
|
|
remainingHeight = Math.max(level - heightOutlet, 0);
|
|
|
|
|
this.state.seconds = remainingHeight * surfaceArea / Math.abs(flowDiff);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
if (flowBased && levelBased) {
|
|
|
|
|
this.logger.debug(
|
|
|
|
|
`Flow vs Level comparison | flow=${flowBased.netFlowRate.toFixed(3)} ` +
|
|
|
|
|
`m3/s, level=${levelBased.netFlowRate.toFixed(3)} m3/s`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
this.logger.debug(`doing nothing with level calc`)
|
|
|
|
|
|
|
|
|
|
const effective = flowBased || levelBased;
|
|
|
|
|
if (effective) {
|
|
|
|
|
this.state = effective.state;
|
|
|
|
|
this.state.netFlowSource = flowBased ? (levelBased ? "flow+level" : "flow") : "level";
|
|
|
|
|
this.logger.debug(`Net-flow state: ${JSON.stringify(this.state)}`);
|
|
|
|
|
} else {
|
|
|
|
|
this.logger.debug("Net-flow state: insufficient data");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return effective;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_calcNetFlowFromMeasurements({ heightOverflow, heightOutlet, surfaceArea }) {
|
|
|
|
|
const flowDiff = this.measurements
|
|
|
|
|
.type("flow")
|
|
|
|
|
.variant("measured")
|
|
|
|
|
.difference({ from: "downstream", to: "upstream", unit: "m3/s" });
|
|
|
|
|
_calcDirection(flowDiff){
|
|
|
|
|
|
|
|
|
|
const level = this.measurements
|
|
|
|
|
.type("level")
|
|
|
|
|
.variant("measured")
|
|
|
|
|
.position("atEquipment")
|
|
|
|
|
.getCurrentValue("m");
|
|
|
|
|
let direction = null;
|
|
|
|
|
|
|
|
|
|
const flowUpstream = this.measurements
|
|
|
|
|
.type("flow")
|
|
|
|
|
.variant("measured")
|
|
|
|
|
.position("upstream")
|
|
|
|
|
.getCurrentValue("m3/s");
|
|
|
|
|
switch (true){
|
|
|
|
|
case flowDiff > flowThreshold:
|
|
|
|
|
direction = "filling";
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
const flowDownstream = this.measurements
|
|
|
|
|
.type("flow")
|
|
|
|
|
.variant("measured")
|
|
|
|
|
.position("downstream")
|
|
|
|
|
.getCurrentValue("m3/s");
|
|
|
|
|
case flowDiff < -flowThreshold:
|
|
|
|
|
direction = "draining";
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case flowDiff < flowThreshold && flowDiff > -flowThreshold:
|
|
|
|
|
direction = "stable";
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
this.logger.warn("Uknown state direction detected??");
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
if (flowDiff === null || level === null) {
|
|
|
|
|
this.logger.warn(`no flowdiff ${flowDiff} or level ${level} found escaping`);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const flowThreshold = 0.1; // m³/s
|
|
|
|
|
const state = { direction: "stable", seconds: 0, netUpstream: flowUpstream ?? 0, netDownstream: flowDownstream ?? 0 };
|
|
|
|
|
|
|
|
|
|
if (flowDiff > flowThreshold) {
|
|
|
|
|
state.direction = "filling";
|
|
|
|
|
const remainingHeight = Math.max(heightOverflow - level, 0);
|
|
|
|
|
state.seconds = remainingHeight * surfaceArea / flowDiff;
|
|
|
|
|
} else if (flowDiff < -flowThreshold) {
|
|
|
|
|
state.direction = "draining";
|
|
|
|
|
const remainingHeight = Math.max(level - heightOutlet, 0);
|
|
|
|
|
state.seconds = remainingHeight * surfaceArea / Math.abs(flowDiff);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.measurements
|
|
|
|
|
.type("netFlowRate")
|
|
|
|
|
.variant("predicted")
|
|
|
|
|
.position("atEquipment")
|
|
|
|
|
.value(flowDiff)
|
|
|
|
|
.unit("m3/s");
|
|
|
|
|
|
|
|
|
|
this.logger.debug(
|
|
|
|
|
`Flow-based net flow | diff=${flowDiff.toFixed(3)} m3/s, level=${level.toFixed(3)} m`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return { source: "flow", netFlowRate: flowDiff, state };
|
|
|
|
|
return direction;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_calcNetFlowFromLevel({ heightOverflow, heightOutlet, surfaceArea }) {
|
|
|
|
|
const levelObj = this.measurements
|
|
|
|
|
.type("level")
|
|
|
|
|
.variant("measured")
|
|
|
|
|
.position("atEquipment");
|
|
|
|
|
|
|
|
|
|
_calcNetFlowFromLevelDiff() {
|
|
|
|
|
const { surfaceArea } = this.basin;
|
|
|
|
|
const levelObj = this.measurements.type("level").variant("measured").position("atEquipment");
|
|
|
|
|
const level = levelObj.getCurrentValue("m");
|
|
|
|
|
const prevLevel = levelObj.getLaggedValue(2, "m"); // { value, timestamp, unit }
|
|
|
|
|
const measurement = levelObj.get();
|
|
|
|
|
@@ -257,38 +391,12 @@ class pumpingStation {
|
|
|
|
|
|
|
|
|
|
const lvlDiff = level - prevLevel.value;
|
|
|
|
|
const lvlRate = lvlDiff / deltaSeconds; // m/s
|
|
|
|
|
const levelRateThreshold = 0.1 / surfaceArea; // same 0.1 m³/s threshold translated to height
|
|
|
|
|
|
|
|
|
|
const state = { direction: "stable", seconds: 0, netUpstream: 0, netDownstream: 0 };
|
|
|
|
|
|
|
|
|
|
if (lvlRate > levelRateThreshold) {
|
|
|
|
|
state.direction = "filling";
|
|
|
|
|
const remainingHeight = Math.max(heightOverflow - level, 0);
|
|
|
|
|
state.seconds = remainingHeight / lvlRate;
|
|
|
|
|
} else if (lvlRate < -levelRateThreshold) {
|
|
|
|
|
state.direction = "draining";
|
|
|
|
|
const remainingHeight = Math.max(level - heightOutlet, 0);
|
|
|
|
|
state.seconds = remainingHeight / Math.abs(lvlRate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const netFlowRate = lvlRate * surfaceArea; // m³/s inferred from level trend
|
|
|
|
|
|
|
|
|
|
this.measurements
|
|
|
|
|
.type("netFlowRate")
|
|
|
|
|
.variant("predicted")
|
|
|
|
|
.position("atEquipment")
|
|
|
|
|
.value(netFlowRate)
|
|
|
|
|
.unit("m3/s");
|
|
|
|
|
|
|
|
|
|
this.logger.warn(
|
|
|
|
|
`Level-based net flow | rate=${lvlRate.toExponential(3)} m/s, inferred=${netFlowRate.toFixed(3)} m3/s`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return { source: "level", netFlowRate, state };
|
|
|
|
|
return netFlowRate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initBasinProperties() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Load and calc basic params
|
|
|
|
|
const volEmptyBasin = this.config.basin.volume;
|
|
|
|
|
@@ -301,8 +409,8 @@ class pumpingStation {
|
|
|
|
|
const surfaceArea = volEmptyBasin / heightBasin;
|
|
|
|
|
const maxVol = heightBasin * surfaceArea; // if Basin where to ever fill up completely this is the water volume
|
|
|
|
|
const maxVolOverflow = heightOverflow * surfaceArea ; // Max water volume before you start loosing water to overflow
|
|
|
|
|
const minVol = heightInlet * surfaceArea;
|
|
|
|
|
const minVolOut = heightOutlet * surfaceArea ; // this will indicate if its an open end or a closed end.
|
|
|
|
|
const minVol = heightOutlet * surfaceArea;
|
|
|
|
|
const minVolOut = heightInlet * surfaceArea ; // this will indicate if its an open end or a closed end.
|
|
|
|
|
|
|
|
|
|
this.basin.volEmptyBasin = volEmptyBasin ;
|
|
|
|
|
this.basin.heightBasin = heightBasin ;
|
|
|
|
|
@@ -315,11 +423,14 @@ class pumpingStation {
|
|
|
|
|
this.basin.minVol = minVol ;
|
|
|
|
|
this.basin.minVolOut = minVolOut ;
|
|
|
|
|
|
|
|
|
|
this.logger.debug(
|
|
|
|
|
`Basin initialized | area=${surfaceArea.toFixed(2)} m², max=${maxVol.toFixed(2)} m³, overflow=${maxVolOverflow.toFixed(2)} m³`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
//init predicted min volume to min vol in order to have a starting point
|
|
|
|
|
this.measurements.type("volume").variant("predicted").position("atEquipment").value(minVol).unit('m3');
|
|
|
|
|
|
|
|
|
|
this.logger.debug(`
|
|
|
|
|
Basin initialized | area=${surfaceArea.toFixed(2)} m²,
|
|
|
|
|
max=${maxVol.toFixed(2)} m³,
|
|
|
|
|
overflow=${maxVolOverflow.toFixed(2)} m³`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_calcVolumeFromLevel(level) {
|
|
|
|
|
@@ -327,21 +438,230 @@ _calcVolumeFromLevel(level) {
|
|
|
|
|
return Math.max(level, 0) * surfaceArea;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_calcLevelFromVolume(vol){
|
|
|
|
|
const surfaceArea = this.basin.surfaceArea;
|
|
|
|
|
return Math.max(vol, 0) / surfaceArea;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getOutput() {
|
|
|
|
|
return {
|
|
|
|
|
volume_m3: this.measurements.type("volume").variant("measured").position("atEquipment").getCurrentValue('m3') ,
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
getOutput() {
|
|
|
|
|
// Improved output object generation
|
|
|
|
|
const output = {};
|
|
|
|
|
//build the output object
|
|
|
|
|
this.measurements.getTypes().forEach(type => {
|
|
|
|
|
this.measurements.getVariants(type).forEach(variant => {
|
|
|
|
|
this.measurements.getPositions(variant).forEach(position => {
|
|
|
|
|
const sample = this.measurements.type(type).variant(variant).position(position);
|
|
|
|
|
output[`${type}.${variant}.${position}`] = sample.getCurrentValue();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
//fill in the rest of the output object
|
|
|
|
|
output["state"] = this.state;
|
|
|
|
|
output["basin"] = this.basin;
|
|
|
|
|
|
|
|
|
|
if(this.flowDrift != null){
|
|
|
|
|
const flowDrift = this.flowDrift;
|
|
|
|
|
output["flowNrmse"] = flowDrift.nrmse;
|
|
|
|
|
output["flowLongterNRMSD"] = flowDrift.longTermNRMSD;
|
|
|
|
|
output["flowImmediateLevel"] = flowDrift.immediateLevel;
|
|
|
|
|
output["flowLongTermLevel"] = flowDrift.longTermLevel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return output;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = pumpingStation;
|
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
|
/* Example: pumping station + rotating machine + measurements (stand-alone) */
|
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
const PumpingStation = require("./specificClass");
|
|
|
|
|
const RotatingMachine = require("../../rotatingMachine/src/specificClass");
|
|
|
|
|
const Measurement = require("../../measurement/src/specificClass");
|
|
|
|
|
|
|
|
|
|
/** Helpers ******************************************************************/
|
|
|
|
|
function createPumpingStationConfig(name) {
|
|
|
|
|
return {
|
|
|
|
|
general: {
|
|
|
|
|
logging: { enabled: true, logLevel: "debug" },
|
|
|
|
|
name,
|
|
|
|
|
id: `${name}-${Date.now()}`,
|
|
|
|
|
unit: "m3/h"
|
|
|
|
|
},
|
|
|
|
|
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) {
|
|
|
|
|
|
|
|
|
|
curve = require('C:/Users/zn375/.node-red/public/fallbackData.json');
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
|
|
general: {
|
|
|
|
|
name: name,
|
|
|
|
|
logging: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
logLevel: "warn",
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
asset: {
|
|
|
|
|
supplier: "Hydrostal",
|
|
|
|
|
type: "pump",
|
|
|
|
|
category: "centrifugal",
|
|
|
|
|
model: "hidrostal-H05K-S03R", // Ensure this field is present.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createMachineStateConfig() {
|
|
|
|
|
return {
|
|
|
|
|
general: {
|
|
|
|
|
logging: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
logLevel: "debug",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Your custom config here (or leave empty for defaults)
|
|
|
|
|
movement: {
|
|
|
|
|
speed: 1,
|
|
|
|
|
},
|
|
|
|
|
time: {
|
|
|
|
|
starting: 2,
|
|
|
|
|
warmingup: 3,
|
|
|
|
|
stopping: 2,
|
|
|
|
|
coolingdown: 3,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// convenience for seeding measurements
|
|
|
|
|
function pushSample(measurement, type, value, unit) {
|
|
|
|
|
const pos = measurement.config.functionality.positionVsParent;
|
|
|
|
|
measurement.measurements
|
|
|
|
|
.type(type)
|
|
|
|
|
.variant("measured")
|
|
|
|
|
.position(pos)
|
|
|
|
|
.value(value, Date.now(), unit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Demo *********************************************************************/
|
|
|
|
|
(async function demoStationWithPump() {
|
|
|
|
|
const station = new PumpingStation(createPumpingStationConfig("PumpingStationDemo"));
|
|
|
|
|
const pump = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig());
|
|
|
|
|
|
|
|
|
|
const levelSensor = new Measurement(createLevelMeasurementConfig("WetWellLevel"));
|
|
|
|
|
const upstreamFlow = new Measurement(createFlowMeasurementConfig("InfluentFlow", "upstream"));
|
|
|
|
|
const downstreamFlow = new Measurement(createFlowMeasurementConfig("PumpDischargeFlow", "downstream"));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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");
|
|
|
|
|
|
|
|
|
|
setInterval(() => station.tick(), 1000);
|
|
|
|
|
|
|
|
|
|
// seed a starting level & flow
|
|
|
|
|
/*
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
// pump increases discharge flow
|
|
|
|
|
/*
|
|
|
|
|
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);
|
|
|
|
|
console.log("Station state:", station.state);
|
|
|
|
|
console.log("Station output:", station.getOutput());
|
|
|
|
|
console.log("Pump state:", pump.state.getCurrentState());
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
//coolprop example
|
|
|
|
|
(async () => {
|
|
|
|
|
const PropsSI = await coolprop.getPropsSI();
|
|
|
|
|
|