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 ,
basinBottomRef : uiConfig . basinBottomRef ,
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 ;
try {
// --- Basin & measurements -------------------------------------------------
const maxVolBeforeOverflow = ps . basin ? . maxVolOverflow ? ? ps . basin ? . maxVol ? ? 0 ;
const volumeMeasurement = ps . measurements . type ( "volume" ) . variant ( "measured" ) . position ( "atEquipment" ) ;
const currentVolume = volumeMeasurement . getCurrentValue ( "m3" ) ? ? 0 ;
const netFlowMeasurement = ps . measurements . type ( "netFlowRate" ) . variant ( "predicted" ) . position ( "atEquipment" ) ;
const netFlowM3s = netFlowMeasurement ? . getCurrentValue ( "m3/s" ) ? ? 0 ;
const netFlowM3h = netFlowM3s * 3600 ;
const percentFull = ps . measurements . type ( "volume" ) . variant ( "procent" ) . position ( "atEquipment" ) . getCurrentValue ( ) ? ? 0 ;
// --- State information ----------------------------------------------------
const direction = ps . state ? . direction || "unknown" ;
const secondsRemaining = ps . state ? . seconds ? ? null ;
const timeRemaining = secondsRemaining ? ` ${ Math . round ( secondsRemaining / 60 ) } ` : 0 + " min" ;
// --- Icon / colour selection ---------------------------------------------
let symbol = "❔" ;
let fill = "grey" ;
switch ( direction ) {
case "filling" :
symbol = "⬆️" ;
fill = "blue" ;
break ;
case "draining" :
symbol = "⬇️" ;
fill = "orange" ;
break ;
case "stable" :
symbol = "⏸️" ;
fill = "green" ;
break ;
default :
symbol = "❔" ;
fill = "grey" ;
break ;
}
// --- Status text ----------------------------------------------------------
const textParts = [
` ${ symbol } ${ percentFull . toFixed ( 1 ) } % ` ,
` V= ${ currentVolume . toFixed ( 2 ) } / ${ maxVolBeforeOverflow . toFixed ( 2 ) } m³ ` ,
` net= ${ netFlowM3h . toFixed ( 1 ) } m³/h ` ,
` t≈ ${ timeRemaining } `
] ;
return {
fill ,
shape : "dot" ,
text : textParts . join ( " | " )
} ;
} catch ( error ) {
this . node . error ( "Error in updateNodeStatus: " + error . message ) ;
return { fill : "red" , shape : "ring" , text : "Status Error" } ;
}
}
// 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 ( ) ;
const processMsg = this . _output . formatMsg ( raw , this . config , 'process' ) ;
const influxMsg = this . _output . formatMsg ( raw , this . config , 'influxdb' ) ;
// 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
/ * c a s e ' s i m u l a t o r ' :
this . source . toggleSimulation ( ) ;
2025-10-07 18:05:54 +02:00
break ;
2025-10-14 08:36:45 +02:00
default :
this . source . handleInput ( msg ) ;
break ;
* /
2026-03-11 13:39:57 +01:00
case 'registerChild' : {
2025-10-16 14:44:45 +02:00
// Register this node as a child of the parent node
const childId = msg . payload ;
2026-03-11 13:39:57 +01:00
const childObj = this . RED . nodes . getNode ( childId ) ;
2025-10-16 14:44:45 +02:00
this . source . childRegistrationUtils . registerChild ( childObj . source , msg . positionVsParent ) ;
break ;
2026-03-11 13:39:57 +01:00
}
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 ;