2025-07-01 17:03:36 +02:00
const { outputUtils , configManager } = require ( "generalFunctions" ) ;
const Specific = require ( "./specificClass" ) ;
class nodeClass {
/ * *
* Create a MeasurementNode .
* @ 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 is the Node-RED node instance, we can use this to send messages and update status
this . RED = RED ; // This is the Node-RED runtime API, we can use this to create endpoints if needed
this . name = nameOfNode ; // This is the name of the node, it should match the file name and the node type in Node-RED
this . source = null ; // Will hold the specific class instance
// Load default & UI config
this . _loadConfig ( uiConfig , this . node ) ;
// Instantiate core Measurement 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 : uiConfig . 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 || "atEquipment" , // Default to 'atEquipment' if not set
} ,
} ;
// Utility for formatting outputs
this . _output = new outputUtils ( ) ;
}
_updateNodeStatus ( ) {
2025-10-02 17:08:41 +02:00
//console.log('Updating node status...');
2025-07-01 17:03:36 +02:00
const mg = this . source ;
const mode = mg . mode ;
const scaling = mg . scaling ;
2025-09-23 11:19:22 +02:00
// Add safety checks for measurements
const totalFlow = mg . measurements
? . type ( "flow" )
? . variant ( "predicted" )
2025-11-13 19:39:32 +01:00
? . position ( "atequipment" )
? . getCurrentValue ( 'm3/h' ) || 0 ;
2025-09-23 11:19:22 +02:00
const totalPower = mg . measurements
? . type ( "power" )
? . variant ( "predicted" )
2025-10-02 17:08:41 +02:00
? . position ( "atEquipment" )
2025-09-23 11:19:22 +02:00
? . getCurrentValue ( ) || 0 ;
// Calculate total capacity based on available machines with safety checks
const availableMachines = Object . values ( mg . machines || { } ) . filter ( ( machine ) => {
// Safety check: ensure machine and machine.state exist
if ( ! machine || ! machine . state || typeof machine . state . getCurrentState !== 'function' ) {
2026-02-23 13:17:39 +01:00
mg . logger ? . warn ( ` Machine missing or invalid: ${ machine ? . config ? . general ? . id || 'unknown' } ` ) ;
2025-09-23 11:19:22 +02:00
return false ;
}
2025-07-01 17:03:36 +02:00
const state = machine . state . getCurrentState ( ) ;
const mode = machine . currentMode ;
return ! (
state === "off" ||
state === "maintenance" ||
mode === "maintenance"
) ;
} ) ;
2025-09-23 11:19:22 +02:00
const totalCapacity = Math . round ( ( mg . dynamicTotals ? . flow ? . max || 0 ) * 1 ) / 1 ;
2025-07-01 17:03:36 +02:00
// Determine overall status based on available machines
2025-09-23 11:19:22 +02:00
const status = availableMachines . length > 0
? ` ${ availableMachines . length } machine(s) connected `
: "No machines" ;
2025-07-01 17:03:36 +02:00
let scalingSymbol = "" ;
2025-09-23 11:19:22 +02:00
switch ( ( scaling || "" ) . toLowerCase ( ) ) {
2025-07-01 17:03:36 +02:00
case "absolute" :
2025-09-23 11:19:22 +02:00
scalingSymbol = "Ⓐ" ;
2025-07-01 17:03:36 +02:00
break ;
case "normalized" :
2025-09-23 11:19:22 +02:00
scalingSymbol = "Ⓝ" ;
2025-07-01 17:03:36 +02:00
break ;
default :
2025-09-23 11:19:22 +02:00
scalingSymbol = mode || "" ;
2025-07-01 17:03:36 +02:00
break ;
}
2025-09-23 11:19:22 +02:00
const text = ` ${ mode || 'Unknown' } | ${ scalingSymbol } : 💨= ${ Math . round ( totalFlow ) } / ${ totalCapacity } | ⚡= ${ Math . round ( totalPower ) } | ${ status } ` ;
2025-07-01 17:03:36 +02:00
return {
fill : availableMachines . length > 0 ? "green" : "red" ,
shape : "dot" ,
text ,
} ;
}
/ * *
* 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 events to Node - RED status updates . Using internal emitter . -- > REMOVE LATER WE NEED ONLY COMPLETE CHILDS AND THEN CHECK FOR UPDATES
* /
_bindEvents ( ) {
this . source . emitter . on ( "mAbs" , ( val ) => {
this . node . status ( {
fill : "green" ,
shape : "dot" ,
text : ` ${ val } ${ this . config . general . unit } ` ,
} ) ;
} ) ;
}
/ * *
* Register this node as a child upstream and downstream .
* Delayed to avoid Node - RED startup race conditions .
* /
_registerChild ( ) {
setTimeout ( ( ) => {
this . node . send ( [
null ,
null ,
{
topic : "registerChild" ,
payload : this . node . id ,
positionVsParent :
this . config ? . functionality ? . positionVsParent || "atEquipment" ,
} ,
] ) ;
} , 100 ) ;
}
/ * *
* Start the periodic tick loop to drive the Measurement class .
* /
_startTickLoop ( ) {
setTimeout ( ( ) => {
this . _tickInterval = setInterval ( ( ) => this . _tick ( ) , 1000 ) ;
// 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 ) ;
} , 1000 ) ;
}
/ * *
* Execute a single tick : update measurement , format and send outputs .
* /
_tick ( ) {
const raw = this . source . getOutput ( ) ;
2025-11-06 11:18:38 +01:00
const processMsg = this . _output . formatMsg ( raw , this . source . config , "process" ) ;
const influxMsg = this . _output . formatMsg ( raw , this . source . config , "influxdb" ) ;
2025-07-01 17:03:36 +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" ,
2025-07-02 10:52:37 +02:00
async ( msg , send , done ) => {
2026-02-23 13:17:39 +01:00
const mg = this . source ;
const RED = this . RED ;
try {
2025-07-01 17:03:36 +02:00
switch ( msg . topic ) {
2026-02-23 13:17:39 +01:00
case "registerChild" : {
const childId = msg . payload ;
const childObj = RED . nodes . getNode ( childId ) ;
if ( ! childObj || ! childObj . source ) {
mg . logger . warn ( ` registerChild skipped: missing child/source for id= ${ childId } ` ) ;
break ;
}
mg . childRegistrationUtils . registerChild ( childObj . source , msg . positionVsParent ) ;
break ;
}
2025-07-01 17:03:36 +02:00
case "setMode" :
2026-02-23 13:17:39 +01:00
mg . setMode ( msg . payload ) ;
2025-07-01 17:03:36 +02:00
break ;
case "setScaling" :
2026-02-23 13:17:39 +01:00
mg . setScaling ( msg . payload ) ;
2025-07-01 17:03:36 +02:00
break ;
2026-02-23 13:17:39 +01:00
case "Qd" : {
2025-07-01 17:03:36 +02:00
const Qd = parseFloat ( msg . payload ) ;
const sourceQd = "parent" ;
if ( isNaN ( Qd ) ) {
2026-02-23 13:17:39 +01:00
mg . logger . error ( ` Invalid demand value: ${ msg . payload } ` ) ;
break ;
2025-07-01 17:03:36 +02:00
}
try {
await mg . handleInput ( sourceQd , Qd ) ;
msg . topic = mg . config . general . name ;
msg . payload = "done" ;
send ( msg ) ;
2026-02-23 13:17:39 +01:00
} catch ( error ) {
mg . logger . error ( ` Failed to process Qd: ${ error . message } ` ) ;
2025-07-01 17:03:36 +02:00
}
break ;
2026-02-23 13:17:39 +01:00
}
2025-07-01 17:03:36 +02:00
default :
2025-07-02 10:52:37 +02:00
mg . logger . warn ( ` Unknown topic: ${ msg . topic } ` ) ;
2025-07-01 17:03:36 +02:00
break ;
}
2026-02-23 13:17:39 +01:00
} catch ( error ) {
mg . logger . error ( ` Input handler failure: ${ error . message } ` ) ;
2025-07-01 17:03:36 +02:00
}
2026-02-23 13:17:39 +01:00
if ( typeof done === 'function' ) done ( ) ;
}
2025-07-01 17:03:36 +02:00
) ;
}
/ * *
* Clean up timers and intervals when Node - RED stops the node .
* /
_attachCloseHandler ( ) {
this . node . on ( "close" , ( done ) => {
clearInterval ( this . _tickInterval ) ;
2025-07-02 10:52:37 +02:00
clearInterval ( this . _statusInterval ) ;
2026-02-23 13:17:39 +01:00
if ( typeof done === 'function' ) done ( ) ;
2025-07-01 17:03:36 +02:00
} ) ;
}
}
module . exports = nodeClass ; // Export the class for Node-RED to use