2025-06-25 17:26:13 +02:00
const EventEmitter = require ( 'events' ) ;
2026-03-11 11:13:26 +01:00
const { loadCurve , gravity , logger , configUtils , configManager , state , nrmse , MeasurementContainer , predict , interpolation , childRegistrationUtils , coolprop , convert } = require ( 'generalFunctions' ) ;
const CANONICAL _UNITS = Object . freeze ( {
pressure : 'Pa' ,
atmPressure : 'Pa' ,
flow : 'm3/s' ,
power : 'W' ,
temperature : 'K' ,
} ) ;
const DEFAULT _IO _UNITS = Object . freeze ( {
pressure : 'mbar' ,
flow : 'm3/h' ,
power : 'kW' ,
temperature : 'C' ,
} ) ;
const DEFAULT _CURVE _UNITS = Object . freeze ( {
pressure : 'mbar' ,
flow : 'm3/h' ,
power : 'kW' ,
control : '%' ,
} ) ;
2025-06-25 17:26:13 +02:00
2026-02-23 13:17:18 +01:00
/ * *
* Rotating machine domain model .
* Combines machine curves , state transitions and measurement reconciliation
* to produce flow / power / efficiency behavior for pumps and similar assets .
* /
2025-06-25 17:26:13 +02:00
class Machine {
/*------------------- Construct and set vars -------------------*/
constructor ( machineConfig = { } , stateConfig = { } , errorMetricsConfig = { } ) {
//basic setup
this . emitter = new EventEmitter ( ) ; // Own EventEmitter
2025-07-01 17:02:51 +02:00
2025-07-01 15:25:07 +02:00
this . logger = new logger ( machineConfig . general . logging . enabled , machineConfig . general . logging . logLevel , machineConfig . general . name ) ;
2025-06-25 17:26:13 +02:00
this . configManager = new configManager ( ) ;
2025-07-01 15:25:07 +02:00
this . defaultConfig = this . configManager . getConfig ( 'rotatingMachine' ) ; // Load default config for rotating machine ( use software type name ? )
2025-06-25 17:26:13 +02:00
this . configUtils = new configUtils ( this . defaultConfig ) ;
2025-07-01 15:25:07 +02:00
// Load a specific curve
this . model = machineConfig . asset . model ; // Get the model from the machineConfig
2026-03-11 11:13:26 +01:00
this . rawCurve = this . model ? loadCurve ( this . model ) : null ;
this . curve = null ;
2025-07-01 15:25:07 +02:00
2025-07-24 13:15:33 +02:00
//Init config and check if it is valid
2025-07-02 16:00:52 +02:00
this . config = this . configUtils . initConfig ( machineConfig ) ;
2025-09-22 16:06:18 +02:00
//add unique name for this node.
this . config = this . configUtils . updateConfig ( this . config , { general : { name : this . config . functionality ? . softwareType + "_" + machineConfig . general . id } } ) ; // add unique name if not present
2026-03-11 11:13:26 +01:00
this . unitPolicy = this . _buildUnitPolicy ( this . config ) ;
this . config = this . configUtils . updateConfig ( this . config , {
general : { unit : this . unitPolicy . output . flow } ,
asset : {
... this . config . asset ,
unit : this . unitPolicy . output . flow ,
curveUnits : this . unitPolicy . curve ,
} ,
} ) ;
2025-07-02 16:00:52 +02:00
2026-03-11 11:13:26 +01:00
if ( ! this . model || ! this . rawCurve ) {
2025-09-23 15:11:06 +02:00
this . logger . error ( ` ${ ! this . model ? 'Model not specified' : 'Curve not found for model ' + this . model } in machineConfig. Cannot make predictions. ` ) ;
2025-07-01 15:25:07 +02:00
// Set prediction objects to null to prevent method calls
this . predictFlow = null ;
this . predictPower = null ;
this . predictCtrl = null ;
this . hasCurve = false ;
}
else {
2026-03-11 11:13:26 +01:00
try {
this . hasCurve = true ;
this . curve = this . _normalizeMachineCurve ( this . rawCurve ) ;
this . config = this . configUtils . updateConfig ( this . config , { asset : { ... this . config . asset , machineCurve : this . curve } } ) ;
//machineConfig = { ...machineConfig, asset: { ...machineConfig.asset, machineCurve: this.curve } }; // Merge curve into machineConfig
this . predictFlow = new predict ( { curve : this . config . asset . machineCurve . nq } ) ; // load nq (x : ctrl , y : flow relationship)
this . predictPower = new predict ( { curve : this . config . asset . machineCurve . np } ) ; // load np (x : ctrl , y : power relationship)
this . predictCtrl = new predict ( { curve : this . reverseCurve ( this . config . asset . machineCurve . nq ) } ) ; // load reversed nq (x: flow, y: ctrl relationship)
} catch ( error ) {
this . logger . error ( ` Curve normalization failed for model ' ${ this . model } ': ${ error . message } ` ) ;
this . predictFlow = null ;
this . predictPower = null ;
this . predictCtrl = null ;
this . hasCurve = false ;
}
2025-07-01 15:25:07 +02:00
}
2025-06-25 17:26:13 +02:00
this . state = new state ( stateConfig , this . logger ) ; // Init State manager and pass logger
this . errorMetrics = new nrmse ( errorMetricsConfig , this . logger ) ;
// Initialize measurements
2025-10-31 18:35:40 +01:00
this . measurements = new MeasurementContainer ( {
autoConvert : true ,
windowSize : 50 ,
defaultUnits : {
2026-03-11 11:13:26 +01:00
pressure : this . unitPolicy . output . pressure ,
flow : this . unitPolicy . output . flow ,
power : this . unitPolicy . output . power ,
temperature : this . unitPolicy . output . temperature ,
atmPressure : 'Pa' ,
} ,
preferredUnits : {
pressure : this . unitPolicy . output . pressure ,
flow : this . unitPolicy . output . flow ,
power : this . unitPolicy . output . power ,
temperature : this . unitPolicy . output . temperature ,
atmPressure : 'Pa' ,
} ,
canonicalUnits : this . unitPolicy . canonical ,
storeCanonical : true ,
strictUnitValidation : true ,
throwOnInvalidUnit : true ,
requireUnitForTypes : [ 'pressure' , 'flow' , 'power' , 'temperature' , 'atmPressure' ] ,
} , this . logger ) ;
2025-10-31 18:35:40 +01:00
2025-06-25 17:26:13 +02:00
this . interpolation = new interpolation ( ) ;
2025-08-07 13:52:06 +02:00
2025-06-25 17:26:13 +02:00
this . flowDrift = null ;
2026-03-11 11:13:26 +01:00
this . powerDrift = null ;
this . pressureDrift = { level : 0 , flags : [ "nominal" ] , source : null } ;
this . driftProfiles = {
flow : {
windowSize : 30 ,
minSamplesForLongTerm : 10 ,
ewmaAlpha : 0.15 ,
alignmentToleranceMs : 2500 ,
strictValidation : true ,
} ,
power : {
windowSize : 30 ,
minSamplesForLongTerm : 10 ,
ewmaAlpha : 0.15 ,
alignmentToleranceMs : 2500 ,
strictValidation : true ,
} ,
} ;
this . errorMetrics . registerMetric ( "flow" , this . driftProfiles . flow ) ;
this . errorMetrics . registerMetric ( "power" , this . driftProfiles . power ) ;
this . predictionHealth = {
quality : "invalid" ,
confidence : 0 ,
pressureSource : null ,
flags : [ "not_initialized" ] ,
} ;
2025-06-25 17:26:13 +02:00
this . currentMode = this . config . mode . current ;
this . currentEfficiencyCurve = { } ;
this . cog = 0 ;
this . NCog = 0 ;
this . cogIndex = 0 ;
this . minEfficiency = 0 ;
this . absDistFromPeak = 0 ;
this . relDistFromPeak = 0 ;
2025-08-07 13:52:06 +02:00
// When position state changes, update position
2025-06-25 17:26:13 +02:00
this . state . emitter . on ( "positionChange" , ( data ) => {
this . logger . debug ( ` Position change detected: ${ data } ` ) ;
this . updatePosition ( ) ;
} ) ;
2025-10-31 18:35:40 +01:00
//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 ( ) ;
} ) ;
2025-11-12 17:40:38 +01:00
//perform init for certain values
this . _init ( ) ;
2025-08-07 13:52:06 +02:00
this . child = { } ; // object to hold child information so we know on what to subscribe
2025-06-25 17:26:13 +02:00
this . childRegistrationUtils = new childRegistrationUtils ( this ) ; // Child registration utility
2026-02-19 17:36:44 +01:00
this . virtualPressureChildIds = {
upstream : "dashboard-sim-upstream" ,
downstream : "dashboard-sim-downstream" ,
} ;
this . virtualPressureChildren = { } ;
this . realPressureChildIds = {
upstream : new Set ( ) ,
downstream : new Set ( ) ,
} ;
2026-03-11 11:13:26 +01:00
this . childMeasurementListeners = new Map ( ) ;
2026-02-19 17:36:44 +01:00
this . _initVirtualPressureChildren ( ) ;
2025-06-25 17:26:13 +02:00
2025-11-12 17:40:38 +01:00
}
2026-02-19 17:36:44 +01:00
_initVirtualPressureChildren ( ) {
const createVirtualChild = ( position ) => {
const id = this . virtualPressureChildIds [ position ] ;
const name = ` dashboard-sim- ${ position } ` ;
const measurements = new MeasurementContainer ( {
autoConvert : true ,
defaultUnits : {
2026-03-11 11:13:26 +01:00
pressure : this . unitPolicy . output . pressure ,
flow : this . unitPolicy . output . flow ,
power : this . unitPolicy . output . power ,
temperature : this . unitPolicy . output . temperature ,
2026-02-19 17:36:44 +01:00
} ,
2026-03-11 11:13:26 +01:00
preferredUnits : {
pressure : this . unitPolicy . output . pressure ,
flow : this . unitPolicy . output . flow ,
power : this . unitPolicy . output . power ,
temperature : this . unitPolicy . output . temperature ,
} ,
canonicalUnits : this . unitPolicy . canonical ,
storeCanonical : true ,
strictUnitValidation : true ,
throwOnInvalidUnit : true ,
requireUnitForTypes : [ 'pressure' ] ,
} , this . logger ) ;
2026-02-19 17:36:44 +01:00
measurements . setChildId ( id ) ;
measurements . setChildName ( name ) ;
measurements . setParentRef ( this ) ;
return {
config : {
general : { id , name } ,
functionality : {
softwareType : "measurement" ,
positionVsParent : position ,
} ,
asset : {
type : "pressure" ,
2026-03-11 11:13:26 +01:00
unit : this . unitPolicy . output . pressure ,
2026-02-19 17:36:44 +01:00
} ,
} ,
measurements ,
} ;
} ;
const upstreamChild = createVirtualChild ( "upstream" ) ;
const downstreamChild = createVirtualChild ( "downstream" ) ;
this . virtualPressureChildren . upstream = upstreamChild ;
this . virtualPressureChildren . downstream = downstreamChild ;
this . registerChild ( upstreamChild , "measurement" ) ;
this . registerChild ( downstreamChild , "measurement" ) ;
}
2025-11-12 17:40:38 +01:00
_init ( ) {
//assume standard temperature is 20degrees
2026-03-11 11:13:26 +01:00
this . measurements . type ( 'temperature' ) . variant ( 'measured' ) . position ( 'atEquipment' ) . value ( 15 , Date . now ( ) , this . unitPolicy . output . temperature ) ;
2025-11-12 17:40:38 +01:00
//assume standard atm pressure is at sea level
2026-03-11 11:13:26 +01:00
this . measurements . type ( 'atmPressure' ) . variant ( 'measured' ) . position ( 'atEquipment' ) . value ( 101325 , Date . now ( ) , 'Pa' ) ;
2026-02-12 10:48:44 +01:00
//populate min and max when curve data is available
2026-03-11 11:13:26 +01:00
const flowunit = this . unitPolicy . canonical . flow ;
2026-02-12 10:48:44 +01:00
if ( this . predictFlow ) {
this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'max' ) . value ( this . predictFlow . currentFxyYMax , Date . now ( ) , flowunit ) ;
2026-03-11 11:13:26 +01:00
this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'min' ) . value ( this . predictFlow . currentFxyYMin , Date . now ( ) , flowunit ) ;
2026-02-12 10:48:44 +01:00
} else {
this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'max' ) . value ( 0 , Date . now ( ) , flowunit ) ;
this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'min' ) . value ( 0 , Date . now ( ) , flowunit ) ;
}
2025-06-25 17:26:13 +02:00
}
2025-10-31 18:35:40 +01:00
_updateState ( ) {
const isOperational = this . _isOperationalState ( ) ;
if ( ! isOperational ) {
//overrule the last prediction this should be 0 now
2026-03-11 11:13:26 +01:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( 0 , Date . now ( ) , this . unitPolicy . canonical . flow ) ;
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( 0 , Date . now ( ) , this . unitPolicy . canonical . flow ) ;
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( 0 , Date . now ( ) , this . unitPolicy . canonical . power ) ;
2025-10-31 18:35:40 +01:00
}
2026-03-11 11:13:26 +01:00
this . _updatePredictionHealth ( ) ;
2025-10-31 18:35:40 +01:00
}
2025-08-07 13:52:06 +02:00
/*------------------- Register child events -------------------*/
2025-09-04 17:07:29 +02:00
registerChild ( child , softwareType ) {
2026-02-19 17:36:44 +01:00
const resolvedSoftwareType = softwareType || child ? . config ? . functionality ? . softwareType || "measurement" ;
this . logger . debug ( 'Setting up child event for softwaretype ' + resolvedSoftwareType ) ;
2025-09-04 17:07:29 +02:00
2026-02-19 17:36:44 +01:00
if ( resolvedSoftwareType === "measurement" ) {
const position = String ( child . config . functionality . positionVsParent || "atEquipment" ) . toLowerCase ( ) ;
2025-09-04 17:07:29 +02:00
const measurementType = child . config . asset . type ;
2026-02-19 17:36:44 +01:00
const childId = child . config ? . general ? . id || ` ${ measurementType } - ${ position } -unknown ` ;
const isVirtualPressureChild = Object . values ( this . virtualPressureChildIds ) . includes ( childId ) ;
if ( measurementType === "pressure" && ! isVirtualPressureChild ) {
this . realPressureChildIds [ position ] ? . add ( childId ) ;
}
2025-10-03 15:41:53 +02:00
//rebuild to measurementype.variant no position and then switch based on values not strings or names.
2025-09-04 17:07:29 +02:00
const eventName = ` ${ measurementType } .measured. ${ position } ` ;
2026-03-11 11:13:26 +01:00
const listenerKey = ` ${ childId } : ${ eventName } ` ;
const existingListener = this . childMeasurementListeners . get ( listenerKey ) ;
if ( existingListener ) {
if ( typeof existingListener . emitter . off === "function" ) {
existingListener . emitter . off ( existingListener . eventName , existingListener . handler ) ;
} else if ( typeof existingListener . emitter . removeListener === "function" ) {
existingListener . emitter . removeListener ( existingListener . eventName , existingListener . handler ) ;
}
}
2025-09-04 17:07:29 +02:00
2025-10-03 15:41:53 +02:00
this . logger . debug ( ` Setting up listener for ${ eventName } from child ${ child . config . general . name } ` ) ;
2025-09-04 17:07:29 +02:00
// Register event listener for measurement updates
2026-03-11 11:13:26 +01:00
const listener = ( eventData ) => {
2025-09-04 17:07:29 +02:00
this . logger . debug ( ` 🔄 ${ position } ${ measurementType } from ${ eventData . childName } : ${ eventData . value } ${ eventData . unit } ` ) ;
2025-08-07 13:52:06 +02:00
2025-10-03 15:41:53 +02:00
2025-11-13 19:39:05 +01:00
this . logger . debug ( ` Emitting... ${ eventName } with data: ` ) ;
2026-03-11 11:13:26 +01:00
// Route through centralized handlers so unit validation/conversion is applied once.
2025-09-04 17:07:29 +02:00
this . _callMeasurementHandler ( measurementType , eventData . value , position , eventData ) ;
2026-03-11 11:13:26 +01:00
} ;
child . measurements . emitter . on ( eventName , listener ) ;
this . childMeasurementListeners . set ( listenerKey , {
emitter : child . measurements . emitter ,
eventName ,
handler : listener ,
2025-09-04 17:07:29 +02:00
} ) ;
2025-08-07 13:52:06 +02:00
}
2025-09-04 17:07:29 +02:00
}
2025-08-08 14:29:15 +02:00
// Centralized handler dispatcher
_callMeasurementHandler ( measurementType , value , position , context ) {
switch ( measurementType ) {
case 'pressure' :
this . updateMeasuredPressure ( value , position , context ) ;
break ;
case 'flow' :
this . updateMeasuredFlow ( value , position , context ) ;
break ;
2026-03-11 11:13:26 +01:00
case 'power' :
this . updateMeasuredPower ( value , position , context ) ;
break ;
2025-08-08 14:29:15 +02:00
case 'temperature' :
this . updateMeasuredTemperature ( value , position , context ) ;
break ;
default :
this . logger . warn ( ` No handler for measurement type: ${ measurementType } ` ) ;
// Generic handler - just update position
this . updatePosition ( ) ;
break ;
}
}
//---------------- END child stuff -------------//
2025-08-07 13:52:06 +02:00
2026-03-11 11:13:26 +01:00
_buildUnitPolicy ( config ) {
const flowOutputUnit = this . _resolveUnitOrFallback (
config ? . general ? . unit ,
'volumeFlowRate' ,
DEFAULT _IO _UNITS . flow ,
'general.flow'
) ;
const pressureOutputUnit = this . _resolveUnitOrFallback (
config ? . asset ? . pressureUnit ,
'pressure' ,
DEFAULT _IO _UNITS . pressure ,
'asset.pressure'
) ;
const powerOutputUnit = this . _resolveUnitOrFallback (
config ? . asset ? . powerUnit ,
'power' ,
DEFAULT _IO _UNITS . power ,
'asset.power'
) ;
const temperatureOutputUnit = this . _resolveUnitOrFallback (
config ? . asset ? . temperatureUnit ,
'temperature' ,
DEFAULT _IO _UNITS . temperature ,
'asset.temperature'
) ;
const curveUnits = this . _resolveCurveUnits ( config ? . asset ? . curveUnits || { } , flowOutputUnit ) ;
2025-06-25 17:26:13 +02:00
2026-03-11 11:13:26 +01:00
return {
canonical : { ... CANONICAL _UNITS } ,
output : {
pressure : pressureOutputUnit ,
flow : flowOutputUnit ,
power : powerOutputUnit ,
temperature : temperatureOutputUnit ,
atmPressure : 'Pa' ,
} ,
curve : curveUnits ,
} ;
}
_resolveCurveUnits ( curveUnits = { } , fallbackFlowUnit = DEFAULT _CURVE _UNITS . flow ) {
const pressure = this . _resolveUnitOrFallback (
curveUnits . pressure ,
'pressure' ,
DEFAULT _CURVE _UNITS . pressure ,
'asset.curveUnits.pressure'
) ;
const flow = this . _resolveUnitOrFallback (
curveUnits . flow ,
'volumeFlowRate' ,
fallbackFlowUnit || DEFAULT _CURVE _UNITS . flow ,
'asset.curveUnits.flow'
) ;
const power = this . _resolveUnitOrFallback (
curveUnits . power ,
'power' ,
DEFAULT _CURVE _UNITS . power ,
'asset.curveUnits.power'
) ;
const control = typeof curveUnits . control === 'string' && curveUnits . control . trim ( )
? curveUnits . control . trim ( )
: DEFAULT _CURVE _UNITS . control ;
return { pressure , flow , power , control } ;
}
_resolveUnitOrFallback ( candidate , expectedMeasure , fallbackUnit , label ) {
const fallback = String ( fallbackUnit || '' ) . trim ( ) ;
const raw = typeof candidate === 'string' ? candidate . trim ( ) : '' ;
if ( ! raw ) return fallback ;
try {
const desc = convert ( ) . describe ( raw ) ;
if ( expectedMeasure && desc . measure !== expectedMeasure ) {
throw new Error ( ` expected ${ expectedMeasure } but got ${ desc . measure } ` ) ;
}
return raw ;
} catch ( error ) {
this . logger . warn ( ` Invalid ${ label } unit ' ${ raw } ' ( ${ error . message } ). Falling back to ' ${ fallback } '. ` ) ;
return fallback ;
}
}
_convertUnitValue ( value , fromUnit , toUnit , contextLabel = 'unit conversion' ) {
const numeric = Number ( value ) ;
if ( ! Number . isFinite ( numeric ) ) {
throw new Error ( ` ${ contextLabel } : value ' ${ value } ' is not finite ` ) ;
}
if ( ! fromUnit || ! toUnit || fromUnit === toUnit ) {
return numeric ;
}
return convert ( numeric ) . from ( fromUnit ) . to ( toUnit ) ;
}
_normalizeCurveSection ( section , fromYUnit , toYUnit , fromPressureUnit , toPressureUnit , sectionName ) {
const normalized = { } ;
for ( const [ pressureKey , pair ] of Object . entries ( section || { } ) ) {
const canonicalPressure = this . _convertUnitValue (
Number ( pressureKey ) ,
fromPressureUnit ,
toPressureUnit ,
` ${ sectionName } pressure axis `
2025-06-25 17:26:13 +02:00
) ;
2026-03-11 11:13:26 +01:00
const xArray = Array . isArray ( pair ? . x ) ? pair . x . map ( Number ) : [ ] ;
const yArray = Array . isArray ( pair ? . y ) ? pair . y . map ( ( v ) => this . _convertUnitValue ( v , fromYUnit , toYUnit , ` ${ sectionName } output ` ) ) : [ ] ;
if ( ! xArray . length || ! yArray . length || xArray . length !== yArray . length ) {
throw new Error ( ` Invalid ${ sectionName } section at pressure ' ${ pressureKey } '. ` ) ;
}
normalized [ String ( canonicalPressure ) ] = {
x : xArray ,
y : yArray ,
} ;
}
return normalized ;
}
_normalizeMachineCurve ( rawCurve , curveUnits = this . unitPolicy . curve ) {
if ( ! rawCurve || typeof rawCurve !== 'object' || ! rawCurve . nq || ! rawCurve . np ) {
throw new Error ( 'Machine curve is missing required nq/np sections.' ) ;
}
return {
nq : this . _normalizeCurveSection (
rawCurve . nq ,
curveUnits . flow ,
this . unitPolicy . canonical . flow ,
curveUnits . pressure ,
this . unitPolicy . canonical . pressure ,
'nq'
) ,
np : this . _normalizeCurveSection (
rawCurve . np ,
curveUnits . power ,
this . unitPolicy . canonical . power ,
curveUnits . pressure ,
this . unitPolicy . canonical . pressure ,
'np'
) ,
} ;
}
isUnitValidForType ( type , unit ) {
return this . measurements ? . isUnitCompatible ? . ( type , unit ) === true ;
}
_resolveMeasurementUnit ( type , providedUnit ) {
const unit = typeof providedUnit === 'string' ? providedUnit . trim ( ) : '' ;
if ( ! unit ) {
throw new Error ( ` Missing unit for ${ type } measurement. ` ) ;
}
if ( ! this . isUnitValidForType ( type , unit ) ) {
throw new Error ( ` Unsupported unit ' ${ unit } ' for ${ type } measurement. ` ) ;
}
return unit ;
}
_measurementPositionForMetric ( metricId ) {
if ( metricId === "power" ) return "atEquipment" ;
return "downstream" ;
}
_resolveProcessRangeForMetric ( metricId , predictedValue , measuredValue ) {
let processMin = NaN ;
let processMax = NaN ;
if ( metricId === "flow" ) {
processMin = Number ( this . predictFlow ? . currentFxyYMin ) ;
processMax = Number ( this . predictFlow ? . currentFxyYMax ) ;
} else if ( metricId === "power" ) {
processMin = Number ( this . predictPower ? . currentFxyYMin ) ;
processMax = Number ( this . predictPower ? . currentFxyYMax ) ;
}
if ( ! Number . isFinite ( processMin ) || ! Number . isFinite ( processMax ) || processMax <= processMin ) {
const p = Number ( predictedValue ) ;
const m = Number ( measuredValue ) ;
const localMin = Math . min ( p , m ) ;
const localMax = Math . max ( p , m ) ;
processMin = Number . isFinite ( localMin ) ? localMin : 0 ;
processMax = Number . isFinite ( localMax ) && localMax > processMin ? localMax : processMin + 1 ;
}
return { processMin , processMax } ;
}
_updateMetricDrift ( metricId , measuredValue , context = { } ) {
const position = this . _measurementPositionForMetric ( metricId ) ;
const predictedValue = Number (
this . measurements
. type ( metricId )
. variant ( "predicted" )
. position ( position )
. getCurrentValue ( )
) ;
const measured = Number ( measuredValue ) ;
if ( ! Number . isFinite ( predictedValue ) || ! Number . isFinite ( measured ) ) return null ;
const { processMin , processMax } = this . _resolveProcessRangeForMetric ( metricId , predictedValue , measured ) ;
const timestamp = Number ( context . timestamp || Date . now ( ) ) ;
const profile = this . driftProfiles [ metricId ] || { } ;
try {
const drift = this . errorMetrics . assessPoint ( metricId , predictedValue , measured , {
... profile ,
processMin ,
processMax ,
predictedTimestamp : timestamp ,
measuredTimestamp : timestamp ,
} ) ;
if ( drift && drift . valid ) {
if ( metricId === "flow" ) this . flowDrift = drift ;
if ( metricId === "power" ) this . powerDrift = drift ;
}
return drift ;
} catch ( error ) {
this . logger . warn ( ` Drift update failed for metric ' ${ metricId } ': ${ error . message } ` ) ;
return null ;
}
}
_updatePressureDriftStatus ( ) {
const status = this . getPressureInitializationStatus ( ) ;
const flags = [ ] ;
let level = 0 ;
if ( ! status . initialized ) {
level = 2 ;
flags . push ( "no_pressure_input" ) ;
} else if ( ! status . hasDifferential ) {
level = 1 ;
flags . push ( "single_side_pressure" ) ;
}
if ( status . hasDifferential ) {
const upstream = this . _getPreferredPressureValue ( "upstream" ) ;
const downstream = this . _getPreferredPressureValue ( "downstream" ) ;
const diff = Number ( downstream ) - Number ( upstream ) ;
if ( Number . isFinite ( diff ) && diff < 0 ) {
level = Math . max ( level , 3 ) ;
flags . push ( "negative_pressure_differential" ) ;
}
}
this . pressureDrift = {
level ,
source : status . source ,
flags : flags . length ? flags : [ "nominal" ] ,
} ;
return this . pressureDrift ;
}
assessDrift ( measurement , processMin , processMax ) {
const metricId = String ( measurement || "" ) . toLowerCase ( ) ;
const position = this . _measurementPositionForMetric ( metricId ) ;
const predictedMeasurement = this . measurements . type ( metricId ) . variant ( "predicted" ) . position ( position ) . getAllValues ( ) ;
const measuredMeasurement = this . measurements . type ( metricId ) . variant ( "measured" ) . position ( position ) . getAllValues ( ) ;
if ( ! predictedMeasurement ? . values || ! measuredMeasurement ? . values ) return null ;
return this . errorMetrics . assessDrift (
predictedMeasurement . values ,
measuredMeasurement . values ,
processMin ,
processMax ,
{
metricId ,
predictedTimestamps : predictedMeasurement . timestamps ,
measuredTimestamps : measuredMeasurement . timestamps ,
... ( this . driftProfiles [ metricId ] || { } ) ,
}
) ;
}
_applyDriftPenalty ( drift , confidence , flags , prefix ) {
if ( ! drift || ! drift . valid || ! Number . isFinite ( drift . nrmse ) ) return confidence ;
if ( drift . immediateLevel >= 3 ) {
confidence -= 0.3 ;
flags . push ( ` ${ prefix } _high_immediate_drift ` ) ;
} else if ( drift . immediateLevel === 2 ) {
confidence -= 0.2 ;
flags . push ( ` ${ prefix } _medium_immediate_drift ` ) ;
} else if ( drift . immediateLevel === 1 ) {
confidence -= 0.1 ;
flags . push ( ` ${ prefix } _low_immediate_drift ` ) ;
}
if ( drift . longTermLevel >= 2 ) {
confidence -= 0.1 ;
flags . push ( ` ${ prefix } _long_term_drift ` ) ;
}
return confidence ;
}
_updatePredictionHealth ( ) {
const status = this . getPressureInitializationStatus ( ) ;
const pressureDrift = this . _updatePressureDriftStatus ( ) ;
const flags = [ ... pressureDrift . flags ] ;
let confidence = 0 ;
const pressureSource = status . source ;
if ( pressureSource === "differential" ) {
confidence = 0.9 ;
} else if ( pressureSource === "upstream" || pressureSource === "downstream" ) {
confidence = 0.55 ;
} else {
confidence = 0.2 ;
}
if ( ! this . _isOperationalState ( ) ) {
confidence = 0 ;
flags . push ( "not_operational" ) ;
}
if ( pressureDrift . level >= 3 ) confidence -= 0.35 ;
else if ( pressureDrift . level === 2 ) confidence -= 0.2 ;
else if ( pressureDrift . level === 1 ) confidence -= 0.1 ;
const currentPosition = Number ( this . state ? . getCurrentPosition ? . ( ) ) ;
const { min , max } = this . _resolveSetpointBounds ( ) ;
if ( Number . isFinite ( currentPosition ) && Number . isFinite ( min ) && Number . isFinite ( max ) && max > min ) {
const span = max - min ;
const edgeDistance = Math . min ( Math . abs ( currentPosition - min ) , Math . abs ( max - currentPosition ) ) ;
if ( edgeDistance < span * 0.05 ) {
confidence -= 0.1 ;
flags . push ( "near_curve_edge" ) ;
}
2025-06-25 17:26:13 +02:00
}
2026-03-11 11:13:26 +01:00
confidence = this . _applyDriftPenalty ( this . flowDrift , confidence , flags , "flow" ) ;
confidence = this . _applyDriftPenalty ( this . powerDrift , confidence , flags , "power" ) ;
confidence = Math . max ( 0 , Math . min ( 1 , confidence ) ) ;
let quality = "invalid" ;
if ( confidence >= 0.8 ) quality = "high" ;
else if ( confidence >= 0.55 ) quality = "medium" ;
else if ( confidence >= 0.3 ) quality = "low" ;
this . predictionHealth = {
quality ,
confidence ,
pressureSource ,
flags : flags . length ? Array . from ( new Set ( flags ) ) : [ "nominal" ] ,
} ;
return this . predictionHealth ;
}
2025-06-25 17:26:13 +02:00
reverseCurve ( curve ) {
const reversedCurve = { } ;
for ( const [ pressure , values ] of Object . entries ( curve ) ) {
reversedCurve [ pressure ] = {
x : [ ... values . y ] , // Previous y becomes new x
y : [ ... values . x ] // Previous x becomes new y
} ;
}
return reversedCurve ;
}
// -------- Config -------- //
updateConfig ( newConfig ) {
this . config = this . configUtils . updateConfig ( this . config , newConfig ) ;
}
// -------- Mode and Input Management -------- //
isValidSourceForMode ( source , mode ) {
const allowedSourcesSet = this . config . mode . allowedSources [ mode ] || [ ] ;
2025-11-05 17:15:47 +01:00
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 ;
2025-06-25 17:26:13 +02:00
}
isValidActionForMode ( action , mode ) {
const allowedActionsSet = this . config . mode . allowedActions [ mode ] || [ ] ;
2025-11-05 17:15:47 +01:00
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 ;
2025-06-25 17:26:13 +02:00
}
async handleInput ( source , action , parameter ) {
2025-10-02 17:09:24 +02:00
2025-11-05 15:47:39 +01:00
//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 ( ) ;
2025-11-05 17:15:47 +01:00
// check for validity of the request
if ( ! this . isValidActionForMode ( action , this . currentMode ) ) { return ; }
if ( ! this . isValidSourceForMode ( source , this . currentMode ) ) { return ; }
2025-06-25 17:26:13 +02:00
this . logger . info ( ` Handling input from source ' ${ source } ' with action ' ${ action } ' in mode ' ${ this . currentMode } '. ` ) ;
2025-10-02 17:09:24 +02:00
2025-06-25 17:26:13 +02:00
try {
switch ( action ) {
2025-11-05 15:47:39 +01:00
case "execsequence" :
2025-10-02 17:09:24 +02:00
return await this . executeSequence ( parameter ) ;
2025-11-05 15:47:39 +01:00
case "execmovement" :
2025-10-02 17:09:24 +02:00
return await this . setpoint ( parameter ) ;
2025-11-05 17:15:47 +01:00
case "entermaintenance" :
return await this . executeSequence ( parameter ) ;
case "exitmaintenance" :
return await this . executeSequence ( parameter ) ;
2025-11-05 15:47:39 +01:00
case "flowmovement" :
2026-03-11 11:13:26 +01:00
// External flow setpoint is interpreted in configured output flow unit.
const canonicalFlowSetpoint = this . _convertUnitValue (
parameter ,
this . unitPolicy . output . flow ,
this . unitPolicy . canonical . flow ,
'flowmovement setpoint'
) ;
2025-06-25 17:26:13 +02:00
// Calculate the control value for a desired flow
2026-03-11 11:13:26 +01:00
const pos = this . calcCtrl ( canonicalFlowSetpoint ) ;
2025-06-25 17:26:13 +02:00
// Move to the desired setpoint
2025-10-02 17:09:24 +02:00
return await this . setpoint ( pos ) ;
2025-11-05 15:47:39 +01:00
case "emergencystop" :
2025-06-25 17:26:13 +02:00
this . logger . warn ( ` Emergency stop activated by ' ${ source } '. ` ) ;
2025-10-02 17:09:24 +02:00
return await this . executeSequence ( "emergencyStop" ) ;
2025-11-05 15:47:39 +01:00
case "statuscheck" :
2025-06-25 17:26:13 +02:00
this . logger . info ( ` Status Check: Mode = ' ${ this . currentMode } ', Source = ' ${ source } '. ` ) ;
break ;
2025-10-02 17:09:24 +02:00
2025-06-25 17:26:13 +02:00
default :
this . logger . warn ( ` Action ' ${ action } ' is not implemented. ` ) ;
break ;
}
this . logger . debug ( ` Action ' ${ action } ' successfully executed ` ) ;
return { status : true , feedback : ` Action ' ${ action } ' successfully executed. ` } ;
} catch ( error ) {
this . logger . error ( ` Error handling input: ${ error } ` ) ;
}
2025-10-02 17:09:24 +02:00
}
abortMovement ( reason = "group override" ) {
if ( this . state ? . abortCurrentMovement ) {
this . state . abortCurrentMovement ( reason ) ;
}
2025-06-25 17:26:13 +02:00
}
setMode ( newMode ) {
2025-07-24 13:15:33 +02:00
const availableModes = this . defaultConfig . mode . current . rules . values . map ( v => v . value ) ;
2025-06-25 17:26:13 +02:00
if ( ! availableModes . includes ( newMode ) ) {
this . logger . warn ( ` Invalid mode ' ${ newMode } '. Allowed modes are: ${ availableModes . join ( ', ' ) } ` ) ;
return ;
}
this . currentMode = newMode ;
this . logger . info ( ` Mode successfully changed to ' ${ newMode } '. ` ) ;
}
// -------- Sequence Handlers -------- //
async executeSequence ( sequenceName ) {
const sequence = this . config . sequences [ sequenceName ] ;
if ( ! sequence || sequence . size === 0 ) {
this . logger . warn ( ` Sequence ' ${ sequenceName } ' not defined. ` ) ;
return ;
}
if ( this . state . getCurrentState ( ) == "operational" && sequenceName == "shutdown" ) {
this . logger . info ( ` Machine will ramp down to position 0 before performing ${ sequenceName } sequence ` ) ;
await this . setpoint ( 0 ) ;
}
this . logger . info ( ` --------- Executing sequence: ${ sequenceName } ------------- ` ) ;
for ( const state of sequence ) {
try {
await this . state . transitionToState ( state ) ;
// Update measurements after state change
} catch ( error ) {
this . logger . error ( ` Error during sequence ' ${ sequenceName } ': ${ error } ` ) ;
break ; // Exit sequence execution on error
}
}
2025-10-02 17:09:24 +02:00
//recalc flow and power
this . updatePosition ( ) ;
2025-06-25 17:26:13 +02:00
}
async setpoint ( setpoint ) {
try {
2026-03-11 11:13:26 +01:00
// Validate and normalize setpoint
if ( ! Number . isFinite ( setpoint ) ) {
this . logger . error ( "Invalid setpoint: Setpoint must be a finite number." ) ;
return ;
}
const { min , max } = this . _resolveSetpointBounds ( ) ;
const constrainedSetpoint = Math . min ( Math . max ( setpoint , min ) , max ) ;
if ( constrainedSetpoint !== setpoint ) {
this . logger . warn ( ` Requested setpoint ${ setpoint } constrained to ${ constrainedSetpoint } (min= ${ min } , max= ${ max } ) ` ) ;
2025-06-25 17:26:13 +02:00
}
2026-03-11 11:13:26 +01:00
this . logger . info ( ` Setting setpoint to ${ constrainedSetpoint } . Current position: ${ this . state . getCurrentPosition ( ) } ` ) ;
2025-07-02 16:00:52 +02:00
2025-06-25 17:26:13 +02:00
// Move to the desired setpoint
2026-03-11 11:13:26 +01:00
await this . state . moveTo ( constrainedSetpoint ) ;
2025-06-25 17:26:13 +02:00
} catch ( error ) {
2026-03-11 11:13:26 +01:00
this . logger . error ( ` Error setting setpoint: ${ error } ` ) ;
2025-06-25 17:26:13 +02:00
}
}
2026-03-11 11:13:26 +01:00
_resolveSetpointBounds ( ) {
const stateMin = Number ( this . state ? . movementManager ? . minPosition ) ;
const stateMax = Number ( this . state ? . movementManager ? . maxPosition ) ;
const curveMin = Number ( this . predictFlow ? . currentFxyXMin ) ;
const curveMax = Number ( this . predictFlow ? . currentFxyXMax ) ;
const minCandidates = [ stateMin , curveMin ] . filter ( Number . isFinite ) ;
const maxCandidates = [ stateMax , curveMax ] . filter ( Number . isFinite ) ;
const fallbackMin = Number . isFinite ( stateMin ) ? stateMin : 0 ;
const fallbackMax = Number . isFinite ( stateMax ) ? stateMax : 100 ;
let min = minCandidates . length ? Math . max ( ... minCandidates ) : fallbackMin ;
let max = maxCandidates . length ? Math . min ( ... maxCandidates ) : fallbackMax ;
if ( min > max ) {
this . logger . warn ( ` Invalid setpoint bounds detected (min= ${ min } , max= ${ max } ). Falling back to movement bounds. ` ) ;
min = fallbackMin ;
max = fallbackMax ;
}
return { min , max } ;
}
2025-06-25 17:26:13 +02:00
// Calculate flow based on current pressure and position
calcFlow ( x ) {
2025-07-02 16:00:52 +02:00
if ( this . hasCurve ) {
2025-08-07 13:52:06 +02:00
if ( ! this . _isOperationalState ( ) ) {
2026-03-11 11:13:26 +01:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( 0 , Date . now ( ) , this . unitPolicy . canonical . flow ) ;
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( 0 , Date . now ( ) , this . unitPolicy . canonical . flow ) ;
2025-07-01 15:25:07 +02:00
this . logger . debug ( ` Machine is not operational. Setting predicted flow to 0. ` ) ;
return 0 ;
}
2025-06-25 17:26:13 +02:00
const cFlow = this . predictFlow . y ( x ) ;
2026-03-11 11:13:26 +01:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( cFlow , Date . now ( ) , this . unitPolicy . canonical . flow ) ;
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( cFlow , Date . now ( ) , this . unitPolicy . canonical . flow ) ;
2025-06-25 17:26:13 +02:00
//this.logger.debug(`Calculated flow: ${cFlow} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cFlow ;
2025-07-01 15:25:07 +02:00
}
// If no curve data is available, log a warning and return 0
this . logger . warn ( ` No curve data available for flow calculation. Returning 0. ` ) ;
2026-03-11 11:13:26 +01:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( 0 , Date . now ( ) , this . unitPolicy . canonical . flow ) ;
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( 0 , Date . now ( ) , this . unitPolicy . canonical . flow ) ;
2025-07-01 15:25:07 +02:00
return 0 ;
2025-06-25 17:26:13 +02:00
}
// Calculate power based on current pressure and position
calcPower ( x ) {
2025-07-02 16:00:52 +02:00
if ( this . hasCurve ) {
2025-08-07 13:52:06 +02:00
if ( ! this . _isOperationalState ( ) ) {
2026-03-11 11:13:26 +01:00
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'atEquipment' ) . value ( 0 , Date . now ( ) , this . unitPolicy . canonical . power ) ;
2025-07-01 15:25:07 +02:00
this . logger . debug ( ` Machine is not operational. Setting predicted power to 0. ` ) ;
return 0 ;
}
2025-06-25 17:26:13 +02:00
//this.predictPower.currentX = x; Decrepated
const cPower = this . predictPower . y ( x ) ;
2026-03-11 11:13:26 +01:00
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'atEquipment' ) . value ( cPower , Date . now ( ) , this . unitPolicy . canonical . power ) ;
2025-06-25 17:26:13 +02:00
//this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cPower ;
2025-07-01 15:25:07 +02:00
}
// If no curve data is available, log a warning and return 0
this . logger . warn ( ` No curve data available for power calculation. Returning 0. ` ) ;
2026-03-11 11:13:26 +01:00
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'atEquipment' ) . value ( 0 , Date . now ( ) , this . unitPolicy . canonical . power ) ;
2025-07-01 15:25:07 +02:00
return 0 ;
2025-06-25 17:26:13 +02:00
}
// calculate the power consumption using only flow and pressure
inputFlowCalcPower ( flow ) {
2025-07-02 16:00:52 +02:00
if ( this . hasCurve ) {
2025-07-01 15:25:07 +02:00
this . predictCtrl . currentX = flow ;
const cCtrl = this . predictCtrl . y ( flow ) ;
this . predictPower . currentX = cCtrl ;
const cPower = this . predictPower . y ( cCtrl ) ;
return cPower ;
}
// If no curve data is available, log a warning and return 0
this . logger . warn ( ` No curve data available for power calculation. Returning 0. ` ) ;
2026-03-11 11:13:26 +01:00
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'atEquipment' ) . value ( 0 , Date . now ( ) , this . unitPolicy . canonical . power ) ;
2025-07-01 15:25:07 +02:00
return 0 ;
2025-06-25 17:26:13 +02:00
}
// Function to predict control value for a desired flow
calcCtrl ( x ) {
2025-07-02 16:00:52 +02:00
if ( this . hasCurve ) {
2025-07-01 15:25:07 +02:00
this . predictCtrl . currentX = x ;
const cCtrl = this . predictCtrl . y ( x ) ;
2025-10-02 17:09:24 +02:00
this . measurements . type ( "ctrl" ) . variant ( "predicted" ) . position ( 'atEquipment' ) . value ( cCtrl ) ;
2025-07-01 15:25:07 +02:00
//this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cCtrl ;
}
// If no curve data is available, log a warning and return 0
this . logger . warn ( ` No curve data available for control calculation. Returning 0. ` ) ;
2026-03-11 11:13:26 +01:00
this . measurements . type ( "ctrl" ) . variant ( "predicted" ) . position ( 'atEquipment' ) . value ( 0 , Date . now ( ) ) ;
2025-07-01 15:25:07 +02:00
return 0 ;
2025-06-25 17:26:13 +02:00
}
2025-08-07 13:52:06 +02:00
// returns the best available pressure measurement to use in the prediction calculation
// this will be either the differential pressure, downstream or upstream pressure
2025-06-25 17:26:13 +02:00
getMeasuredPressure ( ) {
2025-11-13 19:39:05 +01:00
if ( this . hasCurve === false ) {
this . logger . error ( ` No valid curve available to calculate prediction using last known pressure ` ) ;
return 0 ;
}
2026-02-19 17:36:44 +01:00
const upstreamPressure = this . _getPreferredPressureValue ( "upstream" ) ;
const downstreamPressure = this . _getPreferredPressureValue ( "downstream" ) ;
2025-10-02 17:09:24 +02:00
2025-06-25 17:26:13 +02:00
// Both upstream & downstream => differential
2026-02-19 17:36:44 +01:00
if ( upstreamPressure != null && downstreamPressure != null ) {
const pressureDiffValue = downstreamPressure - upstreamPressure ;
this . logger . debug ( ` Pressure differential: ${ pressureDiffValue } ` ) ;
this . predictFlow . fDimension = pressureDiffValue ;
this . predictPower . fDimension = pressureDiffValue ;
this . predictCtrl . fDimension = pressureDiffValue ;
2025-06-25 17:26:13 +02:00
//update the cog
const { cog , minEfficiency } = this . calcCog ( ) ;
// calc efficiency
const efficiency = this . calcEfficiency ( this . predictPower . outputY , this . predictFlow . outputY , "predicted" ) ;
//update the distance from peak
this . calcDistanceBEP ( efficiency , cog , minEfficiency ) ;
2026-02-19 17:36:44 +01:00
return pressureDiffValue ;
2025-06-25 17:26:13 +02:00
}
// Only downstream => use it, warn that it's partial
if ( downstreamPressure != null ) {
2025-10-02 17:09:24 +02:00
this . logger . warn ( ` Using downstream pressure only for prediction: ${ downstreamPressure } This is less acurate!! ` ) ;
2025-06-25 17:26:13 +02:00
this . predictFlow . fDimension = downstreamPressure ;
this . predictPower . fDimension = downstreamPressure ;
this . predictCtrl . fDimension = downstreamPressure ;
//update the cog
const { cog , minEfficiency } = this . calcCog ( ) ;
// calc efficiency
const efficiency = this . calcEfficiency ( this . predictPower . outputY , this . predictFlow . outputY , "predicted" ) ;
//update the distance from peak
this . calcDistanceBEP ( efficiency , cog , minEfficiency ) ;
return downstreamPressure ;
}
2026-02-19 17:36:44 +01:00
// Only upstream => use it, warn that it's partial
if ( upstreamPressure != null ) {
this . logger . warn ( ` Using upstream pressure only for prediction: ${ upstreamPressure } This is less acurate!! ` ) ;
this . predictFlow . fDimension = upstreamPressure ;
this . predictPower . fDimension = upstreamPressure ;
this . predictCtrl . fDimension = upstreamPressure ;
//update the cog
const { cog , minEfficiency } = this . calcCog ( ) ;
// calc efficiency
const efficiency = this . calcEfficiency ( this . predictPower . outputY , this . predictFlow . outputY , "predicted" ) ;
//update the distance from peak
this . calcDistanceBEP ( efficiency , cog , minEfficiency ) ;
return upstreamPressure ;
}
2025-06-25 17:26:13 +02:00
this . logger . error ( ` No valid pressure measurements available to calculate prediction using last known pressure ` ) ;
//set default at 0 => lowest pressure possible
this . predictFlow . fDimension = 0 ;
this . predictPower . fDimension = 0 ;
this . predictCtrl . fDimension = 0 ;
//update the cog
const { cog , minEfficiency } = this . calcCog ( ) ;
// calc efficiency
const efficiency = this . calcEfficiency ( this . predictPower . outputY , this . predictFlow . outputY , "predicted" ) ;
//update the distance from peak
this . calcDistanceBEP ( efficiency , cog , minEfficiency ) ;
2025-11-27 17:46:56 +01:00
//place min and max flow capabilities in containerthis.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin
2026-03-11 11:13:26 +01:00
this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'max' ) . value ( this . predictFlow . currentFxyYMax , Date . now ( ) , this . unitPolicy . canonical . flow ) ;
this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'min' ) . value ( this . predictFlow . currentFxyYMin , Date . now ( ) , this . unitPolicy . canonical . flow ) ;
2025-06-25 17:26:13 +02:00
return 0 ;
}
2026-02-19 17:36:44 +01:00
_getPreferredPressureValue ( position ) {
const realIds = Array . from ( this . realPressureChildIds [ position ] || [ ] ) ;
for ( const childId of realIds ) {
const value = this . measurements
. type ( "pressure" )
. variant ( "measured" )
. position ( position )
. child ( childId )
. getCurrentValue ( ) ;
if ( value != null ) return value ;
}
const virtualId = this . virtualPressureChildIds [ position ] ;
if ( virtualId ) {
const simulatedValue = this . measurements
. type ( "pressure" )
. variant ( "measured" )
. position ( position )
. child ( virtualId )
. getCurrentValue ( ) ;
if ( simulatedValue != null ) return simulatedValue ;
}
return this . measurements
. type ( "pressure" )
. variant ( "measured" )
. position ( position )
. getCurrentValue ( ) ;
}
getPressureInitializationStatus ( ) {
const upstreamPressure = this . _getPreferredPressureValue ( "upstream" ) ;
const downstreamPressure = this . _getPreferredPressureValue ( "downstream" ) ;
const hasUpstream = upstreamPressure != null ;
const hasDownstream = downstreamPressure != null ;
const hasDifferential = hasUpstream && hasDownstream ;
return {
hasUpstream ,
hasDownstream ,
hasDifferential ,
initialized : hasUpstream || hasDownstream || hasDifferential ,
source : hasDifferential ? 'differential' : hasDownstream ? 'downstream' : hasUpstream ? 'upstream' : null ,
} ;
}
updateSimulatedMeasurement ( type , position , value , context = { } ) {
const normalizedType = String ( type || "" ) . toLowerCase ( ) ;
const normalizedPosition = String ( position || "atEquipment" ) . toLowerCase ( ) ;
if ( normalizedType !== "pressure" ) {
this . _callMeasurementHandler ( normalizedType , value , normalizedPosition , context ) ;
return ;
}
if ( ! this . virtualPressureChildIds [ normalizedPosition ] ) {
this . logger . warn ( ` Unsupported simulated pressure position ' ${ normalizedPosition } ' ` ) ;
return ;
}
const child = this . virtualPressureChildren [ normalizedPosition ] ;
if ( ! child ? . measurements ) {
this . logger . error ( ` Virtual pressure child ' ${ normalizedPosition } ' is missing ` ) ;
return ;
}
2026-03-11 11:13:26 +01:00
let measurementUnit ;
try {
measurementUnit = this . _resolveMeasurementUnit ( 'pressure' , context . unit ) ;
} catch ( error ) {
this . logger . warn ( ` Rejected simulated pressure measurement: ${ error . message } ` ) ;
return ;
}
2026-02-19 17:36:44 +01:00
child . measurements
. type ( "pressure" )
. variant ( "measured" )
. position ( normalizedPosition )
2026-03-11 11:13:26 +01:00
. value ( value , context . timestamp || Date . now ( ) , measurementUnit ) ;
2026-02-19 17:36:44 +01:00
}
2025-06-25 17:26:13 +02:00
handleMeasuredFlow ( ) {
const flowDiff = this . measurements . type ( 'flow' ) . variant ( 'measured' ) . difference ( ) ;
// If both are present
if ( flowDiff != null ) {
// In theory, mass flow in = mass flow out, so they should match or be close.
if ( flowDiff . value < 0.001 ) {
// flows match within tolerance
this . logger . debug ( ` Flow match: ${ flowDiff . value } ` ) ;
return flowDiff . value ;
} else {
// Mismatch => decide how to handle. Maybe take the average?
// Or bail out with an error. Example: we bail out here.
this . logger . error ( ` Something wrong with down or upstream flow measurement. Bailing out! ` ) ;
return null ;
}
}
// get
2025-07-02 16:00:52 +02:00
const upstreamFlow = this . measurements . type ( 'flow' ) . variant ( 'measured' ) . position ( 'upstream' ) . getCurrentValue ( ) ;
2025-06-25 17:26:13 +02:00
// Only upstream => might still accept it, but warn
if ( upstreamFlow != null ) {
this . logger . warn ( ` Only upstream flow is present. Using it but results may be incomplete! ` ) ;
return upstreamFlow ;
}
// get
2025-07-02 16:00:52 +02:00
const downstreamFlow = this . measurements . type ( 'flow' ) . variant ( 'measured' ) . position ( 'downstream' ) . getCurrentValue ( ) ;
2025-06-25 17:26:13 +02:00
// Only downstream => might still accept it, but warn
if ( downstreamFlow != null ) {
this . logger . warn ( ` Only downstream flow is present. Using it but results may be incomplete! ` ) ;
return downstreamFlow ;
}
// Neither => error
this . logger . error ( ` No upstream or downstream flow measurement. Bailing out! ` ) ;
return null ;
}
handleMeasuredPower ( ) {
2025-08-07 13:52:06 +02:00
const power = this . measurements . type ( "power" ) . variant ( "measured" ) . position ( "atEquipment" ) . getCurrentValue ( ) ;
2025-06-25 17:26:13 +02:00
// If your system calls it "upstream" or just a single "value", adjust accordingly
if ( power != null ) {
this . logger . debug ( ` Measured power: ${ power } ` ) ;
return power ;
} else {
this . logger . error ( ` No measured power found. Bailing out! ` ) ;
return null ;
}
}
2026-01-29 13:32:39 +01:00
updateMeasuredTemperature ( value , position , context = { } ) {
this . logger . debug ( ` Temperature update: ${ value } at ${ position } from ${ context . childName || 'child' } ( ${ context . childId || 'unknown-id' } ) ` ) ;
2026-03-11 11:13:26 +01:00
let measurementUnit ;
try {
measurementUnit = this . _resolveMeasurementUnit ( 'temperature' , context . unit ) ;
} catch ( error ) {
this . logger . warn ( ` Rejected temperature update: ${ error . message } ` ) ;
return ;
}
this . measurements . type ( "temperature" ) . variant ( "measured" ) . position ( position || 'atEquipment' ) . child ( context . childId ) . value ( value , context . timestamp , measurementUnit ) ;
2026-01-29 13:32:39 +01:00
}
2025-10-07 18:10:45 +02:00
// context handler for pressure updates
2025-08-07 13:52:06 +02:00
updateMeasuredPressure ( value , position , context = { } ) {
2025-10-02 17:09:24 +02:00
2025-08-07 13:52:06 +02:00
this . logger . debug ( ` Pressure update: ${ value } at ${ position } from ${ context . childName || 'child' } ( ${ context . childId || 'unknown-id' } ) ` ) ;
2026-03-11 11:13:26 +01:00
let measurementUnit ;
try {
measurementUnit = this . _resolveMeasurementUnit ( 'pressure' , context . unit ) ;
} catch ( error ) {
this . logger . warn ( ` Rejected pressure update: ${ error . message } ` ) ;
return ;
}
2025-08-07 13:52:06 +02:00
2025-10-07 18:10:45 +02:00
// Store in parent's measurement container
2026-03-11 11:13:26 +01:00
this . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( position ) . child ( context . childId ) . value ( value , context . timestamp , measurementUnit ) ;
2025-08-08 14:29:15 +02:00
// Determine what kind of value to use as pressure (upstream , downstream or difference)
2025-08-07 13:52:06 +02:00
const pressure = this . getMeasuredPressure ( ) ;
this . updatePosition ( ) ;
2026-03-11 11:13:26 +01:00
this . _updatePressureDriftStatus ( ) ;
this . _updatePredictionHealth ( ) ;
2025-08-07 13:52:06 +02:00
this . logger . debug ( ` Using pressure: ${ pressure } for calculations ` ) ;
}
2025-06-25 17:26:13 +02:00
2025-09-23 15:51:16 +02:00
// NEW: Flow handler
2025-08-07 13:52:06 +02:00
updateMeasuredFlow ( value , position , context = { } ) {
if ( ! this . _isOperationalState ( ) ) {
this . logger . warn ( ` Machine not operational, skipping flow update from ${ context . childName || 'unknown' } ` ) ;
return ;
}
2025-06-25 17:26:13 +02:00
2025-08-07 13:52:06 +02:00
this . logger . debug ( ` Flow update: ${ value } at ${ position } from ${ context . childName || 'child' } ` ) ;
2026-03-11 11:13:26 +01:00
let measurementUnit ;
try {
measurementUnit = this . _resolveMeasurementUnit ( 'flow' , context . unit ) ;
} catch ( error ) {
this . logger . warn ( ` Rejected flow update: ${ error . message } ` ) ;
return ;
}
2025-08-07 13:52:06 +02:00
// Store in parent's measurement container
2026-03-11 11:13:26 +01:00
this . measurements . type ( "flow" ) . variant ( "measured" ) . position ( position ) . child ( context . childId ) . value ( value , context . timestamp , measurementUnit ) ;
2025-08-07 13:52:06 +02:00
// Update predicted flow if you have prediction capability
if ( this . predictFlow ) {
2026-03-11 11:13:26 +01:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( this . predictFlow . outputY || 0 , Date . now ( ) , this . unitPolicy . canonical . flow ) ;
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( this . predictFlow . outputY || 0 , Date . now ( ) , this . unitPolicy . canonical . flow ) ;
}
const measuredCanonical = this . measurements
. type ( "flow" )
. variant ( "measured" )
. position ( position )
. getCurrentValue ( this . unitPolicy . canonical . flow ) ;
this . _updateMetricDrift ( "flow" , measuredCanonical , context ) ;
this . _updatePredictionHealth ( ) ;
}
updateMeasuredPower ( value , position , context = { } ) {
if ( ! this . _isOperationalState ( ) ) {
this . logger . warn ( ` Machine not operational, skipping power update from ${ context . childName || 'unknown' } ` ) ;
return ;
2025-06-25 17:26:13 +02:00
}
2026-03-11 11:13:26 +01:00
this . logger . debug ( ` Power update: ${ value } at ${ position } from ${ context . childName || 'child' } ` ) ;
let measurementUnit ;
try {
measurementUnit = this . _resolveMeasurementUnit ( 'power' , context . unit ) ;
} catch ( error ) {
this . logger . warn ( ` Rejected power update: ${ error . message } ` ) ;
return ;
}
this . measurements . type ( "power" ) . variant ( "measured" ) . position ( position ) . child ( context . childId ) . value ( value , context . timestamp , measurementUnit ) ;
if ( this . predictPower ) {
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( this . predictPower . outputY || 0 , Date . now ( ) , this . unitPolicy . canonical . power ) ;
}
const measuredCanonical = this . measurements
. type ( "power" )
. variant ( "measured" )
. position ( position )
. getCurrentValue ( this . unitPolicy . canonical . power ) ;
this . _updateMetricDrift ( "power" , measuredCanonical , context ) ;
this . _updatePredictionHealth ( ) ;
2025-06-25 17:26:13 +02:00
}
2025-08-07 13:52:06 +02:00
// Helper method for operational state check
_isOperationalState ( ) {
const state = this . state . getCurrentState ( ) ;
2026-02-19 17:36:44 +01:00
const activeStates = [ "operational" , "warmingup" , "accelerating" , "decelerating" ] ;
this . logger . debug ( ` Checking operational state ${ this . state . getCurrentState ( ) } ? ${ activeStates . includes ( state ) } ` ) ;
return activeStates . includes ( state ) ;
2025-06-25 17:26:13 +02:00
}
//what is the internal functions that need updating when something changes that has influence on this.
updatePosition ( ) {
2025-10-02 17:09:24 +02:00
2025-08-07 13:52:06 +02:00
if ( this . _isOperationalState ( ) ) {
2025-06-25 17:26:13 +02:00
const currentPosition = this . state . getCurrentPosition ( ) ;
// Update the predicted values based on the new position
const { cPower , cFlow } = this . calcFlowPower ( currentPosition ) ;
// Calc predicted efficiency
const efficiency = this . calcEfficiency ( cPower , cFlow , "predicted" ) ;
//update the cog
const { cog , minEfficiency } = this . calcCog ( ) ;
//update the distance from peak
this . calcDistanceBEP ( efficiency , cog , minEfficiency ) ;
}
2025-10-31 18:35:40 +01:00
2026-03-11 11:13:26 +01:00
this . _updatePredictionHealth ( ) ;
2025-06-25 17:26:13 +02:00
}
calcDistanceFromPeak ( currentEfficiency , peakEfficiency ) {
return Math . abs ( currentEfficiency - peakEfficiency ) ;
}
calcRelativeDistanceFromPeak ( currentEfficiency , maxEfficiency , minEfficiency ) {
let distance = 1 ;
if ( currentEfficiency != null ) {
distance = this . interpolation . interpolate _lin _single _point ( currentEfficiency , maxEfficiency , minEfficiency , 0 , 1 ) ;
}
return distance ;
}
2025-07-24 13:15:33 +02:00
showWorkingCurves ( ) {
// Show the current curves for debugging
const { powerCurve , flowCurve } = this . getCurrentCurves ( ) ;
return {
powerCurve : powerCurve ,
flowCurve : flowCurve ,
cog : this . cog ,
cogIndex : this . cogIndex ,
NCog : this . NCog ,
minEfficiency : this . minEfficiency ,
currentEfficiencyCurve : this . currentEfficiencyCurve ,
absDistFromPeak : this . absDistFromPeak ,
relDistFromPeak : this . relDistFromPeak
} ;
}
2025-06-25 17:26:13 +02:00
// Calculate the center of gravity for current pressure
calcCog ( ) {
//fetch current curve data for power and flow
const { powerCurve , flowCurve } = this . getCurrentCurves ( ) ;
const { efficiencyCurve , peak , peakIndex , minEfficiency } = this . calcEfficiencyCurve ( powerCurve , flowCurve ) ;
// Calculate the normalized center of gravity
2025-11-20 22:29:24 +01:00
const NCog = ( flowCurve . y [ peakIndex ] - this . predictFlow . currentFxyYMin ) / ( this . predictFlow . currentFxyYMax - this . predictFlow . currentFxyYMin ) ; //
2025-06-25 17:26:13 +02:00
//store in object for later retrieval
this . currentEfficiencyCurve = efficiencyCurve ;
this . cog = peak ;
this . cogIndex = peakIndex ;
this . NCog = NCog ;
this . minEfficiency = minEfficiency ;
return { cog : peak , cogIndex : peakIndex , NCog : NCog , minEfficiency : minEfficiency } ;
}
calcEfficiencyCurve ( powerCurve , flowCurve ) {
const efficiencyCurve = [ ] ;
let peak = 0 ;
let peakIndex = 0 ;
let minEfficiency = 0 ;
// Calculate efficiency curve based on power and flow curves
powerCurve . y . forEach ( ( power , index ) => {
// Get flow for the current power
const flow = flowCurve . y [ index ] ;
// higher efficiency is better
efficiencyCurve . push ( Math . round ( ( flow / power ) * 100 ) / 100 ) ;
// Keep track of peak efficiency
peak = Math . max ( peak , efficiencyCurve [ index ] ) ;
peakIndex = peak == efficiencyCurve [ index ] ? index : peakIndex ;
minEfficiency = Math . min ( ... efficiencyCurve ) ;
} ) ;
return { efficiencyCurve , peak , peakIndex , minEfficiency } ;
}
//calc flow power based on pressure and current position
calcFlowPower ( x ) {
// Calculate flow and power
const cFlow = this . calcFlow ( x ) ;
const cPower = this . calcPower ( x ) ;
return { cPower , cFlow } ;
}
2025-11-12 17:40:38 +01:00
calcEfficiency ( power , flow , variant ) {
2026-02-23 13:17:18 +01:00
// Request a pressure differential explicitly in Pascal for hydraulic efficiency.
const pressureDiff = this . measurements
. type ( 'pressure' )
. variant ( 'measured' )
. difference ( { unit : 'Pa' } ) ;
2025-11-12 17:40:38 +01:00
const g = gravity . getStandardGravity ( ) ;
const temp = this . measurements . type ( 'temperature' ) . variant ( 'measured' ) . position ( 'atEquipment' ) . getCurrentValue ( 'K' ) ;
2025-11-28 09:59:51 +01:00
const atmPressure = this . measurements . type ( 'atmPressure' ) . variant ( 'measured' ) . position ( 'atEquipment' ) . getCurrentValue ( 'Pa' ) ;
2026-02-12 10:48:44 +01:00
let rho = null ;
try {
rho = coolprop . PropsSI ( 'D' , 'T' , temp , 'P' , atmPressure , 'WasteWater' ) ;
} catch ( error ) {
// coolprop can throw transient initialization errors; keep machine calculations running.
this . logger . warn ( ` CoolProp density lookup failed: ${ error . message } . Using fallback density. ` ) ;
rho = 1000 ; // kg/m3 fallback for water-like fluids
}
2025-11-12 17:40:38 +01:00
this . logger . debug ( ` temp: ${ temp } atmPressure : ${ atmPressure } rho : ${ rho } pressureDiff: ${ pressureDiff ? . value || 0 } ` ) ;
const flowM3s = this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'atEquipment' ) . getCurrentValue ( 'm3/s' ) ;
const powerWatt = this . measurements . type ( 'power' ) . variant ( 'predicted' ) . position ( 'atEquipment' ) . getCurrentValue ( 'W' ) ;
this . logger . debug ( ` Flow : ${ flowM3s } power: ${ powerWatt } ` ) ;
2025-06-25 17:26:13 +02:00
if ( power != 0 && flow != 0 ) {
2025-11-12 17:40:38 +01:00
const specificFlow = flow / power ;
const specificEnergyConsumption = power / flow ;
this . measurements . type ( "efficiency" ) . variant ( variant ) . position ( 'atEquipment' ) . value ( specificFlow ) ;
this . measurements . type ( "specificEnergyConsumption" ) . variant ( variant ) . position ( 'atEquipment' ) . value ( specificEnergyConsumption ) ;
2026-03-11 11:13:26 +01:00
if ( pressureDiff ? . value != null && Number . isFinite ( flowM3s ) && Number . isFinite ( powerWatt ) && powerWatt > 0 ) {
// Engineering references: P_h = Q * Δp = ρ g Q H, η_h = P_h / P_in
const pressureDiffPa = Number ( pressureDiff . value ) ;
const headMeters = ( Number . isFinite ( rho ) && rho > 0 ) ? pressureDiffPa / ( rho * g ) : null ;
const hydraulicPowerW = pressureDiffPa * flowM3s ;
const nHydraulicEfficiency = hydraulicPowerW / powerWatt ;
if ( Number . isFinite ( headMeters ) ) {
this . measurements . type ( "pumpHead" ) . variant ( variant ) . position ( 'atEquipment' ) . value ( headMeters , Date . now ( ) , 'm' ) ;
}
this . measurements . type ( "hydraulicPower" ) . variant ( variant ) . position ( 'atEquipment' ) . value ( hydraulicPowerW , Date . now ( ) , 'W' ) ;
2025-11-12 17:40:38 +01:00
this . measurements . type ( "nHydraulicEfficiency" ) . variant ( variant ) . position ( 'atEquipment' ) . value ( nHydraulicEfficiency ) ;
}
}
2025-06-25 17:26:13 +02:00
2025-11-12 17:40:38 +01:00
//change this to nhydrefficiency ?
2025-10-02 17:09:24 +02:00
return this . measurements . type ( "efficiency" ) . variant ( variant ) . position ( 'atEquipment' ) . getCurrentValue ( ) ;
2025-06-25 17:26:13 +02:00
}
updateCurve ( newCurve ) {
this . logger . info ( ` Updating machine curve ` ) ;
2026-03-11 11:13:26 +01:00
const normalizedCurve = this . _normalizeMachineCurve ( newCurve ) ;
const newConfig = {
asset : {
machineCurve : normalizedCurve ,
curveUnits : this . unitPolicy . curve ,
} ,
} ;
2025-06-25 17:26:13 +02:00
//validate input of new curve fed to the machine
this . config = this . configUtils . updateConfig ( this . config , newConfig ) ;
//After we passed validation load the curves into their predictors
this . predictFlow . updateCurve ( this . config . asset . machineCurve . nq ) ;
this . predictPower . updateCurve ( this . config . asset . machineCurve . np ) ;
this . predictCtrl . updateCurve ( this . reverseCurve ( this . config . asset . machineCurve . nq ) ) ;
}
getCompleteCurve ( ) {
const powerCurve = this . predictPower . inputCurveData ;
const flowCurve = this . predictFlow . inputCurveData ;
return { powerCurve , flowCurve } ;
}
getCurrentCurves ( ) {
const powerCurve = this . predictPower . currentFxyCurve [ this . predictPower . currentF ] ;
const flowCurve = this . predictFlow . currentFxyCurve [ this . predictFlow . currentF ] ;
return { powerCurve , flowCurve } ;
}
calcDistanceBEP ( efficiency , maxEfficiency , minEfficiency ) {
const absDistFromPeak = this . calcDistanceFromPeak ( efficiency , maxEfficiency ) ;
const relDistFromPeak = this . calcRelativeDistanceFromPeak ( efficiency , maxEfficiency , minEfficiency ) ;
//store internally
this . absDistFromPeak = absDistFromPeak ;
this . relDistFromPeak = relDistFromPeak ;
return { absDistFromPeak : absDistFromPeak , relDistFromPeak : relDistFromPeak } ;
}
getOutput ( ) {
// Improved output object generation
2025-11-05 15:47:39 +01:00
2026-03-11 11:13:26 +01:00
const output = this . measurements . getFlattenedOutput ( {
requestedUnits : this . unitPolicy . output ,
} ) ;
2025-06-25 17:26:13 +02:00
//fill in the rest of the output object
output [ "state" ] = this . state . getCurrentState ( ) ;
output [ "runtime" ] = this . state . getRunTimeHours ( ) ;
output [ "ctrl" ] = this . state . getCurrentPosition ( ) ;
output [ "moveTimeleft" ] = this . state . getMoveTimeLeft ( ) ;
output [ "mode" ] = this . currentMode ;
output [ "cog" ] = this . cog ; // flow / power efficiency
output [ "NCog" ] = this . NCog ; // normalized cog
output [ "NCogPercent" ] = Math . round ( this . NCog * 100 * 100 ) / 100 ;
2025-11-05 15:47:39 +01:00
output [ "maintenanceTime" ] = this . state . getMaintenanceTimeHours ( ) ;
2025-06-25 17:26:13 +02:00
if ( this . flowDrift != null ) {
const flowDrift = this . flowDrift ;
output [ "flowNrmse" ] = flowDrift . nrmse ;
output [ "flowLongterNRMSD" ] = flowDrift . longTermNRMSD ;
2026-03-11 11:13:26 +01:00
output [ "flowLongTermNRMSD" ] = flowDrift . longTermNRMSD ;
2025-06-25 17:26:13 +02:00
output [ "flowImmediateLevel" ] = flowDrift . immediateLevel ;
output [ "flowLongTermLevel" ] = flowDrift . longTermLevel ;
2026-03-11 11:13:26 +01:00
output [ "flowDriftValid" ] = flowDrift . valid ;
2025-06-25 17:26:13 +02:00
}
2026-03-11 11:13:26 +01:00
if ( this . powerDrift != null ) {
const powerDrift = this . powerDrift ;
output [ "powerNrmse" ] = powerDrift . nrmse ;
output [ "powerLongTermNRMSD" ] = powerDrift . longTermNRMSD ;
output [ "powerImmediateLevel" ] = powerDrift . immediateLevel ;
output [ "powerLongTermLevel" ] = powerDrift . longTermLevel ;
output [ "powerDriftValid" ] = powerDrift . valid ;
}
output [ "pressureDriftLevel" ] = this . pressureDrift . level ;
output [ "pressureDriftSource" ] = this . pressureDrift . source ;
output [ "pressureDriftFlags" ] = this . pressureDrift . flags ;
output [ "predictionQuality" ] = this . predictionHealth . quality ;
output [ "predictionConfidence" ] = Math . round ( this . predictionHealth . confidence * 1000 ) / 1000 ;
output [ "predictionPressureSource" ] = this . predictionHealth . pressureSource ;
output [ "predictionFlags" ] = this . predictionHealth . flags ;
2025-06-25 17:26:13 +02:00
//should this all go in the container of measurements?
output [ "effDistFromPeak" ] = this . absDistFromPeak ;
output [ "effRelDistFromPeak" ] = this . relDistFromPeak ;
//this.logger.debug(`Output: ${JSON.stringify(output)}`);
return output ;
}
} // end of class
module . exports = Machine ;