@@ -1,48 +1,3 @@
/**
* @file machine.js
*
* Permission is hereby granted to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to use it for personal
* or non-commercial purposes, with the following restrictions:
*
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
* be copied, merged, distributed, sublicensed, or sold without explicit
* prior written permission from the author.
*
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
* a valid license, obtainable only with the explicit consent of the author.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Ownership of this code remains solely with the original author. Unauthorized
* use of this Software is strictly prohibited.
*
* @summary A class to interact and manipulate machines with a non-euclidian curve
* @description A class to interact and manipulate machines with a non-euclidian curve
* @module machine
* @exports machine
* @version 0.1.0
* @since 0.1.0
*
* Author:
* - Rene De Ren
* Email:
* - r.de.ren@brabantsedelta.nl
*
* Add functionality later
// -------- Operational Metrics -------- //
maintenanceAlert: this.state.checkMaintenanceStatus()
*/
//load local dependencies
const EventEmitter = require ( 'events' ) ;
const EventEmitter = require ( 'events' ) ;
const { loadCurve , logger , configUtils , configManager , state , nrmse , MeasurementContainer , predict , interpolation , childRegistrationUtils } = require ( 'generalFunctions' ) ;
const { loadCurve , logger , configUtils , configManager , state , nrmse , MeasurementContainer , predict , interpolation , childRegistrationUtils } = require ( 'generalFunctions' ) ;
const { name } = require ( '../../generalFunctions/src/convert/lodash/lodash._shimkeys' ) ;
const { name } = require ( '../../generalFunctions/src/convert/lodash/lodash._shimkeys' ) ;
@@ -93,7 +48,17 @@ class Machine {
this . errorMetrics = new nrmse ( errorMetricsConfig , this . logger ) ;
this . errorMetrics = new nrmse ( errorMetricsConfig , this . logger ) ;
// Initialize measurements
// Initialize measurements
this . measurements = new MeasurementContainer ( ) ;
this . measurements = new MeasurementContainer ( {
autoConvert : true ,
windowSize : 50 ,
defaultUnits : {
pressure : 'mbar' ,
flow : this . config . general . unit ,
power : 'kW' ,
temperature : 'C'
}
} ) ;
this . interpolation = new interpolation ( ) ;
this . interpolation = new interpolation ( ) ;
this . flowDrift = null ;
this . flowDrift = null ;
@@ -113,25 +78,114 @@ class Machine {
this . updatePosition ( ) ;
this . updatePosition ( ) ;
} ) ;
} ) ;
//When state changes look if we need to do other updates
this . state . emitter . on ( "stateChange" , ( newState ) => {
this . logger . debug ( ` State change detected: ${ newState } ` ) ;
this . _updateState ( ) ;
} ) ;
this . child = { } ; // object to hold child information so we know on what to subscribe
this . child = { } ; // object to hold child information so we know on what to subscribe
this . childRegistrationUtils = new childRegistrationUtils ( this ) ; // Child registration utility
this . childRegistrationUtils = new childRegistrationUtils ( this ) ; // Child registration utility
// --- KPI tracking ---
this . kpi = {
failures : 0 ,
totalRuntimeHours : 0 ,
totalDowntimeHours : 0 ,
lastFailureTime : null ,
lastRepairTime : null ,
MTBF : 0 ,
MTTR : 0 ,
availability : 0
} ;
this . assetHealth = {
index : 0 // 0 = optimal, 5 = failure
} ;
this . state . emitter . on ( 'stateChange' , ( payload ) => {
const stateStr = typeof payload === 'string'
? payload
: ( payload ? . state ? ? payload ? . newState ? ? payload ) ;
if ( typeof stateStr !== 'string' ) {
this . logger . warn ( ` stateChange event without parsable state: ${ JSON . stringify ( payload ) } ` ) ;
return ;
}
this . _handleStateChangeForKPI ( stateStr ) ;
} ) ;
// --- KPI tracking ---
this . kpi = {
failures : 0 ,
totalRuntimeHours : 0 ,
totalDowntimeHours : 0 ,
lastFailureTime : null ,
lastRepairTime : null ,
MTBF : 0 ,
MTTR : 0 ,
availability : 0
} ;
this . assetHealth = {
index : 0 // 0 = optimal, 5 = failure
} ;
this . state . emitter . on ( 'stateChange' , ( payload ) => {
const stateStr = typeof payload === 'string'
? payload
: ( payload ? . state ? ? payload ? . newState ? ? payload ) ;
if ( typeof stateStr !== 'string' ) {
this . logger . warn ( ` stateChange event without parsable state: ${ JSON . stringify ( payload ) } ` ) ;
return ;
}
this . _handleStateChangeForKPI ( stateStr ) ;
} ) ;
}
_updateState ( ) {
const isOperational = this . _isOperationalState ( ) ;
if ( ! isOperational ) {
//overrule the last prediction this should be 0 now
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( 0 ) ;
}
}
_updateState ( ) {
const isOperational = this . _isOperationalState ( ) ;
if ( ! isOperational ) {
//overrule the last prediction this should be 0 now
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( 0 ) ;
}
}
}
/*------------------- Register child events -------------------*/
/*------------------- Register child events -------------------*/
registerChild ( child , softwareType ) {
registerChild ( child , softwareType ) {
this . logger . debug ( 'Setting up child event listeners' ) ;
this . logger . debug ( 'Setting up child event for softwaretype ' + softwareType ) ;
if ( softwareType === "measurement" ) {
if ( softwareType === "measurement" ) {
const position = child . config . functionality . positionVsParent ;
const position = child . config . functionality . positionVsParent ;
const distance = child . config . functionality . distanceVsParent || 0 ;
const measurementType = child . config . asset . type ;
const measurementType = child . config . asset . type ;
const key = ` ${ measurementType } _ ${ position } ` ;
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 } ` ;
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
// Register event listener for measurement updates
child . measurements . emitter . on ( eventName , ( eventData ) => {
child . measurements . emitter . on ( eventName , ( eventData ) => {
this . logger . debug ( ` 🔄 ${ position } ${ measurementType } from ${ eventData . childName } : ${ eventData . value } ${ eventData . unit } ` ) ;
this . logger . debug ( ` 🔄 ${ position } ${ measurementType } from ${ eventData . childName } : ${ eventData . value } ${ eventData . unit } ` ) ;
console . log ( ` Emitting... ${ eventName } with data: ` ) ;
// Store directly in parent's measurement container
// Store directly in parent's measurement container
this . measurements
this . measurements
. type ( measurementType )
. type ( measurementType )
@@ -156,10 +210,6 @@ _callMeasurementHandler(measurementType, value, position, context) {
this . updateMeasuredFlow ( value , position , context ) ;
this . updateMeasuredFlow ( value , position , context ) ;
break ;
break ;
case 'temperature' :
this . updateMeasuredTemperature ( value , position , context ) ;
break ;
default :
default :
this . logger . warn ( ` No handler for measurement type: ${ measurementType } ` ) ;
this . logger . warn ( ` No handler for measurement type: ${ measurementType } ` ) ;
// Generic handler - just update position
// Generic handler - just update position
@@ -205,46 +255,71 @@ _callMeasurementHandler(measurementType, value, position, context) {
// -------- Mode and Input Management -------- //
// -------- Mode and Input Management -------- //
isValidSourceForMode ( source , mode ) {
isValidSourceForMode ( source , mode ) {
const allowedSourcesSet = this . config . mode . allowedSources [ mode ] || [ ] ;
const allowedSourcesSet = this . config . mode . allowedSources [ mode ] || [ ] ;
return allowedSourcesSet . has ( source ) ;
const allowed = allowedSourcesSet . has ( source ) ;
allowed ?
this . logger . debug ( ` source is allowed proceeding with ${ source } for mode ${ mode } ` ) :
this . logger . warn ( ` ${ source } is not allowed in mode ${ mode } ` ) ;
return allowed ;
}
}
isValidActionForMode ( action , mode ) {
isValidActionForMode ( action , mode ) {
const allowedActionsSet = this . config . mode . allowedActions [ mode ] || [ ] ;
const allowedActionsSet = this . config . mode . allowedActions [ mode ] || [ ] ;
return allowedActionsSet . has ( action ) ;
const allowed = allowedActionsSet . has ( action ) ;
allowed ?
this . logger . debug ( ` Action is allowed proceeding with ${ action } for mode ${ mode } ` ) :
this . logger . warn ( ` ${ action } is not allowed in mode ${ mode } ` ) ;
return allowed ;
}
}
async handleInput ( source , action , parameter ) {
async handleInput ( source , action , parameter ) {
if ( ! this . isValidSourceForMode ( source , this . currentMode ) ) {
let warningTxt = ` Source ' ${ source } ' is not valid for mode ' ${ this . currentMode } '. ` ;
this . logger . warn ( warningTxt ) ;
return { status : false , feedback : warningTxt } ;
}
this . logger . debug ( "hello" ) ;
//sanitize input
if ( typeof action !== 'string' ) { this . logger . error ( ` Action must be string ` ) ; return ; }
//convert to lower case to avoid to many mistakes in commands
action = action . toLowerCase ( ) ;
// check for validity of the request
if ( ! this . isValidActionForMode ( action , this . currentMode ) ) { return ; }
if ( ! this . isValidSourceForMode ( source , this . currentMode ) ) { return ; }
this . logger . debug ( "hello2" ) ;
this . logger . info ( ` Handling input from source ' ${ source } ' with action ' ${ action } ' in mode ' ${ this . currentMode } '. ` ) ;
this . logger . info ( ` Handling input from source ' ${ source } ' with action ' ${ action } ' in mode ' ${ this . currentMode } '. ` ) ;
try {
try {
switch ( action ) {
switch ( action ) {
case "execSequence" :
await this . executeSequence ( parameter ) ;
case "execsequence" :
//recalc flow and power
return await this . executeSequence ( parameter ) ;
this . updatePosition ( ) ;
break ;
case "execmovement" :
case "execMovement" :
return await this . setpoint ( parameter ) ;
await this . setpoint ( parameter ) ;
break ;
case "entermaintenance" :
case "flowMovement" :
return await this . executeSequence ( parameter ) ;
case "exitmaintenance" :
return await this . executeSequence ( parameter ) ;
case "flowmovement" :
// Calculate the control value for a desired flow
// Calculate the control value for a desired flow
const pos = this . calcCtrl ( parameter ) ;
const pos = this . calcCtrl ( parameter ) ;
// Move to the desired setpoint
// Move to the desired setpoint
await this . setpoint ( pos ) ;
return await this . setpoint ( pos ) ;
break ;
case "emergencyS top" :
case "emergencys top" :
this . logger . warn ( ` Emergency stop activated by ' ${ source } '. ` ) ;
this . logger . warn ( ` Emergency stop activated by ' ${ source } '. ` ) ;
await this . executeSequence ( "emergencyStop" ) ;
return await this . executeSequence ( "emergencyStop" ) ;
break ;
case "statusC heck" :
case "statusc heck" :
this . logger . info ( ` Status Check: Mode = ' ${ this . currentMode } ', Source = ' ${ source } '. ` ) ;
this . logger . info ( ` Status Check: Mode = ' ${ this . currentMode } ', Source = ' ${ source } '. ` ) ;
break ;
break ;
default :
default :
this . logger . warn ( ` Action ' ${ action } ' is not implemented. ` ) ;
this . logger . warn ( ` Action ' ${ action } ' is not implemented. ` ) ;
break ;
break ;
@@ -254,6 +329,13 @@ _callMeasurementHandler(measurementType, value, position, context) {
} catch ( error ) {
} catch ( error ) {
this . logger . error ( ` Error handling input: ${ error } ` ) ;
this . logger . error ( ` Error handling input: ${ error } ` ) ;
}
}
}
abortMovement ( reason = "group override" ) {
if ( this . state ? . abortCurrentMovement ) {
this . state . abortCurrentMovement ( reason ) ;
}
}
}
setMode ( newMode ) {
setMode ( newMode ) {
@@ -294,6 +376,9 @@ _callMeasurementHandler(measurementType, value, position, context) {
break ; // Exit sequence execution on error
break ; // Exit sequence execution on error
}
}
}
}
//recalc flow and power
this . updatePosition ( ) ;
}
}
async setpoint ( setpoint ) {
async setpoint ( setpoint ) {
@@ -341,20 +426,20 @@ _callMeasurementHandler(measurementType, value, position, context) {
calcPower ( x ) {
calcPower ( x ) {
if ( this . hasCurve ) {
if ( this . hasCurve ) {
if ( ! this . _isOperationalState ( ) ) {
if ( ! this . _isOperationalState ( ) ) {
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'upstream ' ) . value ( 0 ) ;
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'atEquipment ' ) . value ( 0 ) ;
this . logger . debug ( ` Machine is not operational. Setting predicted power to 0. ` ) ;
this . logger . debug ( ` Machine is not operational. Setting predicted power to 0. ` ) ;
return 0 ;
return 0 ;
}
}
//this.predictPower.currentX = x; Decrepated
//this.predictPower.currentX = x; Decrepated
const cPower = this . predictPower . y ( x ) ;
const cPower = this . predictPower . y ( x ) ;
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'upstream ' ) . value ( cPower ) ;
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'atEquipment ' ) . value ( cPower ) ;
//this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
//this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cPower ;
return cPower ;
}
}
// If no curve data is available, log a warning and return 0
// If no curve data is available, log a warning and return 0
this . logger . warn ( ` No curve data available for power calculation. Returning 0. ` ) ;
this . logger . warn ( ` No curve data available for power calculation. Returning 0. ` ) ;
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'upstream ' ) . value ( 0 ) ;
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'atEquipment ' ) . value ( 0 ) ;
return 0 ;
return 0 ;
}
}
@@ -372,7 +457,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
// If no curve data is available, log a warning and return 0
// If no curve data is available, log a warning and return 0
this . logger . warn ( ` No curve data available for power calculation. Returning 0. ` ) ;
this . logger . warn ( ` No curve data available for power calculation. Returning 0. ` ) ;
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'upstream ' ) . value ( 0 ) ;
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'atEquipment ' ) . value ( 0 ) ;
return 0 ;
return 0 ;
}
}
@@ -382,14 +467,14 @@ _callMeasurementHandler(measurementType, value, position, context) {
if ( this . hasCurve ) {
if ( this . hasCurve ) {
this . predictCtrl . currentX = x ;
this . predictCtrl . currentX = x ;
const cCtrl = this . predictCtrl . y ( x ) ;
const cCtrl = this . predictCtrl . y ( x ) ;
this . measurements . type ( "ctrl" ) . variant ( "predicted" ) . position ( 'upstream ' ) . value ( cCtrl ) ;
this . measurements . type ( "ctrl" ) . variant ( "predicted" ) . position ( 'atEquipment ' ) . value ( cCtrl ) ;
//this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
//this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cCtrl ;
return cCtrl ;
}
}
// If no curve data is available, log a warning and return 0
// If no curve data is available, log a warning and return 0
this . logger . warn ( ` No curve data available for control calculation. Returning 0. ` ) ;
this . logger . warn ( ` No curve data available for control calculation. Returning 0. ` ) ;
this . measurements . type ( "ctrl" ) . variant ( "predicted" ) . position ( 'upstream ' ) . value ( 0 ) ;
this . measurements . type ( "ctrl" ) . variant ( "predicted" ) . position ( 'atEquipment ' ) . value ( 0 ) ;
return 0 ;
return 0 ;
}
}
@@ -398,9 +483,9 @@ _callMeasurementHandler(measurementType, value, position, context) {
// this will be either the differential pressure, downstream or upstream pressure
// this will be either the differential pressure, downstream or upstream pressure
getMeasuredPressure ( ) {
getMeasuredPressure ( ) {
const pressureDiff = this . measurements . type ( 'pressure' ) . variant ( 'measured' ) . difference ( ) ;
const pressureDiff = this . measurements . type ( 'pressure' ) . variant ( 'measured' ) . difference ( ) ;
// Both upstream & downstream => differential
// Both upstream & downstream => differential
if ( pressureDiff != null ) {
if ( pressureDiff ) {
this . logger . debug ( ` Pressure differential: ${ pressureDiff . value } ` ) ;
this . logger . debug ( ` Pressure differential: ${ pressureDiff . value } ` ) ;
this . predictFlow . fDimension = pressureDiff . value ;
this . predictFlow . fDimension = pressureDiff . value ;
this . predictPower . fDimension = pressureDiff . value ;
this . predictPower . fDimension = pressureDiff . value ;
@@ -420,7 +505,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
// Only downstream => use it, warn that it's partial
// Only downstream => use it, warn that it's partial
if ( downstreamPressure != null ) {
if ( downstreamPressure != null ) {
this . logger . warn ( ` Using downstream pressure only for prediction: ${ downstreamPressure } ` ) ;
this . logger . warn ( ` Using downstream pressure only for prediction: ${ downstreamPressure } This is less acurate!! ` ) ;
this . predictFlow . fDimension = downstreamPressure ;
this . predictFlow . fDimension = downstreamPressure ;
this . predictPower . fDimension = downstreamPressure ;
this . predictPower . fDimension = downstreamPressure ;
this . predictCtrl . fDimension = downstreamPressure ;
this . predictCtrl . fDimension = downstreamPressure ;
@@ -502,17 +587,12 @@ _callMeasurementHandler(measurementType, value, position, context) {
}
}
}
}
// rich context handler for pressure updates
// context handler for pressure updates
updateMeasuredPressure ( value , position , context = { } ) {
updateMeasuredPressure ( value , position , context = { } ) {
if ( ! this . _isOperationalState ( ) ) {
this . logger . warn ( ` Machine not operational, skipping pressure update from ${ context . childName || 'unknown' } ` ) ;
return ;
}
// Enhanced logging with child context
this . logger . debug ( ` Pressure update: ${ value } at ${ position } from ${ context . childName || 'child' } ( ${ context . childId || 'unknown-id' } ) ` ) ;
this . logger . debug ( ` Pressure update: ${ value } at ${ position } from ${ context . childName || 'child' } ( ${ context . childId || 'unknown-id' } ) ` ) ;
// Store in parent's measurement container (your existing logic)
// Store in parent's measurement container
this . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( position ) . value ( value , context . timestamp , context . unit ) ;
this . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( position ) . value ( value , context . timestamp , context . unit ) ;
// Determine what kind of value to use as pressure (upstream , downstream or difference)
// Determine what kind of value to use as pressure (upstream , downstream or difference)
@@ -536,18 +616,20 @@ _callMeasurementHandler(measurementType, value, position, context) {
// Update predicted flow if you have prediction capability
// Update predicted flow if you have prediction capability
if ( this . predictFlow ) {
if ( this . predictFlow ) {
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "atEquipment " ) . value ( this . predictFlow . outputY || 0 ) ;
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream " ) . value ( this . predictFlow . outputY || 0 ) ;
}
}
}
}
// Helper method for operational state check
// Helper method for operational state check
_isOperationalState ( ) {
_isOperationalState ( ) {
const state = this . state . getCurrentState ( ) ;
const state = this . state . getCurrentState ( ) ;
this . logger . debug ( ` Checking operational state ${ this . state . getCurrentState ( ) } ? ${ [ "operational" , "accelerating" , "decelerating" ] . includes ( state ) } ` ) ;
return [ "operational" , "accelerating" , "decelerating" ] . includes ( state ) ;
return [ "operational" , "accelerating" , "decelerating" ] . includes ( state ) ;
}
}
//what is the internal functions that need updating when something changes that has influence on this.
//what is the internal functions that need updating when something changes that has influence on this.
updatePosition ( ) {
updatePosition ( ) {
if ( this . _isOperationalState ( ) ) {
if ( this . _isOperationalState ( ) ) {
const currentPosition = this . state . getCurrentPosition ( ) ;
const currentPosition = this . state . getCurrentPosition ( ) ;
@@ -566,6 +648,136 @@ _callMeasurementHandler(measurementType, value, position, context) {
}
}
}
}
/////////////////////////////
/**
* Compute a single drift score in [0..1] using predicted vs measured series.
* Uses min/max of the *predicted* window as normalization range.
* If no usable data -> returns 0 (neutral).
*/
_computeDriftScore ( ) {
try {
const metrics = [
{ key : "pressure" , pos : "downstream" } ,
{ key : "flow" , pos : "downstream" } ,
{ key : "power" , pos : "atEquipment" }
] ;
const values = [ ] ;
for ( const m of metrics ) {
const pred = this . measurements . type ( m . key ) . variant ( "predicted" ) . position ( m . pos ) . getAllValues ( ) ? . values ;
const meas = this . measurements . type ( m . key ) . variant ( "measured" ) . position ( m . pos ) . getAllValues ( ) ? . values ;
if ( ! Array . isArray ( pred ) || ! Array . isArray ( meas ) || pred . length < 2 || meas . length < 2 ) continue ;
const expectedMin = Math . min ( ... pred ) ;
const expectedMax = Math . max ( ... pred ) ;
if ( ! Number . isFinite ( expectedMin ) || ! Number . isFinite ( expectedMax ) || expectedMax === expectedMin ) continue ;
const drift = this . errorMetrics . assessDrift ( pred , meas , expectedMin , expectedMax ) ;
if ( Number . isFinite ( drift ) ) {
// assessDrift is already normalized; keep it in [0..1]
values . push ( Math . max ( 0 , Math . min ( 1 , Math . abs ( drift ) ) ) ) ;
}
}
if ( values . length === 0 ) return 0 ; // neutral if no data
const avg = values . reduce ( ( s , v ) => s + v , 0 ) / values . length ;
return Math . max ( 0 , Math . min ( 1 , avg ) ) ;
} catch ( e ) {
this . logger ? . warn ? . ( ` Drift score error: ${ e . message } ` ) ;
return 0 ;
}
}
_calculateAssetHealthIndex ( ) {
try {
// 1) Hard fail -> worst health
// if (this.state?.getCurrentState && this.state.getCurrentState() === "failed")
if ( [ "off" ] . includes ( this . state ? . getCurrentState ? . ( ) ) ) {
this . assetHealth . index = 5 ;
return 5 ;
}
// 2) Inputs (clamped to 0..1)
const availability = typeof this . kpi ? . availability === 'number' ? this . kpi . availability : 1 ;
const unavailability = 1 - Math . max ( 0 , Math . min ( 1 , availability ) ) ;
const effPenalty = Math . max ( 0 , Math . min ( 1 , typeof this . relDistFromPeak === 'number' ? this . relDistFromPeak : 0 ) ) ;
const driftScore = this . _computeDriftScore ( ) ; // 0..1
// 3) Blend (weights sum to 1.0)
// Tweak these if you like: e.g. make drift more/less important.
const wAvail = 0.4 ; // unavailability weight
const wDrift = 0.4 ; // drift weight
const wEff = 0.2 ; // efficiency distance weight
const score01 = ( wAvail * unavailability ) + ( wDrift * driftScore ) + ( wEff * effPenalty ) ;
// 4) Scale to 0..5 integer, clamp
const index = Math . max ( 0 , Math . min ( 5 , Math . round ( score01 * 5 ) ) ) ;
this . assetHealth . index = index ;
return index ;
} catch ( err ) {
this . logger ? . error ? . ( ` AHI calc error: ${ err . message } ` ) ;
this . assetHealth . index = 0 ;
return 0 ;
}
}
_handleStateChangeForKPI ( newState ) {
const now = Date . now ( ) ;
const runtime = this . state . getRunTimeHours ( ) ;
const lastState = this . state . getPreviousState ? . ( ) || "unknown" ;
// --- Treat OFF as failure and start of downtime ---
if ( newState === "off" ) {
this . kpi . failures ++ ; // always count a new failure when OFF
this . kpi . lastFailureTime = now ; // mark the start of downtime
this . logger . warn ( ` Machine OFF (counted as failure). Total failures: ${ this . kpi . failures } ` ) ;
}
// --- When we leave OFF and become OPERATIONAL, book downtime ---
if ( newState === "operational" ) {
// Only calculate downtime if we had an OFF period before
if ( this . kpi . lastFailureTime != null ) {
const downtimeHours = ( now - this . kpi . lastFailureTime ) / 3600000 ;
this . kpi . totalDowntimeHours += downtimeHours ;
this . kpi . lastRepairTime = now ; // moment of "repaired"
this . kpi . lastFailureTime = null ; // close downtime window
this . logger . info ( ` OFF → OPERATIONAL. Added ${ downtimeHours . toFixed ( 2 ) } h downtime. ` ) ;
}
}
// --- Compute KPI Metrics ---
const failures = this . kpi . failures ;
const downtime = this . kpi . totalDowntimeHours ;
// If no failures yet: MTBF = total runtime; MTTR = 0
this . kpi . MTBF = failures > 0 ? runtime / failures : runtime ;
this . kpi . MTTR = failures > 0 ? downtime / failures : 0 ;
// --- Compute Availability ---
const mtbf = this . kpi . MTBF ? ? 0 ;
const mttr = this . kpi . MTTR ? ? 0 ;
if ( mtbf <= 0 && mttr <= 0 ) {
this . kpi . availability = 1 ; // Default: 100% if no data
} else {
const availability = mtbf / ( mtbf + mttr ) ;
this . kpi . availability = Math . min ( 1 , Math . max ( 0 , availability ) ) ; // clamp 0– 1
}
this . logger . debug (
` KPI updated — MTBF: ${ this . kpi . MTBF . toFixed ( 2 ) } h, MTTR: ${ this . kpi . MTTR . toFixed ( 2 ) } h, ` +
` Availability: ${ ( this . kpi . availability * 100 ) . toFixed ( 2 ) } % `
) ;
}
//////////////////////////////////////////////
calcDistanceFromPeak ( currentEfficiency , peakEfficiency ) {
calcDistanceFromPeak ( currentEfficiency , peakEfficiency ) {
return Math . abs ( currentEfficiency - peakEfficiency ) ;
return Math . abs ( currentEfficiency - peakEfficiency ) ;
@@ -595,7 +807,6 @@ _callMeasurementHandler(measurementType, value, position, context) {
} ;
} ;
}
}
// Calculate the center of gravity for current pressure
// Calculate the center of gravity for current pressure
calcCog ( ) {
calcCog ( ) {
@@ -659,12 +870,12 @@ _callMeasurementHandler(measurementType, value, position, context) {
if ( power != 0 && flow != 0 ) {
if ( power != 0 && flow != 0 ) {
// Calculate efficiency after measurements update
// Calculate efficiency after measurements update
this . measurements . type ( "efficiency" ) . variant ( variant ) . position ( 'downstream ' ) . value ( ( flow / power ) ) ;
this . measurements . type ( "efficiency" ) . variant ( variant ) . position ( 'atEquipment ' ) . value ( ( flow / power ) ) ;
} else {
} else {
this . measurements . type ( "efficiency" ) . variant ( variant ) . position ( 'downstream ' ) . value ( null ) ;
this . measurements . type ( "efficiency" ) . variant ( variant ) . position ( 'atEquipment ' ) . value ( null ) ;
}
}
return this . measurements . type ( "efficiency" ) . variant ( variant ) . position ( 'downstream ' ) . getCurrentValue ( ) ;
return this . measurements . type ( "efficiency" ) . variant ( variant ) . position ( 'atEquipment ' ) . getCurrentValue ( ) ;
}
}
@@ -711,23 +922,12 @@ _callMeasurementHandler(measurementType, value, position, context) {
// Improved output object generation
// Improved output object generation
const output = { } ;
const output = { } ;
//build the output object
this . measurements . getTypes ( ) . forEach ( type => {
this . measurements . getVariants ( type ) . forEach ( variant => {
const downstreamVal = this . measurements . type ( type ) . variant ( variant ) . position ( "downstream" ) . getCurrentValue ( ) ;
Object . entries ( this . measurements . measurements ) . forEach ( ( [ type , variants ] ) => {
const upstreamVal = thi s . measurements . type ( type ) . variant ( variant ) . position( "upstream" ) . getCurrentValue ( ) ;
Object . entrie s( variants ) . forEach ( ( [ variant , positions ] ) => {
Object . entries ( positions ) . forEach ( ( [ position , measurement ] ) => {
if ( downstreamVal ! = null ) {
output [ ` ${ type } . ${ variant } . ${ position } ` ] = measurement . getCurrentValue ( ) ;
output [ ` downstream_ ${ variant } _ ${ type } ` ] = downstreamVal ;
} ) ;
}
if ( upstreamVal != null ) {
output [ ` upstream_ ${ variant } _ ${ type } ` ] = upstreamVal ;
}
if ( downstreamVal != null && upstreamVal != null ) {
const diffVal = this . measurements . type ( type ) . variant ( variant ) . difference ( ) . value ;
output [ ` differential_ ${ variant } _ ${ type } ` ] = diffVal ;
}
} ) ;
} ) ;
} ) ;
} ) ;
@@ -740,6 +940,14 @@ _callMeasurementHandler(measurementType, value, position, context) {
output [ "cog" ] = this . cog ; // flow / power efficiency
output [ "cog" ] = this . cog ; // flow / power efficiency
output [ "NCog" ] = this . NCog ; // normalized cog
output [ "NCog" ] = this . NCog ; // normalized cog
output [ "NCogPercent" ] = Math . round ( this . NCog * 100 * 100 ) / 100 ;
output [ "NCogPercent" ] = Math . round ( this . NCog * 100 * 100 ) / 100 ;
output [ "kpi_MTBF" ] = this . kpi . MTBF ;
output [ "kpi_MTTR" ] = this . kpi . MTTR ;
output [ "kpi_assetAvailability" ] = Math . round ( this . kpi . availability * 100 * 100 ) / 100 ;
output [ "kpi_totalFailuresCount" ] = this . kpi . failures ;
output [ "asset_tag_number" ] = 'L001' ;
// output["asset_tag_number"] = this.assetTagNumber;
output [ "maintenanceTime" ] = this . state . getMaintenanceTimeHours ( ) ;
if ( this . flowDrift != null ) {
if ( this . flowDrift != null ) {
const flowDrift = this . flowDrift ;
const flowDrift = this . flowDrift ;
@@ -754,6 +962,21 @@ _callMeasurementHandler(measurementType, value, position, context) {
output [ "effRelDistFromPeak" ] = this . relDistFromPeak ;
output [ "effRelDistFromPeak" ] = this . relDistFromPeak ;
//this.logger.debug(`Output: ${JSON.stringify(output)}`);
//this.logger.debug(`Output: ${JSON.stringify(output)}`);
/////////////////////////////////
// this._calculateAssetHealthIndex();
// output["assetHealthIndex"] = this.assetHealth.index;
this . _calculateAssetHealthIndex ( ) ;
output [ "assetHealthIndex" ] = this . assetHealth . index ;
// 0 = darkgreen, 1 = green, 2 = yellow, 3 = orange, 4 = red, 5 = darkred
// const healthColors = ["darkgreen", "green", "yellow", "orange", "red", "darkred"];
const healthColors = [ "#006400" , "#008000" , "#FFFF00" , "#FFA500" , "#FF0000" , "#8B0000" ] ;
output [ "assetHealthColor" ] = healthColors [ this . assetHealth . index ] || "unknown" ;
//////////////////////////
return output ;
return output ;
}
}
@@ -763,9 +986,9 @@ _callMeasurementHandler(measurementType, value, position, context) {
module . exports = Machine ;
module . exports = Machine ;
/*------------------- Testing -------------------*/
/*------------------- Testing -------------------*/
/*
curve = require('C:/Users/zn375/.node-red/public/fallbackData.json');
/*
//curve = require('C:/Users/zn375/.node-red/public/fallbackData.json');
//import a child
//import a child
const Child = require('../../measurement/src/specificClass');
const Child = require('../../measurement/src/specificClass');
@@ -781,7 +1004,6 @@ const PT1 = new Child(config={
},
},
functionality:{
functionality:{
softwareType:"measurement",
softwareType:"measurement",
positionVsParent:"upstream",
},
},
asset:{
asset:{
supplier:"Vega",
supplier:"Vega",
@@ -803,7 +1025,6 @@ const PT2 = new Child(config={
},
},
functionality:{
functionality:{
softwareType:"measurement",
softwareType:"measurement",
positionVsParent:"upstream",
},
},
asset:{
asset:{
supplier:"Vega",
supplier:"Vega",
@@ -819,18 +1040,17 @@ console.log(`Creating machine...`);
const machineConfig = {
const machineConfig = {
general: {
general: {
name: "Hy drostal",
name: "Hi drostal",
logging: {
logging: {
enabled: true,
enabled: true,
logLevel: "debug",
logLevel: "debug",
}
}
},
},
asset: {
asset: {
supplier: "Hy drostal",
supplier: "Hi drostal",
type: "pump",
type: "pump",
category: "centrifugal",
category: "centrifugal",
model: "H05K-S03R+HGM1X-X280KO ", // Ensure this field is present.
model: "hidrostal-H05K-S03R ", // Ensure this field is present.
machineCurve: curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"],
}
}
}
}
@@ -859,18 +1079,17 @@ const machine = new Machine(machineConfig, stateConfig);
machine.logger.info(`Registering child...`);
machine.logger.info(`Registering child...`);
machine.childRegistrationUtils.registerChild(PT1, "upstream");
machine.childRegistrationUtils.registerChild(PT1, "upstream");
machine.childRegistrationUtils.registerChild(PT2, "downstream");
machine.childRegistrationUtils.registerChild(PT2, "downstream");
/*
//feed curve to the machine class
//machine.updateCurve(curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"]);
PT1.logger.info(`Enable sim...`);
PT1.logger.info(`Enable sim...`);
PT1.toggleSimulation();
PT1.toggleSimulation();
PT2.logger.info(`Enable sim...`);
PT2.logger.info(`Enable sim...`);
PT2.toggleSimulation();
PT2.toggleSimulation();
machine.getOutput();
*/
//manual test
//manual test
//machine.handleInput("parent", "execSequence", "startup");
//machine.handleInput("parent", "execSequence", "startup");
/*
machine.measurements.type("pressure").variant("measured").position('upstream').value(-200);
machine.measurements.type("pressure").variant("measured").position('upstream').value(-200);
machine.measurements.type("pressure").variant("measured").position('downstream').value(1000);
machine.measurements.type("pressure").variant("measured").position('downstream').value(1000);
@@ -880,8 +1099,8 @@ const tickLoop = setInterval(changeInput,1000);
function changeInput(){
function changeInput(){
PT1.logger.info(`tick...`);
PT1.logger.info(`tick...`);
PT1.tick();
// PT1.tick();
PT2.tick();
// PT2.tick();
}
}
async function testingSequences(){
async function testingSequences(){