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' ) ;
2025-07-24 13:14:19 +02:00
const { loadCurve , logger , configUtils , configManager , state , nrmse , MeasurementContainer , predict , interpolation , childRegistrationUtils } = require ( 'generalFunctions' ) ;
2025-05-14 10:06:08 +02:00
class Valve {
constructor ( valveConfig = { } , stateConfig = { } ) {
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 ) ;
// Load a specific curve
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 ) ;
2025-05-14 10:06:08 +02:00
// Initialize measurements
this . measurements = new MeasurementContainer ( ) ;
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
this . kv = 0 ; //default
this . rho = 1 , 225 //dichtheid van lucht standaard
this . T = 293 ; // temperatuur in K standaard
this . downstreamP = 0.54 //hardcodes for now --> assumed to be constant watercolumn and deltaP diffuser
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
this . state . emitter . on ( "positionChange" , ( data ) => {
this . logger . debug ( ` Position change detected: ${ data } ` ) ;
this . updatePosition ( ) } ) ; //To update deltaP
2025-07-24 13:14:19 +02:00
this . childRegistrationUtils = new childRegistrationUtils ( this ) ; // Child registration utility
this . vCurve = this . curve [ 1.204 ] ; // specificy the desired density RECALC THIS AUTOMTICALLY BASED ON DENSITY OF AIR LATER OLIFANT!!
this . predictKv = new predict ( { curve : this . vCurve } ) ; // load valve size (x : ctrl , y : kv relationship)
2025-10-14 14:08:04 +02:00
//this.logger.debug(`PredictKv initialized with curve: ${JSON.stringify(this.predictKv)}`);
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 } '. ` ) ;
await this . executeSequence ( "emergencyStop" ) ;
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 ) {
const availableModes = defaultConfig . mode . current . rules . values . map ( v => v . value ) ;
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
}
}
}
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 ) {
console . error ( ` Error setting setpoint: ${ error } ` ) ;
}
}
2025-07-31 09:07:11 +02:00
updatePressure ( variant , value , position ) {
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 pressure: variant= ${ variant } , value= ${ value } , position= ${ position } ` ) ;
switch ( variant ) {
case ( "measured" ) :
// put value in measurements container
console . log ( 'wtf ... ' + value ) ;
this . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( position ) . value ( value ) ;
// get latest downstream pressure measurement
const measuredDownStreamP = this . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "downstream" ) . getCurrentValue ( ) ; //update downstream pressure measurement
// update predicted flow measurement
this . updateDeltaPKlep ( value , this . kv , measuredDownStreamP , this . rho , this . T ) ; //update deltaP based on new flow
break ;
case ( "predicted" ) :
// put value in measurements container
this . measurements . type ( "pressure" ) . variant ( "predicted" ) . position ( position ) . value ( value ) ;
const predictedDownStreamP = this . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "downstream" ) . getCurrentValue ( ) ; //update downstream pressure measurement
this . updateDeltaPKlep ( value , this . kv , predictedDownStreamP , this . rho , this . T ) ; //update deltaP based on new flow
break ;
default :
this . logger . warn ( ` Unrecognized variant ' ${ variant } ' for flow update. ` ) ;
break ;
}
}
2025-07-24 13:14:19 +02:00
updateMeasurement ( variant , subType , value , position ) {
this . logger . debug ( ` ---------------------- updating ${ subType } ------------------ ` ) ;
switch ( subType ) {
case "pressure" :
// Update pressure measurement
2025-07-31 09:07:11 +02:00
this . updatePressure ( variant , value , position ) ;
2025-07-24 13:14:19 +02:00
break ;
case "flow" :
this . updateFlow ( variant , value , position ) ;
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
// NOTE: Omdat met zeer kleine getallen wordt gewerkt en er kwadraten in de formule zitten kan het zijn dat we alles *1000 moeten doen
updateDeltaPKlep ( q , kv , downstreamP , rho , temp ) {
//q must be in Nm3/h
//temp must be in K
//q must be in m3/h
//downstreamP must be in bar so transfer from mbar to bar
downstreamP = downstreamP / 1000 ;
//convert downstreamP to absolute bar
downstreamP += 1.01325 ;
2025-07-31 09:07:11 +02:00
if ( kv !== 0 && downstreamP != 0 && q != 0 ) { //check if kv and downstreamP are not zero to avoid division by zero
//calculate deltaP
let deltaP = ( q * * 2 * rho * temp ) / ( 514 * * 2 * kv * * 2 * downstreamP ) ;
//convert deltaP to mbar
deltaP = deltaP * 1000 ;
2025-05-14 10:06:08 +02:00
2025-07-31 09:07:11 +02:00
// Synchroniseer deltaP met het Valve-object
this . deltaPKlep = deltaP
2025-05-14 10:06:08 +02:00
2025-07-31 09:07:11 +02:00
// Opslaan in measurement container
this . measurements . type ( "pressure" ) . variant ( "predicted" ) . position ( "delta" ) . value ( deltaP ) ;
this . logger . info ( 'DeltaP updated to: ' + deltaP ) ;
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
updateFlow ( variant , value , position ) {
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
this . measurements . type ( "flow" ) . variant ( "measured" ) . position ( position ) . value ( value ) ;
// get latest downstream pressure measurement
const measuredDownStreamP = this . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "downstream" ) . getCurrentValue ( ) ; //update downstream pressure measurement
// update predicted flow measurement
this . updateDeltaPKlep ( value , 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
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( position ) . value ( value ) ;
const predictedDownStreamP = this . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "downstream" ) . getCurrentValue ( ) ; //update downstream pressure measurement
this . updateDeltaPKlep ( value , 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 ( ) ;
2025-07-31 09:07:11 +02:00
const measuredFlow = this . measurements . type ( "flow" ) . variant ( "measured" ) . position ( "downstream" ) . getCurrentValue ( ) ; // haal de flow op uit de measurement containe
const predictedFlow = this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . getCurrentValue ( ) ; // haal de predicted flow op uit de measurement container
const currentFlow = predictedFlow ;
2025-07-24 13:14:19 +02:00
const downstreamP = this . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "downstream" ) . getCurrentValue ( ) ; // haal de downstream pressure op uit de measurement container
2025-05-14 10:06:08 +02:00
//const valveSize = 125; //NOTE: nu nog hardcoded maar moet een attribute van de valve worden
this . predictKv . fDimension = 125 ; //load valve size by defining fdimension in predict class
const x = currentPosition ; // dit is de positie van de klep waarvoor we delta P willen berekenen
const y = this . predictKv . y ( x ) ; // haal de waarde van kv op uit de spline
this . kv = y ; //update de kv waarde in de valve class
if ( this . kv < 0.1 ) {
this . kv = 0.1 ; //minimum waarde voor kv
}
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
}
}
getOutput ( ) {
// Improved output object generation
const output = { } ;
//build the output object
this . measurements . getTypes ( ) . forEach ( type => {
this . measurements . getVariants ( ) . forEach ( variant => {
this . measurements . getPositions ( ) . forEach ( position => {
const value = this . measurements . type ( type ) . variant ( variant ) . position ( position ) . getCurrentValue ( ) ; //get the current value of the measurement
if ( value != null ) {
output [ ` ${ position } _ ${ variant } _ ${ type } ` ] = value ;
}
} ) ;
} ) ;
} ) ;
//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 ;