From 6e9ae9fc7eb45a1e1e6ee7027e2848d831f491dc Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Thu, 23 Oct 2025 18:04:18 +0200 Subject: [PATCH 01/21] Need to stich everything together then V1.0 is done. --- src/specificClass.js | 288 ++++++++++++++++++++++++++----------------- 1 file changed, 178 insertions(+), 110 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index 5c6d3d4..d7b2ca2 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -28,6 +28,7 @@ class pumpingStation { 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'); @@ -68,11 +69,30 @@ class pumpingStation { //listen for machine pressure changes this.logger.debug(`Listening for flow changes from machine ${child.config.general.id}`); - //for now lets focus on handling downstream predicted flow - child.measurements.emitter.on("flow.predicted.downstream", (eventData) => { + 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}`); - this.measurements.type('flow').variant('predicted').position('atEquipment').value(eventData.value,eventData.timestamp,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 @@ -80,38 +100,84 @@ class pumpingStation { } + // 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`); + } + } } - //update prediction in outgoing downstream flow - _updateDownstreamFlowPrediction(){ + //in or outgoing flow = direction + _updateVolumePrediction(flowDir){ //get downflow - const downFlowExists = this.measurements.type("flow").variant("predicted").position("atEquipment").exists(); - if(!downFlowExists){return}; + const seriesExists = this.measurements.type("flow").variant("predicted").position(flowDir).exists(); + if(!seriesExists){return}; - const downFlow = this.measurements.type("flow").variant("predicted").position("atEquipment"); - const currDownFlow = downFlow.getLaggedValue(0, "m3/s"); // { value, timestamp, unit } - const prevDownFlow = downFlow.getLaggedValue(1, "m3/s"); // { value, timestamp, unit } + 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 (!currDownFlow || !prevDownFlow) return; + if (!currFLow || !prevFlow) return; - this.logger.debug(`currDownflow = ${currDownFlow.value} , prevDownFlow = ${prevDownFlow.value}`); + this.logger.debug(`currDownflow = ${currFLow.value} , prevDownFlow = ${prevFlow.value}`); // calc difference in time - const deltaT = currDownFlow.timestamp - prevDownFlow.timestamp; + 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 = (currDownFlow.value + prevDownFlow.value) / 2; - const volumeSubstracted = avgFlow * deltaSeconds; + + 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'); - const newVol = currVolume - volumeSubstracted; + 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 @@ -123,10 +189,7 @@ class pumpingStation { } - //update prediction in incomming upstream flow - _updateUpstreamFlowPrediction(){ - } //trigger shutdown when level is too low and trigger no start flag for childs ? safetyVolCheck(){ @@ -146,7 +209,12 @@ class pumpingStation { //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._updateDownstreamFlowPrediction(); + 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(); + } @@ -229,81 +297,81 @@ class pumpingStation { this.measurements.type("volume").variant("measured").position("atEquipment").value(volume).unit('m3'); this.measurements.type("volume").variant("procent").position("atEquipment").value(proc); - - //calc the most important values back to determine state and net up or downstream flow - this._calcNetFlow(); - } _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.value.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 }) { + _calcDirection(flowDiff){ - 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"); + let direction = null; + + switch (true){ + case flowDiff > flowThreshold: + direction = "filling"; + break; + + 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.value.toFixed(3)} m3/s, level=${level.toFixed(3)} m` - ); - - return { source: "flow", netFlowRate: flowDiff, state }; + return direction; } - _calcNetFlowFromLevel({ heightOverflow, heightOutlet, surfaceArea }) { + _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 } @@ -323,29 +391,9 @@ 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() { @@ -383,8 +431,6 @@ class pumpingStation { max=${maxVol.toFixed(2)} m³, overflow=${maxVolOverflow.toFixed(2)} m³` ); - - } _calcVolumeFromLevel(level) { @@ -398,12 +444,34 @@ _calcLevelFromVolume(vol){ } - 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; @@ -584,7 +652,7 @@ function pushSample(measurement, type, value, unit) { 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); -- 2.49.1 From e8f9207a9209633a74c8dcb18be907ca6bbe0828 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:39:06 +0100 Subject: [PATCH 02/21] some major design choises updated --- src/specificClass.js | 171 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 154 insertions(+), 17 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index d7b2ca2..a9da992 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -21,7 +21,13 @@ class pumpingStation { // 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 + this.state = { + direction: "steady", + netFlow: 0, + flowSource: null, + seconds: null, + remainingSource: null + }; // init state object of pumping station to see whats going on // Initialize basin-specific properties and calculate used parameters this.initBasinProperties(); @@ -211,12 +217,80 @@ class pumpingStation { //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 + + //calc the most important values back to determine state and net up or downstream flow this._calcNetFlow(); - this._calcTimeRemaining(); - + const {time:timeleft, source:variant} = this._calcTimeRemaining(); + + + this.logger.debug(`Remaining time ${timeleft}, based on variant ${variant} `); + } + + _calcTimeRemaining(){ + //init timeRemaining + const winningTime = {time:0,source:""}; + + //calculate time left prioritise flow based variant + const { time: flowTime, variant: flowVariant } = this._selectBestRemainingTimeFlowVariant(); + + //if flow doesnt work then use level based varianti to calc timeleft + if(flowVariant == null){ + const {time: levelTime, variant: levelVariant} = this._selectBestRemainingTimeLevelVariant(); + winningTime.time = levelTime; + winningTime.source = levelVariant; + if(levelVariant == null){ + winningTime.time = null; + winningTime.source = null; + } + } + else{ + winningTime.time = flowTime; + winningTime.source = flowVariant; + } + + return winningTime; + } + + // Select remaining time based on flow + level variation measured or predicted and give back {time:0,variant:null}; + _selectBestRemainingTimeFlowVariant(){ + + //define variants + const remainingTimeVariants = [ + { flowVariant: "measured", levelVariant: "measured" }, + { flowVariant: "measured", levelVariant: "predicted" }, + { flowVariant: "predicted", levelVariant: "measured" }, + { flowVariant: "predicted", levelVariant: "predicted" } + ]; + + let remainingT = null; + + for (const variant of remainingTimeVariants) { + const candidate = this._calcRemainingTimeBasedOnFlow(variant); + if (candidate != null) { + remainingT = candidate; + return {time:remainingT,variant:variant}; + } + } + return {time:0,variant:null}; + } + + // Select remaining time based only on level variation measured or predicted and give back {time:0,variant:null}; + _selectBestRemainingTimeLevelVariant(){ + + //define variants (in sequence of priority first measured then predicted etc...) + const remainingTimeVariants = ["measured","predicted"]; + + let remainingT = null; + + for (const variant of remainingTimeVariants) { + const candidate = this._calcRemainingTimeBasedOnLevel(variant); + if (candidate != null) { + remainingT = candidate; + return {time:remainingT,variant:variant}; + } + } + return {time:0,variant:null}; } - _callMeasurementHandler(measurementType, value, position, context) { switch (measurementType) { @@ -319,33 +393,96 @@ class pumpingStation { this.logger.warn(`Can't calculate netflow without the proper measurements or predictions`); return null; } - } - _calcRemainingTime(level,variant){ + //@params : params : example {flowVariant: "predicted",levelVariant: "measured"}; + _calcRemainingTimeBasedOnFlow(params){ + const {flowVariant,levelVariant} = params; + this.logger.debug(`${flowVariant} - ${levelVariant} `); + if( flowVariant === null || levelVariant === null ){ + this.logger.warn(`Cant calculate remaining time without needed variants`); + return 0; + } + const { heightOverflow, heightOutlet, surfaceArea } = this.basin; - const flowDiff = this.measurements.type("flow").variant(variant).difference({ from: "downstream", to: "upstream", unit: "m3/s" }); + const levelexists = this.measurements.type("level").variant(levelVariant).exists({ position: "atEquipment", requireValues: true }); + const flowOutExists = this.measurements.type("flow").variant(flowVariant).exists({ position: "out", requireValues: true }); + const flowInExists = this.measurements.type("flow").variant(flowVariant).exists({ position: "in", requireValues: true }); + let secondsRemaining = 0; + + if( ! flowOutExists || ! flowInExists || ! levelexists){ + this.logger.warn(`Cant calculate remaining time without needed parameters ${flowOutExists} , ${flowInExists} , ${levelexists}`); + return null; + } + + const flowDiff = this.measurements.type("flow").variant(flowVariant).difference({ from: "downstream", to: "upstream", unit: "m3/s" }); + const level = this.measurements.type("level").variant(levelVariant).type('atEquipment').getCurrentValue('m'); + let remainingHeight = 0; switch(true){ + case(flowDiff>0): remainingHeight = Math.max(heightOverflow - level, 0); - this.state.seconds = remainingHeight * surfaceArea / flowDiff; - break; + secondsRemaining = remainingHeight * surfaceArea / flowDiff; + return secondsRemaining; case(flowDiff<0): remainingHeight = Math.max(level - heightOutlet, 0); - this.state.seconds = remainingHeight * surfaceArea / Math.abs(flowDiff); - break; + secondsRemaining = remainingHeight * surfaceArea / Math.abs(flowDiff); + return secondsRemaining; default: - this.logger.debug(`doing nothing with level calc`) - + this.logger.debug(`Flowdiff is 0 not doing anything.`); + return secondsRemaining; } } - _calcDirection(flowDiff){ + //@params : variant : example "predicted","measured" + _calcRemainingTimeBasedOnLevel(variant){ + + const levelObj = this.measurements.type("level").variant(variant).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 = Math.abs(lvlDiff / deltaSeconds); // m/s + + switch(true){ + + case(lvlRate>0): + remainingHeight = Math.max(heightOverflow - level, 0); + secondsRemaining = remainingHeight / lvlRate; // seconds + return secondsRemaining; + + case(lvlRate<0): + remainingHeight = Math.max(level - heightOutlet, 0); + secondsRemaining = remainingHeight / lvlRate; + return secondsRemaining; + + default: + this.logger.debug(`Flowdiff is 0 not doing anything.`); + return secondsRemaining; + } + + } + + //Give a flowDifference and calculate direction => spits out filling , draining or stable + _calcDirectionBasedOnFlow(flowDiff){ let direction = null; @@ -370,9 +507,9 @@ class pumpingStation { return direction; } - _calcNetFlowFromLevelDiff() { + _calcNetFlowFromLevelDiff(variant) { const { surfaceArea } = this.basin; - const levelObj = this.measurements.type("level").variant("measured").position("atEquipment"); + const levelObj = this.measurements.type("level").variant(variant).position("atEquipment"); const level = levelObj.getCurrentValue("m"); const prevLevel = levelObj.getLaggedValue(2, "m"); // { value, timestamp, unit } const measurement = levelObj.get(); -- 2.49.1 From 5a1eff37d7236d55d6091fed5d51c59483fb653b Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:45:48 +0100 Subject: [PATCH 03/21] Need to remove wobble on level only --- src/specificClass.js | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index a9da992..85a6833 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -219,15 +219,14 @@ class pumpingStation { 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._calcNetFlow(); const {time:timeleft, source:variant} = this._calcTimeRemaining(); - - this.logger.debug(`Remaining time ${timeleft}, based on variant ${variant} `); + this.logger.debug(`Remaining time ${Math.round(timeleft/60/60*100)/100} h, based on variant ${variant} `); } _calcTimeRemaining(){ - //init timeRemaining + //init timeRemaining const winningTime = {time:0,source:""}; //calculate time left prioritise flow based variant @@ -377,7 +376,7 @@ class pumpingStation { 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_LevelSensor = this._calcNetFlowFromLevelDiff("measured"); const netFlow_PredictedFlow = Math.abs(this.measurements.type('flow').variant('predicted').difference({ from: "in", to: "out", unit: "m3/s" })); switch (true){ @@ -442,36 +441,39 @@ class pumpingStation { //@params : variant : example "predicted","measured" _calcRemainingTimeBasedOnLevel(variant){ + const {heightOverflow,heightOutlet} = this.basin; const levelObj = this.measurements.type("level").variant(variant).position("atEquipment"); const level = levelObj.getCurrentValue("m"); - const prevLevel = levelObj.getLaggedValue(2, "m"); // { value, timestamp, unit } + const prevLevelSample = levelObj.getLaggedSample(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`); + if (level === null || prevLevelSample == null || latestTimestamp == null) { + this.logger.warn(`no flowdiff ${level}, previous level ${prevLevelSample}, latestTimestamp ${latestTimestamp} found escaping`); return null; } - const deltaSeconds = (latestTimestamp - prevLevel.timestamp) / 1000; + const deltaSeconds = (latestTimestamp - prevLevelSample.timestamp) / 1000; if (deltaSeconds <= 0) { - this.logger.warn(`Level fallback: invalid Δt=${deltaSeconds} , LatestTimestamp : ${latestTimestamp}, PrevTimestamp : ${prevLevel.timestamp}`); + this.logger.warn(`Level fallback: invalid Δt=${deltaSeconds} , LatestTimestamp : ${latestTimestamp}, PrevTimestamp : ${prevLevelSample.value}`); return null; } - const lvlDiff = level - prevLevel.value; - const lvlRate = Math.abs(lvlDiff / deltaSeconds); // m/s + const lvlDiff = level - prevLevelSample.value; + const lvlRate = lvlDiff / deltaSeconds; // m/s + let secondsRemaining = 0; + let remainingHeight = 0; switch(true){ case(lvlRate>0): remainingHeight = Math.max(heightOverflow - level, 0); - secondsRemaining = remainingHeight / lvlRate; // seconds + secondsRemaining = remainingHeight / Math.abs(lvlRate); // seconds return secondsRemaining; case(lvlRate<0): remainingHeight = Math.max(level - heightOutlet, 0); - secondsRemaining = remainingHeight / lvlRate; + secondsRemaining = remainingHeight / Math.abs(lvlRate); return secondsRemaining; default: @@ -511,22 +513,22 @@ class pumpingStation { const { surfaceArea } = this.basin; const levelObj = this.measurements.type("level").variant(variant).position("atEquipment"); const level = levelObj.getCurrentValue("m"); - const prevLevel = levelObj.getLaggedValue(2, "m"); // { value, timestamp, unit } + const prevLevelSample = levelObj.getLaggedSample(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`); + if (level === null || prevLevelSample == null || latestTimestamp == null) { + this.logger.warn(`no flowdiff ${level}, previous level ${prevLevelSample}, latestTimestamp ${latestTimestamp} found escaping`); return null; } - const deltaSeconds = (latestTimestamp - prevLevel.timestamp) / 1000; + const deltaSeconds = (latestTimestamp - prevLevelSample.timestamp) / 1000; if (deltaSeconds <= 0) { - this.logger.warn(`Level fallback: invalid Δt=${deltaSeconds} , LatestTimestamp : ${latestTimestamp}, PrevTimestamp : ${prevLevel.timestamp}`); + this.logger.warn(`Level fallback: invalid Δt=${deltaSeconds} , LatestTimestamp : ${latestTimestamp}, PrevTimestamp : ${prevLevelSample.timestamp}`); return null; } - const lvlDiff = level - prevLevel.value; + const lvlDiff = level - prevLevelSample.value; const lvlRate = lvlDiff / deltaSeconds; // m/s const netFlowRate = lvlRate * surfaceArea; // m³/s inferred from level trend @@ -562,6 +564,7 @@ class pumpingStation { //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.measurements.type("volume").variant("predicted").position("atEquipment").value(maxVol).unit('m3'); this.logger.debug(` Basin initialized | area=${surfaceArea.toFixed(2)} m², -- 2.49.1 From 69f68adffea1fb93d35a75d3f88adf0cb68ed6e6 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:55:48 +0100 Subject: [PATCH 04/21] testing codex --- src/specificClass.js | 37 ++- src/specificClass2.js | 681 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 702 insertions(+), 16 deletions(-) create mode 100644 src/specificClass2.js diff --git a/src/specificClass.js b/src/specificClass.js index 85a6833..b5671c4 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -85,7 +85,6 @@ class pumpingStation { }); break; - case("upstream"): //check for predicted outgoing flow at the connected child pumpingsation child.measurements.emitter.on("flow.predicted.downstream", (eventData) => { @@ -152,8 +151,8 @@ class pumpingStation { const prevFlow = series.getLaggedValue(1, "m3/s"); // { value, timestamp, unit } if (!currFLow || !prevFlow) return; - - this.logger.debug(`currDownflow = ${currFLow.value} , prevDownFlow = ${prevFlow.value}`); + + this.logger.debug(`Flowdir ${flowDir} => currFlow ${currFLow.value} , prevflow = ${prevFlow.value}`); // calc difference in time const deltaT = currFLow.timestamp - prevFlow.timestamp; @@ -222,7 +221,7 @@ class pumpingStation { //this._calcNetFlow(); const {time:timeleft, source:variant} = this._calcTimeRemaining(); - this.logger.debug(`Remaining time ${Math.round(timeleft/60/60*100)/100} h, based on variant ${variant} `); + this.logger.debug(`Remaining time ~${Math.round(timeleft/60/60*10)/10} h, based on variant ${variant} `); } _calcTimeRemaining(){ @@ -624,7 +623,7 @@ const PumpingStation = require("./specificClass"); const RotatingMachine = require("../../rotatingMachine/src/specificClass"); const Measurement = require("../../measurement/src/specificClass"); -/** Helpers ******************************************************************/ +//Helpers function createPumpingStationConfig(name) { return { general: { @@ -754,10 +753,11 @@ function pushSample(measurement, type, value, unit) { .value(value, Date.now(), unit); } -/** Demo *********************************************************************/ +// Demo (async function demoStationWithPump() { const station = new PumpingStation(createPumpingStationConfig("PumpingStationDemo")); - const pump = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig()); + const pump1 = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig()); + const pump2 = new RotatingMachine(createMachineConfig("Pump2"), createMachineStateConfig()); const levelSensor = new Measurement(createLevelMeasurementConfig("WetWellLevel")); const upstreamFlow = new Measurement(createFlowMeasurementConfig("InfluentFlow", "upstream")); @@ -765,15 +765,16 @@ function pushSample(measurement, type, value, unit) { // 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"); + //pump.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.positionVsParent); + station.childRegistrationUtils.registerChild(pump1,"downstream"); + station.childRegistrationUtils.registerChild(pump2,"upstream"); setInterval(() => station.tick(), 1000); @@ -782,7 +783,7 @@ function pushSample(measurement, type, value, unit) { 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)); @@ -791,13 +792,17 @@ function pushSample(measurement, type, value, unit) { 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); +await pump1.handleInput("parent", "execSequence", "startup"); +await pump2.handleInput("parent", "execSequence", "startup"); +await pump1.handleInput("parent", "execMovement", 5); +await pump2.handleInput("parent", "execMovement", 5); console.log("Station state:", station.state); console.log("Station output:", station.getOutput()); - console.log("Pump state:", pump.state.getCurrentState()); + console.log("Pump state:", pump1.state.getCurrentState()); + console.log("Pump state:", pump2.state.getCurrentState()); })(); diff --git a/src/specificClass2.js b/src/specificClass2.js new file mode 100644 index 0000000..f771840 --- /dev/null +++ b/src/specificClass2.js @@ -0,0 +1,681 @@ +const EventEmitter = require('events'); +const { + logger, + configUtils, + configManager, + childRegistrationUtils, + MeasurementContainer, + coolprop, + interpolation +} = require('generalFunctions'); + +const FLOW_VARIANTS = ['measured', 'predicted']; +const LEVEL_VARIANTS = ['measured', 'predicted']; +const FLOW_POSITIONS = { + inflow: ['in', 'upstream'], + outflow: ['out', 'downstream'] +}; + +class PumpingStationV2 { + 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('level', 'm'); + this.measurements.setPreferredUnit('volume', 'm3'); + this.childRegistrationUtils = new childRegistrationUtils(this); + this.machines = {}; + this.stations = {}; + + 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') { + this._registerPredictedFlowChild(child); + return; + } + + this.logger.warn(`Unsupported child software type: ${softwareType}`); + } + + tick() { + const snapshot = this._takeMeasurementSnapshot(); + + this._updatePredictedVolume(snapshot); + + const netFlow = this._selectBestNetFlow(snapshot); + const remaining = this._computeRemainingTime(snapshot, netFlow); + + this.state = { + direction: netFlow.direction, + netFlow: netFlow.value, + flowSource: netFlow.source, + seconds: remaining.seconds, + remainingSource: remaining.source + }; + + this.logger.debug( + `Remaining time (${remaining.source ?? 'n/a'}): ${ + remaining.seconds != null ? `${Math.round((remaining.seconds / 60 / 60) * 10) / 10} h` : 'n/a' + }` + ); + } + + /* ------------------------------------------------------------------ */ + /* Helpers */ + /* ------------------------------------------------------------------ */ + + _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); + }); + } + + _registerPredictedFlowChild(child) { + const position = child.config.functionality.positionVsParent; + const childName = child.config.general.name; + + const listener = (eventName, posKey) => { + child.measurements.emitter.on(eventName, (eventData) => { + this.logger.debug( + `Predicted flow update from ${childName} (${position}) -> ${eventData.value} ${eventData.unit}` + ); + this.measurements + .type('flow') + .variant('predicted') + .position(posKey) + .value(eventData.value, eventData.timestamp, eventData.unit); + }); + }; + + if (position === 'downstream' || position === 'atEquipment' || position === 'out') { + listener('flow.predicted.downstream', 'out'); + } else if (position === 'upstream' || position === 'in') { + listener('flow.predicted.downstream', 'in'); + } else { + this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`); + } + } + + _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('volume') + .variant('percent') + .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: {} + }; + + for (const variant of FLOW_VARIANTS) { + snapshot.flows[variant] = this._snapshotFlowsForVariant(variant); + } + + for (const variant of LEVEL_VARIANTS) { + snapshot.levels[variant] = this._snapshotLevelForVariant(variant); + snapshot.levelRates[variant] = this._estimateLevelRate(snapshot.levels[variant]); + } + + return snapshot; + } + + _snapshotFlowsForVariant(variant) { + const inflowSeries = this._locateSeries('flow', variant, FLOW_POSITIONS.inflow); + const outflowSeries = this._locateSeries('flow', variant, FLOW_POSITIONS.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, + series: null, + current: null, + previous: null + }; + } + + try { + const current = seriesInfo.series.getLaggedSample(0); + const previous = seriesInfo.series.getLaggedSample(1); + return { + exists: Boolean(current), + series: seriesInfo.series, + current, + previous + }; + } catch (err) { + this.logger.debug( + `Failed to read samples for ${seriesInfo.type}.${seriesInfo.variant}.${seriesInfo.position}: ${err.message}` + ); + return { + exists: false, + series: seriesInfo.series, + current: null, + previous: null + }; + } + } + + _locateSeries(type, variant, positions) { + for (const position of positions) { + try { + this.measurements.type(type).variant(variant); + const exists = this.measurements.exists({ position, requireValues: true }); + if (!exists) continue; + const series = this.measurements.type(type).variant(variant).position(position); + return { type, variant, position, series }; + } 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 FLOW_VARIANTS) { + const flow = snapshot.flows[variant]; + if (!flow.inflow.exists || !flow.outflow.exists) continue; + + const inflow = flow.inflow.current?.value ?? null; + const outflow = flow.outflow.current?.value ?? null; + if (!Number.isFinite(inflow) || !Number.isFinite(outflow)) continue; + + const net = inflow - outflow; // positive => filling + if (!Number.isFinite(net)) continue; + + return { + value: net, + source: variant, + direction: this._deriveDirection(net) + }; + } + + // fallback using level trend + for (const variant of LEVEL_VARIANTS) { + 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 LEVEL_VARIANTS) { + 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 inflowCur = predicted.inflow.current; + const inflowPrev = predicted.inflow.previous ?? inflowCur; + const outflowCur = predicted.outflow.current; + const outflowPrev = predicted.outflow.previous ?? outflowCur; + + const timestampNow = + inflowCur?.timestamp ?? outflowCur?.timestamp ?? inflowPrev?.timestamp ?? outflowPrev?.timestamp; + const timestampPrev = inflowPrev?.timestamp ?? outflowPrev?.timestamp ?? timestampNow; + + if (!Number.isFinite(timestampNow) || !Number.isFinite(timestampPrev)) return; + + const deltaSeconds = (timestampNow - timestampPrev) / 1000; + if (!Number.isFinite(deltaSeconds) || deltaSeconds <= 0) return; + + const avgInflow = this._averageSampleValues(inflowCur, inflowPrev); + const avgOutflow = this._averageSampleValues(outflowCur, outflowPrev); + + const netVolumeChange = (avgInflow - avgOutflow) * deltaSeconds; + if (!Number.isFinite(netVolumeChange) || netVolumeChange === 0) return; + + const volumeSeries = this.measurements + .type('volume') + .variant('predicted') + .position('atEquipment'); + + const currentVolume = volumeSeries.getCurrentValue('m3') ?? this.basin.minVol; + const nextVolume = currentVolume + netVolumeChange; + + const writeTimestamp = Number.isFinite(timestampNow) ? timestampNow : Date.now(); + + 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'); + } + + _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(minVol) + .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.state = this.state; + output.basin = this.basin; + return output; + } +} + +module.exports = PumpingStationV2; + +/* ------------------------------------------------------------------------- */ +/* 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: 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) { + return { + general: { + name, + logging: { enabled: true, logLevel: 'debug' } + }, + functionality: { + positionVsParent: 'downstream' + }, + 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 PumpingStationV2(createPumpingStationConfig('PumpingStationDemo')); + const pump = new RotatingMachine(createMachineConfig('Pump1'), 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(pump, '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)); + + station.tick(); + console.log('Initial state:', station.state); + + await pump.handleInput('parent', 'execSequence', 'startup'); + await pump.handleInput('parent', 'execMovement', 50); + + console.log('Station state:', station.state); + console.log('Station output:', station.getOutput()); + })().catch((err) => { + console.error('Demo failed:', err); + }); +} -- 2.49.1 From 2a31c7ec69fda4d4ecc1f03f2d74b36fd6f300b9 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:04:26 +0100 Subject: [PATCH 05/21] working pumpingstation with machines --- src/specificClass.js | 1297 ++++++++++++++++++----------------------- src/specificClass2.js | 681 ---------------------- 2 files changed, 564 insertions(+), 1414 deletions(-) delete mode 100644 src/specificClass2.js diff --git a/src/specificClass.js b/src/specificClass.js index b5671c4..d59a3d6 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -1,843 +1,674 @@ const EventEmitter = require('events'); const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation} = require('generalFunctions'); - -class pumpingStation { - constructor(config={}) { - - this.emitter = new EventEmitter(); // Own EventEmitter - this.configManager = new configManager(); +class PumpingStationV2 { + 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); - // Init after config is set - 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('level', 'm'); + this.measurements.setPreferredUnit('volume', 'm3'); + this.childRegistrationUtils = new childRegistrationUtils(this); + this.machines = {}; + this.stations = {}; - // General properties - this.measurements = new MeasurementContainer({ - autoConvert: true - }); + //variants in determining what gets priority + this.flowVariants = ['measured', 'predicted']; + this.levelVariants = ['measured', 'predicted']; + this.flowPositions = { inflow: ['in', 'upstream'], outflow: ['out', 'downstream'] }; - // init basin object in pumping station this.basin = {}; this.state = { - direction: "steady", + direction: 'steady', netFlow: 0, flowSource: null, seconds: null, remainingSource: null - }; // init state object of pumping station to see whats going on + }; + + const thresholdFromConfig = Number(this.config.general?.flowThreshold); + this.flowThreshold = Number.isFinite(thresholdFromConfig) ? thresholdFromConfig : 1e-4; - // 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'); + this.logger.debug('PumpingStationV2 initialized'); } - /*------------------- Register child events -------------------*/ registerChild(child, softwareType) { - this.logger.debug('Setting up child event for softwaretype ' + softwareType); + this.logger.debug(`Registering child (${softwareType}) "${child.config.general.name}"`); - //define what to do with measurements - 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:`); - // Store directly in parent's measurement container - 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(`Flowdir ${flowDir} => currFlow ${currFLow.value} , prevflow = ${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.`); + if (softwareType === 'measurement') { + this._registerMeasurementChild(child); 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(); - const {time:timeleft, source:variant} = this._calcTimeRemaining(); - - this.logger.debug(`Remaining time ~${Math.round(timeleft/60/60*10)/10} h, based on variant ${variant} `); - } - - _calcTimeRemaining(){ - //init timeRemaining - const winningTime = {time:0,source:""}; - - //calculate time left prioritise flow based variant - const { time: flowTime, variant: flowVariant } = this._selectBestRemainingTimeFlowVariant(); - - //if flow doesnt work then use level based varianti to calc timeleft - if(flowVariant == null){ - const {time: levelTime, variant: levelVariant} = this._selectBestRemainingTimeLevelVariant(); - winningTime.time = levelTime; - winningTime.source = levelVariant; - if(levelVariant == null){ - winningTime.time = null; - winningTime.source = null; - } - } - else{ - winningTime.time = flowTime; - winningTime.source = flowVariant; + if (softwareType === 'machine' || softwareType === 'pumpingStation') { + this._registerPredictedFlowChild(child); + return; } - return winningTime; + this.logger.warn(`Unsupported child software type: ${softwareType}`); } - // Select remaining time based on flow + level variation measured or predicted and give back {time:0,variant:null}; - _selectBestRemainingTimeFlowVariant(){ + tick() { + const snapshot = this._takeMeasurementSnapshot(); - //define variants - const remainingTimeVariants = [ - { flowVariant: "measured", levelVariant: "measured" }, - { flowVariant: "measured", levelVariant: "predicted" }, - { flowVariant: "predicted", levelVariant: "measured" }, - { flowVariant: "predicted", levelVariant: "predicted" } - ]; + this._updatePredictedVolume(snapshot); - let remainingT = null; + const netFlow = this._selectBestNetFlow(snapshot); + const remaining = this._computeRemainingTime(snapshot, netFlow); - for (const variant of remainingTimeVariants) { - const candidate = this._calcRemainingTimeBasedOnFlow(variant); - if (candidate != null) { - remainingT = candidate; - return {time:remainingT,variant:variant}; - } - } - return {time:0,variant:null}; + 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`); } - // Select remaining time based only on level variation measured or predicted and give back {time:0,variant:null}; - _selectBestRemainingTimeLevelVariant(){ - //define variants (in sequence of priority first measured then predicted etc...) - const remainingTimeVariants = ["measured","predicted"]; + _registerMeasurementChild(child) { + const position = child.config.functionality.positionVsParent; + const measurementType = child.config.asset.type; + const eventName = `${measurementType}.measured.${position}`; - let remainingT = null; + child.measurements.emitter.on(eventName, (eventData) => { + this.logger.debug( + `Measurement update ${eventName} <- ${eventData.childName}: ${eventData.value} ${eventData.unit}` + ); - for (const variant of remainingTimeVariants) { - const candidate = this._calcRemainingTimeBasedOnLevel(variant); - if (candidate != null) { - remainingT = candidate; - return {time:remainingT,variant:variant}; - } - } - return {time:0,variant:null}; + this.measurements + .type(measurementType) + .variant('measured') + .position(position) + .value(eventData.value, eventData.timestamp, eventData.unit); + + this._handleMeasurement(measurementType, eventData.value, position, eventData); + }); } - _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; + _registerPredictedFlowChild(child) { + const position = child.config.functionality.positionVsParent; + const childName = child.config.general.name; - 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; - } - } + const listener = (eventName, posKey) => { + child.measurements.emitter.on(eventName, (eventData) => { + this.logger.debug( + `Predicted flow update from ${childName} (${position}) -> ${eventData.value} ${eventData.unit}` + ); + this.measurements + .type('flow') + .variant('predicted') + .position(posKey) + .value(eventData.value, eventData.timestamp, eventData.unit); + }); + }; - // context handler for pressure updates - updateMeasuredPressure(value, position, context = {}) { - - // init temp - 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'); - this.logger.debug(`Temperature is : ${kelvinTemp}`); + if (position === 'downstream' || position === 'atequipment' || position === 'out') { + listener('flow.predicted.downstream', 'out'); + } else if (position === 'upstream' || position === 'in') { + listener('flow.predicted.downstream', 'in'); } else { - kelvinTemp = mTemp; + this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`); } - 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 + } + + _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('volume') + .variant('percent') + .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 pressure_Pa = this.measurements.type("pressure").variant("measured").position(position).getCurrentValue('Pa'); - const level = pressure_Pa / density * g; + const level = pressurePa / (density * g); - this.measurements.type("level").variant("predicted").position(position).value(level); - //updatePredictedLevel(); ?? OLIFANT! - - //calculate how muc flow went in or out based on pressure difference - this.logger.debug(`Using pressure: ${value} for calculations`); - + this.measurements.type('level').variant('predicted').position(position).value(level, context.timestamp, 'm'); } - 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); + _takeMeasurementSnapshot() { + const snapshot = { + flows: {}, + levels: {}, + levelRates: {} + }; - //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.logger.debug(`basin minvol : ${this.basin.minVol}, cur volume : ${volume} / ${this.basin.maxVolOverflow}`); + for (const variant of this.flowVariants) { + snapshot.flows[variant] = this._snapshotFlowsForVariant(variant); + } - 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); - + for (const variant of this.levelVariants) { + snapshot.levels[variant] = this._snapshotLevelForVariant(variant); + snapshot.levelRates[variant] = this._estimateLevelRate(snapshot.levels[variant]); + } + + return snapshot; } - _calcNetFlow() { - let netFlow = null; + _snapshotFlowsForVariant(variant) { + const inflowSeries = this._locateSeries('flow', variant, this.flowPositions.inflow); + const outflowSeries = this._locateSeries('flow', variant, this.flowPositions.outflow); - const netFlow_FlowSensor = Math.abs(this.measurements.type("flow").variant("measured").difference({ from: "downstream", to: "upstream", unit: "m3/s" })); - const netFlow_LevelSensor = this._calcNetFlowFromLevelDiff("measured"); - const netFlow_PredictedFlow = Math.abs(this.measurements.type('flow').variant('predicted').difference({ from: "in", to: "out", unit: "m3/s" })); + return { + variant, + inflow: this._seriesSamples(inflowSeries), + outflow: this._seriesSamples(outflowSeries) + }; + } - 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; + _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 }; } } - //@params : params : example {flowVariant: "predicted",levelVariant: "measured"}; - _calcRemainingTimeBasedOnFlow(params){ - const {flowVariant,levelVariant} = params; - this.logger.debug(`${flowVariant} - ${levelVariant} `); + _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; - if( flowVariant === null || levelVariant === null ){ - this.logger.warn(`Cant calculate remaining time without needed variants`); - return 0; + 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 + + 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; - const levelexists = this.measurements.type("level").variant(levelVariant).exists({ position: "atEquipment", requireValues: true }); - const flowOutExists = this.measurements.type("flow").variant(flowVariant).exists({ position: "out", requireValues: true }); - const flowInExists = this.measurements.type("flow").variant(flowVariant).exists({ position: "in", requireValues: true }); - let secondsRemaining = 0; - - if( ! flowOutExists || ! flowInExists || ! levelexists){ - this.logger.warn(`Cant calculate remaining time without needed parameters ${flowOutExists} , ${flowInExists} , ${levelexists}`); - return null; + if (!Number.isFinite(surfaceArea) || surfaceArea <= 0) { + this.logger.warn('Invalid basin surface area.'); + return { seconds: null, source: null }; } - const flowDiff = this.measurements.type("flow").variant(flowVariant).difference({ from: "downstream", to: "upstream", unit: "m3/s" }); - const level = this.measurements.type("level").variant(levelVariant).type('atEquipment').getCurrentValue('m'); - let remainingHeight = 0; + for (const variant of this.levelVariants) { + const levelSnap = snapshot.levels[variant]; + const current = levelSnap.samples.current?.value ?? null; + if (!Number.isFinite(current)) continue; - switch(true){ + const remainingHeight = + netFlow.value > 0 + ? Math.max(heightOverflow - current, 0) + : Math.max(current - heightOutlet, 0); - case(flowDiff>0): - remainingHeight = Math.max(heightOverflow - level, 0); - secondsRemaining = remainingHeight * surfaceArea / flowDiff; - return secondsRemaining; + const seconds = (remainingHeight * surfaceArea) / Math.abs(netFlow.value); + if (!Number.isFinite(seconds)) continue; - case(flowDiff<0): - remainingHeight = Math.max(level - heightOutlet, 0); - secondsRemaining = remainingHeight * surfaceArea / Math.abs(flowDiff); - return secondsRemaining; - - default: - this.logger.debug(`Flowdiff is 0 not doing anything.`); - return secondsRemaining; - } + return { seconds, source: `${netFlow.source}/${variant}` }; + } + this.logger.warn('No level data available to compute remaining time.'); + return { seconds: null, source: netFlow.source }; } - //@params : variant : example "predicted","measured" - _calcRemainingTimeBasedOnLevel(variant){ + _updatePredictedVolume(snapshot) { + const predicted = snapshot.flows.predicted; + if (!predicted) return; - const {heightOverflow,heightOutlet} = this.basin; - const levelObj = this.measurements.type("level").variant(variant).position("atEquipment"); - const level = levelObj.getCurrentValue("m"); - const prevLevelSample = levelObj.getLaggedSample(2, "m"); // { value, timestamp, unit } - const measurement = levelObj.get(); - const latestTimestamp = measurement?.getLatestTimestamp(); + const now = Date.now(); + const inflowSample = predicted.inflow.current ?? predicted.inflow.previous ?? null; + const outflowSample = predicted.outflow.current ?? predicted.outflow.previous ?? null; - if (level === null || prevLevelSample == null || latestTimestamp == null) { - this.logger.warn(`no flowdiff ${level}, previous level ${prevLevelSample}, latestTimestamp ${latestTimestamp} found escaping`); - return null; + if (!this._predictedFlowState) { + this._predictedFlowState = { + inflow: inflowSample?.value ?? 0, + outflow: outflowSample?.value ?? 0, + lastTimestamp: inflowSample?.timestamp ?? outflowSample?.timestamp ?? now + }; } - const deltaSeconds = (latestTimestamp - prevLevelSample.timestamp) / 1000; - if (deltaSeconds <= 0) { - this.logger.warn(`Level fallback: invalid Δt=${deltaSeconds} , LatestTimestamp : ${latestTimestamp}, PrevTimestamp : ${prevLevelSample.value}`); - return null; + 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; } - const lvlDiff = level - prevLevelSample.value; - const lvlRate = lvlDiff / deltaSeconds; // m/s - let secondsRemaining = 0; - let remainingHeight = 0; + let deltaSeconds = (timestampNow - timestampPrev) / 1000; + if (!Number.isFinite(deltaSeconds) || deltaSeconds <= 0) { + deltaSeconds = 0; + } - switch(true){ + 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; + } - case(lvlRate>0): - remainingHeight = Math.max(heightOverflow - level, 0); - secondsRemaining = remainingHeight / Math.abs(lvlRate); // seconds - return secondsRemaining; + const writeTimestamp = timestampPrev + Math.max(deltaSeconds, 0) * 1000; - case(lvlRate<0): - remainingHeight = Math.max(level - heightOutlet, 0); - secondsRemaining = remainingHeight / Math.abs(lvlRate); - return secondsRemaining; + const volumeSeries = this.measurements + .type('volume') + .variant('predicted') + .position('atEquipment'); - default: - this.logger.debug(`Flowdiff is 0 not doing anything.`); - return secondsRemaining; - } + 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'); + + this._predictedFlowState.lastTimestamp = writeTimestamp; } - //Give a flowDifference and calculate direction => spits out filling , draining or stable - _calcDirectionBasedOnFlow(flowDiff){ - - let direction = null; - - switch (true){ - case flowDiff > flowThreshold: - direction = "filling"; - break; - - case flowDiff < -flowThreshold: - direction = "draining"; - break; - - case flowDiff < flowThreshold && flowDiff > -flowThreshold: - direction = "stable"; - break; - - default: - this.logger.warn("Uknown state direction detected??"); - return null; - - } - return direction; + _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; } - _calcNetFlowFromLevelDiff(variant) { - const { surfaceArea } = this.basin; - const levelObj = this.measurements.type("level").variant(variant).position("atEquipment"); - const level = levelObj.getCurrentValue("m"); - const prevLevelSample = levelObj.getLaggedSample(2, "m"); // { value, timestamp, unit } - const measurement = levelObj.get(); - const latestTimestamp = measurement?.getLatestTimestamp(); - - if (level === null || prevLevelSample == null || latestTimestamp == null) { - this.logger.warn(`no flowdiff ${level}, previous level ${prevLevelSample}, latestTimestamp ${latestTimestamp} found escaping`); - return null; - } - - const deltaSeconds = (latestTimestamp - prevLevelSample.timestamp) / 1000; - if (deltaSeconds <= 0) { - this.logger.warn(`Level fallback: invalid Δt=${deltaSeconds} , LatestTimestamp : ${latestTimestamp}, PrevTimestamp : ${prevLevelSample.timestamp}`); - return null; - } - - const lvlDiff = level - prevLevelSample.value; - const lvlRate = lvlDiff / deltaSeconds; // m/s - const netFlowRate = lvlRate * surfaceArea; // m³/s inferred from level trend - - return netFlowRate; + _deriveDirection(netFlow) { + if (netFlow > this.flowThreshold) return 'filling'; + if (netFlow < -this.flowThreshold) return 'draining'; + return 'steady'; } + /* ------------------------------------------------------------------ */ + /* Basin Calculations */ + /* ------------------------------------------------------------------ */ + 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 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 - const maxVolOverflow = heightOverflow * surfaceArea ; // Max water volume before you start loosing water to overflow - const minVol = heightOutlet * surfaceArea; - const minVolOut = heightInlet * surfaceArea ; // this will indicate if its an open end or a closed end. + const surfaceArea = volEmptyBasin / heightBasin; + const maxVol = heightBasin * surfaceArea; + const maxVolOverflow = heightOverflow * surfaceArea; + const minVol = heightOutlet * surfaceArea; + const minVolOut = heightInlet * surfaceArea; - 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 ; + this.basin = { + volEmptyBasin, + heightBasin, + heightInlet, + heightOutlet, + heightOverflow, + surfaceArea, + maxVol, + maxVolOverflow, + minVol, + minVolOut + }; - //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.measurements.type("volume").variant("predicted").position("atEquipment").value(maxVol).unit('m3'); + this.measurements + .type('volume') + .variant('predicted') + .position('atEquipment') + .value(maxVol) + .unit('m3'); - this.logger.debug(` - Basin initialized | area=${surfaceArea.toFixed(2)} m², - max=${maxVol.toFixed(2)} m³, - overflow=${maxVolOverflow.toFixed(2)} m³` + this.logger.debug( + `Basin initialized | area=${surfaceArea.toFixed(2)} m2, max=${maxVol.toFixed(2)} m3, overflow=${maxVolOverflow.toFixed(2)} m3` ); } -_calcVolumeFromLevel(level) { - const surfaceArea = this.basin.surfaceArea; - return Math.max(level, 0) * surfaceArea; -} + _calcVolumeFromLevel(level) { + return Math.max(level, 0) * this.basin.surfaceArea; + } -_calcLevelFromVolume(vol){ - const surfaceArea = this.basin.surfaceArea; - return Math.max(vol, 0) / surfaceArea; -} + _calcLevelFromVolume(volume) { + return Math.max(volume, 0) / this.basin.surfaceArea; + } + /* ------------------------------------------------------------------ */ + /* Output */ + /* ------------------------------------------------------------------ */ -getOutput() { - // Improved output object generation + getOutput() { 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(); + 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(); }); }); }); - //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; - } - - + output.state = this.state; + output.basin = this.basin; 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", +module.exports = PumpingStationV2; + +/* ------------------------------------------------------------------------- */ +/* 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 }, - }, - // Your custom config here (or leave empty for defaults) - movement: { - speed: 1, - }, - time: { - starting: 2, - warmingup: 3, - stopping: 2, - coolingdown: 3, - }, + 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 + } + }; } -} -// 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 pump1 = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig()); - const pump2 = new RotatingMachine(createMachineConfig("Pump2"), 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(pump1,"downstream"); - station.childRegistrationUtils.registerChild(pump2,"upstream"); - - 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 pump1.handleInput("parent", "execSequence", "startup"); -await pump2.handleInput("parent", "execSequence", "startup"); -await pump1.handleInput("parent", "execMovement", 5); -await pump2.handleInput("parent", "execMovement", 5); - console.log("Station state:", station.state); - console.log("Station output:", station.getOutput()); - console.log("Pump state:", pump1.state.getCurrentState()); - console.log("Pump state:", pump2.state.getCurrentState()); -})(); - - -/* -//coolprop example -(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`); + 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' } + }; } - 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')); + 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 PumpingStationV2(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); + }); +} diff --git a/src/specificClass2.js b/src/specificClass2.js deleted file mode 100644 index f771840..0000000 --- a/src/specificClass2.js +++ /dev/null @@ -1,681 +0,0 @@ -const EventEmitter = require('events'); -const { - logger, - configUtils, - configManager, - childRegistrationUtils, - MeasurementContainer, - coolprop, - interpolation -} = require('generalFunctions'); - -const FLOW_VARIANTS = ['measured', 'predicted']; -const LEVEL_VARIANTS = ['measured', 'predicted']; -const FLOW_POSITIONS = { - inflow: ['in', 'upstream'], - outflow: ['out', 'downstream'] -}; - -class PumpingStationV2 { - 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('level', 'm'); - this.measurements.setPreferredUnit('volume', 'm3'); - this.childRegistrationUtils = new childRegistrationUtils(this); - this.machines = {}; - this.stations = {}; - - 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') { - this._registerPredictedFlowChild(child); - return; - } - - this.logger.warn(`Unsupported child software type: ${softwareType}`); - } - - tick() { - const snapshot = this._takeMeasurementSnapshot(); - - this._updatePredictedVolume(snapshot); - - const netFlow = this._selectBestNetFlow(snapshot); - const remaining = this._computeRemainingTime(snapshot, netFlow); - - this.state = { - direction: netFlow.direction, - netFlow: netFlow.value, - flowSource: netFlow.source, - seconds: remaining.seconds, - remainingSource: remaining.source - }; - - this.logger.debug( - `Remaining time (${remaining.source ?? 'n/a'}): ${ - remaining.seconds != null ? `${Math.round((remaining.seconds / 60 / 60) * 10) / 10} h` : 'n/a' - }` - ); - } - - /* ------------------------------------------------------------------ */ - /* Helpers */ - /* ------------------------------------------------------------------ */ - - _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); - }); - } - - _registerPredictedFlowChild(child) { - const position = child.config.functionality.positionVsParent; - const childName = child.config.general.name; - - const listener = (eventName, posKey) => { - child.measurements.emitter.on(eventName, (eventData) => { - this.logger.debug( - `Predicted flow update from ${childName} (${position}) -> ${eventData.value} ${eventData.unit}` - ); - this.measurements - .type('flow') - .variant('predicted') - .position(posKey) - .value(eventData.value, eventData.timestamp, eventData.unit); - }); - }; - - if (position === 'downstream' || position === 'atEquipment' || position === 'out') { - listener('flow.predicted.downstream', 'out'); - } else if (position === 'upstream' || position === 'in') { - listener('flow.predicted.downstream', 'in'); - } else { - this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`); - } - } - - _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('volume') - .variant('percent') - .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: {} - }; - - for (const variant of FLOW_VARIANTS) { - snapshot.flows[variant] = this._snapshotFlowsForVariant(variant); - } - - for (const variant of LEVEL_VARIANTS) { - snapshot.levels[variant] = this._snapshotLevelForVariant(variant); - snapshot.levelRates[variant] = this._estimateLevelRate(snapshot.levels[variant]); - } - - return snapshot; - } - - _snapshotFlowsForVariant(variant) { - const inflowSeries = this._locateSeries('flow', variant, FLOW_POSITIONS.inflow); - const outflowSeries = this._locateSeries('flow', variant, FLOW_POSITIONS.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, - series: null, - current: null, - previous: null - }; - } - - try { - const current = seriesInfo.series.getLaggedSample(0); - const previous = seriesInfo.series.getLaggedSample(1); - return { - exists: Boolean(current), - series: seriesInfo.series, - current, - previous - }; - } catch (err) { - this.logger.debug( - `Failed to read samples for ${seriesInfo.type}.${seriesInfo.variant}.${seriesInfo.position}: ${err.message}` - ); - return { - exists: false, - series: seriesInfo.series, - current: null, - previous: null - }; - } - } - - _locateSeries(type, variant, positions) { - for (const position of positions) { - try { - this.measurements.type(type).variant(variant); - const exists = this.measurements.exists({ position, requireValues: true }); - if (!exists) continue; - const series = this.measurements.type(type).variant(variant).position(position); - return { type, variant, position, series }; - } 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 FLOW_VARIANTS) { - const flow = snapshot.flows[variant]; - if (!flow.inflow.exists || !flow.outflow.exists) continue; - - const inflow = flow.inflow.current?.value ?? null; - const outflow = flow.outflow.current?.value ?? null; - if (!Number.isFinite(inflow) || !Number.isFinite(outflow)) continue; - - const net = inflow - outflow; // positive => filling - if (!Number.isFinite(net)) continue; - - return { - value: net, - source: variant, - direction: this._deriveDirection(net) - }; - } - - // fallback using level trend - for (const variant of LEVEL_VARIANTS) { - 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 LEVEL_VARIANTS) { - 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 inflowCur = predicted.inflow.current; - const inflowPrev = predicted.inflow.previous ?? inflowCur; - const outflowCur = predicted.outflow.current; - const outflowPrev = predicted.outflow.previous ?? outflowCur; - - const timestampNow = - inflowCur?.timestamp ?? outflowCur?.timestamp ?? inflowPrev?.timestamp ?? outflowPrev?.timestamp; - const timestampPrev = inflowPrev?.timestamp ?? outflowPrev?.timestamp ?? timestampNow; - - if (!Number.isFinite(timestampNow) || !Number.isFinite(timestampPrev)) return; - - const deltaSeconds = (timestampNow - timestampPrev) / 1000; - if (!Number.isFinite(deltaSeconds) || deltaSeconds <= 0) return; - - const avgInflow = this._averageSampleValues(inflowCur, inflowPrev); - const avgOutflow = this._averageSampleValues(outflowCur, outflowPrev); - - const netVolumeChange = (avgInflow - avgOutflow) * deltaSeconds; - if (!Number.isFinite(netVolumeChange) || netVolumeChange === 0) return; - - const volumeSeries = this.measurements - .type('volume') - .variant('predicted') - .position('atEquipment'); - - const currentVolume = volumeSeries.getCurrentValue('m3') ?? this.basin.minVol; - const nextVolume = currentVolume + netVolumeChange; - - const writeTimestamp = Number.isFinite(timestampNow) ? timestampNow : Date.now(); - - 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'); - } - - _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(minVol) - .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.state = this.state; - output.basin = this.basin; - return output; - } -} - -module.exports = PumpingStationV2; - -/* ------------------------------------------------------------------------- */ -/* 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: 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) { - return { - general: { - name, - logging: { enabled: true, logLevel: 'debug' } - }, - functionality: { - positionVsParent: 'downstream' - }, - 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 PumpingStationV2(createPumpingStationConfig('PumpingStationDemo')); - const pump = new RotatingMachine(createMachineConfig('Pump1'), 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(pump, '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)); - - station.tick(); - console.log('Initial state:', station.state); - - await pump.handleInput('parent', 'execSequence', 'startup'); - await pump.handleInput('parent', 'execMovement', 50); - - console.log('Station state:', station.state); - console.log('Station output:', station.getOutput()); - })().catch((err) => { - console.error('Demo failed:', err); - }); -} -- 2.49.1 From f243761f00f927b44bc31f8fff7a82b135c44594 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Mon, 3 Nov 2025 07:42:51 +0100 Subject: [PATCH 06/21] Updated node status --- src/nodeClass.js | 97 +++++++++++++++++++++----------------------- src/specificClass.js | 60 ++++++++++++++++++--------- 2 files changed, 86 insertions(+), 71 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 23d156d..47ec062 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -101,65 +101,60 @@ class nodeClass { _updateNodeStatus() { const ps = this.source; - try { - // --- Basin & measurements ------------------------------------------------- - const maxVolBeforeOverflow = ps.basin?.maxVolOverflow ?? ps.basin?.maxVol ?? 0; - const volumeMeasurement = ps.measurements.type("volume").variant("measured").position("atEquipment"); - const currentVolume = volumeMeasurement.getCurrentValue("m3") ?? 0; - const netFlowMeasurement = ps.measurements.type("netFlowRate").variant("predicted").position("atEquipment"); - const netFlowM3s = netFlowMeasurement?.getCurrentValue("m3/s") ?? 0; - const netFlowM3h = netFlowM3s * 3600; - const percentFull = ps.measurements.type("volume").variant("procent").position("atEquipment").getCurrentValue() ?? 0; - // --- State information ---------------------------------------------------- - const direction = ps.state?.direction || "unknown"; - const secondsRemaining = ps.state?.seconds ?? null; - - const timeRemaining = secondsRemaining ? `${Math.round(secondsRemaining / 60)}` : 0 + " min"; - - // --- Icon / colour selection --------------------------------------------- - let symbol = "❔"; - let fill = "grey"; - - switch (direction) { - case "filling": - symbol = "⬆️"; - fill = "blue"; - break; - case "draining": - symbol = "⬇️"; - fill = "orange"; - break; - case "stable": - symbol = "⏸️"; - fill = "green"; - break; - default: - symbol = "❔"; - fill = "grey"; - break; + const pickVariant = (type, prefer = ['measured', 'predicted'], position = 'atEquipment', unit) => { + for (const variant of prefer) { + const chain = ps.measurements.type(type).variant(variant).position(position); + const value = unit ? chain.getCurrentValue(unit) : chain.getCurrentValue(); + if (value != null) return { value, variant }; } + return { value: null, variant: null }; + }; - // --- Status text ---------------------------------------------------------- - const textParts = [ - `${symbol} ${percentFull.toFixed(1)}%`, - `V=${currentVolume.toFixed(2)} / ${maxVolBeforeOverflow.toFixed(2)} m³`, - `net=${netFlowM3h.toFixed(1)} m³/h`, - `t≈${timeRemaining}` - ]; + const vol = pickVariant('volume', ['measured', 'predicted'], 'atEquipment', 'm3'); + const volPercent = pickVariant('volumePercent', ['measured','predicted'], 'atEquipment'); // already unitless + const level = pickVariant('level', ['measured', 'predicted'], 'atEquipment', 'm'); + const netFlow = pickVariant('netFlowRate', ['measured', 'predicted'], 'atEquipment', 'm3/s'); - return { - fill, - shape: "dot", - text: textParts.join(" | ") - }; - } catch (error) { - this.node.error("Error in updateNodeStatus: " + error.message); - return { fill: "red", shape: "ring", text: "Status Error" }; + const maxVolBeforeOverflow = ps.basin?.maxVolOverflow ?? ps.basin?.maxVol ?? 0; + const currentVolume = vol.value ?? 0; + const currentvolPercent = volPercent.value ?? 0; + const netFlowM3h = (netFlow.value ?? 0) * 3600; + + const direction = ps.state?.direction ?? 'unknown'; + const secondsRemaining = ps.state?.seconds ?? null; + const timeRemainingMinutes = secondsRemaining != null ? Math.round(secondsRemaining / 60) : null; + + const badgePieces = []; + badgePieces.push(`${currentvolPercent.toFixed(1)}% `); + badgePieces.push( + `V=${currentVolume.toFixed(2)} / ${maxVolBeforeOverflow.toFixed(2)} m³ (${'n/a'})` + ); + badgePieces.push(`net=${netFlowM3h.toFixed(1)} m³/h (${'n/a'})`); + if (timeRemainingMinutes != null) { + badgePieces.push(`t≈${timeRemainingMinutes} min)`); } + + const { symbol, fill } = (() => { + switch (direction) { + case 'filling': return { symbol: '⬆️', fill: 'blue' }; + case 'draining': return { symbol: '⬇️', fill: 'orange' }; + case 'steady': return { symbol: '⏸️', fill: 'green' }; + default: return { symbol: '❔', fill: 'grey' }; + } + })(); + + badgePieces[0] = `${symbol} ${badgePieces[0]}`; + + return { + fill, + shape: 'dot', + text: badgePieces.join(' | ') + }; } + // any time based functions here _startTickLoop() { setTimeout(() => { diff --git a/src/specificClass.js b/src/specificClass.js index d59a3d6..d9ee587 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -1,6 +1,6 @@ const EventEmitter = require('events'); const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation} = require('generalFunctions'); -class PumpingStationV2 { +class PumpingStation { constructor(config = {}) { this.emitter = new EventEmitter(); this.configManager = new configManager(); @@ -18,6 +18,8 @@ class PumpingStationV2 { this.machines = {}; this.stations = {}; + + //variants in determining what gets priority this.flowVariants = ['measured', 'predicted']; this.levelVariants = ['measured', 'predicted']; @@ -61,6 +63,8 @@ class PumpingStationV2 { this._updatePredictedVolume(snapshot); const netFlow = this._selectBestNetFlow(snapshot); + //write netflow in measurment container + //this.measurements() const remaining = this._computeRemainingTime(snapshot, netFlow); this.state = { @@ -160,8 +164,8 @@ class PumpingStationV2 { .value(volume, context.timestamp, 'm3'); this.measurements - .type('volume') - .variant('percent') + .type('volumePercent') + .variant('measured') .position('atequipment') .value(percent, context.timestamp, '%'); } @@ -416,6 +420,22 @@ class PumpingStationV2 { .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; } @@ -465,7 +485,7 @@ class PumpingStationV2 { .type('volume') .variant('predicted') .position('atEquipment') - .value(maxVol) + .value(maxVolOverflow) .unit('m3'); this.logger.debug( @@ -501,12 +521,12 @@ class PumpingStationV2 { } } -module.exports = PumpingStationV2; +module.exports = PumpingStation; /* ------------------------------------------------------------------------- */ /* Example usage */ /* ------------------------------------------------------------------------- */ - +/* if (require.main === module) { const Measurement = require('../../measurement/src/specificClass'); const RotatingMachine = require('../../rotatingMachine/src/specificClass'); @@ -525,10 +545,10 @@ if (require.main === module) { }, basin: { volume: 43.75, - height: 3.5, - heightInlet: 0.3, + height: 10, + heightInlet: 3, heightOutlet: 0.2, - heightOverflow: 3.0 + heightOverflow: 3.2 }, hydraulics: { refHeight: 'NAP', @@ -630,7 +650,7 @@ if (require.main === module) { } (async function demo() { - const station = new PumpingStationV2(createPumpingStationConfig('PumpingStationDemo')); + const station = new PumpingStation(createPumpingStationConfig('PumpingStationDemo')); const pump1 = new RotatingMachine(createMachineConfig('Pump1','downstream'), createMachineStateConfig()); const pump2 = new RotatingMachine(createMachineConfig('Pump2','upstream'), createMachineStateConfig()); @@ -638,20 +658,19 @@ if (require.main === module) { 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(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'); -*/ + + //seedSample(levelSensor, 'level', 1.8, 'm'); + //seedSample(inflowSensor, 'flow', 0.35, 'm3/s'); + //seedSample(outflowSensor, 'flow', 0.20, 'm3/s'); + setInterval( () => station.tick(), 1000); @@ -672,3 +691,4 @@ if (require.main === module) { console.error('Demo failed:', err); }); } + //*/ -- 2.49.1 From d44cbc978bb090dd6b112939d653dbdbe7e8ac00 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:17:22 +0100 Subject: [PATCH 07/21] updates visual --- src/nodeClass.js | 8 ++++---- src/specificClass.js | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 47ec062..b00364f 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -114,12 +114,12 @@ class nodeClass { const vol = pickVariant('volume', ['measured', 'predicted'], 'atEquipment', 'm3'); const volPercent = pickVariant('volumePercent', ['measured','predicted'], 'atEquipment'); // already unitless const level = pickVariant('level', ['measured', 'predicted'], 'atEquipment', 'm'); - const netFlow = pickVariant('netFlowRate', ['measured', 'predicted'], 'atEquipment', 'm3/s'); + const netFlow = pickVariant('netFlowRate', ['measured', 'predicted'], 'atEquipment', 'm3/h'); const maxVolBeforeOverflow = ps.basin?.maxVolOverflow ?? ps.basin?.maxVol ?? 0; const currentVolume = vol.value ?? 0; const currentvolPercent = volPercent.value ?? 0; - const netFlowM3h = (netFlow.value ?? 0) * 3600; + const netFlowM3h = netFlow.value ?? 0; const direction = ps.state?.direction ?? 'unknown'; const secondsRemaining = ps.state?.seconds ?? null; @@ -128,9 +128,9 @@ class nodeClass { const badgePieces = []; badgePieces.push(`${currentvolPercent.toFixed(1)}% `); badgePieces.push( - `V=${currentVolume.toFixed(2)} / ${maxVolBeforeOverflow.toFixed(2)} m³ (${'n/a'})` + `V=${currentVolume.toFixed(2)} / ${maxVolBeforeOverflow.toFixed(2)} m³` ); - badgePieces.push(`net=${netFlowM3h.toFixed(1)} m³/h (${'n/a'})`); + badgePieces.push(`net: ${netFlowM3h.toFixed(0)} m³/h`); if (timeRemainingMinutes != null) { badgePieces.push(`t≈${timeRemainingMinutes} min)`); } diff --git a/src/specificClass.js b/src/specificClass.js index d9ee587..c306f96 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -12,12 +12,12 @@ class PumpingStation { 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 @@ -64,7 +64,7 @@ class PumpingStation { const netFlow = this._selectBestNetFlow(snapshot); //write netflow in measurment container - //this.measurements() + const remaining = this._computeRemainingTime(snapshot, netFlow); this.state = { @@ -301,12 +301,16 @@ class PumpingStation { _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) }; } -- 2.49.1 From 1848486f1c15c88e4b9b9bf486d18164b52c0214 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:19:20 +0100 Subject: [PATCH 08/21] bug fixes output formatting --- src/nodeClass.js | 4 ++-- src/specificClass.js | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index b00364f..e0a4b2a 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -177,8 +177,8 @@ class nodeClass { //pumping station needs time based ticks to recalc level when predicted this.source.tick(); const raw = this.source.getOutput(); - const processMsg = this._output.formatMsg(raw, this.config, 'process'); - const influxMsg = this._output.formatMsg(raw, this.config, 'influxdb'); + const processMsg = this._output.formatMsg(raw, this.source.config, 'process'); + const influxMsg = this._output.formatMsg(raw, this.source.config, 'influxdb'); // Send only updated outputs on ports 0 & 1 this.node.send([processMsg, influxMsg]); diff --git a/src/specificClass.js b/src/specificClass.js index c306f96..d4dbdfe 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -519,8 +519,16 @@ class PumpingStation { }); }); - output.state = this.state; - output.basin = this.basin; + 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; } } -- 2.49.1 From 9e4b149b6482880b2b3206e671853ed9947148a8 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:46:54 +0100 Subject: [PATCH 09/21] fixed multiple children being able to pull and push to pumpingstation --- src/specificClass.js | 120 ++++++++++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 30 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index d4dbdfe..d2a79bb 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -23,7 +23,9 @@ class PumpingStation { //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 = { @@ -49,7 +51,7 @@ class PumpingStation { return; } - if (softwareType === 'machine' || softwareType === 'pumpingStation') { + if (softwareType === 'machine' || softwareType === 'pumpingStation' || softwareType === 'machineGroupController') { this._registerPredictedFlowChild(child); return; } @@ -57,16 +59,38 @@ class PumpingStation { 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); - //write netflow in measurment container - const remaining = this._computeRemainingTime(snapshot, netFlow); + this._safeGuardSystem(snapshot,remaining.seconds); + this.state = { direction: netFlow.direction, netFlow: netFlow.value, @@ -100,32 +124,63 @@ class PumpingStation { }); } + //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 listener = (eventName, posKey) => { - child.measurements.emitter.on(eventName, (eventData) => { - this.logger.debug( - `Predicted flow update from ${childName} (${position}) -> ${eventData.value} ${eventData.unit}` - ); - this.measurements - .type('flow') - .variant('predicted') - .position(posKey) - .value(eventData.value, eventData.timestamp, eventData.unit); - }); + 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); }; - if (position === 'downstream' || position === 'atequipment' || position === 'out') { - listener('flow.predicted.downstream', 'out'); - } else if (position === 'upstream' || position === 'in') { - listener('flow.predicted.downstream', 'in'); - } else { - this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`); + 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': @@ -213,13 +268,18 @@ class PumpingStation { const snapshot = { flows: {}, levels: {}, - levelRates: {} + 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]); @@ -228,15 +288,17 @@ class PumpingStation { 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) - }; + return {variant, inflow: this._seriesSamples(inflowSeries), outflow: this._seriesSamples(outflowSeries) }; } _snapshotLevelForVariant(variant) { @@ -342,6 +404,7 @@ class PumpingStation { return { seconds: null, source: null }; } + for (const variant of this.levelVariants) { const levelSnap = snapshot.levels[variant]; const current = levelSnap.samples.current?.value ?? null; @@ -405,10 +468,7 @@ class PumpingStation { const writeTimestamp = timestampPrev + Math.max(deltaSeconds, 0) * 1000; - const volumeSeries = this.measurements - .type('volume') - .variant('predicted') - .position('atEquipment'); + const volumeSeries = this.measurements.type('volume').variant('predicted').position('atEquipment'); const currentVolume = volumeSeries.getCurrentValue('m3') ?? this.basin.minVol; -- 2.49.1 From 43eb97407f2613bdded42fa1e9ba321e1246911d Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:07:56 +0100 Subject: [PATCH 10/21] added safeguarding when vol gets too low for machines, --- src/nodeClass.js | 8 +++ src/specificClass.js | 126 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 117 insertions(+), 17 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index e0a4b2a..3d1c376 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -204,6 +204,14 @@ class nodeClass { const childObj = this.RED.nodes.getNode(childId); this.source.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent); break; + case 'calibratePredictedVolume': + const calibratedVolume = this.source.measurements + .type('volume') + .variant('measured') + .position('atequipment') + .getCurrentValue('m3'); + this.source.calibratePredictedVolume(calibratedVolume); + break; } done(); }); diff --git a/src/specificClass.js b/src/specificClass.js index d2a79bb..ceb8628 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -16,9 +16,9 @@ class PumpingStation { this.measurements.setPreferredUnit('level', 'm'); this.measurements.setPreferredUnit('volume', 'm3'); this.childRegistrationUtils = new childRegistrationUtils(this); - this.machines = {}; - this.stations = {}; - + this.machines = {}; + this.stations = {}; + this.machineGroups = {}; //variants in determining what gets priority this.flowVariants = ['measured', 'predicted']; @@ -51,15 +51,36 @@ class PumpingStation { return; } + //for machines register them for control + if(softwareType === 'machine'){ + const childId = child.config.general.id; + this.machines[childId] = child; + this.logger.debug(`Registered machine child "${child.config.general.name}" with id "${childId}"`); + } + + // for pumping stations register them for control + if(softwareType === 'pumpingStation'){ + const childId = child.config.general.id; + this.stations[childId] = child; + this.logger.debug(`Registered pumping station child "${child.config.general.name}" with id "${childId}"`); + } + + // for machine group controllers register them for control + if(softwareType === 'machineGroupController'){ + const childId = child.config.general.id; + this.machineGroups[childId] = child; + this.logger.debug(`Registered machine group controller child "${child.config.general.name}" with id "${childId}"`); + } + + //for all childs that can provide predicted flow data if (softwareType === 'machine' || softwareType === 'pumpingStation' || softwareType === 'machineGroupController') { this._registerPredictedFlowChild(child); - return; } this.logger.warn(`Unsupported child software type: ${softwareType}`); } - _safeGuardSystem(snapshot,remainingTime){ + _safeGuardSystem(snapshot,remainingTime,direction){ let vol = null; for (const variant of this.volVariants){ @@ -67,17 +88,89 @@ class PumpingStation { //go through with variants until we find one that exists if (!volsnap.samples.exists){ continue}; - const vol = volsnap.samples.current?.value ?? null; + 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){} - */ + //if we cant get a volume we cant control blind turn all pumps off. + Object.entries(this.machines).forEach(([machineId, machine]) => { + machine.handleInput('parent', 'execSequence', 'shutdown'); + }); + this.logger.warn('No volume data available to safe guard system; shutting down all machines.'); + return; + } + //get threshholds from config + const timeThreshhold = this.config.control.timeThreshholdSeconds; //seconds + const triggerHighVol = this.basin.maxVolOverflow * ( this.config.control.thresholdHighVolume/100 ); + const triggerLowVol = this.basin.minVolOut * ( this.config.control.thresholdLowVolume/100); + + // trigger conditions for draining + if(direction == "draining"){ + this.logger.debug( + `Safe-guard (draining): vol=${vol != null ? vol.toFixed(2) + ' m3' : 'N/A'}; ` + + `remainingTime=${Number.isFinite(remainingTime) ? remainingTime.toFixed(1) + ' s' : 'N/A'}; ` + + `direction=${String(direction)}; triggerLowVol=${Number.isFinite(triggerLowVol) ? triggerLowVol.toFixed(2) + ' m3' : 'N/A'}` + ); + + if( (remainingTime < timeThreshhold && remainingTime !== null) || vol < triggerLowVol || vol == null){ + //shut down all downstream or atequipment machines,pumping stations and machine groups + Object.entries(this.machines).forEach(([machineId, machine]) => { + machine.handleInput('parent', 'execSequence', 'shutdown'); + this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down machine "${machineId}"`); + }); + Object.entries(this.stations).forEach(([stationId, station]) => { + station.handleInput('parent', 'execSequence', 'shutdown'); + this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down station "${stationId}"`); + }); + Object.entries(this.machineGroups).forEach(([groupId, group]) => { + group.handleInput('parent', 'execSequence', 'shutdown'); + this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down machine group "${groupId}"`); + }); + } + } + + // trigger conditions for filling + if(direction == "filling"){ + + } + } + + //calibrate the predicted volume to a known value + calibratePredictedVolume(calibratedVol, timestamp = Date.now()){ + + const volumeChain = this.measurements + .type('volume') + .variant('predicted') + .position('atequipment'); + + //if we have existing values clear them out + const volumeMeasurement = volumeChain.exists() ? volumeChain.get() : null; + if (volumeMeasurement) { + volumeMeasurement.values = []; + volumeMeasurement.timestamps = []; + } + + volumeChain.value(calibratedVol, timestamp, 'm3').unit('m3'); + + const levelChain = this.measurements + .type('level') + .variant('predicted') + .position('atequipment'); + + const levelMeasurement = levelChain.exists() ? levelChain.get() : null; + if (levelMeasurement) { + levelMeasurement.values = []; + levelMeasurement.timestamps = []; + } + + levelChain.value(this._calcLevelFromVolume(calibratedVol), timestamp, 'm'); + + this._predictedFlowState = { + inflow: 0, + outflow: 0, + lastTimestamp: timestamp + }; } tick() { @@ -89,7 +182,7 @@ class PumpingStation { const netFlow = this._selectBestNetFlow(snapshot); const remaining = this._computeRemainingTime(snapshot, netFlow); - this._safeGuardSystem(snapshot,remaining.seconds); + this._safeGuardSystem(snapshot,remaining.seconds,netFlow.direction); this.state = { direction: netFlow.direction, @@ -151,9 +244,7 @@ class PumpingStation { 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.logger.debug(`Predicted flow update from ${childName} (${childId}, ${posKey}) -> ${value} ${unit}`); this.predictedFlowChildren.get(childId)[posKey] = value; this._refreshAggregatedPredictedFlow(posKey, timestamp, unit); @@ -201,6 +292,7 @@ class PumpingStation { _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); @@ -598,7 +690,7 @@ module.exports = PumpingStation; /* ------------------------------------------------------------------------- */ /* Example usage */ /* ------------------------------------------------------------------------- */ -/* + if (require.main === module) { const Measurement = require('../../measurement/src/specificClass'); const RotatingMachine = require('../../rotatingMachine/src/specificClass'); -- 2.49.1 From fbfcec4b47577da8d3f6466ea93314ee370b9d48 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:20:23 +0100 Subject: [PATCH 11/21] Added simpel case for level control --- src/specificClass.js | 113 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 16 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index ceb8628..8d731ca 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -20,6 +20,10 @@ class PumpingStation { this.stations = {}; this.machineGroups = {}; + //fetch control mode from config by default + this.mode = this.config.control.mode; + this._levelState = { crossed: new Set(), dwellUntil: null }; + //variants in determining what gets priority this.flowVariants = ['measured', 'predicted']; this.levelVariants = ['measured', 'predicted']; @@ -80,16 +84,11 @@ class PumpingStation { this.logger.warn(`Unsupported child software type: ${softwareType}`); } - _safeGuardSystem(snapshot,remainingTime,direction){ - let vol = null; + _safetyController(snapshot,remainingTime,direction){ - 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}; + this.safetyControllerActive = false; - vol = volsnap.samples.current?.value ?? null; - } + const vol = this._resolveVolume(snapshot); if(vol == null){ //if we cant get a volume we cant control blind turn all pumps off. @@ -97,13 +96,14 @@ class PumpingStation { machine.handleInput('parent', 'execSequence', 'shutdown'); }); this.logger.warn('No volume data available to safe guard system; shutting down all machines.'); + this.safetyControllerActive = true; return; } //get threshholds from config - const timeThreshhold = this.config.control.timeThreshholdSeconds; //seconds - const triggerHighVol = this.basin.maxVolOverflow * ( this.config.control.thresholdHighVolume/100 ); - const triggerLowVol = this.basin.minVolOut * ( this.config.control.thresholdLowVolume/100); + const timeThreshhold = this.config.safety.timeleftToFullOrEmptyThresholdSeconds; //seconds + const triggerHighVol = this.basin.maxVolOverflow * ( this.config.safety.overfillThresholdPercent/100 ); + const triggerLowVol = this.basin.minVolOut * ( this.config.safety.dryRunThresholdPercent/100 ); // trigger conditions for draining if(direction == "draining"){ @@ -127,13 +127,90 @@ class PumpingStation { group.handleInput('parent', 'execSequence', 'shutdown'); this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down machine group "${groupId}"`); }); + this.safetyControllerActive = true; } } - // trigger conditions for filling - if(direction == "filling"){ - + } + + changeMode(newMode){ + + if ( this.config.control.allowedModes.has(newMode) ){ + this.mode = newMode; + this.logger.info(`Control mode changed from ${currentMode} to ${newMode}`); } + else{ + this.logger.warn(`Attempted to change to unsupported control mode: ${newMode}`); + } + + } + + async _controlLevelBased(snapshot, remainingTime) { + // current volume as a percentage of usable capacity + const vol = this._resolveVolume(snapshot); + if (vol == null) { + this.logger.warn('No valid volume found for level-based control'); + return; + } + + const { thresholds, timeThresholdSeconds } = this.config.control.levelbased; + const percentFull = (vol / this.basin.maxVolOverflow) * 100; + + // pick thresholds that are now crossed but were not crossed before + const newlyCrossed = thresholds.filter(t => percentFull >= t && !this._levelState.crossed.has(t)); + this.logger.debug(`Level-based control: vol=${vol.toFixed(2)} m³ (${percentFull.toFixed(1)}%), newly crossed thresholds: [${newlyCrossed.join(', ')}]`); + if (!newlyCrossed.length) return; + + const now = Date.now(); + if (!this._levelState.dwellUntil) { + this._levelState.dwellUntil = now + timeThresholdSeconds * 1000; + this.logger.debug(`Level-based control: waiting ${timeThresholdSeconds}s before acting`); + return; + } + this.logger.debug(`Level-based control: dwelling for another ${Math.round((this._levelState.dwellUntil - now) / 1000)} seconds`); + if (now < this._levelState.dwellUntil) return; // still waiting + + this._levelState.dwellUntil = null; // dwell satisfied, let pumps start + + for (const threshold of newlyCrossed) { + const nextMachine = this._nextIdleMachine(); + if (!nextMachine) break; + this._levelState.crossed.add(threshold); + this.logger.info( + `level-based control: threshold ${threshold}% reached, starting "${nextMachine.config.general.name}" (vol=${vol.toFixed(2)} m³)` + ); + await nextMachine.handleInput('parent', 'execSequence', 'startup'); + } + } + + _resolveVolume(snapshot) { + for (const variant of this.volVariants) { + const volsnap = snapshot.vols[variant]; + if (volsnap?.samples?.exists) return volsnap.samples.current?.value ?? null; + } + return null; + } + + _nextIdleMachine() { + return Object.values(this.machines).find(m => !m._isOperationalState()); + } + + //control logic + _controlLogic(snapshot, remainingTime){ + const mode = this.mode; + + switch(mode){ + case "levelbased": + this.logger.debug(`Executing level-based control logic`); + this._controlLevelBased(snapshot, remainingTime); + break; + case "flowbased": + this._controlFlowBased(); + break; + default: + this.logger.warn(`Unsupported control mode: ${mode}`); + } + } //calibrate the predicted volume to a known value @@ -178,11 +255,15 @@ class PumpingStation { this._updatePredictedVolume(snapshot); - const netFlow = this._selectBestNetFlow(snapshot); const remaining = this._computeRemainingTime(snapshot, netFlow); - this._safeGuardSystem(snapshot,remaining.seconds,netFlow.direction); + //check safety conditions + this._safetyController(snapshot,remaining.seconds,netFlow.direction); + if(this.safetyControllerActive) return; + + //if safety not active proceed with normal control + this._controlLogic(snapshot,remaining.seconds); this.state = { direction: netFlow.direction, -- 2.49.1 From 4cc529b1c2ca60e72189bd1b43948681c27c0771 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:37:09 +0100 Subject: [PATCH 12/21] Fixes next idle machine for level control --- src/specificClass.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index 8d731ca..4fb2ffe 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -1,5 +1,6 @@ const EventEmitter = require('events'); const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation} = require('generalFunctions'); + class PumpingStation { constructor(config = {}) { this.emitter = new EventEmitter(); @@ -192,7 +193,10 @@ class PumpingStation { } _nextIdleMachine() { - return Object.values(this.machines).find(m => !m._isOperationalState()); + return Object.values(this.machines).find((machine) => { + const position = machine?.config?.functionality?.positionVsParent; + return ( position === 'downstream' || position === 'atEquipment') && !machine._isOperationalState(); + }); } //control logic @@ -370,6 +374,7 @@ class PumpingStation { } } + _onLevelMeasurement(position, value, context = {}) { const levelSeries = this.measurements.type('level').variant('measured').position(position); const levelMeters = levelSeries.getCurrentValue('m'); @@ -688,11 +693,13 @@ class PumpingStation { return 'steady'; } + /* ------------------------------------------------------------------ */ /* Basin Calculations */ /* ------------------------------------------------------------------ */ initBasinProperties() { + const volEmptyBasin = this.config.basin.volume; const heightBasin = this.config.basin.height; const heightInlet = this.config.basin.heightInlet; @@ -718,12 +725,7 @@ class PumpingStation { minVolOut }; - this.measurements - .type('volume') - .variant('predicted') - .position('atEquipment') - .value(maxVolOverflow) - .unit('m3'); + 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` @@ -771,7 +773,7 @@ module.exports = PumpingStation; /* ------------------------------------------------------------------------- */ /* Example usage */ /* ------------------------------------------------------------------------- */ - +/* if (require.main === module) { const Measurement = require('../../measurement/src/specificClass'); const RotatingMachine = require('../../rotatingMachine/src/specificClass'); -- 2.49.1 From 0a6c7ee2e1dcb4bea81e1a0e167718d58f85d216 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:37:41 +0100 Subject: [PATCH 13/21] Further bug fixes and optimized level control for groups and machines alike --- src/specificClass.js | 191 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 163 insertions(+), 28 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index 4fb2ffe..b179014 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -64,25 +64,27 @@ class PumpingStation { } // for pumping stations register them for control - if(softwareType === 'pumpingStation'){ + if(softwareType === 'pumpingstation'){ const childId = child.config.general.id; this.stations[childId] = child; this.logger.debug(`Registered pumping station child "${child.config.general.name}" with id "${childId}"`); } // for machine group controllers register them for control - if(softwareType === 'machineGroupController'){ + if(softwareType === 'machinegroup'){ const childId = child.config.general.id; this.machineGroups[childId] = child; - this.logger.debug(`Registered machine group controller child "${child.config.general.name}" with id "${childId}"`); + this._registerPredictedFlowChild(child); + this.logger.debug(`Registered machine group child "${child.config.general.name}" with id "${childId}"`); } //for all childs that can provide predicted flow data - if (softwareType === 'machine' || softwareType === 'pumpingStation' || softwareType === 'machineGroupController') { + if (softwareType === 'machine' || softwareType === 'pumpingstation' || softwareType === 'machinegroup') { + this.logger.debug(`Registering predicted flow child ${child.config.general.name} with software type: ${softwareType}"`); this._registerPredictedFlowChild(child); } - this.logger.warn(`Unsupported child software type: ${softwareType}`); + //this.logger.warn(`Unsupported child software type: ${softwareType}`); } _safetyController(snapshot,remainingTime,direction){ @@ -117,21 +119,49 @@ class PumpingStation { if( (remainingTime < timeThreshhold && remainingTime !== null) || vol < triggerLowVol || vol == null){ //shut down all downstream or atequipment machines,pumping stations and machine groups Object.entries(this.machines).forEach(([machineId, machine]) => { - machine.handleInput('parent', 'execSequence', 'shutdown'); - this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down machine "${machineId}"`); + const position = machine?.config?.functionality?.positionVsParent; + if ((position === 'downstream' || position === 'atEquipment') && machine._isOperationalState()) { + machine.handleInput('parent', 'execSequence', 'shutdown'); + this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down machine "${machineId}"`); + } }); Object.entries(this.stations).forEach(([stationId, station]) => { station.handleInput('parent', 'execSequence', 'shutdown'); this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down station "${stationId}"`); }); Object.entries(this.machineGroups).forEach(([groupId, group]) => { - group.handleInput('parent', 'execSequence', 'shutdown'); + group.turnOffAllMachines(); this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down machine group "${groupId}"`); }); this.safetyControllerActive = true; } } + if(direction == "filling"){ + this.logger.debug(`Safe-guard (filling): vol=${vol != null ? vol.toFixed(2) + ' m3' : 'N/A'}; ` + + `remainingTime=${Number.isFinite(remainingTime) ? remainingTime.toFixed(1) + ' s' : 'N/A'}; ` + + `direction=${String(direction)}; triggerHighVol=${Number.isFinite(triggerHighVol) ? triggerHighVol.toFixed(2) + ' m3' : 'N/A'}` + ); + if( (remainingTime < timeThreshhold && remainingTime !== null) || vol > triggerHighVol || vol == null){ + //shut down all upstream machines,pumping stations and machine groups + Object.entries(this.machines).forEach(([machineId, machine]) => { + const position = machine?.config?.functionality?.positionVsParent; + if ((position === 'upstream' ) && machine._isOperationalState()) { + machine.handleInput('parent', 'execSequence', 'shutdown'); + } + }); + Object.entries(this.machineGroups).forEach(([groupId, group]) => { + group.turnOffAllMachines(); + }); + Object.entries(this.stations).forEach(([stationId, station]) => { + station.handleInput('parent', 'execSequence', 'shutdown'); + }); + + this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down all upstream machines/stations/groups`); + this.safetyControllerActive = true; + } + } + } changeMode(newMode){ @@ -146,7 +176,10 @@ class PumpingStation { } + /* old levelcontrol async _controlLevelBased(snapshot, remainingTime) { + + // current volume as a percentage of usable capacity const vol = this._resolveVolume(snapshot); if (vol == null) { @@ -168,6 +201,7 @@ class PumpingStation { this.logger.debug(`Level-based control: waiting ${timeThresholdSeconds}s before acting`); return; } + this.logger.debug(`Level-based control: dwelling for another ${Math.round((this._levelState.dwellUntil - now) / 1000)} seconds`); if (now < this._levelState.dwellUntil) return; // still waiting @@ -183,6 +217,98 @@ class PumpingStation { await nextMachine.handleInput('parent', 'execSequence', 'startup'); } } +*/ + async _controlLevelBased(snapshot, remainingTime) { + + + // current volume as a percentage of usable capacity + const vol = this._resolveVolume(snapshot); + if (vol == null) { + this.logger.warn('No valid volume found for level-based control'); + return; + } + + const { thresholds, timeThresholdSeconds } = this.config.control.levelbased; + const percentFull = (vol / this.basin.maxVolOverflow) * 100; + + // pick thresholds that are now crossed but were not crossed before + const newlyCrossed = thresholds.filter(t => percentFull >= t && !this._levelState.crossed.has(t)); + this.logger.debug(`Level-based control: vol=${vol.toFixed(2)} m³ (${percentFull.toFixed(1)}%), newly crossed thresholds: [${newlyCrossed.join(', ')}]`); + if (!newlyCrossed.length) return; + + const now = Date.now(); + if (!this._levelState.dwellUntil) { + this._levelState.dwellUntil = now + timeThresholdSeconds * 1000; + this.logger.debug(`Level-based control: waiting ${timeThresholdSeconds}s before acting`); + return; + } + + this.logger.debug(`Level-based control: dwelling for another ${Math.round((this._levelState.dwellUntil - now) / 1000)} seconds`); + if (now < this._levelState.dwellUntil) return; // still waiting + + this._levelState.dwellUntil = null; // dwell satisfied, let pumps start + + this._levelState.dwellUntil = null; + + newlyCrossed.forEach((threshold) => this._levelState.crossed.add(threshold)); + + const percentControl = this._calculateLevelControlPercent(thresholds); + if (percentControl <= 0) { + this.logger.debug('Level-based control: percent control resolved to 0%, skipping commands'); + return; + } + + this.logger.info( + `level-based control: thresholds [${newlyCrossed.join(', ')}]% reached, requesting ${percentControl.toFixed(1)}% control (vol=${vol.toFixed(2)} m³)` + ); + + await this._applyMachineGroupLevelControl(percentControl); + await this._applyIdleMachineLevelControl(percentControl); + } + + async _applyMachineGroupLevelControl(percentControl) { + this.logger.debug(`Applying level control to machine groups: ${percentControl.toFixed(1)}% displaying machine groups ${Object.keys(this.machineGroups).join(', ')}`); + if (!this.machineGroups || Object.keys(this.machineGroups).length === 0) return; + + await Promise.all( + Object.values(this.machineGroups).map(async (group) => { + try { + await group.handleInput('parent', percentControl); + } catch (err) { + this.logger.error(`Failed to send level control to group "${group.config.general.name}": ${err.message}`); + } + }) + ); + } + + async _applyIdleMachineLevelControl(percentControl) { + const idleMachines = Object.values(this.machines).filter((machine) => { + const pos = machine?.config?.functionality?.positionVsParent; + return (pos === 'downstream' || pos === 'atEquipment') && !machine._isOperationalState(); + }); + + if (!idleMachines.length) return; + + const perMachine = percentControl / idleMachines.length; + + for (const machine of idleMachines) { + try { + await machine.handleInput('parent', 'execSequence', 'startup'); + await machine.handleInput('parent', 'execMovement', perMachine); + } catch (err) { + this.logger.error(`Failed to start idle machine "${machine.config.general.name}": ${err.message}`); + } + } + } + + _calculateLevelControlPercent(thresholds = []) { + if (!thresholds.length) return 0; + const total = thresholds.length; + const crossed = this._levelState.crossed.size; + const pct = (crossed / total) * 100; + return Math.min(100, Math.max(0, pct)); + } + _resolveVolume(snapshot) { for (const variant of this.volVariants) { @@ -281,7 +407,6 @@ class PumpingStation { 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; @@ -304,24 +429,41 @@ class PumpingStation { //register machines or pumping stations that can provide predicted flow data _registerPredictedFlowChild(child) { - const position = child.config.functionality.positionVsParent; + const position = (child.config.functionality.positionVsParent || '').toLowerCase(); 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; + let posKey; + let eventNames; - if (!posKey) { - this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`); - return; + switch (position) { + case 'downstream': + case 'out': + case 'atequipment': + posKey = 'out'; + eventNames = [ + 'flow.predicted.downstream', + 'flow.predicted.atequipment' + ]; + break; + + case 'upstream': + case 'in': + posKey = 'in'; + eventNames = [ + 'flow.predicted.upstream', + 'flow.predicted.atequipment' + ]; + break; + + default: + this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`); + return; } if (!this.predictedFlowChildren.has(childId)) { this.predictedFlowChildren.set(childId, { in: 0, out: 0 }); + this.logger.debug(`Initialized predicted flow tracking for child ${childName} (${childId})`); } const handler = (eventData = {}) => { @@ -335,14 +477,7 @@ class PumpingStation { 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); - } + eventNames.forEach((eventName) => child.measurements.emitter.on(eventName, handler)); } _refreshAggregatedPredictedFlow(direction, timestamp = Date.now(), unit = 'm3/s') { @@ -725,7 +860,7 @@ class PumpingStation { minVolOut }; - this.measurements.type('volume').variant('predicted').position('atEquipment').value(maxVolOverflow).unit('m3'); + this.measurements.type('volume').variant('predicted').position('atEquipment').value(heightOutlet).unit('m3'); this.logger.debug( `Basin initialized | area=${surfaceArea.toFixed(2)} m2, max=${maxVol.toFixed(2)} m3, overflow=${maxVolOverflow.toFixed(2)} m3` -- 2.49.1 From 5a575a29fe7cc1e2ad8a74d862e64012d0e805a3 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:15:46 +0100 Subject: [PATCH 14/21] updated pumpingstation --- pumpingStation.html | 13 +++++++ src/nodeClass.js | 9 ++--- src/specificClass.js | 88 ++++++++++++++++---------------------------- 3 files changed, 47 insertions(+), 63 deletions(-) diff --git a/pumpingStation.html b/pumpingStation.html index 8c56986..6103316 100644 --- a/pumpingStation.html +++ b/pumpingStation.html @@ -24,6 +24,7 @@ heightInlet: { value: 0.8 }, // m, centre of inlet pipe above floor heightOutlet: { value: 0.2 }, // m, centre of outlet pipe above floor heightOverflow: { value: 0.9 }, // m, overflow elevation + minHeightBasedOn: { value: "outlet" }, // basis for minimum height check: inlet or outlet // Advanced reference information refHeight: { value: "NAP" }, // reference height @@ -86,6 +87,10 @@ refHeightEl.value = this.refHeight || "NAP"; } + const minHeightBasedOnEl = document.getElementById("node-input-minHeightBasedOn"); + if (minHeightBasedOnEl) { + minHeightBasedOnEl.value = this.minHeightBasedOn; + } //------------------- END OF CUSTOM config UI ELEMENTS ------------------- // }, @@ -98,6 +103,7 @@ //node specific node.refHeight = document.getElementById("node-input-refHeight").value || "NAP"; + node.minHeightBasedOn = document.getElementById("node-input-minHeightBasedOn").value || "outlet"; node.simulator = document.getElementById("node-input-simulator").checked; ["basinVolume","basinHeight","heightInlet","heightOutlet","heightOverflow","basinBottomRef"] @@ -151,6 +157,13 @@