forked from RnD/pumpingStation
working pumpingstation level and net flow calc
This commit is contained in:
@@ -53,6 +53,17 @@ class nodeClass {
|
||||
functionality: {
|
||||
positionVsParent: uiConfig.positionVsParent,// Default to 'atEquipment' if not specified
|
||||
distance: uiConfig.hasDistance ? uiConfig.distance : undefined
|
||||
},
|
||||
basin:{
|
||||
volume: uiConfig.basinVolume,
|
||||
height: uiConfig.basinHeight,
|
||||
heightInlet: uiConfig.heightInlet,
|
||||
heightOutlet: uiConfig.heightOutlet,
|
||||
heightOverflow: uiConfig.heightOverflow,
|
||||
},
|
||||
hydraulics:{
|
||||
refHeight: uiConfig.refHeight,
|
||||
basinBottomRef: uiConfig.basinBottomRef,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -128,6 +139,12 @@ class nodeClass {
|
||||
this.source.handleInput(msg);
|
||||
break;
|
||||
*/
|
||||
case 'registerChild':
|
||||
// Register this node as a child of the parent node
|
||||
const childId = msg.payload;
|
||||
const childObj = this.RED.nodes.getNode(childId);
|
||||
this.source.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
|
||||
break;
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const EventEmitter = require('events');
|
||||
const {logger,configUtils,configManager,MeasurementContainer,coolprop} = require('generalFunctions');
|
||||
const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop} = require('generalFunctions');
|
||||
|
||||
class pumpingStation {
|
||||
constructor(config={}) {
|
||||
@@ -15,26 +15,20 @@ class pumpingStation {
|
||||
|
||||
// General properties
|
||||
this.measurements = new MeasurementContainer({
|
||||
autoConvert: true,
|
||||
windowSize: this.config.smoothing.smoothWindow
|
||||
autoConvert: true
|
||||
});
|
||||
|
||||
// init basin object in pumping station
|
||||
this.basin = {
|
||||
volumeWater : null,// Total volume of water in the basin, calculated from water level and basin di
|
||||
emptyVolume : null,// Volume in the basin when empty (at level of outlet pipe)
|
||||
fullVolume : null,// Volume in the basin when at level of overflow point
|
||||
crossSectionalArea: null,// Cross-sectional area of the basin, used to calculate volume from water level
|
||||
};
|
||||
|
||||
// pumping station specifics
|
||||
this.calculatedFlowrate = null,// Function to calculate flow rate based on water level rise or fall NO MEASUREMENT this is the predicted value which should match a flowrate if we have it and we have to check mass balance ? Look at the pumps connected to the group controller or directly to this node and check incoming vs outgoing?
|
||||
this.timeBeforeOverflow = null,// Time before the basin overflows at current inflow rate at level of heightOutlet
|
||||
this.timeBeforeEmpty = null,// Time before the basin empties at current outflow rate at level of heightInlet
|
||||
this.basin = {};
|
||||
this.state = { direction:"", netDownstream:0, netUpstream:0, seconds:0}; // init state object of pumping station to see whats going on
|
||||
|
||||
// Initialize basin-specific properties and calculate used parameters
|
||||
this.initBasinProperties();
|
||||
|
||||
this.child = {}; // object to hold child information so we know on what to subscribe
|
||||
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
|
||||
|
||||
this.logger.debug('pumpstation Initialized with all helpers');
|
||||
}
|
||||
|
||||
/*------------------- Register child events -------------------*/
|
||||
@@ -54,7 +48,7 @@ class pumpingStation {
|
||||
child.measurements.emitter.on(eventName, (eventData) => {
|
||||
this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
|
||||
|
||||
console.log(` Emitting... ${eventName} with data:`);
|
||||
this.logger.debug(` Emitting... ${eventName} with data:`);
|
||||
// Store directly in parent's measurement container
|
||||
this.measurements
|
||||
.type(measurementType)
|
||||
@@ -124,19 +118,185 @@ class pumpingStation {
|
||||
const level = pressure_Pa / density * g;
|
||||
|
||||
this.measurements.type("level").variant("predicted").position(position).value(level);
|
||||
//updatePredictedLevel(); ??
|
||||
|
||||
//calculate how muc flow went in or out based on pressure difference
|
||||
this.logger.debug(`Using pressure: ${value} for calculations`);
|
||||
|
||||
|
||||
}
|
||||
|
||||
updateMeasuredLevel(value,position, context = {}){
|
||||
// Store in parent's measurement container for the first time
|
||||
this.measurements.type("level").variant("measured").position(position).value(value, context.timestamp, context.unit);
|
||||
|
||||
//fetch level in meter
|
||||
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');
|
||||
|
||||
//calc the most important values back to determine state and net up or downstream flow
|
||||
this._calcNetFlow();
|
||||
|
||||
}
|
||||
|
||||
|
||||
_calcNetFlow() {
|
||||
const { heightOverflow, heightOutlet, surfaceArea } = this.basin;
|
||||
|
||||
const flowBased = this._calcNetFlowFromMeasurements({
|
||||
heightOverflow,
|
||||
heightOutlet,
|
||||
surfaceArea
|
||||
});
|
||||
|
||||
const levelBased = this._calcNetFlowFromLevel({
|
||||
heightOverflow,
|
||||
heightOutlet,
|
||||
surfaceArea
|
||||
});
|
||||
|
||||
if (flowBased && levelBased) {
|
||||
this.logger.debug(
|
||||
`Flow vs Level comparison | flow=${flowBased.netFlowRate.toFixed(3)} ` +
|
||||
`m3/s, level=${levelBased.netFlowRate.toFixed(3)} m3/s`
|
||||
);
|
||||
}
|
||||
|
||||
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" });
|
||||
|
||||
const level = this.measurements
|
||||
.type("level")
|
||||
.variant("measured")
|
||||
.position("atEquipment")
|
||||
.getCurrentValue("m");
|
||||
|
||||
const flowUpstream = this.measurements
|
||||
.type("flow")
|
||||
.variant("measured")
|
||||
.position("upstream")
|
||||
.getCurrentValue("m3/s");
|
||||
|
||||
const flowDownstream = this.measurements
|
||||
.type("flow")
|
||||
.variant("measured")
|
||||
.position("downstream")
|
||||
.getCurrentValue("m3/s");
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
_calcNetFlowFromLevel({ heightOverflow, heightOutlet, surfaceArea }) {
|
||||
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();
|
||||
const latestTimestamp = measurement?.getLatestTimestamp();
|
||||
|
||||
if (level === null || !prevLevel || latestTimestamp == null) {
|
||||
this.logger.warn(`no flowdiff ${level}, previous level ${prevLevel}, latestTimestamp ${latestTimestamp} found escaping`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const deltaSeconds = (latestTimestamp - prevLevel.timestamp) / 1000;
|
||||
if (deltaSeconds <= 0) {
|
||||
this.logger.warn(`Level fallback: invalid Δt=${deltaSeconds} , LatestTimestamp : ${latestTimestamp}, PrevTimestamp : ${prevLevel.timestamp}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
initBasinProperties() {
|
||||
|
||||
|
||||
// Load and calc basic params
|
||||
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;
|
||||
|
||||
//calculated params
|
||||
const surfaceArea = volEmptyBasin / heightBasin;
|
||||
const maxVol = heightBasin * surfaceArea; // if Basin where to ever fill up completely this is the water volume
|
||||
@@ -170,7 +330,8 @@ _calcVolumeFromLevel(level) {
|
||||
|
||||
getOutput() {
|
||||
return {
|
||||
volume: this.volume,
|
||||
volume_m3: this.measurements.type("volume").variant("measured").position("atEquipment").getCurrentValue('m3') ,
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -179,9 +340,9 @@ module.exports = pumpingStation;
|
||||
|
||||
/*
|
||||
|
||||
// */
|
||||
|
||||
//
|
||||
|
||||
//coolprop example
|
||||
(async () => {
|
||||
const PropsSI = await coolprop.getPropsSI();
|
||||
|
||||
@@ -214,3 +375,4 @@ module.exports = pumpingStation;
|
||||
console.error('Example known-good call:', PropsSI('D', 'T', 298.15, 'P', 101325, 'Water'));
|
||||
}
|
||||
})();
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user