2025-10-07 18:05:54 +02:00
const EventEmitter = require ( 'events' ) ;
2025-10-16 14:44:45 +02:00
const { logger , configUtils , configManager , childRegistrationUtils , MeasurementContainer , coolprop } = 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 ) ;
// 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 = { } ;
this . state = { direction : "" , netDownstream : 0 , netUpstream : 0 , seconds : 0 } ; // init state object of pumping station to see whats going on
2025-10-14 16:32:44 +02:00
// Initialize basin-specific properties and calculate used parameters
2025-10-07 18:05:54 +02:00
this . initBasinProperties ( ) ;
2025-10-16 14:44:45 +02:00
this . child = { } ; // object to hold child information so we know on what to subscribe
this . childRegistrationUtils = new childRegistrationUtils ( this ) ; // Child registration utility
this . logger . debug ( 'pumpstation Initialized with all helpers' ) ;
2025-10-07 18:05:54 +02:00
}
/*------------------- Register child events -------------------*/
registerChild ( child , softwareType ) {
this . logger . debug ( 'Setting up child event for softwaretype ' + softwareType ) ;
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
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 ) ;
} ) ;
}
}
_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 ;
}
}
// 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' ) ;
} 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-16 14:44:45 +02:00
//updatePredictedLevel(); ??
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 ) ;
this . measurements . type ( "volume" ) . variant ( "measured" ) . position ( "atEquipment" ) . value ( volume ) . unit ( 'm3' ) ;
//calc the most important values back to determine state and net up or downstream flow
this . _calcNetFlow ( ) ;
}
_calcNetFlow ( ) {
const { heightOverflow , heightOutlet , surfaceArea } = this . basin ;
const flowBased = this . _calcNetFlowFromMeasurements ( {
heightOverflow ,
heightOutlet ,
surfaceArea
} ) ;
const levelBased = this . _calcNetFlowFromLevel ( {
heightOverflow ,
heightOutlet ,
surfaceArea
} ) ;
if ( flowBased && levelBased ) {
this . logger . debug (
` Flow vs Level comparison | flow= ${ flowBased . netFlowRate . toFixed ( 3 ) } ` +
` m3/s, level= ${ levelBased . netFlowRate . toFixed ( 3 ) } m3/s `
) ;
}
const effective = flowBased || levelBased ;
if ( effective ) {
this . state = effective . state ;
this . state . netFlowSource = flowBased ? ( levelBased ? "flow+level" : "flow" ) : "level" ;
this . logger . debug ( ` Net-flow state: ${ JSON . stringify ( this . state ) } ` ) ;
} else {
this . logger . debug ( "Net-flow state: insufficient data" ) ;
}
return effective ;
}
_calcNetFlowFromMeasurements ( { heightOverflow , heightOutlet , surfaceArea } ) {
const flowDiff = this . measurements
. type ( "flow" )
. variant ( "measured" )
. difference ( { from : "downstream" , to : "upstream" , unit : "m3/s" } ) ;
const level = this . measurements
. type ( "level" )
. variant ( "measured" )
. position ( "atEquipment" )
. getCurrentValue ( "m" ) ;
const flowUpstream = this . measurements
. type ( "flow" )
. variant ( "measured" )
. position ( "upstream" )
. getCurrentValue ( "m3/s" ) ;
const flowDownstream = this . measurements
. type ( "flow" )
. variant ( "measured" )
. position ( "downstream" )
. getCurrentValue ( "m3/s" ) ;
if ( flowDiff === null || level === null ) {
this . logger . warn ( ` no flowdiff ${ flowDiff } or level ${ level } found escaping ` ) ;
return null ;
}
const flowThreshold = 0.1 ; // m³/s
const state = { direction : "stable" , seconds : 0 , netUpstream : flowUpstream ? ? 0 , netDownstream : flowDownstream ? ? 0 } ;
if ( flowDiff > flowThreshold ) {
state . direction = "filling" ;
const remainingHeight = Math . max ( heightOverflow - level , 0 ) ;
state . seconds = remainingHeight * surfaceArea / flowDiff ;
} else if ( flowDiff < - flowThreshold ) {
state . direction = "draining" ;
const remainingHeight = Math . max ( level - heightOutlet , 0 ) ;
state . seconds = remainingHeight * surfaceArea / Math . abs ( flowDiff ) ;
}
this . measurements
. type ( "netFlowRate" )
. variant ( "predicted" )
. position ( "atEquipment" )
. value ( flowDiff )
. unit ( "m3/s" ) ;
this . logger . debug (
` Flow-based net flow | diff= ${ flowDiff . toFixed ( 3 ) } m3/s, level= ${ level . toFixed ( 3 ) } m `
) ;
return { source : "flow" , netFlowRate : flowDiff , state } ;
}
_calcNetFlowFromLevel ( { heightOverflow , heightOutlet , surfaceArea } ) {
const levelObj = this . measurements
. type ( "level" )
. variant ( "measured" )
. position ( "atEquipment" ) ;
const level = levelObj . getCurrentValue ( "m" ) ;
const prevLevel = levelObj . getLaggedValue ( 2 , "m" ) ; // { value, timestamp, unit }
const measurement = levelObj . get ( ) ;
const latestTimestamp = measurement ? . getLatestTimestamp ( ) ;
if ( level === null || ! prevLevel || latestTimestamp == null ) {
this . logger . warn ( ` no flowdiff ${ level } , previous level ${ prevLevel } , latestTimestamp ${ latestTimestamp } found escaping ` ) ;
return null ;
}
const deltaSeconds = ( latestTimestamp - prevLevel . timestamp ) / 1000 ;
if ( deltaSeconds <= 0 ) {
this . logger . warn ( ` Level fallback: invalid Δt= ${ deltaSeconds } , LatestTimestamp : ${ latestTimestamp } , PrevTimestamp : ${ prevLevel . timestamp } ` ) ;
return null ;
}
const lvlDiff = level - prevLevel . value ;
const lvlRate = lvlDiff / deltaSeconds ; // m/s
const levelRateThreshold = 0.1 / surfaceArea ; // same 0.1 m³/s threshold translated to height
const state = { direction : "stable" , seconds : 0 , netUpstream : 0 , netDownstream : 0 } ;
if ( lvlRate > levelRateThreshold ) {
state . direction = "filling" ;
const remainingHeight = Math . max ( heightOverflow - level , 0 ) ;
state . seconds = remainingHeight / lvlRate ;
} else if ( lvlRate < - levelRateThreshold ) {
state . direction = "draining" ;
const remainingHeight = Math . max ( level - heightOutlet , 0 ) ;
state . seconds = remainingHeight / Math . abs ( lvlRate ) ;
}
const netFlowRate = lvlRate * surfaceArea ; // m³/s inferred from level trend
this . measurements
. type ( "netFlowRate" )
. variant ( "predicted" )
. position ( "atEquipment" )
. value ( netFlowRate )
. unit ( "m3/s" ) ;
this . logger . warn (
` Level-based net flow | rate= ${ lvlRate . toExponential ( 3 ) } m/s, inferred= ${ netFlowRate . toFixed ( 3 ) } m3/s `
) ;
return { source : "level" , netFlowRate , state } ;
2025-10-07 18:05:54 +02:00
}
initBasinProperties ( ) {
2025-10-14 16:32:44 +02:00
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
const minVol = heightInlet * surfaceArea ;
const minVolOut = heightOutlet * surfaceArea ; // this will indicate if its an open end or a closed end.
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 . 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-07 18:05:54 +02:00
getOutput ( ) {
return {
2025-10-16 14:44:45 +02:00
volume _m3 : this . measurements . type ( "volume" ) . variant ( "measured" ) . position ( "atEquipment" ) . getCurrentValue ( 'm3' ) ,
2025-10-07 18:05:54 +02:00
} ;
}
}
2025-10-14 13:51:32 +02:00
module . exports = pumpingStation ;
2025-10-07 18:05:54 +02:00
/ *
2025-10-16 14:44:45 +02:00
//
2025-10-07 18:05: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
* /