2025-10-14 13:51:32 +02:00
2025-10-07 18:05:54 +02:00
const { outputUtils , configManager } = require ( 'generalFunctions' ) ;
const Specific = require ( "./specificClass" ) ;
class nodeClass {
/ * *
* Create a node .
* @ param { object } uiConfig - Node - RED node configuration .
* @ param { object } RED - Node - RED runtime API .
* @ param { object } nodeInstance - The Node - RED node instance .
* @ param { string } nameOfNode - The name of the node , used for
* /
constructor ( uiConfig , RED , nodeInstance , nameOfNode ) {
// Preserve RED reference for HTTP endpoints if needed
this . node = nodeInstance ;
this . RED = RED ;
this . name = nameOfNode ;
// Load default & UI config
this . _loadConfig ( uiConfig , this . node ) ;
// Instantiate core class
this . _setupSpecificClass ( ) ;
// Wire up event and lifecycle handlers
this . _bindEvents ( ) ;
this . _registerChild ( ) ;
this . _startTickLoop ( ) ;
this . _attachInputHandler ( ) ;
this . _attachCloseHandler ( ) ;
}
/ * *
* Load and merge default config with user - defined settings .
* @ param { object } uiConfig - Raw config from Node - RED UI .
* /
_loadConfig ( uiConfig , node ) {
const cfgMgr = new configManager ( ) ;
this . defaultConfig = cfgMgr . getConfig ( this . name ) ;
// Merge UI config over defaults
this . config = {
general : {
name : this . name ,
id : node . id , // node.id is for the child registration process
unit : uiConfig . unit , // add converter options later to convert to default units (need like a model that defines this which units we are going to use and then conver to those standards)
logging : {
enabled : uiConfig . enableLog ,
logLevel : uiConfig . logLevel
}
} ,
functionality : {
positionVsParent : uiConfig . positionVsParent , // Default to 'atEquipment' if not specified
distance : uiConfig . hasDistance ? uiConfig . distance : undefined
2025-10-16 14:44:45 +02:00
} ,
basin : {
volume : uiConfig . basinVolume ,
height : uiConfig . basinHeight ,
heightInlet : uiConfig . heightInlet ,
heightOutlet : uiConfig . heightOutlet ,
heightOverflow : uiConfig . heightOverflow ,
} ,
hydraulics : {
refHeight : uiConfig . refHeight ,
2025-11-20 12:15:46 +01:00
minHeightBasedOn : uiConfig . minHeightBasedOn ,
2025-10-16 14:44:45 +02:00
basinBottomRef : uiConfig . basinBottomRef ,
2025-11-25 14:57:39 +01:00
} ,
2025-11-27 17:46:24 +01:00
control : {
mode : uiConfig . controlMode ,
levelbased : {
startLevel : uiConfig . startLevel ,
stopLevel : uiConfig . stopLevel ,
minFlowLevel : uiConfig . minFlowLevel ,
maxFlowLevel : uiConfig . maxFlowLevel
}
} ,
2025-11-25 14:57:39 +01:00
safety : {
enableDryRunProtection : uiConfig . enableDryRunProtection ,
dryRunThresholdPercent : uiConfig . dryRunThresholdPercent ,
enableOverfillProtection : uiConfig . enableOverfillProtection ,
overfillThresholdPercent : uiConfig . overfillThresholdPercent ,
timeleftToFullOrEmptyThresholdSeconds : uiConfig . timeleftToFullOrEmptyThresholdSeconds
2025-10-07 18:05:54 +02:00
}
} ;
console . log ( ` position vs child for ${ this . name } is ${ this . config . functionality . positionVsParent } the distance is ${ this . config . functionality . distance } ` ) ;
// Utility for formatting outputs
this . _output = new outputUtils ( ) ;
}
/ * *
* Instantiate the core logic and store as source .
* /
_setupSpecificClass ( ) {
this . source = new Specific ( this . config ) ;
this . node . source = this . source ; // Store the source in the node instance for easy access
}
/ * *
* Bind Node - RED status updates .
* /
_bindEvents ( ) {
}
2025-10-21 12:45:19 +02:00
// init registration msg
2025-10-07 18:05:54 +02:00
_registerChild ( ) {
setTimeout ( ( ) => {
this . node . send ( [
null ,
null ,
{ topic : 'registerChild' , payload : this . node . id , positionVsParent : this . config ? . functionality ? . positionVsParent || 'atEquipment' , distance : this . config ? . functionality ? . distance || null } ,
] ) ;
} , 100 ) ;
}
2025-10-21 12:45:19 +02:00
_updateNodeStatus ( ) {
const ps = this . source ;
2025-11-03 07:42:51 +01:00
const pickVariant = ( type , prefer = [ 'measured' , 'predicted' ] , position = 'atEquipment' , unit ) => {
for ( const variant of prefer ) {
const chain = ps . measurements . type ( type ) . variant ( variant ) . position ( position ) ;
const value = unit ? chain . getCurrentValue ( unit ) : chain . getCurrentValue ( ) ;
if ( value != null ) return { value , variant } ;
2025-10-21 12:45:19 +02:00
}
2025-11-03 07:42:51 +01:00
return { value : null , variant : null } ;
} ;
2025-10-21 12:45:19 +02:00
2025-11-03 07:42:51 +01:00
const vol = pickVariant ( 'volume' , [ 'measured' , 'predicted' ] , 'atEquipment' , 'm3' ) ;
const volPercent = pickVariant ( 'volumePercent' , [ 'measured' , 'predicted' ] , 'atEquipment' ) ; // already unitless
const level = pickVariant ( 'level' , [ 'measured' , 'predicted' ] , 'atEquipment' , 'm' ) ;
2025-11-03 09:17:22 +01:00
const netFlow = pickVariant ( 'netFlowRate' , [ 'measured' , 'predicted' ] , 'atEquipment' , 'm3/h' ) ;
2025-11-03 07:42:51 +01:00
const maxVolBeforeOverflow = ps . basin ? . maxVolOverflow ? ? ps . basin ? . maxVol ? ? 0 ;
const currentVolume = vol . value ? ? 0 ;
const currentvolPercent = volPercent . value ? ? 0 ;
2025-11-03 09:17:22 +01:00
const netFlowM3h = netFlow . value ? ? 0 ;
2025-11-03 07:42:51 +01:00
const direction = ps . state ? . direction ? ? 'unknown' ;
const secondsRemaining = ps . state ? . seconds ? ? null ;
const timeRemainingMinutes = secondsRemaining != null ? Math . round ( secondsRemaining / 60 ) : null ;
const badgePieces = [ ] ;
badgePieces . push ( ` ${ currentvolPercent . toFixed ( 1 ) } % ` ) ;
badgePieces . push (
2025-11-03 09:17:22 +01:00
` V= ${ currentVolume . toFixed ( 2 ) } / ${ maxVolBeforeOverflow . toFixed ( 2 ) } m³ `
2025-11-03 07:42:51 +01:00
) ;
2025-11-03 09:17:22 +01:00
badgePieces . push ( ` net: ${ netFlowM3h . toFixed ( 0 ) } m³/h ` ) ;
2025-11-03 07:42:51 +01:00
if ( timeRemainingMinutes != null ) {
badgePieces . push ( ` t≈ ${ timeRemainingMinutes } min) ` ) ;
2025-10-21 12:45:19 +02:00
}
2025-11-03 07:42:51 +01:00
const { symbol , fill } = ( ( ) => {
switch ( direction ) {
case 'filling' : return { symbol : '⬆️' , fill : 'blue' } ;
case 'draining' : return { symbol : '⬇️' , fill : 'orange' } ;
case 'steady' : return { symbol : '⏸️' , fill : 'green' } ;
default : return { symbol : '❔' , fill : 'grey' } ;
}
} ) ( ) ;
badgePieces [ 0 ] = ` ${ symbol } ${ badgePieces [ 0 ] } ` ;
return {
fill ,
shape : 'dot' ,
text : badgePieces . join ( ' | ' )
} ;
2025-10-21 12:45:19 +02:00
}
2025-11-03 07:42:51 +01:00
2025-10-21 12:45:19 +02:00
// any time based functions here
2025-10-07 18:05:54 +02:00
_startTickLoop ( ) {
setTimeout ( ( ) => {
this . _tickInterval = setInterval ( ( ) => this . _tick ( ) , 1000 ) ;
2025-10-21 12:45:19 +02:00
// Update node status on nodered screen every second ( this is not the best way to do this, but it works for now)
this . _statusInterval = setInterval ( ( ) => {
const status = this . _updateNodeStatus ( ) ;
this . node . status ( status ) ;
} , 1000 ) ;
2025-10-07 18:05:54 +02:00
} , 1000 ) ;
}
/ * *
* Execute a single tick : update measurement , format and send outputs .
* /
_tick ( ) {
2025-10-23 09:51:54 +02:00
//pumping station needs time based ticks to recalc level when predicted
this . source . tick ( ) ;
2025-10-07 18:05:54 +02:00
const raw = this . source . getOutput ( ) ;
2025-11-06 11:19:20 +01:00
const processMsg = this . _output . formatMsg ( raw , this . source . config , 'process' ) ;
const influxMsg = this . _output . formatMsg ( raw , this . source . config , 'influxdb' ) ;
2025-10-07 18:05:54 +02:00
// Send only updated outputs on ports 0 & 1
this . node . send ( [ processMsg , influxMsg ] ) ;
}
/ * *
* Attach the node ' s input handler , routing control messages to the class .
* /
_attachInputHandler ( ) {
this . node . on ( 'input' , ( msg , send , done ) => {
switch ( msg . topic ) {
2025-10-14 08:36:45 +02:00
//example
2025-11-20 12:15:46 +01:00
case 'changemode' :
this . source . changeMode ( msg . payload ) ;
2025-10-07 18:05:54 +02:00
break ;
2025-10-16 14:44:45 +02:00
case 'registerChild' :
// Register this node as a child of the parent node
const childId = msg . payload ;
const childObj = this . RED . nodes . getNode ( childId ) ;
this . source . childRegistrationUtils . registerChild ( childObj . source , msg . positionVsParent ) ;
break ;
2025-11-07 15:07:56 +01:00
case 'calibratePredictedVolume' :
const calibratedVolume = this . source . measurements
. type ( 'volume' )
. variant ( 'measured' )
. position ( 'atequipment' )
. getCurrentValue ( 'm3' ) ;
this . source . calibratePredictedVolume ( calibratedVolume ) ;
break ;
2025-11-27 17:46:24 +01:00
case 'q_in' : {
// payload can be number or { value, unit, timestamp }
const val = Number ( msg . payload ) ;
const unit = msg ? . unit || 'm3/s' ;
const ts = msg ? . timestamp || Date . now ( ) ;
this . source . setManualInflow ( val , ts , unit ) ;
break ;
}
2025-10-07 18:05:54 +02:00
}
done ( ) ;
} ) ;
}
/ * *
* Clean up timers and intervals when Node - RED stops the node .
* /
_attachCloseHandler ( ) {
this . node . on ( 'close' , ( done ) => {
clearInterval ( this . _tickInterval ) ;
2025-10-21 12:45:19 +02:00
clearInterval ( this . _statusInterval ) ;
2025-10-07 18:05:54 +02:00
done ( ) ;
} ) ;
}
}
module . exports = nodeClass ;