2025-10-07 18:05:54 +02:00
const EventEmitter = require ( 'events' ) ;
2025-10-21 12:45:19 +02:00
const { logger , configUtils , configManager , childRegistrationUtils , MeasurementContainer , coolprop , interpolation } = require ( 'generalFunctions' ) ;
2025-10-07 18:05:54 +02:00
2025-10-14 08:36:45 +02:00
class pumpingStation {
2025-10-07 18:05:54 +02:00
constructor ( config = { } ) {
this . emitter = new EventEmitter ( ) ; // Own EventEmitter
this . configManager = new configManager ( ) ;
2025-10-14 08:36:45 +02:00
this . defaultConfig = this . configManager . getConfig ( 'pumpingStation' ) ;
2025-10-07 18:05:54 +02:00
this . configUtils = new configUtils ( this . defaultConfig ) ;
this . config = this . configUtils . initConfig ( config ) ;
2025-10-21 12:45:19 +02:00
this . interpolate = new interpolation ( ) ;
2025-10-07 18:05:54 +02:00
// Init after config is set
this . logger = new logger ( this . config . general . logging . enabled , this . config . general . logging . logLevel , this . config . general . name ) ;
// General properties
this . measurements = new MeasurementContainer ( {
2025-10-16 14:44:45 +02:00
autoConvert : true
2025-10-07 18:05:54 +02:00
} ) ;
2025-10-14 16:32:44 +02:00
// init basin object in pumping station
2025-10-16 14:44:45 +02:00
this . basin = { } ;
2025-10-27 16:39:06 +01:00
this . state = {
direction : "steady" ,
netFlow : 0 ,
flowSource : null ,
seconds : null ,
remainingSource : null
} ; // init state object of pumping station to see whats going on
2025-10-14 16:32:44 +02:00
// Initialize basin-specific properties and calculate used parameters
2025-10-07 18:05:54 +02:00
this . initBasinProperties ( ) ;
2025-10-23 09:51:54 +02:00
this . parent = { } ; // object to hold parent information for when we follow flow directions.
2025-10-16 14:44:45 +02:00
this . child = { } ; // object to hold child information so we know on what to subscribe
2025-10-21 13:44:31 +02:00
this . machines = { } ; // object to hold child machine information
2025-10-23 18:04:18 +02:00
this . stations = { } ; // object to hold station information
2025-10-16 14:44:45 +02:00
this . childRegistrationUtils = new childRegistrationUtils ( this ) ; // Child registration utility
this . logger . debug ( 'pumpstation Initialized with all helpers' ) ;
2025-10-07 18:05:54 +02:00
}
/*------------------- Register child events -------------------*/
registerChild ( child , softwareType ) {
this . logger . debug ( 'Setting up child event for softwaretype ' + softwareType ) ;
2025-10-21 13:44:31 +02:00
//define what to do with measurements
2025-10-07 18:05:54 +02:00
if ( softwareType === "measurement" ) {
const position = child . config . functionality . positionVsParent ;
const distance = child . config . functionality . distanceVsParent || 0 ;
const measurementType = child . config . asset . type ;
const key = ` ${ measurementType } _ ${ position } ` ;
//rebuild to measurementype.variant no position and then switch based on values not strings or names.
const eventName = ` ${ measurementType } .measured. ${ position } ` ;
this . logger . debug ( ` Setting up listener for ${ eventName } from child ${ child . config . general . name } ` ) ;
// Register event listener for measurement updates
child . measurements . emitter . on ( eventName , ( eventData ) => {
this . logger . debug ( ` 🔄 ${ position } ${ measurementType } from ${ eventData . childName } : ${ eventData . value } ${ eventData . unit } ` ) ;
2025-10-16 14:44:45 +02:00
this . logger . debug ( ` Emitting... ${ eventName } with data: ` ) ;
2025-10-07 18:05:54 +02:00
// Store directly in parent's measurement container
2025-10-21 13:44:31 +02:00
this . measurements . type ( measurementType ) . variant ( "measured" ) . position ( position ) . value ( eventData . value , eventData . timestamp , eventData . unit ) ;
2025-10-07 18:05:54 +02:00
// Call the appropriate handler
this . _callMeasurementHandler ( measurementType , eventData . value , position , eventData ) ;
} ) ;
}
2025-10-21 13:44:31 +02:00
//define what to do when machines are connected
if ( softwareType == "machine" ) {
// Check if the machine is already registered
this . machines [ child . config . general . id ] === undefined ? this . machines [ child . config . general . id ] = child : this . logger . warn ( ` Machine ${ child . config . general . id } is already registered. ` ) ;
//listen for machine pressure changes
2025-10-23 09:51:54 +02:00
this . logger . debug ( ` Listening for flow changes from machine ${ child . config . general . id } ` ) ;
2025-10-21 13:44:31 +02:00
2025-10-23 18:04:18 +02:00
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 ) => {
2025-10-21 13:44:31 +02:00
this . logger . debug ( ` Flow prediction update from ${ child . config . general . id } : ${ eventData . value } ${ eventData . unit } ` ) ;
2025-10-23 18:04:18 +02:00
//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 } ` ) ;
}
2025-10-23 09:51:54 +02:00
}
// add one for group later
if ( softwareType == "machineGroup" ) {
}
2025-10-21 13:44:31 +02:00
2025-10-23 18:04:18 +02:00
// 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 ` ) ;
}
}
2025-10-21 13:44:31 +02:00
}
2025-10-23 18:04:18 +02:00
//in or outgoing flow = direction
_updateVolumePrediction ( flowDir ) {
2025-10-21 13:44:31 +02:00
2025-10-23 09:51:54 +02:00
//get downflow
2025-10-23 18:04:18 +02:00
const seriesExists = this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( flowDir ) . exists ( ) ;
if ( ! seriesExists ) { return } ;
2025-10-23 09:51:54 +02:00
2025-10-23 18:04:18 +02:00
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 }
2025-10-23 09:51:54 +02:00
2025-10-23 18:04:18 +02:00
if ( ! currFLow || ! prevFlow ) return ;
2025-10-27 19:55:48 +01:00
this . logger . debug ( ` Flowdir ${ flowDir } => currFlow ${ currFLow . value } , prevflow = ${ prevFlow . value } ` ) ;
2025-10-23 09:51:54 +02:00
// calc difference in time
2025-10-23 18:04:18 +02:00
const deltaT = currFLow . timestamp - prevFlow . timestamp ;
2025-10-23 09:51:54 +02:00
const deltaSeconds = deltaT / 1000 ;
if ( deltaSeconds <= 0 ) {
this . logger . warn ( ` Flow integration aborted; invalid Δt= ${ deltaSeconds } s. ` ) ;
return ;
2025-10-21 13:44:31 +02:00
}
2025-10-23 18:04:18 +02:00
const avgFlow = ( currFLow . value + prevFlow . value ) / 2 ;
const calcVol = avgFlow * deltaSeconds ;
2025-10-23 09:51:54 +02:00
//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' ) ;
2025-10-23 18:04:18 +02:00
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!' ) ;
}
2025-10-23 09:51:54 +02:00
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 ( ) {
2025-10-21 13:44:31 +02:00
}
2025-10-23 09:51:54 +02:00
//update measured temperature to adjust density of liquid
2025-10-21 13:44:31 +02:00
updateMeasuredTemperature ( ) {
}
2025-10-23 09:51:54 +02:00
//update measured flow and recalc
2025-10-21 13:44:31 +02:00
updateMeasuredFlow ( ) {
2025-10-07 18:05:54 +02:00
}
2025-10-23 09:51:54 +02:00
//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
2025-10-23 18:04:18 +02:00
this . _updateVolumePrediction ( "out" ) ; //check for changes in outgoing flow
this . _updateVolumePrediction ( "in" ) ; // check for changes in incomming flow
2025-10-27 16:39:06 +01:00
//calc the most important values back to determine state and net up or downstream flow
2025-10-27 17:45:48 +01:00
//this._calcNetFlow();
2025-10-27 16:39:06 +01:00
const { time : timeleft , source : variant } = this . _calcTimeRemaining ( ) ;
2025-10-27 19:55:48 +01:00
this . logger . debug ( ` Remaining time ~ ${ Math . round ( timeleft / 60 / 60 * 10 ) / 10 } h, based on variant ${ variant } ` ) ;
2025-10-27 16:39:06 +01:00
}
_calcTimeRemaining ( ) {
2025-10-27 17:45:48 +01:00
//init timeRemaining
2025-10-27 16:39:06 +01:00
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 } ;
2025-10-23 09:51:54 +02:00
}
2025-10-21 13:44:31 +02:00
2025-10-07 18:05:54 +02:00
_callMeasurementHandler ( measurementType , value , position , context ) {
switch ( measurementType ) {
case 'pressure' :
this . updateMeasuredPressure ( value , position , context ) ;
break ;
case 'flow' :
this . updateMeasuredFlow ( value , position , context ) ;
break ;
case 'temperature' :
this . updateMeasuredTemperature ( value , position , context ) ;
break ;
case 'level' :
this . updateMeasuredLevel ( value , position , context ) ;
break ;
default :
this . logger . warn ( ` No handler for measurement type: ${ measurementType } ` ) ;
// Generic handler - just update position
this . updatePosition ( ) ;
break ;
}
2025-10-21 13:44:31 +02:00
}
2025-10-07 18:05:54 +02:00
// context handler for pressure updates
updateMeasuredPressure ( value , position , context = { } ) {
2025-10-14 16:32:44 +02:00
// init temp
2025-10-07 18:05:54 +02:00
let kelvinTemp = null ;
//pressure updates come from pressure boxes inside the basin they get converted to a level and stored as level measured at position inlet or outlet
this . logger . debug ( ` Pressure update: ${ value } at ${ position } from ${ context . childName || 'child' } ( ${ context . childId || 'unknown-id' } ) ` ) ;
// Store in parent's measurement container for the first time
this . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( position ) . value ( value , context . timestamp , context . unit ) ;
//convert pressure to level based on density of water and height of pressure sensor
const mTemp = this . measurements . type ( "temperature" ) . variant ( "measured" ) . position ( "atEquipment" ) . getCurrentValue ( 'K' ) ; //default to 20C if no temperature measurement
//prefer measured temp but otherwise assume nominal temp for wastewater
if ( mTemp === null ) {
this . logger . warn ( ` No temperature measurement available, defaulting to 15C for pressure to level conversion. ` ) ;
this . measurements . type ( "temperature" ) . variant ( "assumed" ) . position ( "atEquipment" ) . value ( 15 , Date . now ( ) , "C" ) ;
kelvinTemp = this . measurements . type ( 'temperature' ) . variant ( 'assumed' ) . position ( 'atEquipment' ) . getCurrentValue ( 'K' ) ;
2025-10-21 13:44:31 +02:00
this . logger . debug ( ` Temperature is : ${ kelvinTemp } ` ) ;
2025-10-07 18:05:54 +02:00
} else {
kelvinTemp = mTemp ;
}
this . logger . debug ( ` Using temperature: ${ kelvinTemp } K for calculations ` ) ;
const density = coolprop . PropsSI ( 'D' , 'T' , kelvinTemp , 'P' , 101325 , 'Water' ) ; //density in kg/m3 at temp and surface pressure
2025-10-14 16:32:44 +02:00
const g = 9.80665 ;
2025-10-14 16:45:09 +02:00
const pressure _Pa = this . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( position ) . getCurrentValue ( 'Pa' ) ;
const level = pressure _Pa / density * g ;
this . measurements . type ( "level" ) . variant ( "predicted" ) . position ( position ) . value ( level ) ;
2025-10-21 12:45:19 +02:00
//updatePredictedLevel(); ?? OLIFANT!
2025-10-07 18:05:54 +02:00
//calculate how muc flow went in or out based on pressure difference
2025-10-14 16:45:09 +02:00
this . logger . debug ( ` Using pressure: ${ value } for calculations ` ) ;
2025-10-16 14:44:45 +02:00
}
updateMeasuredLevel ( value , position , context = { } ) {
// Store in parent's measurement container for the first time
this . measurements . type ( "level" ) . variant ( "measured" ) . position ( position ) . value ( value , context . timestamp , context . unit ) ;
//fetch level in meter
const level = this . measurements . type ( "level" ) . variant ( "measured" ) . position ( position ) . getCurrentValue ( 'm' ) ;
//calc vol in m3
const volume = this . _calcVolumeFromLevel ( level ) ;
2025-10-21 12:45:19 +02:00
this . logger . debug ( ` basin minvol : ${ this . basin . minVol } , cur volume : ${ volume } / ${ this . basin . maxVolOverflow } ` ) ;
const proc = this . interpolate . interpolate _lin _single _point ( volume , this . basin . minVol , this . basin . maxVolOverflow , 0 , 100 ) ;
2025-10-21 13:44:31 +02:00
this . logger . debug ( ` PROC volume : ${ proc } ` ) ;
2025-10-16 14:44:45 +02:00
this . measurements . type ( "volume" ) . variant ( "measured" ) . position ( "atEquipment" ) . value ( volume ) . unit ( 'm3' ) ;
2025-10-21 13:44:31 +02:00
this . measurements . type ( "volume" ) . variant ( "procent" ) . position ( "atEquipment" ) . value ( proc ) ;
2025-10-21 12:45:19 +02:00
2025-10-16 14:44:45 +02:00
}
_calcNetFlow ( ) {
2025-10-23 18:04:18 +02:00
let netFlow = null ;
const netFlow _FlowSensor = Math . abs ( this . measurements . type ( "flow" ) . variant ( "measured" ) . difference ( { from : "downstream" , to : "upstream" , unit : "m3/s" } ) ) ;
2025-10-27 17:45:48 +01:00
const netFlow _LevelSensor = this . _calcNetFlowFromLevelDiff ( "measured" ) ;
2025-10-23 18:04:18 +02:00
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 ;
}
}
2025-10-27 16:39:06 +01:00
//@params : params : example {flowVariant: "predicted",levelVariant: "measured"};
_calcRemainingTimeBasedOnFlow ( params ) {
const { flowVariant , levelVariant } = params ;
this . logger . debug ( ` ${ flowVariant } - ${ levelVariant } ` ) ;
2025-10-23 18:04:18 +02:00
2025-10-27 16:39:06 +01:00
if ( flowVariant === null || levelVariant === null ) {
this . logger . warn ( ` Cant calculate remaining time without needed variants ` ) ;
return 0 ;
}
2025-10-16 14:44:45 +02:00
const { heightOverflow , heightOutlet , surfaceArea } = this . basin ;
2025-10-27 16:39:06 +01:00
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 ;
2025-10-16 14:44:45 +02:00
2025-10-23 18:04:18 +02:00
switch ( true ) {
2025-10-27 16:39:06 +01:00
2025-10-23 18:04:18 +02:00
case ( flowDiff > 0 ) :
remainingHeight = Math . max ( heightOverflow - level , 0 ) ;
2025-10-27 16:39:06 +01:00
secondsRemaining = remainingHeight * surfaceArea / flowDiff ;
return secondsRemaining ;
2025-10-16 14:44:45 +02:00
2025-10-23 18:04:18 +02:00
case ( flowDiff < 0 ) :
remainingHeight = Math . max ( level - heightOutlet , 0 ) ;
2025-10-27 16:39:06 +01:00
secondsRemaining = remainingHeight * surfaceArea / Math . abs ( flowDiff ) ;
return secondsRemaining ;
2025-10-16 14:44:45 +02:00
2025-10-23 18:04:18 +02:00
default :
2025-10-27 16:39:06 +01:00
this . logger . debug ( ` Flowdiff is 0 not doing anything. ` ) ;
return secondsRemaining ;
}
2025-10-16 14:44:45 +02:00
2025-10-27 16:39:06 +01:00
}
//@params : variant : example "predicted","measured"
_calcRemainingTimeBasedOnLevel ( variant ) {
2025-10-27 17:45:48 +01:00
const { heightOverflow , heightOutlet } = this . basin ;
2025-10-27 16:39:06 +01:00
const levelObj = this . measurements . type ( "level" ) . variant ( variant ) . position ( "atEquipment" ) ;
const level = levelObj . getCurrentValue ( "m" ) ;
2025-10-27 17:45:48 +01:00
const prevLevelSample = levelObj . getLaggedSample ( 2 , "m" ) ; // { value, timestamp, unit }
2025-10-27 16:39:06 +01:00
const measurement = levelObj . get ( ) ;
const latestTimestamp = measurement ? . getLatestTimestamp ( ) ;
2025-10-27 17:45:48 +01:00
if ( level === null || prevLevelSample == null || latestTimestamp == null ) {
this . logger . warn ( ` no flowdiff ${ level } , previous level ${ prevLevelSample } , latestTimestamp ${ latestTimestamp } found escaping ` ) ;
2025-10-27 16:39:06 +01:00
return null ;
}
2025-10-27 17:45:48 +01:00
const deltaSeconds = ( latestTimestamp - prevLevelSample . timestamp ) / 1000 ;
2025-10-27 16:39:06 +01:00
if ( deltaSeconds <= 0 ) {
2025-10-27 17:45:48 +01:00
this . logger . warn ( ` Level fallback: invalid Δt= ${ deltaSeconds } , LatestTimestamp : ${ latestTimestamp } , PrevTimestamp : ${ prevLevelSample . value } ` ) ;
2025-10-27 16:39:06 +01:00
return null ;
}
2025-10-27 17:45:48 +01:00
const lvlDiff = level - prevLevelSample . value ;
const lvlRate = lvlDiff / deltaSeconds ; // m/s
let secondsRemaining = 0 ;
let remainingHeight = 0 ;
2025-10-27 16:39:06 +01:00
switch ( true ) {
case ( lvlRate > 0 ) :
remainingHeight = Math . max ( heightOverflow - level , 0 ) ;
2025-10-27 17:45:48 +01:00
secondsRemaining = remainingHeight / Math . abs ( lvlRate ) ; // seconds
2025-10-27 16:39:06 +01:00
return secondsRemaining ;
case ( lvlRate < 0 ) :
remainingHeight = Math . max ( level - heightOutlet , 0 ) ;
2025-10-27 17:45:48 +01:00
secondsRemaining = remainingHeight / Math . abs ( lvlRate ) ;
2025-10-27 16:39:06 +01:00
return secondsRemaining ;
default :
this . logger . debug ( ` Flowdiff is 0 not doing anything. ` ) ;
return secondsRemaining ;
2025-10-23 18:04:18 +02:00
}
2025-10-16 14:44:45 +02:00
}
2025-10-27 16:39:06 +01:00
//Give a flowDifference and calculate direction => spits out filling , draining or stable
_calcDirectionBasedOnFlow ( flowDiff ) {
2025-10-16 14:44:45 +02:00
2025-10-23 18:04:18 +02:00
let direction = null ;
2025-10-16 14:44:45 +02:00
2025-10-23 18:04:18 +02:00
switch ( true ) {
case flowDiff > flowThreshold :
direction = "filling" ;
break ;
2025-10-16 14:44:45 +02:00
2025-10-23 18:04:18 +02:00
case flowDiff < - flowThreshold :
direction = "draining" ;
break ;
2025-10-16 14:44:45 +02:00
2025-10-23 18:04:18 +02:00
case flowDiff < flowThreshold && flowDiff > - flowThreshold :
direction = "stable" ;
break ;
2025-10-16 14:44:45 +02:00
2025-10-23 18:04:18 +02:00
default :
this . logger . warn ( "Uknown state direction detected??" ) ;
return null ;
2025-10-16 14:44:45 +02:00
2025-10-23 18:04:18 +02:00
}
return direction ;
2025-10-16 14:44:45 +02:00
}
2025-10-27 16:39:06 +01:00
_calcNetFlowFromLevelDiff ( variant ) {
2025-10-23 18:04:18 +02:00
const { surfaceArea } = this . basin ;
2025-10-27 16:39:06 +01:00
const levelObj = this . measurements . type ( "level" ) . variant ( variant ) . position ( "atEquipment" ) ;
2025-10-16 14:44:45 +02:00
const level = levelObj . getCurrentValue ( "m" ) ;
2025-10-27 17:45:48 +01:00
const prevLevelSample = levelObj . getLaggedSample ( 2 , "m" ) ; // { value, timestamp, unit }
2025-10-16 14:44:45 +02:00
const measurement = levelObj . get ( ) ;
const latestTimestamp = measurement ? . getLatestTimestamp ( ) ;
2025-10-27 17:45:48 +01:00
if ( level === null || prevLevelSample == null || latestTimestamp == null ) {
this . logger . warn ( ` no flowdiff ${ level } , previous level ${ prevLevelSample } , latestTimestamp ${ latestTimestamp } found escaping ` ) ;
2025-10-16 14:44:45 +02:00
return null ;
}
2025-10-27 17:45:48 +01:00
const deltaSeconds = ( latestTimestamp - prevLevelSample . timestamp ) / 1000 ;
2025-10-16 14:44:45 +02:00
if ( deltaSeconds <= 0 ) {
2025-10-27 17:45:48 +01:00
this . logger . warn ( ` Level fallback: invalid Δt= ${ deltaSeconds } , LatestTimestamp : ${ latestTimestamp } , PrevTimestamp : ${ prevLevelSample . timestamp } ` ) ;
2025-10-16 14:44:45 +02:00
return null ;
}
2025-10-27 17:45:48 +01:00
const lvlDiff = level - prevLevelSample . value ;
2025-10-16 14:44:45 +02:00
const lvlRate = lvlDiff / deltaSeconds ; // m/s
const netFlowRate = lvlRate * surfaceArea ; // m³/s inferred from level trend
2025-10-23 18:04:18 +02:00
return netFlowRate ;
2025-10-07 18:05:54 +02:00
}
initBasinProperties ( ) {
2025-10-16 14:44:45 +02:00
2025-10-14 16:32:44 +02:00
// Load and calc basic params
const volEmptyBasin = this . config . basin . volume ;
const heightBasin = this . config . basin . height ;
const heightInlet = this . config . basin . heightInlet ;
const heightOutlet = this . config . basin . heightOutlet ;
2025-10-16 14:44:45 +02:00
const heightOverflow = this . config . basin . heightOverflow ;
2025-10-14 16:32:44 +02:00
//calculated params
const surfaceArea = volEmptyBasin / heightBasin ;
const maxVol = heightBasin * surfaceArea ; // if Basin where to ever fill up completely this is the water volume
const maxVolOverflow = heightOverflow * surfaceArea ; // Max water volume before you start loosing water to overflow
2025-10-21 12:45:19 +02:00
const minVol = heightOutlet * surfaceArea ;
const minVolOut = heightInlet * surfaceArea ; // this will indicate if its an open end or a closed end.
2025-10-14 16:32:44 +02:00
this . basin . volEmptyBasin = volEmptyBasin ;
this . basin . heightBasin = heightBasin ;
this . basin . heightInlet = heightInlet ;
this . basin . heightOutlet = heightOutlet ;
this . basin . heightOverflow = heightOverflow ;
this . basin . surfaceArea = surfaceArea ;
this . basin . maxVol = maxVol ;
this . basin . maxVolOverflow = maxVolOverflow ;
this . basin . minVol = minVol ;
this . basin . minVolOut = minVolOut ;
2025-10-21 13:44:31 +02:00
//init predicted min volume to min vol in order to have a starting point
this . measurements . type ( "volume" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( minVol ) . unit ( 'm3' ) ;
2025-10-27 17:45:48 +01:00
this . measurements . type ( "volume" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( maxVol ) . unit ( 'm3' ) ;
2025-10-21 13:44:31 +02:00
this . logger . debug ( `
Basin initialized | area = $ { surfaceArea . toFixed ( 2 ) } m² ,
max = $ { maxVol . toFixed ( 2 ) } m³ ,
overflow = $ { maxVolOverflow . toFixed ( 2 ) } m³ `
) ;
2025-10-07 18:05:54 +02:00
}
2025-10-14 16:32:44 +02:00
_calcVolumeFromLevel ( level ) {
const surfaceArea = this . basin . surfaceArea ;
return Math . max ( level , 0 ) * surfaceArea ;
}
2025-10-23 09:51:54 +02:00
_calcLevelFromVolume ( vol ) {
const surfaceArea = this . basin . surfaceArea ;
return Math . max ( vol , 0 ) / surfaceArea ;
}
2025-10-07 18:05:54 +02:00
2025-10-23 18:04:18 +02:00
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 ;
}
2025-10-16 14:44:45 +02:00
2025-10-23 18:04:18 +02:00
return output ;
}
2025-10-07 18:05:54 +02:00
}
2025-10-14 13:51:32 +02:00
module . exports = pumpingStation ;
2025-10-07 18:05:54 +02:00
2025-10-23 09:51:54 +02:00
/* ------------------------------------------------------------------------- */
/* Example: pumping station + rotating machine + measurements (stand-alone) */
/* ------------------------------------------------------------------------- */
const PumpingStation = require ( "./specificClass" ) ;
const RotatingMachine = require ( "../../rotatingMachine/src/specificClass" ) ;
const Measurement = require ( "../../measurement/src/specificClass" ) ;
2025-10-27 19:55:48 +01:00
//Helpers
2025-10-23 09:51:54 +02:00
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" }
} ;
}
2025-10-07 18:05:54 +02:00
2025-10-23 09:51:54 +02:00
function createMachineConfig ( name ) {
curve = require ( 'C:/Users/zn375/.node-red/public/fallbackData.json' ) ;
return {
general : {
name : name ,
logging : {
enabled : true ,
logLevel : "warn" ,
}
} ,
asset : {
supplier : "Hydrostal" ,
type : "pump" ,
category : "centrifugal" ,
model : "hidrostal-H05K-S03R" , // Ensure this field is present.
}
}
}
function createMachineStateConfig ( ) {
return {
general : {
logging : {
enabled : true ,
logLevel : "debug" ,
} ,
} ,
// Your custom config here (or leave empty for defaults)
movement : {
speed : 1 ,
} ,
time : {
starting : 2 ,
warmingup : 3 ,
stopping : 2 ,
coolingdown : 3 ,
} ,
}
}
2025-10-07 18:05:54 +02:00
2025-10-23 09:51:54 +02:00
// 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 ) ;
}
2025-10-27 19:55:48 +01:00
// Demo
2025-10-23 09:51:54 +02:00
( async function demoStationWithPump ( ) {
const station = new PumpingStation ( createPumpingStationConfig ( "PumpingStationDemo" ) ) ;
2025-10-27 19:55:48 +01:00
const pump1 = new RotatingMachine ( createMachineConfig ( "Pump1" ) , createMachineStateConfig ( ) ) ;
const pump2 = new RotatingMachine ( createMachineConfig ( "Pump2" ) , createMachineStateConfig ( ) ) ;
2025-10-23 09:51:54 +02:00
const levelSensor = new Measurement ( createLevelMeasurementConfig ( "WetWellLevel" ) ) ;
const upstreamFlow = new Measurement ( createFlowMeasurementConfig ( "InfluentFlow" , "upstream" ) ) ;
const downstreamFlow = new Measurement ( createFlowMeasurementConfig ( "PumpDischargeFlow" , "downstream" ) ) ;
// station uses the sensors
2025-10-27 19:55:48 +01:00
2025-10-23 09:51:54 +02:00
station . childRegistrationUtils . registerChild ( levelSensor , levelSensor . config . functionality . softwareType ) ;
station . childRegistrationUtils . registerChild ( upstreamFlow , upstreamFlow . config . functionality . softwareType ) ;
station . childRegistrationUtils . registerChild ( downstreamFlow , downstreamFlow . config . functionality . softwareType ) ;
2025-10-27 19:55:48 +01:00
2025-10-23 09:51:54 +02:00
// pump owns the downstream flow sensor
2025-10-27 19:55:48 +01:00
//pump.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.positionVsParent);
station . childRegistrationUtils . registerChild ( pump1 , "downstream" ) ;
station . childRegistrationUtils . registerChild ( pump2 , "upstream" ) ;
2025-10-23 09:51:54 +02:00
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" ) ;
2025-10-27 19:55:48 +01:00
//*/
2025-10-23 09:51:54 +02:00
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" ) ;
2025-10-27 19:55:48 +01:00
//*/
2025-10-23 18:04:18 +02:00
console . log ( "Station output:" , station . getOutput ( ) ) ;
2025-10-27 19:55:48 +01:00
await pump1 . handleInput ( "parent" , "execSequence" , "startup" ) ;
await pump2 . handleInput ( "parent" , "execSequence" , "startup" ) ;
await pump1 . handleInput ( "parent" , "execMovement" , 5 ) ;
await pump2 . handleInput ( "parent" , "execMovement" , 5 ) ;
2025-10-23 09:51:54 +02:00
console . log ( "Station state:" , station . state ) ;
console . log ( "Station output:" , station . getOutput ( ) ) ;
2025-10-27 19:55:48 +01:00
console . log ( "Pump state:" , pump1 . state . getCurrentState ( ) ) ;
console . log ( "Pump state:" , pump2 . state . getCurrentState ( ) ) ;
2025-10-23 09:51:54 +02:00
} ) ( ) ;
/ *
2025-10-16 14:44:45 +02:00
//coolprop example
2025-10-07 18:05:54 +02:00
( async ( ) => {
const PropsSI = await coolprop . getPropsSI ( ) ;
// 👇 replace these with your real inputs
const tC _input = 25 ; // °C
const pPa _input = 101325 ; // Pa
// Sanitize & convert
const T = Number ( tC _input ) + 273.15 ; // K
const P = Number ( pPa _input ) ; // Pa
const fluid = 'Water' ;
// Preconditions
if ( ! Number . isFinite ( T ) || ! Number . isFinite ( P ) ) {
throw new Error ( ` Bad inputs: T= ${ T } K, P= ${ P } Pa ` ) ;
}
if ( T <= 0 ) throw new Error ( ` Temperature must be in Kelvin (>0). Got ${ T } . ` ) ;
if ( P <= 0 ) throw new Error ( ` Pressure must be >0 Pa. Got ${ P } . ` ) ;
// Try T,P order
let rho = PropsSI ( 'D' , 'T' , T , 'P' , P , fluid ) ;
// Fallback: P,T order (should be equivalent)
if ( ! Number . isFinite ( rho ) ) rho = PropsSI ( 'D' , 'P' , P , 'T' , T , fluid ) ;
console . log ( { T , P , rho } ) ;
if ( ! Number . isFinite ( rho ) ) {
console . error ( 'Still Infinity. Extra checks:' ) ;
console . error ( 'typeof T:' , typeof T , 'typeof P:' , typeof P ) ;
console . error ( 'Example known-good call:' , PropsSI ( 'D' , 'T' , 298.15 , 'P' , 101325 , 'Water' ) ) ;
}
} ) ( ) ;
2025-10-16 14:44:45 +02:00
* /