Files
pumpingStation/src/specificClass.js

396 lines
16 KiB
JavaScript
Raw Normal View History

2025-10-07 18:05:54 +02:00
const EventEmitter = require('events');
2025-10-21 12:45:19 +02:00
const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation} = require('generalFunctions');
2025-10-07 18:05:54 +02:00
2025-10-14 08:36:45 +02:00
class pumpingStation {
2025-10-07 18:05:54 +02:00
constructor(config={}) {
this.emitter = new EventEmitter(); // Own EventEmitter
this.configManager = new configManager();
2025-10-14 08:36:45 +02:00
this.defaultConfig = this.configManager.getConfig('pumpingStation');
2025-10-07 18:05:54 +02:00
this.configUtils = new configUtils(this.defaultConfig);
this.config = this.configUtils.initConfig(config);
2025-10-21 12:45:19 +02:00
this.interpolate = new interpolation();
2025-10-07 18:05:54 +02:00
// Init after config is set
this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name);
// General properties
this.measurements = new MeasurementContainer({
autoConvert: true
2025-10-07 18:05:54 +02:00
});
2025-10-14 16:32:44 +02:00
// init basin object in pumping station
this.basin = {};
this.state = { direction:"", netDownstream:0, netUpstream:0, seconds:0}; // init state object of pumping station to see whats going on
2025-10-14 16:32:44 +02:00
// Initialize basin-specific properties and calculate used parameters
2025-10-07 18:05:54 +02:00
this.initBasinProperties();
this.child = {}; // object to hold child information so we know on what to subscribe
2025-10-21 13:44:31 +02:00
this.machines = {}; // object to hold child machine information
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
this.logger.debug('pumpstation Initialized with all helpers');
2025-10-07 18:05:54 +02:00
}
/*------------------- Register child events -------------------*/
registerChild(child, softwareType) {
this.logger.debug('Setting up child event for softwaretype ' + softwareType);
2025-10-21 13:44:31 +02:00
//define what to do with measurements
2025-10-07 18:05:54 +02:00
if(softwareType === "measurement"){
const position = child.config.functionality.positionVsParent;
const distance = child.config.functionality.distanceVsParent || 0;
const measurementType = child.config.asset.type;
const key = `${measurementType}_${position}`;
//rebuild to measurementype.variant no position and then switch based on values not strings or names.
const eventName = `${measurementType}.measured.${position}`;
this.logger.debug(`Setting up listener for ${eventName} from child ${child.config.general.name}`);
// Register event listener for measurement updates
child.measurements.emitter.on(eventName, (eventData) => {
this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
this.logger.debug(` Emitting... ${eventName} with data:`);
2025-10-07 18:05:54 +02:00
// Store directly in parent's measurement container
2025-10-21 13:44:31 +02:00
this.measurements.type(measurementType).variant("measured").position(position).value(eventData.value, eventData.timestamp, eventData.unit);
2025-10-07 18:05:54 +02:00
// Call the appropriate handler
this._callMeasurementHandler(measurementType, eventData.value, position, eventData);
});
}
2025-10-21 13:44:31 +02:00
//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 pressure changes from machine ${child.config.general.id}`);
//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.updateMachineFlowPrediction();
});
}
}
//how to handle when there are machines connected and there is an updated predicted flow variable
updateMachineFlowPrediction(){
//check if container exists
const hasMeasuredFlow = measurements.type("flow").variant("measured").exists();
//if there is no down / upstream flow being measured we can take the machines flow to calculate the flow and update predicted level
if( ! hasMeasuredFlow ) {
}
}
updateMeasuredTemperature(){
}
updateMeasuredFlow(){
2025-10-07 18:05:54 +02:00
}
2025-10-21 13:44:31 +02:00
2025-10-07 18:05:54 +02:00
_callMeasurementHandler(measurementType, value, position, context) {
switch (measurementType) {
case 'pressure':
this.updateMeasuredPressure(value, position, context);
break;
case 'flow':
this.updateMeasuredFlow(value, position, context);
break;
case 'temperature':
this.updateMeasuredTemperature(value, position, context);
break;
case 'level':
this.updateMeasuredLevel(value, position, context);
break;
default:
this.logger.warn(`No handler for measurement type: ${measurementType}`);
// Generic handler - just update position
this.updatePosition();
break;
}
2025-10-21 13:44:31 +02:00
}
2025-10-07 18:05:54 +02:00
// context handler for pressure updates
updateMeasuredPressure(value, position, context = {}) {
2025-10-14 16:32:44 +02:00
// init temp
2025-10-07 18:05:54 +02:00
let kelvinTemp = null;
//pressure updates come from pressure boxes inside the basin they get converted to a level and stored as level measured at position inlet or outlet
this.logger.debug(`Pressure update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`);
// Store in parent's measurement container for the first time
this.measurements.type("pressure").variant("measured").position(position).value(value, context.timestamp, context.unit);
//convert pressure to level based on density of water and height of pressure sensor
const mTemp = this.measurements.type("temperature").variant("measured").position("atEquipment").getCurrentValue('K'); //default to 20C if no temperature measurement
//prefer measured temp but otherwise assume nominal temp for wastewater
if(mTemp === null){
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');
2025-10-21 13:44:31 +02:00
this.logger.debug(`Temperature is : ${kelvinTemp}`);
2025-10-07 18:05:54 +02:00
} else {
kelvinTemp = mTemp;
}
this.logger.debug(`Using temperature: ${kelvinTemp} K for calculations`);
const density = coolprop.PropsSI('D','T',kelvinTemp,'P',101325,'Water'); //density in kg/m3 at temp and surface pressure
2025-10-14 16:32:44 +02:00
const g = 9.80665;
2025-10-14 16:45:09 +02:00
const pressure_Pa = this.measurements.type("pressure").variant("measured").position(position).getCurrentValue('Pa');
const level = pressure_Pa / density * g;
this.measurements.type("level").variant("predicted").position(position).value(level);
2025-10-21 12:45:19 +02:00
//updatePredictedLevel(); ?? OLIFANT!
2025-10-07 18:05:54 +02:00
//calculate how muc flow went in or out based on pressure difference
2025-10-14 16:45:09 +02:00
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);
2025-10-21 12:45:19 +02:00
this.logger.debug(`basin minvol : ${this.basin.minVol}, cur volume : ${volume} / ${this.basin.maxVolOverflow}`);
const proc = this.interpolate.interpolate_lin_single_point(volume,this.basin.minVol,this.basin.maxVolOverflow,0,100);
2025-10-21 13:44:31 +02:00
this.logger.debug(`PROC volume : ${proc}`);
this.measurements.type("volume").variant("measured").position("atEquipment").value(volume).unit('m3');
2025-10-21 13:44:31 +02:00
this.measurements.type("volume").variant("procent").position("atEquipment").value(proc);
2025-10-21 12:45:19 +02:00
//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 }) {
2025-10-21 12:45:19 +02:00
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);
}
2025-10-21 12:45:19 +02:00
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 }) {
2025-10-21 13:44:31 +02:00
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
2025-10-21 13:44:31 +02:00
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 };
2025-10-07 18:05:54 +02:00
}
initBasinProperties() {
2025-10-14 16:32:44 +02:00
// 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;
2025-10-14 16:32:44 +02:00
//calculated params
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
2025-10-21 12:45:19 +02:00
const minVol = heightOutlet * surfaceArea;
const minVolOut = heightInlet * surfaceArea ; // this will indicate if its an open end or a closed end.
2025-10-14 16:32:44 +02:00
this.basin.volEmptyBasin = volEmptyBasin ;
this.basin.heightBasin = heightBasin ;
this.basin.heightInlet = heightInlet ;
this.basin.heightOutlet = heightOutlet ;
this.basin.heightOverflow = heightOverflow ;
this.basin.surfaceArea = surfaceArea ;
this.basin.maxVol = maxVol ;
this.basin.maxVolOverflow = maxVolOverflow;
this.basin.minVol = minVol ;
this.basin.minVolOut = minVolOut ;
2025-10-21 13:44:31 +02:00
//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)} ,
max=${maxVol.toFixed(2)} ,
overflow=${maxVolOverflow.toFixed(2)} `
);
2025-10-14 16:32:44 +02:00
2025-10-07 18:05:54 +02:00
}
2025-10-14 16:32:44 +02:00
_calcVolumeFromLevel(level) {
const surfaceArea = this.basin.surfaceArea;
return Math.max(level, 0) * surfaceArea;
}
2025-10-07 18:05:54 +02:00
getOutput() {
return {
volume_m3: this.measurements.type("volume").variant("measured").position("atEquipment").getCurrentValue('m3') ,
2025-10-07 18:05:54 +02:00
};
}
}
2025-10-14 13:51:32 +02:00
module.exports = pumpingStation;
2025-10-07 18:05:54 +02:00
/*
//
2025-10-07 18:05:54 +02:00
//coolprop example
2025-10-07 18:05:54 +02:00
(async () => {
const PropsSI = await coolprop.getPropsSI();
// 👇 replace these with your real inputs
const tC_input = 25; // °C
const pPa_input = 101325; // Pa
// Sanitize & convert
const T = Number(tC_input) + 273.15; // K
const P = Number(pPa_input); // Pa
const fluid = 'Water';
// Preconditions
if (!Number.isFinite(T) || !Number.isFinite(P)) {
throw new Error(`Bad inputs: T=${T} K, P=${P} Pa`);
}
if (T <= 0) throw new Error(`Temperature must be in Kelvin (>0). Got ${T}.`);
if (P <= 0) throw new Error(`Pressure must be >0 Pa. Got ${P}.`);
// Try T,P order
let rho = PropsSI('D', 'T', T, 'P', P, fluid);
// Fallback: P,T order (should be equivalent)
if (!Number.isFinite(rho)) rho = PropsSI('D', 'P', P, 'T', T, fluid);
console.log({ T, P, rho });
if (!Number.isFinite(rho)) {
console.error('Still Infinity. Extra checks:');
console.error('typeof T:', typeof T, 'typeof P:', typeof P);
console.error('Example known-good call:', PropsSI('D', 'T', 298.15, 'P', 101325, 'Water'));
}
})();
*/