2025-07-24 13:14:19 +02:00
/ * *
* @ file valve . 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 .
*
* Author :
* - Rene De Ren
* Email :
* - r . de . ren @ brabantsedelta . nl
*
* Future Improvements :
* - Time - based stability checks
* - Warmup handling
* - Dynamic outlier detection thresholds
* - Dynamic smoothing window and methods
* - Alarm and threshold handling
* - Maintenance mode
* - Historical data and trend analysis
* /
2025-05-14 10:06:08 +02:00
/ * *
* @ file valveClass . 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
... .
* /
2025-07-24 13:14:19 +02:00
//load local dependencies
2025-05-14 10:06:08 +02:00
const EventEmitter = require ( 'events' ) ;
2026-03-11 11:13:17 +01:00
const { loadCurve , logger , configUtils , configManager , state , MeasurementContainer , predict , childRegistrationUtils , convert } = require ( 'generalFunctions' ) ;
const { ValveHydraulicModel , normalizeServiceType } = require ( './hydraulicModel' ) ;
const SERVICE _TYPES = new Set ( [ 'gas' , 'liquid' ] ) ;
const DEFAULT _SOURCE _SERVICE _TYPE = Object . freeze ( {
machine : 'liquid' ,
rotatingmachine : 'liquid' ,
machinegroup : 'liquid' ,
machinegroupcontrol : 'liquid' ,
pumpingstation : 'liquid' ,
} ) ;
const CANONICAL _UNITS = Object . freeze ( {
pressure : 'Pa' ,
flow : 'm3/s' ,
temperature : 'K' ,
} ) ;
const DEFAULT _IO _UNITS = Object . freeze ( {
pressure : 'mbar' ,
flow : 'm3/h' ,
temperature : 'C' ,
} ) ;
const FORMULA _UNITS = Object . freeze ( {
pressure : 'mbar' ,
flow : 'm3/h' ,
temperature : 'K' ,
} ) ;
const FALLBACK _SUPPLIER _CURVE = Object . freeze ( {
'1.204' : {
'125' : {
x : [ 0 , 100 ] ,
y : [ 0 , 1 ] ,
} ,
} ,
} ) ;
2025-05-14 10:06:08 +02:00
class Valve {
2026-03-11 11:13:17 +01:00
constructor ( valveConfig = { } , stateConfig = { } , runtimeOptions = { } ) {
2025-07-24 13:14:19 +02:00
//basic setup
this . emitter = new EventEmitter ( ) ; // nodig voor ontvangen en uitvoeren van events emit() --> Zien als internet berichten (niet bedraad in node-red)
this . logger = new logger ( valveConfig . general . logging . enabled , valveConfig . general . logging . logLevel , valveConfig . general . name ) ;
this . configManager = new configManager ( ) ;
this . defaultConfig = this . configManager . getConfig ( 'valve' ) ; // Load default config for rotating machine ( use software type name ? )
this . configUtils = new configUtils ( this . defaultConfig ) ;
2026-03-11 11:13:17 +01:00
// Load supplier-specific curve data (if available for model)
2025-07-24 13:14:19 +02:00
this . model = valveConfig . asset . model ; // Get the model from the valveConfig
this . curve = this . model ? loadCurve ( this . model ) : null ;
//Init config and check if it is valid
this . config = this . configUtils . initConfig ( valveConfig ) ;
2026-03-11 11:13:17 +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 } ,
} ) ;
2025-07-24 13:14:19 +02:00
2025-05-14 10:06:08 +02:00
// Initialize measurements
2026-03-11 11:13:17 +01:00
this . measurements = new MeasurementContainer ( {
autoConvert : true ,
defaultUnits : {
pressure : this . unitPolicy . output . pressure ,
flow : this . unitPolicy . output . flow ,
temperature : this . unitPolicy . output . temperature ,
} ,
preferredUnits : {
pressure : this . unitPolicy . output . pressure ,
flow : this . unitPolicy . output . flow ,
temperature : this . unitPolicy . output . temperature ,
} ,
canonicalUnits : this . unitPolicy . canonical ,
storeCanonical : true ,
strictUnitValidation : true ,
throwOnInvalidUnit : true ,
requireUnitForTypes : [ 'pressure' , 'flow' , 'temperature' ] ,
} , this . logger ) ;
2025-05-14 10:06:08 +02:00
this . child = { } ; // object to hold child information so we know on what to subscribe
// Init after config is set
2025-07-24 13:14:19 +02:00
this . state = new state ( stateConfig , this . logger ) ; // Init State manager and pass logger
2025-05-14 10:06:08 +02:00
this . state . stateManager . currentState = "operational" ; // Set default state to operational
2026-03-11 11:13:17 +01:00
this . kv = 0 ; // default
const configuredServiceType = this . _normalizeOptionalServiceType ( runtimeOptions ? . serviceType || valveConfig ? . asset ? . serviceType ) ;
this . expectedServiceType = configuredServiceType ;
this . serviceType = configuredServiceType || normalizeServiceType ( runtimeOptions ? . serviceType || valveConfig ? . asset ? . serviceType ) ;
this . upstreamFluidSources = new Map ( ) ;
this . _fluidContractListeners = new Map ( ) ;
this . fluidCompatibility = {
status : configuredServiceType ? 'pending' : 'unknown' ,
expectedServiceType : configuredServiceType || null ,
receivedServiceType : null ,
upstreamServiceTypes : [ ] ,
sourceCount : 0 ,
message : configuredServiceType
? ` Waiting for upstream fluid contract ( ${ configuredServiceType } ). `
: 'No upstream fluid contract available.' ,
} ;
this . hydraulicModel = new ValveHydraulicModel (
{
serviceType : this . serviceType ,
gasChokedRatioLimit : runtimeOptions ? . gasChokedRatioLimit ? ? valveConfig ? . asset ? . gasChokedRatioLimit ,
} ,
this . logger
) ;
this . rho = this . _resolvePositiveNumber (
runtimeOptions ? . fluidDensity ,
valveConfig ? . asset ? . fluidDensity ,
this . hydraulicModel . defaultDensity
) ;
this . T = this . _resolvePositiveNumber (
runtimeOptions ? . fluidTemperatureK ,
valveConfig ? . asset ? . fluidTemperatureK ,
this . hydraulicModel . defaultTemperatureK
) ;
2025-05-14 10:06:08 +02:00
this . currentMode = this . config . mode . current ;
// wanneer hij deze ontvangt is de positie van de klep verandererd en gaat hij de updateposition functie aanroepen wat dan alle metingen en standen gaat updaten
2026-03-11 11:13:17 +01:00
this . _onPositionChange = ( data ) => {
2025-05-14 10:06:08 +02:00
this . logger . debug ( ` Position change detected: ${ data } ` ) ;
2026-03-11 11:13:17 +01:00
this . updatePosition ( ) ;
} ;
this . state . emitter . on ( "positionChange" , this . _onPositionChange ) ; //To update deltaP
2025-05-14 10:06:08 +02:00
2025-07-24 13:14:19 +02:00
this . childRegistrationUtils = new childRegistrationUtils ( this ) ; // Child registration utility
2026-03-11 11:13:17 +01:00
this . _initSupplierCurvePredictor ( ) ;
2025-05-14 10:06:08 +02:00
}
// -------- Config -------- //
updateConfig ( newConfig ) {
this . config = this . configUtils . updateConfig ( this . config , newConfig ) ;
}
isValidSourceForMode ( source , mode ) {
const allowedSourcesSet = this . config . mode . allowedSources [ mode ] || [ ] ;
return allowedSourcesSet . has ( source ) ;
}
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 . info ( ` Handling input from source ' ${ source } ' with action ' ${ action } ' in mode ' ${ this . currentMode } '. ` ) ;
try {
switch ( action ) {
case "execSequence" :
await this . executeSequence ( parameter ) ;
break ;
case "execMovement" : // past het setpoint aan - movement van klep stand
await this . setpoint ( parameter ) ;
break ;
case "emergencyStop" :
this . logger . warn ( ` Emergency stop activated by ' ${ source } '. ` ) ;
2026-03-11 11:13:17 +01:00
await this . executeSequence ( "emergencystop" ) ;
break ;
case "emergencystop" :
this . logger . warn ( ` Emergency stop activated by ' ${ source } '. ` ) ;
await this . executeSequence ( "emergencystop" ) ;
2025-05-14 10:06:08 +02:00
break ;
case "statusCheck" :
this . logger . info ( ` Status Check: Mode = ' ${ this . currentMode } ', Source = ' ${ source } '. ` ) ;
break ;
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 } ` ) ;
}
}
setMode ( newMode ) {
2026-02-23 13:17:22 +01:00
const availableModes = Array . isArray ( this . defaultConfig ? . mode ? . current ? . rules ? . values )
? this . defaultConfig . mode . current . rules . values . map ( v => v . value )
: Object . keys ( this . config ? . mode ? . allowedSources || { } ) ;
2025-05-14 10:06:08 +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 } '. ` ) ;
}
2026-03-11 11:13:17 +01:00
_buildUnitPolicy ( config = { } ) {
const flowUnit = this . _resolveUnitOrFallback (
config ? . general ? . unit || config ? . asset ? . unit ,
'volumeFlowRate' ,
DEFAULT _IO _UNITS . flow
) ;
return {
canonical : { ... CANONICAL _UNITS } ,
output : {
flow : flowUnit ,
pressure : DEFAULT _IO _UNITS . pressure ,
temperature : DEFAULT _IO _UNITS . temperature ,
}
} ;
}
_resolveUnitOrFallback ( candidate , expectedMeasure , fallbackUnit ) {
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 } ', got ' ${ desc . measure } ' ` ) ;
}
return raw ;
} catch ( error ) {
this . logger ? . warn ? . ( ` Invalid unit ' ${ raw } ' ( ${ error . message } ); falling back to ' ${ fallback } '. ` ) ;
return fallback ;
}
}
_outputUnitForType ( type ) {
switch ( String ( type || '' ) . toLowerCase ( ) ) {
case 'flow' :
return this . unitPolicy . output . flow ;
case 'pressure' :
return this . unitPolicy . output . pressure ;
case 'temperature' :
return this . unitPolicy . output . temperature ;
default :
return null ;
}
}
_readMeasurement ( type , variant , position , unit = null ) {
const requestedUnit = unit || this . _outputUnitForType ( type ) ;
return this . measurements
. type ( type )
. variant ( variant )
. position ( position )
. getCurrentValue ( requestedUnit || undefined ) ;
}
_writeMeasurement ( type , variant , position , value , unit = null , timestamp = Date . now ( ) ) {
if ( ! Number . isFinite ( value ) ) {
return ;
}
this . measurements
. type ( type )
. variant ( variant )
. position ( position )
. value ( value , timestamp , unit || undefined ) ;
}
_resolvePositiveNumber ( ... candidates ) {
for ( const candidate of candidates ) {
const parsed = Number ( candidate ) ;
if ( Number . isFinite ( parsed ) && parsed > 0 ) {
return parsed ;
}
}
return undefined ;
}
_normalizeOptionalServiceType ( value ) {
const raw = String ( value || '' ) . trim ( ) . toLowerCase ( ) ;
if ( SERVICE _TYPES . has ( raw ) ) {
return raw ;
}
return null ;
}
_deriveDefaultServiceTypeForSoftwareType ( softwareType ) {
const key = String ( softwareType || '' ) . trim ( ) . toLowerCase ( ) ;
return DEFAULT _SOURCE _SERVICE _TYPE [ key ] || null ;
}
_extractFluidContractFromChild ( child , softwareType ) {
const sourceType = String ( softwareType || child ? . config ? . functionality ? . softwareType || '' ) . trim ( ) . toLowerCase ( ) ;
let contractFromChild = null ;
if ( typeof child ? . getFluidContract === 'function' ) {
try {
contractFromChild = child . getFluidContract ( ) ;
} catch ( error ) {
this . logger . warn ( ` Failed to read child fluid contract: ${ error . message } ` ) ;
}
}
const contractStatus = String ( contractFromChild ? . status || '' ) . trim ( ) . toLowerCase ( ) ;
if ( contractStatus === 'conflict' ) {
return {
status : 'conflict' ,
serviceType : null ,
sourceType ,
} ;
}
const contractType = this . _normalizeOptionalServiceType ( contractFromChild ? . serviceType ) ;
if ( contractType ) {
return {
status : 'resolved' ,
serviceType : contractType ,
sourceType ,
} ;
}
const directType = this . _normalizeOptionalServiceType (
child ? . serviceType
|| child ? . expectedServiceType
|| child ? . config ? . asset ? . serviceType
) ;
if ( directType ) {
return {
status : 'resolved' ,
serviceType : directType ,
sourceType ,
} ;
}
const fallbackType = this . _deriveDefaultServiceTypeForSoftwareType ( sourceType ) ;
if ( fallbackType ) {
return {
status : 'inferred' ,
serviceType : fallbackType ,
sourceType ,
} ;
}
return {
status : 'unknown' ,
serviceType : null ,
sourceType ,
} ;
}
_bindFluidContractListener ( sourceId , child , sourceType ) {
if ( ! sourceId || this . _fluidContractListeners . has ( sourceId ) ) {
return ;
}
if ( ! child ? . emitter || typeof child . emitter . on !== 'function' ) {
return ;
}
const handler = ( ) => {
const latest = this . _extractFluidContractFromChild ( child , sourceType ) ;
const existing = this . upstreamFluidSources . get ( sourceId ) || { } ;
existing . contract = latest ;
this . upstreamFluidSources . set ( sourceId , existing ) ;
this . _updateFluidCompatibilityState ( ) ;
} ;
child . emitter . on ( 'fluidContractChange' , handler ) ;
this . _fluidContractListeners . set ( sourceId , {
emitter : child . emitter ,
handler ,
} ) ;
}
_computeFluidCompatibilitySnapshot ( ) {
const expectedServiceType = this . expectedServiceType || null ;
const contracts = Array . from ( this . upstreamFluidSources . values ( ) )
. map ( ( entry ) => entry ? . contract )
. filter ( Boolean ) ;
const upstreamServiceTypes = Array . from ( new Set (
contracts
. map ( ( contract ) => this . _normalizeOptionalServiceType ( contract . serviceType ) )
. filter ( Boolean )
) ) ;
const hasConflict = contracts . some ( ( contract ) => String ( contract . status || '' ) . toLowerCase ( ) === 'conflict' ) ;
const sourceCount = this . upstreamFluidSources . size ;
if ( hasConflict || upstreamServiceTypes . length > 1 ) {
return {
status : 'conflict' ,
expectedServiceType ,
receivedServiceType : upstreamServiceTypes . length === 1 ? upstreamServiceTypes [ 0 ] : null ,
upstreamServiceTypes ,
sourceCount ,
message : ` Conflicting upstream fluids detected: ${ upstreamServiceTypes . join ( ', ' ) || 'unknown' } . ` ,
} ;
}
if ( upstreamServiceTypes . length === 1 ) {
const receivedServiceType = upstreamServiceTypes [ 0 ] ;
if ( expectedServiceType && expectedServiceType !== receivedServiceType ) {
return {
status : 'mismatch' ,
expectedServiceType ,
receivedServiceType ,
upstreamServiceTypes ,
sourceCount ,
message : ` Expected ${ expectedServiceType } , received ${ receivedServiceType } . ` ,
} ;
}
return {
status : expectedServiceType ? 'match' : 'inferred' ,
expectedServiceType ,
receivedServiceType ,
upstreamServiceTypes ,
sourceCount ,
message : expectedServiceType
? ` Fluid contract validated: ${ receivedServiceType } . `
: ` Fluid inferred from upstream: ${ receivedServiceType } . ` ,
} ;
}
return {
status : expectedServiceType ? 'pending' : 'unknown' ,
expectedServiceType ,
receivedServiceType : null ,
upstreamServiceTypes : [ ] ,
sourceCount ,
message : expectedServiceType
? ` Waiting for upstream fluid contract ( ${ expectedServiceType } ). `
: 'No upstream fluid contract available.' ,
} ;
}
_updateFluidCompatibilityState ( ) {
const next = this . _computeFluidCompatibilitySnapshot ( ) ;
const previous = this . fluidCompatibility || { } ;
const changed = (
previous . status !== next . status
|| previous . expectedServiceType !== next . expectedServiceType
|| previous . receivedServiceType !== next . receivedServiceType
|| previous . sourceCount !== next . sourceCount
|| ( previous . message || '' ) !== ( next . message || '' )
) ;
this . fluidCompatibility = next ;
if ( ! changed ) {
return ;
}
if ( next . status === 'mismatch' || next . status === 'conflict' ) {
this . logger . warn ( ` Fluid compatibility warning: ${ next . message } ` ) ;
} else {
this . logger . info ( ` Fluid compatibility update: ${ next . message } ` ) ;
}
this . emitter . emit ( 'fluidCompatibilityChange' , this . getFluidCompatibility ( ) ) ;
this . emitter . emit ( 'fluidContractChange' , this . getFluidContract ( ) ) ;
}
getFluidCompatibility ( ) {
const state = this . fluidCompatibility || { } ;
return {
status : state . status || 'unknown' ,
expectedServiceType : state . expectedServiceType || null ,
receivedServiceType : state . receivedServiceType || null ,
upstreamServiceTypes : Array . isArray ( state . upstreamServiceTypes ) ? [ ... state . upstreamServiceTypes ] : [ ] ,
sourceCount : Number ( state . sourceCount ) || 0 ,
message : state . message || '' ,
} ;
}
getFluidContract ( ) {
const compatibility = this . getFluidCompatibility ( ) ;
if ( compatibility . status === 'conflict' ) {
return {
status : 'conflict' ,
serviceType : null ,
expectedServiceType : compatibility . expectedServiceType ,
observedServiceType : compatibility . receivedServiceType ,
source : 'valve' ,
} ;
}
const advertisedServiceType = compatibility . expectedServiceType || null ;
return {
status : advertisedServiceType ? 'resolved' : 'unknown' ,
serviceType : advertisedServiceType ,
expectedServiceType : compatibility . expectedServiceType ,
observedServiceType : compatibility . receivedServiceType ,
source : 'valve' ,
} ;
}
registerChild ( child , softwareType ) {
if ( ! child || typeof child !== 'object' ) {
this . logger . warn ( 'registerChild skipped: invalid child payload' ) ;
return false ;
}
const sourceType = String ( softwareType || child ? . config ? . functionality ? . softwareType || '' ) . trim ( ) . toLowerCase ( ) ;
const sourceId = child ? . config ? . general ? . id
|| child ? . config ? . general ? . name
|| ` source- ${ this . upstreamFluidSources . size + 1 } ` ;
const contract = this . _extractFluidContractFromChild ( child , sourceType ) ;
this . upstreamFluidSources . set ( sourceId , {
child ,
sourceType ,
contract ,
} ) ;
this . _bindFluidContractListener ( sourceId , child , sourceType ) ;
this . _updateFluidCompatibilityState ( ) ;
this . logger . info ( ` Source ' ${ sourceId } ' ( ${ sourceType || 'unknown' } ) registered for fluid contract. ` ) ;
return true ;
}
_initSupplierCurvePredictor ( ) {
const supplierCurve = this . _resolveSupplierCurveData ( ) ;
const densityTarget = Number . isFinite ( this . rho ) && this . rho > 0 ? this . rho : this . hydraulicModel . defaultDensity ;
const densityKey = this . _pickNearestNumericKey ( Object . keys ( supplierCurve ) , densityTarget ) ;
const densityCurveFamily = supplierCurve [ densityKey ] ;
const diameterTarget = Number ( this . config ? . asset ? . valveDiameter ) ;
const diameterKey = this . _pickNearestNumericKey (
Object . keys ( densityCurveFamily || { } ) ,
Number . isFinite ( diameterTarget ) && diameterTarget > 0 ? diameterTarget : 125
) ;
this . curveSelection = {
densityKey : Number ( densityKey ) ,
diameterKey : Number ( diameterKey ) ,
} ;
this . rho = Number . isFinite ( this . rho ) && this . rho > 0 ? this . rho : this . hydraulicModel . defaultDensity ;
this . T = Number . isFinite ( this . T ) && this . T > 0 ? this . T : this . hydraulicModel . defaultTemperatureK ;
this . predictKv = new predict ( { curve : densityCurveFamily || FALLBACK _SUPPLIER _CURVE [ '1.204' ] } ) ;
this . predictKv . fDimension = this . curveSelection . diameterKey ;
this . logger . info (
` Using supplier curve model=' ${ this . model || "inline" } ', densityCurve= ${ this . curveSelection . densityKey } , diameter= ${ this . curveSelection . diameterKey } , serviceType= ${ this . serviceType } `
) ;
}
_resolveSupplierCurveData ( ) {
if ( this . _isValidSupplierCurveData ( this . curve ) ) {
return this . curve ;
}
if ( this . _isValidSupplierCurveData ( this . config ? . asset ? . valveCurve ) ) {
return this . config . asset . valveCurve ;
}
this . logger . warn ( "No valid supplier curve data found, using fallback curve." ) ;
return FALLBACK _SUPPLIER _CURVE ;
}
_isValidSupplierCurveData ( curveData ) {
if ( ! curveData || typeof curveData !== "object" ) {
return false ;
}
const densityKeys = Object . keys ( curveData ) ;
if ( ! densityKeys . length ) {
return false ;
}
for ( const densityKey of densityKeys ) {
const diameters = curveData [ densityKey ] ;
if ( ! diameters || typeof diameters !== "object" ) {
return false ;
}
const diameterKeys = Object . keys ( diameters ) ;
if ( ! diameterKeys . length ) {
return false ;
}
for ( const diameterKey of diameterKeys ) {
const curve = diameters [ diameterKey ] ;
if ( ! Array . isArray ( curve ? . x ) || ! Array . isArray ( curve ? . y ) || curve . x . length < 2 || curve . x . length !== curve . y . length ) {
return false ;
}
}
}
return true ;
}
_pickNearestNumericKey ( keys , target ) {
const numericKeys = keys . map ( ( key ) => Number ( key ) ) . filter ( ( value ) => Number . isFinite ( value ) ) ;
if ( ! numericKeys . length ) {
return String ( target ) ;
}
let selected = numericKeys [ 0 ] ;
let selectedDistance = Math . abs ( selected - target ) ;
for ( const key of numericKeys ) {
const distance = Math . abs ( key - target ) ;
if ( distance < selectedDistance ) {
selected = key ;
selectedDistance = distance ;
}
}
return String ( selected ) ;
}
_predictKvForPosition ( positionPercent ) {
if ( ! this . predictKv ) {
return 0.1 ;
}
try {
this . predictKv . fDimension = this . curveSelection ? . diameterKey || this . predictKv . fDimension ;
const kv = Number ( this . predictKv . y ( positionPercent ) ) ;
if ( ! Number . isFinite ( kv ) ) {
return 0.1 ;
}
return Math . max ( 0.1 , kv ) ;
} catch ( error ) {
this . logger . warn ( ` Failed to predict Kv for position= ${ positionPercent } : ${ error . message } ` ) ;
return 0.1 ;
}
}
2025-05-14 10:06:08 +02:00
// -------- 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
}
}
}
async setpoint ( setpoint ) {
try {
// Validate setpoint
if ( typeof setpoint !== 'number' || setpoint < 0 ) {
throw new Error ( "Invalid setpoint: Setpoint must be a non-negative number." ) ;
}
// Move to the desired setpoint
await this . state . moveTo ( setpoint ) ;
} catch ( error ) {
2026-02-23 13:17:22 +01:00
this . logger . error ( ` Error setting setpoint: ${ error } ` ) ;
2025-05-14 10:06:08 +02:00
}
}
2026-03-11 11:13:17 +01:00
updatePressure ( variant , value , position , unit = this . unitPolicy . output . pressure ) {
2025-07-31 09:07:11 +02:00
if ( value === null || value === undefined ) {
2026-03-11 11:13:17 +01:00
this . logger . warn ( ` Received null or undefined value for pressure update. Variant: ${ variant } , Position: ${ position } ` ) ;
2025-07-31 09:07:11 +02:00
return ;
}
this . logger . debug ( ` Updating pressure: variant= ${ variant } , value= ${ value } , position= ${ position } ` ) ;
switch ( variant ) {
case ( "measured" ) :
// put value in measurements container
2026-03-11 11:13:17 +01:00
this . _writeMeasurement ( "pressure" , "measured" , position , Number ( value ) , unit ) ;
2025-07-31 09:07:11 +02:00
// get latest downstream pressure measurement
2026-03-11 11:13:17 +01:00
const measuredDownStreamP = this . _readMeasurement ( "pressure" , "measured" , "downstream" , FORMULA _UNITS . pressure ) ;
const measuredFlow = this . _readMeasurement ( "flow" , "measured" , "downstream" , FORMULA _UNITS . flow ) ;
const predictedFlow = this . _readMeasurement ( "flow" , "predicted" , "downstream" , FORMULA _UNITS . flow ) ;
const activeFlow = Number . isFinite ( predictedFlow ) ? predictedFlow : measuredFlow ;
2025-07-31 09:07:11 +02:00
// update predicted flow measurement
2026-03-11 11:13:17 +01:00
this . updateDeltaPKlep ( activeFlow , this . kv , measuredDownStreamP , this . rho , this . T ) ; //update deltaP based on new flow
2025-07-31 09:07:11 +02:00
break ;
case ( "predicted" ) :
// put value in measurements container
2026-03-11 11:13:17 +01:00
this . _writeMeasurement ( "pressure" , "predicted" , position , Number ( value ) , unit ) ;
const predictedDownStreamP = this . _readMeasurement ( "pressure" , "predicted" , "downstream" , FORMULA _UNITS . pressure ) ;
const measuredFlowFromPred = this . _readMeasurement ( "flow" , "measured" , "downstream" , FORMULA _UNITS . flow ) ;
const predictedFlowFromPred = this . _readMeasurement ( "flow" , "predicted" , "downstream" , FORMULA _UNITS . flow ) ;
const activeFlowFromPred = Number . isFinite ( predictedFlowFromPred ) ? predictedFlowFromPred : measuredFlowFromPred ;
this . updateDeltaPKlep ( activeFlowFromPred , this . kv , predictedDownStreamP , this . rho , this . T ) ; //update deltaP based on new flow
2025-07-31 09:07:11 +02:00
break ;
default :
this . logger . warn ( ` Unrecognized variant ' ${ variant } ' for flow update. ` ) ;
break ;
}
}
2026-03-11 11:13:17 +01:00
updateMeasurement ( variant , subType , value , position , unit ) {
2025-07-24 13:14:19 +02:00
this . logger . debug ( ` ---------------------- updating ${ subType } ------------------ ` ) ;
switch ( subType ) {
case "pressure" :
// Update pressure measurement
2026-03-11 11:13:17 +01:00
this . updatePressure ( variant , value , position , unit || this . unitPolicy . output . pressure ) ;
2025-07-24 13:14:19 +02:00
break ;
case "flow" :
2026-03-11 11:13:17 +01:00
this . updateFlow ( variant , value , position , unit || this . unitPolicy . output . flow ) ;
2025-07-24 13:14:19 +02:00
break ;
case "power" :
// Update power measurement
break ;
default :
this . logger . error ( ` Type ' ${ subType } ' not recognized for measured update. ` ) ;
return ;
}
}
2025-05-14 10:06:08 +02:00
2026-03-11 11:13:17 +01:00
// NOTE: q in m3/h (normalized basis), downstreamP in mbar(g), temp in K
2025-05-14 10:06:08 +02:00
updateDeltaPKlep ( q , kv , downstreamP , rho , temp ) {
2026-03-11 11:13:17 +01:00
const result = this . hydraulicModel . calculateDeltaPMbar ( {
qM3h : q ,
kv ,
downstreamGaugeMbar : downstreamP ,
rho ,
tempK : temp ,
} ) ;
if ( ! result || ! Number . isFinite ( result . deltaPMbar ) ) {
return ;
}
2025-05-14 10:06:08 +02:00
2026-03-11 11:13:17 +01:00
const deltaP = result . deltaPMbar ;
this . deltaPKlep = deltaP ;
this . hydraulicDiagnostics = result . details || null ;
2025-07-31 09:07:11 +02:00
2026-03-11 11:13:17 +01:00
this . _writeMeasurement ( "pressure" , "predicted" , "delta" , deltaP , this . unitPolicy . output . pressure ) ;
this . logger . info ( 'DeltaP updated to: ' + deltaP ) ;
2025-05-14 10:06:08 +02:00
2026-03-11 11:13:17 +01:00
this . emitter . emit ( 'deltaPChange' , deltaP ) ; // Emit event to notify valveGroupController of deltaP change
this . logger . info ( 'DeltaPChange emitted to valveGroupController' ) ;
}
2025-05-14 10:06:08 +02:00
2025-07-24 13:14:19 +02:00
// Als er een nieuwe flow door de klep komt doordat de machines harder zijn gaan werken, dan update deze functie dit ook in de valve attributes en measurements
2026-03-11 11:13:17 +01:00
updateFlow ( variant , value , position , unit = this . unitPolicy . output . flow ) {
2025-07-31 09:07:11 +02:00
if ( value === null || value === undefined ) {
this . logger . warn ( ` Received null or undefined value for flow update. Variant: ${ variant } , Position: ${ position } ` ) ;
return ;
}
this . logger . debug ( ` Updating flow: variant= ${ variant } , value= ${ value } , position= ${ position } ` ) ;
2025-07-24 13:14:19 +02:00
switch ( variant ) {
case ( "measured" ) :
2025-07-31 09:07:11 +02:00
// put value in measurements container
2026-03-11 11:13:17 +01:00
this . _writeMeasurement ( "flow" , "measured" , position , Number ( value ) , unit ) ;
2025-07-31 09:07:11 +02:00
// get latest downstream pressure measurement
2026-03-11 11:13:17 +01:00
const measuredDownStreamP = this . _readMeasurement ( "pressure" , "measured" , "downstream" , FORMULA _UNITS . pressure ) ;
const measuredFlow = this . _readMeasurement ( "flow" , "measured" , position , FORMULA _UNITS . flow ) ;
2025-07-31 09:07:11 +02:00
// update predicted flow measurement
2026-03-11 11:13:17 +01:00
this . updateDeltaPKlep ( measuredFlow , this . kv , measuredDownStreamP , this . rho , this . T ) ; //update deltaP based on new flow
2025-07-24 13:14:19 +02:00
break ;
case ( "predicted" ) :
2025-07-31 09:07:11 +02:00
// put value in measurements container
2026-03-11 11:13:17 +01:00
this . _writeMeasurement ( "flow" , "predicted" , position , Number ( value ) , unit ) ;
const predictedDownStreamP = this . _readMeasurement ( "pressure" , "measured" , "downstream" , FORMULA _UNITS . pressure ) ;
const predictedFlow = this . _readMeasurement ( "flow" , "predicted" , position , FORMULA _UNITS . flow ) ;
this . updateDeltaPKlep ( predictedFlow , this . kv , predictedDownStreamP , this . rho , this . T ) ; //update deltaP based on new flow
2025-07-24 13:14:19 +02:00
break ;
default :
this . logger . warn ( ` Unrecognized variant ' ${ variant } ' for flow update. ` ) ;
break ;
}
2025-05-14 10:06:08 +02:00
}
updatePosition ( ) { //update alle parameters nadat er een verandering is geweest in stand van klep
if ( this . state . getCurrentState ( ) == "operational" || this . state . getCurrentState ( ) == "accelerating" || this . state . getCurrentState ( ) == "decelerating" ) {
this . logger . debug ( 'Calculating new deltaP' ) ;
const currentPosition = this . state . getCurrentPosition ( ) ;
2026-03-11 11:13:17 +01:00
const measuredFlow = this . _readMeasurement ( "flow" , "measured" , "downstream" , FORMULA _UNITS . flow ) ;
const predictedFlow = this . _readMeasurement ( "flow" , "predicted" , "downstream" , FORMULA _UNITS . flow ) ;
const currentFlow = Number . isFinite ( predictedFlow ) ? predictedFlow : measuredFlow ;
2025-07-31 09:07:11 +02:00
2026-03-11 11:13:17 +01:00
const downstreamP = this . _readMeasurement ( "pressure" , "measured" , "downstream" , FORMULA _UNITS . pressure ) ;
2025-05-14 10:06:08 +02:00
const x = currentPosition ; // dit is de positie van de klep waarvoor we delta P willen berekenen
2026-03-11 11:13:17 +01:00
const y = this . _predictKvForPosition ( x ) ; // haal de waarde van kv op uit de supplierscurve
2025-05-14 10:06:08 +02:00
this . kv = y ; //update de kv waarde in de valve class
this . logger . debug ( ` Kv value for position valve ${ x } is ${ this . kv } ` ) ; // log de waarde van kv
2025-07-24 13:14:19 +02:00
this . updateDeltaPKlep ( currentFlow , this . kv , downstreamP , this . rho , this . T ) ; //update deltaP
2025-05-14 10:06:08 +02:00
}
}
2026-03-11 11:13:17 +01:00
showCurve ( ) {
return {
model : this . model || null ,
serviceType : this . serviceType ,
expectedServiceType : this . expectedServiceType ,
gasChokedRatioLimit : this . hydraulicModel ? . gasChokedRatioLimit ,
selectedDensity : this . curveSelection ? . densityKey ? ? null ,
selectedDiameter : this . curveSelection ? . diameterKey ? ? null ,
curve : this . predictKv ? . currentFxyCurve ? . [ this . predictKv ? . fDimension ] || null ,
hydraulics : this . hydraulicDiagnostics || null ,
} ;
}
destroy ( ) {
if ( this . _onPositionChange && this . state ? . emitter ? . off ) {
this . state . emitter . off ( "positionChange" , this . _onPositionChange ) ;
}
for ( const { emitter , handler } of this . _fluidContractListeners . values ( ) ) {
if ( typeof emitter ? . off === 'function' ) {
emitter . off ( 'fluidContractChange' , handler ) ;
} else if ( typeof emitter ? . removeListener === 'function' ) {
emitter . removeListener ( 'fluidContractChange' , handler ) ;
}
}
this . _fluidContractListeners . clear ( ) ;
}
2025-05-14 10:06:08 +02:00
getOutput ( ) {
// Improved output object generation
const output = { } ;
//build the output object
2026-03-11 11:13:17 +01:00
Object . entries ( this . measurements . measurements || { } ) . forEach ( ( [ type , variants ] ) => {
Object . entries ( variants || { } ) . forEach ( ( [ variant , positions ] ) => {
Object . keys ( positions || { } ) . forEach ( ( position ) => {
const value = this . _readMeasurement ( type , variant , position , this . _outputUnitForType ( type ) ) ;
if ( value != null ) {
output [ ` ${ position } _ ${ variant } _ ${ type } ` ] = value ;
}
} ) ;
2025-05-14 10:06:08 +02:00
} ) ;
} ) ;
//fill in the rest of the output object
output [ "state" ] = this . state . getCurrentState ( ) ;
output [ "percentageOpen" ] = this . state . getCurrentPosition ( ) ;
output [ "moveTimeleft" ] = this . state . getMoveTimeLeft ( ) ;
output [ "mode" ] = this . currentMode ;
//this.logger.debug(`Output: ${JSON.stringify(output)}`);
return output ;
}
}
module . exports = Valve ;