From 65807881d5487f0006790f9388fe38ffd6f32db9 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:44:45 +0200 Subject: [PATCH] working pumpingstation level and net flow calc --- src/nodeClass.js | 17 ++++ src/specificClass.js | 200 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 198 insertions(+), 19 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index fa244f7..404686a 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -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(); }); diff --git a/src/specificClass.js b/src/specificClass.js index e93e0c5..87c1339 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -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; + 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')); } })(); +*/