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-11-12 17:37:09 +01:00
2025-11-03 07:42:51 +01:00
class PumpingStation {
2025-10-28 17:04:26 +01:00
constructor ( config = { } ) {
this . emitter = new 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-28 17:04:26 +01:00
this . logger = new logger ( this . config . general . logging . enabled , this . config . general . logging . logLevel , this . config . general . name ) ;
2025-10-07 18:05:54 +02:00
2025-10-28 17:04:26 +01:00
this . measurements = new MeasurementContainer ( { autoConvert : true } ) ;
this . measurements . setPreferredUnit ( 'flow' , 'm3/s' ) ;
2025-11-03 09:17:22 +01:00
this . measurements . setPreferredUnit ( 'netFlowRate' , 'm3/s' ) ;
2025-10-28 17:04:26 +01:00
this . measurements . setPreferredUnit ( 'level' , 'm' ) ;
this . measurements . setPreferredUnit ( 'volume' , 'm3' ) ;
this . childRegistrationUtils = new childRegistrationUtils ( this ) ;
2025-11-07 15:07:56 +01:00
this . machines = { } ;
this . stations = { } ;
this . machineGroups = { } ;
2025-11-03 07:42:51 +01:00
2025-11-10 16:20:23 +01:00
//fetch control mode from config by default
this . mode = this . config . control . mode ;
this . _levelState = { crossed : new Set ( ) , dwellUntil : null } ;
2025-10-28 17:04:26 +01:00
//variants in determining what gets priority
this . flowVariants = [ 'measured' , 'predicted' ] ;
this . levelVariants = [ 'measured' , 'predicted' ] ;
2025-11-06 16:46:54 +01:00
this . volVariants = [ 'measured' , 'predicted' ] ;
2025-10-28 17:04:26 +01:00
this . flowPositions = { inflow : [ 'in' , 'upstream' ] , outflow : [ 'out' , 'downstream' ] } ;
2025-11-06 16:46:54 +01:00
this . predictedFlowChildren = new Map ( ) ; // childId -> { in: 0, out: 0 }
2025-10-07 18:05:54 +02:00
2025-10-16 14:44:45 +02:00
this . basin = { } ;
2025-10-27 16:39:06 +01:00
this . state = {
2025-10-28 17:04:26 +01:00
direction : 'steady' ,
2025-10-27 16:39:06 +01:00
netFlow : 0 ,
flowSource : null ,
seconds : null ,
remainingSource : null
2025-10-28 17:04:26 +01:00
} ;
2025-10-14 16:32:44 +02:00
2025-10-28 17:04:26 +01:00
const thresholdFromConfig = Number ( this . config . general ? . flowThreshold ) ;
this . flowThreshold = Number . isFinite ( thresholdFromConfig ) ? thresholdFromConfig : 1e-4 ;
2025-10-16 14:44:45 +02:00
2025-10-28 17:04:26 +01:00
this . initBasinProperties ( ) ;
this . logger . debug ( 'PumpingStationV2 initialized' ) ;
2025-10-07 18:05:54 +02:00
}
registerChild ( child , softwareType ) {
2025-10-28 17:04:26 +01:00
this . logger . debug ( ` Registering child ( ${ softwareType } ) " ${ child . config . general . name } " ` ) ;
if ( softwareType === 'measurement' ) {
this . _registerMeasurementChild ( child ) ;
return ;
2025-10-07 18:05:54 +02:00
}
2025-10-21 13:44:31 +02:00
2025-11-07 15:07:56 +01:00
//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
2025-11-13 19:37:41 +01:00
if ( softwareType === 'pumpingstation' ) {
2025-11-07 15:07:56 +01:00
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
2025-11-13 19:37:41 +01:00
if ( softwareType === 'machinegroup' ) {
2025-11-07 15:07:56 +01:00
const childId = child . config . general . id ;
this . machineGroups [ childId ] = child ;
2025-11-13 19:37:41 +01:00
this . _registerPredictedFlowChild ( child ) ;
this . logger . debug ( ` Registered machine group child " ${ child . config . general . name } " with id " ${ childId } " ` ) ;
2025-11-07 15:07:56 +01:00
}
//for all childs that can provide predicted flow data
2025-11-13 19:37:41 +01:00
if ( softwareType === 'machine' || softwareType === 'pumpingstation' || softwareType === 'machinegroup' ) {
this . logger . debug ( ` Registering predicted flow child ${ child . config . general . name } with software type: ${ softwareType } " ` ) ;
2025-10-28 17:04:26 +01:00
this . _registerPredictedFlowChild ( child ) ;
}
2025-10-23 18:04:18 +02:00
2025-11-13 19:37:41 +01:00
//this.logger.warn(`Unsupported child software type: ${softwareType}`);
2025-10-28 17:04:26 +01:00
}
2025-10-23 09:51:54 +02:00
2025-11-10 16:20:23 +01:00
_safetyController ( snapshot , remainingTime , direction ) {
2025-11-06 16:46:54 +01:00
2025-11-10 16:20:23 +01:00
this . safetyControllerActive = false ;
2025-11-06 16:46:54 +01:00
2025-11-10 16:20:23 +01:00
const vol = this . _resolveVolume ( snapshot ) ;
2025-11-06 16:46:54 +01:00
if ( vol == null ) {
2025-11-07 15:07:56 +01:00
//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.' ) ;
2025-11-10 16:20:23 +01:00
this . safetyControllerActive = true ;
2025-11-07 15:07:56 +01:00
return ;
}
2025-11-25 14:57:39 +01:00
const {
enableDryRunProtection ,
dryRunThresholdPercent ,
enableOverfillProtection ,
overfillThresholdPercent ,
timeleftToFullOrEmptyThresholdSeconds
} = this . config . safety || { } ;
const dryRunEnabled = Boolean ( enableDryRunProtection ) ;
const overfillEnabled = Boolean ( enableOverfillProtection ) ;
const timeProtectionEnabled = timeleftToFullOrEmptyThresholdSeconds > 0 ;
const triggerHighVol = this . basin . maxVolOverflow * ( ( Number ( overfillThresholdPercent ) || 0 ) / 100 ) ;
const triggerLowVol = this . basin . minVol * ( 1 + ( ( Number ( dryRunThresholdPercent ) || 0 ) / 100 ) ) ;
2025-11-07 15:07:56 +01:00
// 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' } `
) ;
2025-11-25 14:57:39 +01:00
const timeTriggered = timeProtectionEnabled && remainingTime != null && remainingTime < timeleftToFullOrEmptyThresholdSeconds ;
const dryRunTriggered = dryRunEnabled && vol < triggerLowVol ;
if ( timeTriggered || dryRunTriggered ) {
2025-11-07 15:07:56 +01:00
//shut down all downstream or atequipment machines,pumping stations and machine groups
Object . entries ( this . machines ) . forEach ( ( [ machineId , machine ] ) => {
2025-11-13 19:37:41 +01:00
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 } " ` ) ;
}
2025-11-07 15:07:56 +01:00
} ) ;
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 ] ) => {
2025-11-13 19:37:41 +01:00
group . turnOffAllMachines ( ) ;
2025-11-07 15:07:56 +01:00
this . logger . warn ( ` Safe guard triggered: vol= ${ vol . toFixed ( 2 ) } m3, remainingTime= ${ remainingTime ? remainingTime . toFixed ( 1 ) : 'N/A' } s; shutting down machine group " ${ groupId } " ` ) ;
} ) ;
2025-11-10 16:20:23 +01:00
this . safetyControllerActive = true ;
2025-11-07 15:07:56 +01:00
}
}
2025-11-13 19:37:41 +01:00
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' } `
) ;
2025-11-25 14:57:39 +01:00
const timeTriggered = timeProtectionEnabled && remainingTime != null && remainingTime < timeleftToFullOrEmptyThresholdSeconds ;
const overfillTriggered = overfillEnabled && vol > triggerHighVol ;
if ( timeTriggered || overfillTriggered ) {
2025-11-13 19:37:41 +01:00
//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 ;
}
}
2025-11-10 16:20:23 +01:00
}
changeMode ( newMode ) {
if ( this . config . control . allowedModes . has ( newMode ) ) {
2025-11-20 12:15:46 +01:00
const currentMode = this . mode ;
this . logger . info ( ` Control mode changing from ${ currentMode } to ${ newMode } ` ) ;
2025-11-10 16:20:23 +01:00
this . mode = newMode ;
}
else {
this . logger . warn ( ` Attempted to change to unsupported control mode: ${ newMode } ` ) ;
2025-11-07 15:07:56 +01:00
}
2025-11-10 16:20:23 +01:00
}
2025-11-13 19:37:41 +01:00
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 ) ) ;
}
2025-11-10 16:20:23 +01:00
_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 ( ) {
2025-11-12 17:37:09 +01:00
return Object . values ( this . machines ) . find ( ( machine ) => {
const position = machine ? . config ? . functionality ? . positionVsParent ;
return ( position === 'downstream' || position === 'atEquipment' ) && ! machine . _isOperationalState ( ) ;
} ) ;
2025-11-10 16:20:23 +01:00
}
//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 ;
2025-11-20 12:15:46 +01:00
case "manual" :
this . _manualControl ( ) ;
break ;
2025-11-10 16:20:23 +01:00
default :
this . logger . warn ( ` Unsupported control mode: ${ mode } ` ) ;
}
2025-11-07 15:07:56 +01:00
}
2025-11-20 12:15:46 +01:00
_manualControl ( ) {
// Nothing to do - manual mode
}
2025-11-07 15:07:56 +01:00
//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' ) ;
2025-11-06 16:46:54 +01:00
2025-11-07 15:07:56 +01:00
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
} ;
2025-11-06 16:46:54 +01:00
}
2025-10-28 17:04:26 +01:00
tick ( ) {
const snapshot = this . _takeMeasurementSnapshot ( ) ;
2025-10-23 09:51:54 +02:00
2025-10-28 17:04:26 +01:00
this . _updatePredictedVolume ( snapshot ) ;
2025-11-06 16:46:54 +01:00
2025-10-28 17:04:26 +01:00
const netFlow = this . _selectBestNetFlow ( snapshot ) ;
const remaining = this . _computeRemainingTime ( snapshot , netFlow ) ;
2025-11-10 16:20:23 +01:00
//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 ) ;
2025-11-06 16:46:54 +01:00
2025-10-28 17:04:26 +01:00
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 ` ) ;
2025-10-21 13:44:31 +02:00
}
2025-10-28 17:04:26 +01:00
_registerMeasurementChild ( child ) {
const position = child . config . functionality . positionVsParent ;
const measurementType = child . config . asset . type ;
const eventName = ` ${ measurementType } .measured. ${ position } ` ;
2025-10-23 09:51:54 +02:00
2025-10-28 17:04:26 +01:00
child . measurements . emitter . on ( eventName , ( eventData ) => {
this . logger . debug (
` Measurement update ${ eventName } <- ${ eventData . childName } : ${ eventData . value } ${ eventData . unit } `
) ;
2025-10-23 09:51:54 +02:00
2025-10-28 17:04:26 +01:00
this . measurements
. type ( measurementType )
. variant ( 'measured' )
. position ( position )
. value ( eventData . value , eventData . timestamp , eventData . unit ) ;
2025-10-23 09:51:54 +02:00
2025-10-28 17:04:26 +01:00
this . _handleMeasurement ( measurementType , eventData . value , position , eventData ) ;
} ) ;
}
2025-10-23 09:51:54 +02:00
2025-11-06 16:46:54 +01:00
//register machines or pumping stations that can provide predicted flow data
2025-10-28 17:04:26 +01:00
_registerPredictedFlowChild ( child ) {
2025-11-13 19:37:41 +01:00
const position = ( child . config . functionality . positionVsParent || '' ) . toLowerCase ( ) ;
2025-10-28 17:04:26 +01:00
const childName = child . config . general . name ;
2025-11-06 16:46:54 +01:00
const childId = child . config . general . id ? ? childName ;
2025-10-28 17:04:26 +01:00
2025-11-13 19:37:41 +01:00
let posKey ;
let eventNames ;
switch ( position ) {
case 'downstream' :
case 'out' :
case 'atequipment' :
posKey = 'out' ;
eventNames = [
'flow.predicted.downstream' ,
'flow.predicted.atequipment'
] ;
break ;
2025-10-23 09:51:54 +02:00
2025-11-13 19:37:41 +01:00
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 ;
2025-10-28 17:04:26 +01:00
}
2025-11-06 16:46:54 +01:00
if ( ! this . predictedFlowChildren . has ( childId ) ) {
this . predictedFlowChildren . set ( childId , { in : 0 , out : 0 } ) ;
2025-11-13 19:37:41 +01:00
this . logger . debug ( ` Initialized predicted flow tracking for child ${ childName } ( ${ childId } ) ` ) ;
2025-11-06 16:46:54 +01:00
}
const handler = ( eventData = { } ) => {
const value = Number . isFinite ( eventData . value ) ? eventData . value : 0 ;
const timestamp = eventData . timestamp ? ? Date . now ( ) ;
const unit = eventData . unit ? ? 'm3/s' ;
2025-11-07 15:07:56 +01:00
this . logger . debug ( ` Predicted flow update from ${ childName } ( ${ childId } , ${ posKey } ) -> ${ value } ${ unit } ` ) ;
2025-11-06 16:46:54 +01:00
this . predictedFlowChildren . get ( childId ) [ posKey ] = value ;
this . _refreshAggregatedPredictedFlow ( posKey , timestamp , unit ) ;
} ;
2025-11-13 19:37:41 +01:00
eventNames . forEach ( ( eventName ) => child . measurements . emitter . on ( eventName , handler ) ) ;
2025-11-06 16:46:54 +01:00
}
_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 ) ;
2025-10-28 17:04:26 +01:00
}
2025-10-23 18:04:18 +02:00
2025-10-28 17:04:26 +01:00
_handleMeasurement ( measurementType , value , position , context ) {
switch ( measurementType ) {
case 'level' :
this . _onLevelMeasurement ( position , value , context ) ;
2025-10-23 18:04:18 +02:00
break ;
2025-10-28 17:04:26 +01:00
case 'pressure' :
this . _onPressureMeasurement ( position , value , context ) ;
break ;
case 'flow' :
// Additional flow-specific logic could go here if needed
2025-10-23 18:04:18 +02:00
break ;
default :
2025-10-28 17:04:26 +01:00
this . logger . debug ( ` Unhandled measurement type " ${ measurementType } ", storing only. ` ) ;
break ;
}
}
2025-10-23 09:51:54 +02:00
2025-11-12 17:37:09 +01:00
2025-10-28 17:04:26 +01:00
_onLevelMeasurement ( position , value , context = { } ) {
2025-11-25 14:57:39 +01:00
this . measurements . type ( 'level' ) . variant ( 'measured' ) . position ( position ) . value ( value ) . unit ( context . unit ) ;
2025-10-28 17:04:26 +01:00
const levelSeries = this . measurements . type ( 'level' ) . variant ( 'measured' ) . position ( position ) ;
const levelMeters = levelSeries . getCurrentValue ( 'm' ) ;
2025-11-07 15:07:56 +01:00
2025-10-28 17:04:26 +01:00
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
) ;
2025-10-23 09:51:54 +02:00
2025-10-28 17:04:26 +01:00
this . measurements
. type ( 'volume' )
. variant ( 'measured' )
. position ( 'atequipment' )
. value ( volume , context . timestamp , 'm3' ) ;
this . measurements
2025-11-03 07:42:51 +01:00
. type ( 'volumePercent' )
. variant ( 'measured' )
2025-10-28 17:04:26 +01:00
. position ( 'atequipment' )
. value ( percent , context . timestamp , '%' ) ;
2025-10-23 09:51:54 +02:00
}
2025-10-28 17:04:26 +01:00
_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' ) ;
}
2025-10-23 09:51:54 +02:00
2025-10-28 17:04:26 +01:00
if ( kelvinTemp == null ) return ;
2025-10-23 09:51:54 +02:00
2025-10-28 17:04:26 +01:00
const density = coolprop . PropsSI ( 'D' , 'T' , kelvinTemp , 'P' , 101325 , 'Water' ) ;
const pressurePa = this . measurements
. type ( 'pressure' )
. variant ( 'measured' )
. position ( position )
. getCurrentValue ( 'Pa' ) ;
2025-10-21 13:44:31 +02:00
2025-10-28 17:04:26 +01:00
if ( ! Number . isFinite ( pressurePa ) || ! Number . isFinite ( density ) ) return ;
2025-10-21 13:44:31 +02:00
2025-10-28 17:04:26 +01:00
const g = 9.80665 ;
const level = pressurePa / ( density * g ) ;
2025-10-21 13:44:31 +02:00
2025-10-28 17:04:26 +01:00
this . measurements . type ( 'level' ) . variant ( 'predicted' ) . position ( position ) . value ( level , context . timestamp , 'm' ) ;
2025-10-21 13:44:31 +02:00
}
2025-10-28 17:04:26 +01:00
_takeMeasurementSnapshot ( ) {
const snapshot = {
flows : { } ,
levels : { } ,
2025-11-06 16:46:54 +01:00
levelRates : { } ,
vols : { } ,
2025-10-28 17:04:26 +01:00
} ;
for ( const variant of this . flowVariants ) {
snapshot . flows [ variant ] = this . _snapshotFlowsForVariant ( variant ) ;
}
2025-11-06 16:46:54 +01:00
for ( const variant of this . volVariants ) {
snapshot . vols [ variant ] = this . _snapshotVolsForVariant ( variant ) ;
}
2025-10-28 17:04:26 +01:00
for ( const variant of this . levelVariants ) {
snapshot . levels [ variant ] = this . _snapshotLevelForVariant ( variant ) ;
snapshot . levelRates [ variant ] = this . _estimateLevelRate ( snapshot . levels [ variant ] ) ;
}
2025-10-21 13:44:31 +02:00
2025-10-28 17:04:26 +01:00
return snapshot ;
2025-10-07 18:05:54 +02:00
}
2025-11-06 16:46:54 +01:00
_snapshotVolsForVariant ( variant ) {
const volumeSeries = this . _locateSeries ( 'volume' , variant , [ 'atequipment' ] ) ;
return { variant , samples : this . _seriesSamples ( volumeSeries ) } ;
}
2025-10-28 17:04:26 +01:00
_snapshotFlowsForVariant ( variant ) {
const inflowSeries = this . _locateSeries ( 'flow' , variant , this . flowPositions . inflow ) ;
const outflowSeries = this . _locateSeries ( 'flow' , variant , this . flowPositions . outflow ) ;
2025-10-27 16:39:06 +01:00
2025-11-06 16:46:54 +01:00
return { variant , inflow : this . _seriesSamples ( inflowSeries ) , outflow : this . _seriesSamples ( outflowSeries ) } ;
2025-10-28 17:04:26 +01:00
}
2025-10-27 16:39:06 +01:00
2025-10-28 17:04:26 +01:00
_snapshotLevelForVariant ( variant ) {
const levelSeries = this . _locateSeries ( 'level' , variant , [ 'atequipment' ] ) ;
return {
variant ,
samples : this . _seriesSamples ( levelSeries )
} ;
2025-10-27 16:39:06 +01:00
}
2025-10-28 17:04:26 +01:00
_seriesSamples ( seriesInfo ) {
if ( ! seriesInfo ) {
return { exists : false , measurement : null , current : null , previous : null } ;
2025-10-27 16:39:06 +01:00
}
2025-10-28 17:04:26 +01:00
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 } ;
}
2025-10-27 16:39:06 +01:00
}
2025-10-28 17:04:26 +01:00
_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 ;
2025-10-27 16:39:06 +01:00
2025-10-28 17:04:26 +01:00
const measurement = chain . get ( ) ;
if ( ! measurement ) continue ;
2025-10-27 16:39:06 +01:00
2025-10-28 17:04:26 +01:00
return { type , variant , position , measurement } ;
} catch ( err ) {
// ignore missing combinations
2025-10-27 16:39:06 +01:00
}
}
2025-10-28 17:04:26 +01:00
return null ;
2025-10-27 16:39:06 +01:00
}
2025-10-28 17:04:26 +01:00
_estimateLevelRate ( levelSnapshot ) {
if ( ! levelSnapshot . samples . exists ) { return null } ;
const { current , previous } = levelSnapshot . samples ;
if ( ! current || ! previous || previous . timestamp == null ) { return null } ;
2025-10-27 16:39:06 +01:00
2025-10-28 17:04:26 +01:00
const deltaT = ( current . timestamp - previous . timestamp ) / 1000 ;
if ( ! Number . isFinite ( deltaT ) || deltaT <= 0 ) { return null } ;
2025-10-27 16:39:06 +01:00
2025-10-28 17:04:26 +01:00
const deltaLevel = current . value - previous . value ;
return deltaLevel / deltaT ;
2025-10-23 09:51:54 +02:00
}
2025-10-21 13:44:31 +02:00
2025-10-28 17:04:26 +01:00
_selectBestNetFlow ( snapshot ) {
for ( const variant of this . flowVariants ) {
const flow = snapshot . flows [ variant ] ;
2025-11-03 09:17:22 +01:00
2025-10-28 17:04:26 +01:00
if ( ! flow . inflow . exists && ! flow . outflow . exists ) continue ;
2025-10-07 18:05:54 +02:00
2025-10-28 17:04:26 +01:00
const inflow = flow . inflow . current ? . value ? ? 0 ;
const outflow = flow . outflow . current ? . value ? ? 0 ;
const net = inflow - outflow ; // positive => filling
2025-10-14 16:45:09 +02:00
2025-11-03 09:17:22 +01:00
this . measurements . type ( 'netFlowRate' ) . variant ( variant ) . position ( 'atequipment' ) . value ( net ) . unit ( 'm3/s' ) ;
this . logger . debug ( ` inflow : ${ inflow } - outflow : ${ outflow } ` ) ;
2025-10-28 17:04:26 +01:00
return { value : net , source : variant , direction : this . _deriveDirection ( net ) } ;
}
2025-10-07 18:05:54 +02:00
2025-10-28 17:04:26 +01:00
// 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 )
} ;
}
2025-10-16 14:44:45 +02:00
2025-10-28 17:04:26 +01:00
this . logger . warn ( 'No usable measurements to compute net flow; assuming steady.' ) ;
return { value : 0 , source : null , direction : 'steady' } ;
2025-10-16 14:44:45 +02:00
}
2025-10-28 17:04:26 +01:00
_computeRemainingTime ( snapshot , netFlow ) {
if ( ! netFlow || Math . abs ( netFlow . value ) < this . flowThreshold ) {
return { seconds : null , source : null } ;
2025-10-23 18:04:18 +02:00
}
2025-10-16 14:44:45 +02:00
const { heightOverflow , heightOutlet , surfaceArea } = this . basin ;
2025-10-28 17:04:26 +01:00
if ( ! Number . isFinite ( surfaceArea ) || surfaceArea <= 0 ) {
this . logger . warn ( 'Invalid basin surface area.' ) ;
return { seconds : null , source : null } ;
2025-10-27 16:39:06 +01:00
}
2025-11-06 16:46:54 +01:00
2025-10-28 17:04:26 +01:00
for ( const variant of this . levelVariants ) {
const levelSnap = snapshot . levels [ variant ] ;
const current = levelSnap . samples . current ? . value ? ? null ;
if ( ! Number . isFinite ( current ) ) continue ;
2025-10-16 14:44:45 +02:00
2025-10-28 17:04:26 +01:00
const remainingHeight =
netFlow . value > 0
? Math . max ( heightOverflow - current , 0 )
: Math . max ( current - heightOutlet , 0 ) ;
2025-10-27 16:39:06 +01:00
2025-10-28 17:04:26 +01:00
const seconds = ( remainingHeight * surfaceArea ) / Math . abs ( netFlow . value ) ;
if ( ! Number . isFinite ( seconds ) ) continue ;
2025-10-16 14:44:45 +02:00
2025-10-28 17:04:26 +01:00
return { seconds , source : ` ${ netFlow . source } / ${ variant } ` } ;
}
2025-10-16 14:44:45 +02:00
2025-10-28 17:04:26 +01:00
this . logger . warn ( 'No level data available to compute remaining time.' ) ;
return { seconds : null , source : netFlow . source } ;
2025-10-27 16:39:06 +01:00
}
2025-10-28 17:04:26 +01:00
_updatePredictedVolume ( snapshot ) {
const predicted = snapshot . flows . predicted ;
if ( ! predicted ) return ;
2025-10-27 16:39:06 +01:00
2025-10-28 17:04:26 +01:00
const now = Date . now ( ) ;
const inflowSample = predicted . inflow . current ? ? predicted . inflow . previous ? ? null ;
const outflowSample = predicted . outflow . current ? ? predicted . outflow . previous ? ? null ;
2025-10-27 16:39:06 +01:00
2025-10-28 17:04:26 +01:00
if ( ! this . _predictedFlowState ) {
this . _predictedFlowState = {
inflow : inflowSample ? . value ? ? 0 ,
outflow : outflowSample ? . value ? ? 0 ,
lastTimestamp : inflowSample ? . timestamp ? ? outflowSample ? . timestamp ? ? now
} ;
2025-10-27 16:39:06 +01:00
}
2025-10-28 17:04:26 +01:00
if ( inflowSample ) this . _predictedFlowState . inflow = inflowSample . value ;
if ( outflowSample ) this . _predictedFlowState . outflow = outflowSample . value ;
2025-10-27 16:39:06 +01:00
2025-10-28 17:04:26 +01:00
const latestObservedTimestamp =
inflowSample ? . timestamp ? ? outflowSample ? . timestamp ? ? this . _predictedFlowState . lastTimestamp ;
2025-10-27 16:39:06 +01:00
2025-10-28 17:04:26 +01:00
const timestampPrev = this . _predictedFlowState . lastTimestamp ? ? latestObservedTimestamp ;
2025-10-27 16:39:06 +01:00
2025-10-28 17:04:26 +01:00
let timestampNow = latestObservedTimestamp ;
if ( ! Number . isFinite ( timestampNow ) || timestampNow <= timestampPrev ) {
timestampNow = now ;
}
2025-10-27 16:39:06 +01:00
2025-10-28 17:04:26 +01:00
let deltaSeconds = ( timestampNow - timestampPrev ) / 1000 ;
if ( ! Number . isFinite ( deltaSeconds ) || deltaSeconds <= 0 ) {
deltaSeconds = 0 ;
}
2025-10-16 14:44:45 +02:00
2025-10-28 17:04:26 +01:00
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 ;
}
2025-10-16 14:44:45 +02:00
2025-10-28 17:04:26 +01:00
const writeTimestamp = timestampPrev + Math . max ( deltaSeconds , 0 ) * 1000 ;
2025-10-16 14:44:45 +02:00
2025-11-06 16:46:54 +01:00
const volumeSeries = this . measurements . type ( 'volume' ) . variant ( 'predicted' ) . position ( 'atEquipment' ) ;
2025-10-16 14:44:45 +02:00
2025-10-28 17:04:26 +01:00
const currentVolume = volumeSeries . getCurrentValue ( 'm3' ) ? ? this . basin . minVol ;
2025-10-16 14:44:45 +02:00
2025-10-28 17:04:26 +01:00
const nextVolume = currentVolume + netVolumeChange ;
2025-10-16 14:44:45 +02:00
2025-10-28 17:04:26 +01:00
volumeSeries . value ( nextVolume , writeTimestamp , 'm3' ) . unit ( 'm3' ) ;
2025-10-16 14:44:45 +02:00
2025-10-28 17:04:26 +01:00
const nextLevel = this . _calcLevelFromVolume ( nextVolume ) ;
this . measurements
. type ( 'level' )
. variant ( 'predicted' )
. position ( 'atEquipment' )
. value ( nextLevel , writeTimestamp , 'm' )
. unit ( 'm' ) ;
2025-10-16 14:44:45 +02:00
2025-11-03 07:42:51 +01:00
//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 ) ;
2025-10-28 17:04:26 +01:00
this . _predictedFlowState . lastTimestamp = writeTimestamp ;
2025-10-16 14:44:45 +02:00
}
2025-10-28 17:04:26 +01:00
_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 ;
}
2025-10-16 14:44:45 +02:00
2025-10-28 17:04:26 +01:00
_deriveDirection ( netFlow ) {
if ( netFlow > this . flowThreshold ) return 'filling' ;
if ( netFlow < - this . flowThreshold ) return 'draining' ;
return 'steady' ;
2025-10-07 18:05:54 +02:00
}
2025-11-12 17:37:09 +01:00
2025-10-28 17:04:26 +01:00
/* ------------------------------------------------------------------ */
/* Basin Calculations */
/* ------------------------------------------------------------------ */
2025-10-07 18:05:54 +02:00
initBasinProperties ( ) {
2025-11-12 17:37:09 +01:00
2025-11-20 12:15:46 +01:00
//is min height based on inlet or outlet elevation?
const minHeightBasedOn = this . config . hydraulics . minHeightBasedOn ;
const volEmptyBasin = this . config . basin . volume ; //volume when basin is empty
const heightBasin = this . config . basin . height ; //total height of basin
const heightInlet = this . config . basin . heightInlet ; //height at which inlet is located
const heightOutlet = this . config . basin . heightOutlet ; //height at which outlet is located
const heightOverflow = this . config . basin . heightOverflow ; //height at which overflow occurs
2025-10-28 17:04:26 +01:00
2025-11-20 12:15:46 +01:00
const surfaceArea = volEmptyBasin / heightBasin ; //assume uniform cross section for now
const maxVol = heightBasin * surfaceArea ; //maximum volume when basin is full
const maxVolOverflow = heightOverflow * surfaceArea ; //maximum volume before overflow occurs
const minVolOut = heightOutlet * surfaceArea ; //minimum volume to have outlet just above basin bottom
const minVolIn = heightInlet * surfaceArea ; //minimum volume to have inlet just above waterline
const minVol = ( minHeightBasedOn === "inlet" ) ? minVolIn : minVolOut ;
this . logger . debug ( ` Basin min volume based on ${ minHeightBasedOn } : ${ minVol . toFixed ( 2 ) } m3 ` ) ;
2025-10-28 17:04:26 +01:00
this . basin = {
volEmptyBasin ,
heightBasin ,
heightInlet ,
heightOutlet ,
heightOverflow ,
surfaceArea ,
maxVol ,
maxVolOverflow ,
2025-11-20 12:15:46 +01:00
minVolIn ,
minVolOut ,
2025-10-28 17:04:26 +01:00
minVol ,
2025-11-20 12:15:46 +01:00
minHeightBasedOn
2025-10-28 17:04:26 +01:00
} ;
2025-11-20 12:15:46 +01:00
this . measurements . type ( 'volume' ) . variant ( 'predicted' ) . position ( 'atEquipment' ) . value ( minVol ) . unit ( 'm3' ) ;
2025-10-28 17:04:26 +01:00
this . logger . debug (
` Basin initialized | area= ${ surfaceArea . toFixed ( 2 ) } m2, max= ${ maxVol . toFixed ( 2 ) } m3, overflow= ${ maxVolOverflow . toFixed ( 2 ) } m3 `
2025-10-21 13:44:31 +02:00
) ;
2025-10-07 18:05:54 +02:00
}
2025-10-28 17:04:26 +01:00
_calcVolumeFromLevel ( level ) {
return Math . max ( level , 0 ) * this . basin . surfaceArea ;
}
2025-10-14 16:32:44 +02:00
2025-10-28 17:04:26 +01:00
_calcLevelFromVolume ( volume ) {
return Math . max ( volume , 0 ) / this . basin . surfaceArea ;
}
2025-10-23 09:51:54 +02:00
2025-10-28 17:04:26 +01:00
/* ------------------------------------------------------------------ */
/* Output */
/* ------------------------------------------------------------------ */
2025-10-07 18:05:54 +02:00
2025-10-28 17:04:26 +01:00
getOutput ( ) {
2025-10-23 18:04:18 +02:00
const output = { } ;
2025-10-28 17:04:26 +01:00
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 ( ) ;
2025-10-23 18:04:18 +02:00
} ) ;
} ) ;
} ) ;
2025-11-06 11:19:20 +01:00
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 ;
2025-11-20 12:15:46 +01:00
output . minVolIn = this . basin . minVolIn ;
output . minHeightBasedOn = this . basin . minHeightBasedOn ;
2025-10-23 18:04:18 +02:00
return output ;
2025-10-28 17:04:26 +01:00
}
2025-10-07 18:05:54 +02:00
}
2025-11-03 07:42:51 +01:00
module . exports = PumpingStation ;
2025-10-07 18:05:54 +02:00
2025-10-23 09:51:54 +02:00
/* ------------------------------------------------------------------------- */
2025-10-28 17:04:26 +01:00
/* Example usage */
2025-10-23 09:51:54 +02:00
/* ------------------------------------------------------------------------- */
2025-11-12 17:37:09 +01:00
/ *
2025-10-28 17:04:26 +01:00
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 ,
2025-11-03 07:42:51 +01:00
height : 10 ,
heightInlet : 3 ,
2025-10-28 17:04:26 +01:00
heightOutlet : 0.2 ,
2025-11-03 07:42:51 +01:00
heightOverflow : 3.2
2025-10-28 17:04:26 +01:00
} ,
hydraulics : {
refHeight : 'NAP' ,
basinBottomRef : 0
}
} ;
}
2025-10-23 09:51:54 +02:00
2025-10-28 17:04:26 +01:00
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' }
} ;
}
2025-10-23 09:51:54 +02:00
2025-10-28 17:04:26 +01:00
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-28 17:04:26 +01:00
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'
}
} ;
}
2025-10-23 09:51:54 +02:00
2025-10-28 17:04:26 +01:00
function createMachineStateConfig ( ) {
return {
general : {
2025-10-23 09:51:54 +02:00
logging : {
2025-10-28 17:04:26 +01:00
enabled : true ,
logLevel : 'debug'
2025-10-23 09:51:54 +02:00
}
2025-10-28 17:04:26 +01:00
} ,
movement : { speed : 1 } ,
time : {
starting : 2 ,
warmingup : 3 ,
stopping : 2 ,
coolingdown : 3
}
} ;
2025-10-23 09:51:54 +02:00
}
2025-10-28 17:04:26 +01:00
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 ) ;
2025-10-23 09:51:54 +02:00
}
2025-10-07 18:05:54 +02:00
2025-10-28 17:04:26 +01:00
( async function demo ( ) {
2025-11-03 07:42:51 +01:00
const station = new PumpingStation ( createPumpingStationConfig ( 'PumpingStationDemo' ) ) ;
2025-10-28 17:04:26 +01:00
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' ) ) ;
2025-11-25 14:57:39 +01:00
station . childRegistrationUtils . registerChild ( levelSensor , levelSensor . config . functionality . softwareType ) ;
2025-11-03 07:42:51 +01:00
//station.childRegistrationUtils.registerChild(inflowSensor, inflowSensor.config.functionality.softwareType);
//station.childRegistrationUtils.registerChild(outflowSensor, outflowSensor.config.functionality.softwareType);
2025-10-28 17:04:26 +01:00
station . childRegistrationUtils . registerChild ( pump1 , 'machine' ) ;
station . childRegistrationUtils . registerChild ( pump2 , 'machine' ) ;
// Seed initial measurements
2025-11-03 07:42:51 +01:00
2025-11-25 14:57:39 +01:00
seedSample ( levelSensor , 'level' , 1.8 , 'm' ) ;
2025-11-03 07:42:51 +01:00
//seedSample(inflowSensor, 'flow', 0.35, 'm3/s');
//seedSample(outflowSensor, 'flow', 0.20, 'm3/s');
2025-10-28 17:04:26 +01:00
setInterval (
( ) => station . tick ( ) , 1000 ) ;
2025-10-23 09:51:54 +02:00
2025-10-28 17:04:26 +01:00
await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
2025-10-07 18:05:54 +02:00
2025-10-28 17:04:26 +01:00
console . log ( 'Initial state:' , station . state ) ;
2025-10-07 18:05:54 +02:00
2025-10-28 17:04:26 +01:00
await pump1 . handleInput ( 'parent' , 'execSequence' , 'startup' ) ;
await pump1 . handleInput ( 'parent' , 'execMovement' , 10 ) ;
2025-10-07 18:05:54 +02:00
2025-10-28 17:04:26 +01:00
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 ) ;
} ) ;
}
2025-11-03 07:42:51 +01:00
//*/