2025-07-01 17:03:36 +02:00
//load local dependencies
const EventEmitter = require ( "events" ) ;
const { logger , configUtils , configManager , MeasurementContainer , interpolation , childRegistrationUtils } = require ( 'generalFunctions' ) ;
class MachineGroup {
constructor ( machineGroupConfig = { } ) {
this . emitter = new EventEmitter ( ) ; // Own EventEmitter
this . configManager = new configManager ( ) ; // Config manager to handle dynamic config loading
this . defaultConfig = this . configManager . getConfig ( 'machineGroupControl' ) ; // Load default config for rotating machine ( use software type name ? )
this . configUtils = new configUtils ( this . defaultConfig ) ; // this will handle the config endpoints so we can load them dynamically
this . config = this . configUtils . initConfig ( machineGroupConfig ) ; // verify and set the config for the machine group
// Init after config is set
this . logger = new logger ( this . config . general . logging . enabled , this . config . general . logging . logLevel , this . config . general . name ) ;
// Initialize measurements
this . measurements = new MeasurementContainer ( ) ;
this . interpolation = new interpolation ( ) ;
2025-07-31 09:10:34 +02:00
// Machines and child data
2025-07-01 17:03:36 +02:00
this . machines = { } ;
this . child = { } ;
this . scaling = this . config . scaling . current ;
this . mode = this . config . mode . current ;
this . absDistFromPeak = 0 ;
this . relDistFromPeak = 0 ;
// Combination curve data
this . dynamicTotals = { flow : { min : Infinity , max : 0 } , power : { min : Infinity , max : 0 } , NCog : 0 } ;
this . absoluteTotals = { flow : { min : Infinity , max : 0 } , power : { min : Infinity , max : 0 } } ;
//this always last in the constructor
this . childRegistrationUtils = new childRegistrationUtils ( this ) ;
this . logger . info ( "MachineGroup initialized." ) ;
}
2025-09-04 17:07:18 +02:00
registerChild ( child , softwareType ) {
this . logger . debug ( 'Setting up childs specific for this class' ) ;
if ( softwareType == "machine" ) {
// Check if the machine is already registered
this . machines [ child . config . general . id ] === undefined ? this . machines [ child . config . general . id ] = child : this . logger . warn ( ` Machine ${ child . config . general . id } is already registered. ` ) ;
2025-09-23 15:50:40 +02:00
//listen for machine pressure changes
this . logger . debug ( ` Listening for pressure changes from machine ${ child . config . general . id } ` ) ;
child . measurements . emitter . on ( "pressure.measured.differential" , ( eventData ) => {
2025-10-02 17:08:41 +02:00
this . logger . debug ( ` Pressure update from ${ child . config . general . id } : ${ eventData . value } ${ eventData . unit } ` ) ;
this . handlePressureChange ( ) ;
} ) ;
child . measurements . emitter . on ( "pressure.measured.downstream" , ( eventData ) => {
this . logger . debug ( ` Pressure update from ${ child . config . general . id } : ${ eventData . value } ${ eventData . unit } ` ) ;
this . handlePressureChange ( ) ;
} ) ;
child . measurements . emitter . on ( "flow.predicted.downstream" , ( eventData ) => {
this . logger . debug ( ` Flow prediction update from ${ child . config . general . id } : ${ eventData . value } ${ eventData . unit } ` ) ;
//later change to this.handleFlowPredictionChange();
this . handlePressureChange ( ) ;
2025-09-23 15:50:40 +02:00
} ) ;
2025-09-23 11:19:22 +02:00
2025-10-02 17:08:41 +02:00
2025-09-04 17:07:18 +02:00
}
}
2025-08-07 13:52:56 +02:00
2025-07-01 17:03:36 +02:00
calcAbsoluteTotals ( ) {
const absoluteTotals = { flow : { min : Infinity , max : 0 } , power : { min : Infinity , max : 0 } } ;
Object . values ( this . machines ) . forEach ( machine => {
const totals = { flow : { min : Infinity , max : 0 } , power : { min : Infinity , max : 0 } } ;
//fetch min flow ever seen over all machines
Object . entries ( machine . predictFlow . inputCurve ) . forEach ( ( [ pressure , xyCurve ] , index ) => {
const minFlow = Math . min ( ... xyCurve . y ) ;
const maxFlow = Math . max ( ... xyCurve . y ) ;
const minPower = Math . min ( ... machine . predictPower . inputCurve [ pressure ] . y ) ;
const maxPower = Math . max ( ... machine . predictPower . inputCurve [ pressure ] . y ) ;
// min ever seen for 1 machine
if ( minFlow < totals . flow . min ) { totals . flow . min = minFlow ; }
if ( minPower < totals . power . min ) { totals . power . min = minPower ; }
if ( maxFlow > totals . flow . max ) { totals . flow . max = maxFlow ; }
if ( maxPower > totals . power . max ) { totals . power . max = maxPower ; }
} ) ;
2025-10-02 17:08:41 +02:00
2025-07-01 17:03:36 +02:00
//surplus machines for max flow and power
if ( totals . flow . min < absoluteTotals . flow . min ) { absoluteTotals . flow . min = totals . flow . min ; }
if ( totals . power . min < absoluteTotals . power . min ) { absoluteTotals . power . min = totals . power . min ; }
absoluteTotals . flow . max += totals . flow . max ;
absoluteTotals . power . max += totals . power . max ;
} ) ;
2025-10-02 17:08:41 +02:00
if ( absoluteTotals . flow . min === Infinity ) {
this . logger . warn ( ` Flow min ${ absoluteTotals . flow . min } is Infinity. Setting to 0. ` ) ;
absoluteTotals . flow . min = 0 ;
}
if ( absoluteTotals . power . min === Infinity ) {
this . logger . warn ( ` Power min ${ absoluteTotals . power . min } is Infinity. Setting to 0. ` ) ;
absoluteTotals . power . min = 0 ;
}
if ( absoluteTotals . flow . max === - Infinity ) {
this . logger . warn ( ` Flow max ${ absoluteTotals . flow . max } is -Infinity. Setting to 0. ` ) ;
absoluteTotals . flow . max = 0 ;
}
if ( absoluteTotals . power . max === - Infinity ) {
this . logger . warn ( ` Power max ${ absoluteTotals . power . max } is -Infinity. Setting to 0. ` ) ;
absoluteTotals . power . max = 0 ;
}
// Place data in object for external use
this . absoluteTotals = absoluteTotals ;
2025-07-01 17:03:36 +02:00
return absoluteTotals ;
}
//max and min current flow and power based on their actual pressure curve
calcDynamicTotals ( ) {
2025-10-02 17:08:41 +02:00
const dynamicTotals = { flow : { min : Infinity , max : 0 , act : 0 } , power : { min : Infinity , max : 0 , act : 0 } , NCog : 0 } ;
2025-07-01 17:03:36 +02:00
2025-09-23 15:50:40 +02:00
this . logger . debug ( ` \n --------- Calculating dynamic totals for ${ Object . keys ( this . machines ) . length } machines. @ current pressure settings : ---------- ` ) ;
2025-10-02 17:08:41 +02:00
2025-07-01 17:03:36 +02:00
Object . values ( this . machines ) . forEach ( machine => {
2025-09-23 15:50:40 +02:00
this . logger . debug ( ` Processing machine with id: ${ machine . config . general . id } ` ) ;
this . logger . debug ( ` Current pressure settings: ${ JSON . stringify ( machine . predictFlow . currentF ) } ` ) ;
2025-07-01 17:03:36 +02:00
//fetch min flow ever seen over all machines
const minFlow = machine . predictFlow . currentFxyYMin ;
const maxFlow = machine . predictFlow . currentFxyYMax ;
const minPower = machine . predictPower . currentFxyYMin ;
const maxPower = machine . predictPower . currentFxyYMax ;
2025-10-02 17:08:41 +02:00
const actFlow = machine . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . getCurrentValue ( ) ;
const actPower = machine . measurements . type ( "power" ) . variant ( "predicted" ) . position ( "atEquipment" ) . getCurrentValue ( ) ;
this . logger . debug ( ` Machine ${ machine . config . general . id } - Min Flow: ${ minFlow } , Max Flow: ${ maxFlow } , Min Power: ${ minPower } , Max Power: ${ maxPower } , NCog: ${ machine . NCog } ` ) ;
2025-07-01 17:03:36 +02:00
if ( minFlow < dynamicTotals . flow . min ) { dynamicTotals . flow . min = minFlow ; }
if ( minPower < dynamicTotals . power . min ) { dynamicTotals . power . min = minPower ; }
dynamicTotals . flow . max += maxFlow ;
dynamicTotals . power . max += maxPower ;
2025-10-02 17:08:41 +02:00
dynamicTotals . flow . act += actFlow ;
dynamicTotals . power . act += actPower ;
2025-07-01 17:03:36 +02:00
//fetch total Normalized Cog over all machines
dynamicTotals . NCog += machine . NCog ;
} ) ;
2025-10-02 17:08:41 +02:00
// Place data in object for external use
this . dynamicTotals = dynamicTotals ;
2025-07-01 17:03:36 +02:00
return dynamicTotals ;
}
activeTotals ( ) {
const totals = { flow : { min : 0 , max : 0 } , power : { min : 0 , max : 0 } , countActiveMachines : 0 } ;
Object . entries ( this . machines ) . forEach ( ( [ id , machine ] ) => {
this . logger . debug ( ` Processing machine with id: ${ id } ` ) ;
if ( this . isMachineActive ( id ) ) {
//fetch min flow ever seen over all machines
const minFlow = machine . predictFlow . currentFxyYMin ;
const maxFlow = machine . predictFlow . currentFxyYMax ;
const minPower = machine . predictPower . currentFxyYMin ;
const maxPower = machine . predictPower . currentFxyYMax ;
totals . flow . min += minFlow ;
totals . flow . max += maxFlow ;
totals . power . min += minPower ;
totals . power . max += maxPower ;
totals . countActiveMachines ++ ;
}
} ) ;
return totals ;
}
handlePressureChange ( ) {
2025-10-02 17:08:41 +02:00
this . logger . info ( "---------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>Pressure change detected." ) ;
// Recalculate totals
const { flow , power } = this . calcDynamicTotals ( ) ;
this . logger . debug ( ` Dynamic Totals after pressure change - Flow: Min ${ flow . min } , Max ${ flow . max } , Act ${ flow . act } | Power: Min ${ power . min } , Max ${ power . max } , Act ${ power . act } ` ) ;
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( flow . act ) ;
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( power . act ) ;
2025-07-01 17:03:36 +02:00
const { maxEfficiency , lowestEfficiency } = this . calcGroupEfficiency ( this . machines ) ;
2025-10-02 17:08:41 +02:00
const efficiency = this . measurements . type ( "efficiency" ) . variant ( "predicted" ) . position ( "atEquipment" ) . getCurrentValue ( ) ;
2025-07-01 17:03:36 +02:00
this . calcDistanceBEP ( efficiency , maxEfficiency , lowestEfficiency ) ;
}
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 ;
}
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 } ;
}
checkSpecialCases ( machines , Qd ) {
Object . values ( machines ) . forEach ( machine => {
2025-09-23 11:19:22 +02:00
2025-07-01 17:03:36 +02:00
const state = machine . state . getCurrentState ( ) ;
const mode = machine . currentMode ;
//add special cases
if ( state === "operational" && ( mode == "virtualControl" || mode === "fysicalControl" ) ) {
let flow = 0 ;
if ( machine . measurements . type ( "flow" ) . variant ( "measured" ) . position ( "downstream" ) . getCurrentValue ( ) ) {
flow = machine . measurements . type ( "flow" ) . variant ( "measured" ) . position ( "downstream" ) . getCurrentValue ( ) ;
}
else if ( machine . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . getCurrentValue ( ) ) {
flow = machine . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . getCurrentValue ( ) ;
}
else {
this . logger . error ( "Dont perform calculation at all seeing that there is a machine working but we dont know the flow its producing" ) ;
//abort the calculation
return false ;
}
//Qd is less because we allready have machines delivering flow on manual control
Qd = Qd - flow ;
}
} ) ;
return Qd ;
}
validPumpCombinations ( machines , Qd , PowerCap = Infinity ) {
let subsets = [ [ ] ] ;
// adjust demand flow when there are machines being controlled by a manual source
Qd = this . checkSpecialCases ( machines , Qd ) ;
// Generate all possible subsets of machines (power set)
Object . keys ( machines ) . forEach ( machineId => {
const state = machines [ machineId ] . state . getCurrentState ( ) ;
const validActionForMode = machines [ machineId ] . isValidActionForMode ( "execSequence" , "auto" ) ;
// Reasons why a machine is not valid for the combination
2025-09-23 15:03:57 +02:00
if ( state === "off" || state === "coolingdown" || state === "stopping" || state === "emergencystop" || ! validActionForMode ) {
2025-07-01 17:03:36 +02:00
return ;
}
// go through each machine and add it to the subsets
let newSubsets = subsets . map ( set => [ ... set , machineId ] ) ;
subsets = subsets . concat ( newSubsets ) ;
} ) ;
// Filter for non-empty subsets that can meet or exceed demand flow
const combinations = subsets . filter ( subset => {
if ( subset . length === 0 ) return false ;
// Calculate total and minimum flow for the subset in one pass
const { maxFlow , minFlow , maxPower } = subset . reduce (
( acc , machineId ) => {
const machine = machines [ machineId ] ;
const minFlow = machine . predictFlow . currentFxyYMin ;
const maxFlow = machine . predictFlow . currentFxyYMax ;
const maxPower = machine . predictPower . currentFxyYMax ;
return {
maxFlow : acc . maxFlow + maxFlow ,
minFlow : acc . minFlow + minFlow ,
maxPower : acc . maxPower + maxPower
} ;
} ,
{ maxFlow : 0 , minFlow : 0 , maxPower : 0 }
) ;
// If total flow can deliver the demand
if ( maxFlow >= Qd && minFlow <= Qd && maxPower <= PowerCap ) {
return true ;
}
else {
return false ;
}
} ) ;
return combinations ;
}
calcBestCombination ( combinations , Qd ) {
let bestCombination = null ;
//keep track of totals
let bestPower = Infinity ;
let bestFlow = 0 ;
let bestCog = 0 ;
combinations . forEach ( combination => {
let flowDistribution = [ ] ; // Stores the flow distribution for the best combination
let totalCoG = 0 ;
let totalPower = 0 ;
let totalFlow = 0 ;
// Calculate the total CoG for the current combination
combination . forEach ( machineId => { totalCoG += ( Math . round ( this . machines [ machineId ] . NCog * 100 ) / 100 ) ; } ) ;
// Calculate the total power for the current combination
combination . forEach ( machineId => {
let flow = 0 ;
// Prevent division by zero
if ( totalCoG === 0 ) {
// Distribute flow equally among all pumps
flow = Qd / combination . length ;
} else {
// Normal CoG-based distribution
flow = ( this . machines [ machineId ] . NCog / totalCoG ) * Qd ;
this . logger . debug ( ` Machine Normalized CoG-based distribution ${ machineId } flow: ${ flow } ` ) ;
}
totalFlow += flow ;
totalPower += this . machines [ machineId ] . inputFlowCalcPower ( flow ) ;
flowDistribution . push ( { machineId : machineId , flow : flow } ) ;
} ) ;
// Update the best combination if the current one is better
if ( totalPower < bestPower ) {
this . logger . debug ( ` New best combination found: ${ totalPower } < ${ bestPower } ` ) ;
this . logger . debug ( ` combination ${ JSON . stringify ( flowDistribution ) } ` ) ;
bestPower = totalPower ;
bestFlow = totalFlow ;
bestCog = totalCoG ;
bestCombination = flowDistribution ;
}
} ) ;
return { bestCombination , bestPower , bestFlow , bestCog } ;
}
// -------- Mode and Input Management -------- //
isValidActionForMode ( action , mode ) {
const allowedActionsSet = this . config . mode . allowedActions [ mode ] || [ ] ;
return allowedActionsSet . has ( action ) ;
}
setScaling ( scaling ) {
2025-09-23 11:19:22 +02:00
const scalingSet = new Set ( this . defaultConfig . scaling . current . rules . values . map ( ( value ) => value . value ) ) ;
2025-07-01 17:03:36 +02:00
scalingSet . has ( scaling ) ? this . scaling = scaling : this . logger . warn ( ` ${ scaling } is not a valid scaling option. ` ) ;
2025-09-23 11:19:22 +02:00
this . logger . debug ( ` Scaling set to: ${ scaling } ` ) ;
2025-07-01 17:03:36 +02:00
}
2025-10-02 17:08:41 +02:00
async abortActiveMovements ( reason = "new demand" ) {
await Promise . all ( Object . values ( this . machines ) . map ( async machine => {
this . logger . warn ( ` Aborting active movements for machine ${ machine . config . general . id } due to: ${ reason } ` ) ;
if ( typeof machine . abortMovement === "function" ) {
await machine . abortMovement ( reason ) ;
}
} ) ) ;
}
2025-07-01 17:03:36 +02:00
//handle input from parent / user / UI
async optimalControl ( Qd , powerCap = Infinity ) {
2025-10-02 17:08:41 +02:00
2025-07-01 17:03:36 +02:00
try {
//we need to force the pressures of all machines to be equal to the highest pressure measured in the group
// this is to ensure a correct evaluation of the flow and power consumption
const pressures = Object . entries ( this . machines ) . map ( ( [ machineId , machine ] ) => {
return {
downstream : machine . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "downstream" ) . getCurrentValue ( ) ,
upstream : machine . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "upstream" ) . getCurrentValue ( )
} ;
} ) ;
const maxDownstream = Math . max ( ... pressures . map ( p => p . downstream ) ) ;
const minUpstream = Math . min ( ... pressures . map ( p => p . upstream ) ) ;
2025-10-02 17:08:41 +02:00
this . logger . debug ( ` Max downstream pressure: ${ maxDownstream } , Min upstream pressure: ${ minUpstream } ` ) ;
2025-07-01 17:03:36 +02:00
//set the pressures
Object . entries ( this . machines ) . forEach ( ( [ machineId , machine ] ) => {
if ( machine . state . getCurrentState ( ) !== "operational" && machine . state . getCurrentState ( ) !== "accelerating" && machine . state . getCurrentState ( ) !== "decelerating" ) {
2025-10-02 17:08:41 +02:00
//Equilize pressures over all machines so we can make a proper calculation
2025-07-01 17:03:36 +02:00
machine . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "downstream" ) . value ( maxDownstream ) ;
machine . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "upstream" ) . value ( minUpstream ) ;
2025-10-02 17:08:41 +02:00
2025-07-01 17:03:36 +02:00
// after updating the measurement directly we need to force the update of the value OLIFANT this is not so clear now in the code
// we need to find a better way to do this but for now it works
machine . getMeasuredPressure ( ) ;
}
} ) ;
2025-10-02 17:08:41 +02:00
//fetch dynamic totals
const dynamicTotals = this . dynamicTotals ;
2025-07-01 17:03:36 +02:00
const machineStates = Object . entries ( this . machines ) . reduce ( ( acc , [ machineId , machine ] ) => {
acc [ machineId ] = machine . state . getCurrentState ( ) ;
return acc ;
} , { } ) ;
if ( Qd <= 0 ) {
//if Qd is 0 turn all machines off and exit early
}
if ( Qd < dynamicTotals . flow . min && Qd > 0 ) {
//Capping Qd to lowest possible value
this . logger . warn ( ` Flow demand ${ Qd } is below minimum possible flow ${ dynamicTotals . flow . min } . Capping to minimum flow. ` ) ;
Qd = dynamicTotals . flow . min ;
}
else if ( Qd > dynamicTotals . flow . max ) {
//Capping Qd to highest possible value
this . logger . warn ( ` Flow demand ${ Qd } is above maximum possible flow ${ dynamicTotals . flow . max } . Capping to maximum flow. ` ) ;
Qd = dynamicTotals . flow . max ;
}
// fetch all valid combinations that meet expectations
2025-10-02 17:08:41 +02:00
const combinations = this . validPumpCombinations ( this . machines , Qd , powerCap ) ;
2025-07-01 17:03:36 +02:00
const bestResult = this . calcBestCombination ( combinations , Qd ) ;
if ( bestResult . bestCombination === null ) {
this . logger . warn ( ` Demand: ${ Qd . toFixed ( 2 ) } -> No valid combination found => not updating control ` ) ;
return ;
}
2025-10-02 17:08:41 +02:00
2025-07-01 17:03:36 +02:00
const debugInfo = bestResult . bestCombination . map ( ( { machineId , flow } ) => ` ${ machineId } : ${ flow . toFixed ( 2 ) } units ` ) . join ( " | " ) ;
this . logger . debug ( ` Moving to demand: ${ Qd . toFixed ( 2 ) } -> Pumps: [ ${ debugInfo } ] => Total Power: ${ bestResult . bestPower . toFixed ( 2 ) } ` ) ;
//store the total delivered power
2025-10-02 17:08:41 +02:00
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( bestResult . bestPower ) ;
2025-07-01 17:03:36 +02:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( bestResult . bestFlow ) ;
2025-10-02 17:08:41 +02:00
this . measurements . type ( "efficiency" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( bestResult . bestFlow / bestResult . bestPower ) ;
this . measurements . type ( "Ncog" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( bestResult . bestCog ) ;
2025-07-01 17:03:36 +02:00
await Promise . all ( Object . entries ( this . machines ) . map ( async ( [ machineId , machine ] ) => {
2025-10-02 17:08:41 +02:00
// Find the flow for this machine in the best combination
this . logger . debug ( ` Searching for machine ${ machineId } with state ${ machineStates [ machineId ] } in best combination. ` ) ;
const pumpInfo = bestResult . bestCombination . find ( item => item . machineId == machineId ) ;
2025-07-01 17:03:36 +02:00
let flow ;
if ( pumpInfo !== undefined ) {
flow = pumpInfo . flow ;
} else {
2025-10-02 17:08:41 +02:00
this . logger . debug ( ` Machine ${ machineId } not in best combination, setting flow control to 0 ` ) ;
2025-07-01 17:03:36 +02:00
flow = 0 ;
}
if ( ( flow <= 0 ) && ( machineStates [ machineId ] === "operational" || machineStates [ machineId ] === "accelerating" || machineStates [ machineId ] === "decelerating" ) ) {
await machine . handleInput ( "parent" , "execSequence" , "shutdown" ) ;
}
2025-10-02 17:08:41 +02:00
if ( machineStates [ machineId ] === "idle" && flow > 0 ) {
2025-07-01 17:03:36 +02:00
await machine . handleInput ( "parent" , "execSequence" , "startup" ) ;
2025-10-02 17:08:41 +02:00
await machine . handleInput ( "parent" , "flowMovement" , flow ) ;
2025-07-01 17:03:36 +02:00
}
2025-10-02 17:08:41 +02:00
if ( machineStates [ machineId ] === "operational" && flow > 0 ) {
2025-07-01 17:03:36 +02:00
await machine . handleInput ( "parent" , "flowMovement" , flow ) ;
}
} ) ) ;
}
catch ( err ) {
this . logger . error ( err ) ;
}
}
// Equalize pressure across all machines for machines that are not running. This is needed to ensure accurate flow and power predictions.
equalizePressure ( ) {
// Get current pressures from all machines
const pressures = Object . entries ( this . machines ) . map ( ( [ machineId , machine ] ) => {
return {
downstream : machine . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "downstream" ) . getCurrentValue ( ) ,
upstream : machine . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "upstream" ) . getCurrentValue ( )
} ;
} ) ;
// Find the highest downstream and lowest upstream pressure
const maxDownstream = Math . max ( ... pressures . map ( p => p . downstream ) ) ;
const minUpstream = Math . min ( ... pressures . map ( p => p . upstream ) ) ;
// Set consistent pressures across machines
Object . entries ( this . machines ) . forEach ( ( [ machineId , machine ] ) => {
if ( ! this . isMachineActive ( machineId ) ) {
machine . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "downstream" ) . value ( maxDownstream ) ;
machine . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( "upstream" ) . value ( minUpstream ) ;
// Update the measured pressure value
const pressure = machine . getMeasuredPressure ( ) ;
this . logger . debug ( ` Setting pressure for machine ${ machineId } to ${ pressure } ` ) ;
}
} ) ;
}
isMachineActive ( machineId ) {
if ( this . machines [ machineId ] . state . getCurrentState ( ) === "operational" || this . machines [ machineId ] . state . getCurrentState ( ) === "accelerating" || this . machines [ machineId ] . state . getCurrentState ( ) === "decelerating" ) {
return true ;
}
return false ;
}
capFlowDemand ( Qd , dynamicTotals ) {
if ( Qd < dynamicTotals . flow . min && Qd > 0 ) {
this . logger . warn ( ` Flow demand ${ Qd } is below minimum possible flow ${ dynamicTotals . flow . min } . Capping to minimum flow. ` ) ;
Qd = dynamicTotals . flow . min ;
} else if ( Qd > dynamicTotals . flow . max ) {
this . logger . warn ( ` Flow demand ${ Qd } is above maximum possible flow ${ dynamicTotals . flow . max } . Capping to maximum flow. ` ) ;
Qd = dynamicTotals . flow . max ;
}
return Qd ;
}
sortMachinesByPriority ( priorityList ) {
let machinesInPriorityOrder ;
if ( priorityList && Array . isArray ( priorityList ) ) {
machinesInPriorityOrder = priorityList
. filter ( id => this . machines [ id ] )
. map ( id => ( { id , machine : this . machines [ id ] } ) ) ;
} else {
machinesInPriorityOrder = Object . entries ( this . machines )
2025-10-02 17:08:41 +02:00
. map ( ( [ id , machine ] ) => ( { id : id , machine } ) )
2025-07-01 17:03:36 +02:00
. sort ( ( a , b ) => a . id - b . id ) ;
}
return machinesInPriorityOrder ;
}
filterOutUnavailableMachines ( list ) {
const newList = list . filter ( ( { id , machine } ) => {
const state = machine . state . getCurrentState ( ) ;
const validActionForMode = machine . isValidActionForMode ( "execSequence" , "auto" ) ;
2025-09-23 15:03:57 +02:00
return ! ( state === "off" || state === "coolingdown" || state === "stopping" || state === "emergencystop" || ! validActionForMode ) ;
2025-07-01 17:03:36 +02:00
} ) ;
return newList ;
}
calcGroupEfficiency ( machines ) {
let cumEfficiency = 0 ;
let machineCount = 0 ;
let lowestEfficiency = Infinity ;
// Calculate the average efficiency of all machines -> peak is the average of them all
Object . entries ( machines ) . forEach ( ( [ machineId , machine ] ) => {
cumEfficiency += machine . cog ;
if ( machine . cog < lowestEfficiency ) {
lowestEfficiency = machine . cog ;
}
machineCount ++ ;
} ) ;
const maxEfficiency = cumEfficiency / machineCount ;
return { maxEfficiency , lowestEfficiency } ;
}
//move machines assuming equal control in flow and a priority list
async equalFlowControl ( Qd , powerCap = Infinity , priorityList = null ) {
try {
// equalize pressure across all machines
this . equalizePressure ( ) ;
// Update dynamic totals
const dynamicTotals = this . calcDynamicTotals ( ) ;
// Cap flow demand to min/max possible values
Qd = this . capFlowDemand ( Qd , dynamicTotals ) ;
// Get machines sorted by priority
let machinesInPriorityOrder = this . sortMachinesByPriority ( priorityList ) ;
// Filter out machines that are unavailable for control
machinesInPriorityOrder = this . filterOutUnavailableMachines ( machinesInPriorityOrder ) ;
// Initialize flow distribution
let flowDistribution = [ ] ;
let totalFlow = 0 ;
let totalPower = 0 ;
let totalCog = 0 ;
const activeTotals = this . activeTotals ( ) ;
// Distribute flow equally among all available machines
switch ( true ) {
case ( Qd < activeTotals . flow . min && activeTotals . flow . min !== 0 ) : {
let availableFlow = activeTotals . flow . min ;
for ( let i = machinesInPriorityOrder . length - 1 ; i >= 0 && availableFlow > Qd ; i -- ) {
const machine = machinesInPriorityOrder [ i ] ;
if ( this . isMachineActive ( machine . id ) ) {
flowDistribution . push ( { machineId : machine . id , flow : 0 } ) ;
availableFlow -= machine . machine . predictFlow . currentFxyYMin ;
}
}
// Determine remaining active machines (not shut down).
const remainingMachines = machinesInPriorityOrder . filter (
( { id } ) =>
this . isMachineActive ( id ) &&
! flowDistribution . some ( item => item . machineId === id )
) ;
// Evenly distribute Qd among the remaining machines.
const distributedFlow = Qd / remainingMachines . length ;
for ( let machine of remainingMachines ) {
flowDistribution . push ( { machineId : machine . id , flow : distributedFlow } ) ;
totalFlow += distributedFlow ;
totalPower += machine . machine . inputFlowCalcPower ( distributedFlow ) ;
}
break ;
}
case ( Qd > activeTotals . flow . max ) :
// Case 2: Demand is above the maximum available flow.
// Start the non-active machine with the highest priority and distribute Qd over all available machines.
let i = 1 ;
while ( totalFlow < Qd && i <= machinesInPriorityOrder . length ) {
Qd = Qd / i ;
if ( machinesInPriorityOrder [ i - 1 ] . machine . predictFlow . currentFxyYMax >= Qd ) {
for ( let i2 = 0 ; i2 < i ; i2 ++ ) {
if ( ! this . isMachineActive ( machinesInPriorityOrder [ i2 ] . id ) ) {
flowDistribution . push ( { machineId : machinesInPriorityOrder [ i2 ] . id , flow : Qd } ) ;
totalFlow += Qd ;
totalPower += machinesInPriorityOrder [ i2 ] . machine . inputFlowCalcPower ( Qd ) ;
}
}
}
i ++ ;
}
break ;
default :
// Default case: Demand is within the active range.
const countActiveMachines = machinesInPriorityOrder . filter ( ( { id } ) => this . isMachineActive ( id ) ) . length ;
Qd /= countActiveMachines ;
// Simply distribute the demand equally among all available machines.
for ( let i = 0 ; i < countActiveMachines ; i ++ ) {
flowDistribution . push ( { machineId : machinesInPriorityOrder [ i ] . id , flow : Qd } ) ;
totalFlow += Qd ;
totalPower += machinesInPriorityOrder [ i ] . machine . inputFlowCalcPower ( Qd ) ;
}
break ;
}
// Log information about flow distribution
const debugInfo = flowDistribution
. filter ( ( { flow } ) => flow > 0 )
. map ( ( { machineId , flow } ) => ` ${ machineId } : ${ flow . toFixed ( 2 ) } units ` )
. join ( " | " ) ;
this . logger . debug ( ` Priority control for demand: ${ totalFlow . toFixed ( 2 ) } -> Active pumps: [ ${ debugInfo } ] => Total Power: ${ totalPower . toFixed ( 2 ) } ` ) ;
// Store measurements
2025-10-02 17:08:41 +02:00
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( totalPower ) ;
2025-07-01 17:03:36 +02:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( totalFlow ) ;
2025-10-02 17:08:41 +02:00
this . measurements . type ( "efficiency" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( totalFlow / totalPower ) ;
this . measurements . type ( "Ncog" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( totalCog ) ;
2025-07-01 17:03:36 +02:00
2025-10-02 17:08:41 +02:00
this . logger . debug ( ` Flow distribution: ${ JSON . stringify ( flowDistribution ) } ` ) ;
2025-07-01 17:03:36 +02:00
// Apply the flow distribution to machines
await Promise . all ( flowDistribution . map ( async ( { machineId , flow } ) => {
const machine = this . machines [ machineId ] ;
2025-10-02 17:08:41 +02:00
this . logger . debug ( this . machines [ machineId ] . state ) ;
2025-07-01 17:03:36 +02:00
const currentState = this . machines [ machineId ] . state . getCurrentState ( ) ;
if ( flow <= 0 && ( currentState === "operational" || currentState === "accelerating" || currentState === "decelerating" ) ) {
await machine . handleInput ( "parent" , "execSequence" , "shutdown" ) ;
}
else if ( currentState === "idle" && flow > 0 ) {
await machine . handleInput ( "parent" , "execSequence" , "startup" ) ;
}
else if ( currentState === "operational" && flow > 0 ) {
await machine . handleInput ( "parent" , "flowMovement" , flow ) ;
}
} ) ) ;
}
catch ( err ) {
this . logger . error ( err ) ;
}
}
//only valid with equal machines
async prioPercentageControl ( input , priorityList = null ) {
try {
// stop all machines if input is negative
if ( input < 0 ) {
//turn all machines off
await Promise . all ( Object . entries ( this . machines ) . map ( async ( [ machineId , machine ] ) => {
if ( this . isMachineActive ( machineId ) ) { await machine . handleInput ( "parent" , "execSequence" , "shutdown" ) ; }
} ) ) ;
return ;
}
//capp input to 100
input > 100 ? input = 100 : input = input ;
const numOfMachines = Object . keys ( this . machines ) . length ;
const procentTotal = numOfMachines * input ;
const machinesNeeded = Math . ceil ( procentTotal / 100 ) ;
const activeTotals = this . activeTotals ( ) ;
const machinesActive = activeTotals . countActiveMachines ;
// Get machines sorted by priority
let machinesInPriorityOrder = this . sortMachinesByPriority ( priorityList ) ;
const ctrlDistribution = [ ] ; //{machineId : 0, flow : 0} push for each machine
if ( machinesNeeded > machinesActive ) {
//start extra machine and put all active machines at min control
machinesInPriorityOrder . forEach ( ( { id , machine } , index ) => {
if ( index < machinesNeeded ) {
ctrlDistribution . push ( { machineId : id , ctrl : 0 } ) ;
}
} ) ;
}
if ( machinesNeeded < machinesActive ) {
machinesInPriorityOrder . forEach ( ( { id , machine } , index ) => {
if ( this . isMachineActive ( id ) ) {
if ( index < machinesNeeded ) {
ctrlDistribution . push ( { machineId : id , ctrl : 100 } ) ;
}
else {
//turn machine off
ctrlDistribution . push ( { machineId : id , ctrl : - 1 } ) ;
}
}
} ) ;
}
if ( machinesNeeded === machinesActive ) {
// distribute input equally among active machines (0 - 100%)
const ctrlPerMachine = procentTotal / machinesActive ;
machinesInPriorityOrder . forEach ( ( { id , machine } ) => {
if ( this . isMachineActive ( id ) ) {
// ensure ctrl is capped between 0 and 100%
const ctrlValue = Math . max ( 0 , Math . min ( ctrlPerMachine , 100 ) ) ;
ctrlDistribution . push ( { machineId : id , ctrl : ctrlValue } ) ;
}
} ) ;
}
const debugInfo = ctrlDistribution . map ( ( { machineId , ctrl } ) => ` ${ machineId } : ${ ctrl . toFixed ( 2 ) } % ` ) . join ( " | " ) ;
this . logger . debug ( ` Priority control for input: ${ input . toFixed ( 2 ) } -> Active pumps: [ ${ debugInfo } ] ` ) ;
// Apply the ctrl distribution to machines
await Promise . all ( ctrlDistribution . map ( async ( { machineId , ctrl } ) => {
const machine = this . machines [ machineId ] ;
const currentState = this . machines [ machineId ] . state . getCurrentState ( ) ;
if ( ctrl < 0 && ( currentState === "operational" || currentState === "accelerating" || currentState === "decelerating" ) ) {
await machine . handleInput ( "parent" , "execSequence" , "shutdown" ) ;
}
else if ( currentState === "idle" && ctrl >= 0 ) {
await machine . handleInput ( "parent" , "execSequence" , "startup" ) ;
}
else if ( currentState === "operational" && ctrl > 0 ) {
await machine . handleInput ( "parent" , "execMovement" , ctrl ) ;
}
} ) ) ;
const totalPower = [ ] ;
const totalFlow = [ ] ;
// fetch and store measurements
Object . entries ( this . machines ) . forEach ( ( [ machineId , machine ] ) => {
2025-10-02 17:08:41 +02:00
const powerValue = machine . measurements . type ( "power" ) . variant ( "predicted" ) . position ( "atEquipment" ) . getCurrentValue ( ) ;
2025-07-01 17:03:36 +02:00
const flowValue = machine . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . getCurrentValue ( ) ;
2025-10-02 17:08:41 +02:00
2025-07-01 17:03:36 +02:00
if ( powerValue !== null ) {
totalPower . push ( powerValue ) ;
}
if ( flowValue !== null ) {
totalFlow . push ( flowValue ) ;
}
} ) ;
2025-10-02 17:08:41 +02:00
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( totalPower . reduce ( ( a , b ) => a + b , 0 ) ) ;
2025-07-01 17:03:36 +02:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( totalFlow . reduce ( ( a , b ) => a + b , 0 ) ) ;
2025-10-02 17:08:41 +02:00
2025-07-01 17:03:36 +02:00
if ( totalPower . reduce ( ( a , b ) => a + b , 0 ) > 0 ) {
2025-10-02 17:08:41 +02:00
this . measurements . type ( "efficiency" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( totalFlow . reduce ( ( a , b ) => a + b , 0 ) / totalPower . reduce ( ( a , b ) => a + b , 0 ) ) ;
2025-07-01 17:03:36 +02:00
}
}
catch ( err ) {
this . logger . error ( err ) ;
}
}
2025-10-02 17:08:41 +02:00
async handleInput ( source , demand , powerCap = Infinity , priorityList = null ) {
//abort current movements
await this . abortActiveMovements ( "new demand received" ) ;
2025-07-01 17:03:36 +02:00
const scaling = this . scaling ;
const mode = this . mode ;
2025-10-02 17:08:41 +02:00
const dynamicTotals = this . calcDynamicTotals ( ) ;
const demandQ = parseFloat ( demand ) ;
let demandQout = 0 ; // keep output Q by default 0 for safety
this . logger . debug ( ` Handling input from ${ source } : Demand = ${ demand } , Power Cap = ${ powerCap } , Priority List = ${ priorityList } ` ) ;
2025-07-01 17:03:36 +02:00
switch ( scaling ) {
case "absolute" :
2025-10-02 17:08:41 +02:00
if ( isNaN ( demandQ ) ) {
this . logger . warn ( ` Invalid absolute flow demand: ${ demand } . Must be a number. ` ) ;
demandQout = 0 ;
return ;
}
if ( demandQ < absoluteTotals . flow . min ) {
this . logger . warn ( ` Flow demand ${ demandQ } is below minimum possible flow ${ absoluteTotals . flow . min } . Capping to minimum flow. ` ) ;
demandQout = this . absoluteTotals . flow . min ;
} else if ( demandQout > absoluteTotals . flow . max ) {
this . logger . warn ( ` Flow demand ${ demandQ } is above maximum possible flow ${ absoluteTotals . flow . max } . Capping to maximum flow. ` ) ;
demandQout = absoluteTotals . flow . max ;
} else if ( demandQout <= 0 ) {
this . logger . debug ( ` Turning machines off ` ) ;
demandQout = 0 ;
//return early and turn all machines off
this . turnOffAllMachines ( ) ;
return ;
2025-07-01 17:03:36 +02:00
}
break ;
case "normalized" :
2025-10-02 17:08:41 +02:00
this . logger . debug ( ` Normalizing flow demand: ${ demandQ } with min: ${ dynamicTotals . flow . min } and max: ${ dynamicTotals . flow . max } ` ) ;
if ( demand < 0 ) {
this . logger . debug ( ` Turning machines off ` ) ;
demandQout = 0 ;
//return early and turn all machines off
this . turnOffAllMachines ( ) ;
return ;
}
else {
// Scale demand to 0-100% linear between min and max flow this is auto capped
demandQout = this . interpolation . interpolate _lin _single _point ( demandQ , 0 , 100 , dynamicTotals . flow . min , dynamicTotals . flow . max ) ;
this . logger . debug ( ` Normalized flow demand ${ demandQ } % to: ${ demandQout } Q units ` ) ;
}
2025-07-01 17:03:36 +02:00
break ;
}
2025-10-02 17:08:41 +02:00
// Execute control based on mode
2025-07-01 17:03:36 +02:00
switch ( mode ) {
case "prioritycontrol" :
2025-10-02 17:08:41 +02:00
this . logger . debug ( ` Calculating prio control. Input flow demand: ${ demandQ } scaling : ${ scaling } -> ${ demandQout } ` ) ;
await this . equalFlowControl ( demandQout , powerCap , priorityList ) ;
2025-07-01 17:03:36 +02:00
break ;
2025-10-02 17:08:41 +02:00
2025-07-01 17:03:36 +02:00
case "prioritypercentagecontrol" :
2025-10-02 17:08:41 +02:00
this . logger . debug ( ` Calculating prio percentage control. Input flow demand: ${ demandQ } scaling : ${ scaling } -> ${ demandQout } ` ) ;
2025-07-01 17:03:36 +02:00
if ( scaling !== "normalized" ) {
this . logger . warn ( "Priority percentage control is only valid with normalized scaling." ) ;
return ;
}
2025-10-02 17:08:41 +02:00
await this . prioPercentageControl ( demandQout , priorityList ) ;
2025-07-01 17:03:36 +02:00
break ;
2025-10-03 15:33:37 +02:00
2025-07-01 17:03:36 +02:00
case "optimalcontrol" :
2025-10-02 17:08:41 +02:00
this . logger . debug ( ` Calculating optimal control. Input flow demand: ${ demandQ } scaling : ${ scaling } -> ${ demandQout } ` ) ;
await this . optimalControl ( demandQout , powerCap ) ;
2025-07-01 17:03:36 +02:00
break ;
2025-09-23 15:03:57 +02:00
default :
this . logger . warn ( ` ${ mode } is not a valid mode. ` ) ;
break ;
2025-07-01 17:03:36 +02:00
}
//recalc distance from BEP
const { maxEfficiency , lowestEfficiency } = this . calcGroupEfficiency ( this . machines ) ;
const efficiency = this . measurements . type ( "efficiency" ) . variant ( "predicted" ) . position ( "downstream" ) . getCurrentValue ( ) ;
this . calcDistanceBEP ( efficiency , maxEfficiency , lowestEfficiency ) ;
}
2025-10-02 17:08:41 +02:00
async turnOffAllMachines ( ) {
await Promise . all ( Object . entries ( this . machines ) . map ( async ( [ machineId , machine ] ) => {
if ( this . isMachineActive ( machineId ) ) { await machine . handleInput ( "parent" , "execSequence" , "shutdown" ) ; }
} ) ) ;
}
2025-09-23 15:03:57 +02:00
setMode ( mode ) {
this . mode = mode ;
2025-07-01 17:03:36 +02:00
}
getOutput ( ) {
// Improved output object generation
const output = { } ;
//build the output object
this . measurements . getTypes ( ) . forEach ( type => {
this . measurements . getVariants ( type ) . forEach ( variant => {
const downstreamVal = this . measurements . type ( type ) . variant ( variant ) . position ( "downstream" ) . getCurrentValue ( ) ;
2025-10-02 17:08:41 +02:00
const atEquipmentVal = this . measurements . type ( type ) . variant ( variant ) . position ( "atEquipment" ) . getCurrentValue ( ) ;
2025-07-01 17:03:36 +02:00
const upstreamVal = this . measurements . type ( type ) . variant ( variant ) . position ( "upstream" ) . getCurrentValue ( ) ;
if ( downstreamVal != null ) {
output [ ` downstream_ ${ variant } _ ${ type } ` ] = downstreamVal ;
}
if ( upstreamVal != null ) {
output [ ` upstream_ ${ variant } _ ${ type } ` ] = upstreamVal ;
}
2025-10-02 17:08:41 +02:00
if ( atEquipmentVal != null ) {
output [ ` atEquipment_ ${ variant } _ ${ type } ` ] = atEquipmentVal ;
}
2025-07-01 17:03:36 +02:00
if ( downstreamVal != null && upstreamVal != null ) {
const diffVal = this . measurements . type ( type ) . variant ( variant ) . difference ( ) . value ;
output [ ` differential_ ${ variant } _ ${ type } ` ] = diffVal ;
}
} ) ;
} ) ;
//fill in the rest of the output object
output [ "mode" ] = this . mode ;
output [ "scaling" ] = this . scaling ;
output [ "flow" ] = this . flow ;
output [ "power" ] = this . power ;
output [ "NCog" ] = this . NCog ; // normalized cog
output [ "absDistFromPeak" ] = this . absDistFromPeak ;
output [ "relDistFromPeak" ] = this . relDistFromPeak ;
//this.logger.debug(`Output: ${JSON.stringify(output)}`);
return output ;
}
}
module . exports = MachineGroup ;
/ *
2025-10-02 17:08:41 +02:00
2025-09-23 11:19:22 +02:00
const Machine = require ( '../../rotatingMachine/src/specificClass' ) ;
const Measurement = require ( '../../measurement/src/specificClass' ) ;
const specs = require ( '../../generalFunctions/datasets/assetData/curves/hidrostal-H05K-S03R.json' ) ;
2025-10-02 17:08:41 +02:00
const { max } = require ( "mathjs" ) ;
2025-07-01 17:03:36 +02:00
2025-09-23 11:19:22 +02:00
function createBaseMachineConfig ( machineNum , name , specs ) {
2025-07-01 17:03:36 +02:00
return {
general : {
2025-10-02 17:08:41 +02:00
logging : { enabled : true , logLevel : "debug" } ,
2025-07-01 17:03:36 +02:00
name : name ,
2025-09-23 11:19:22 +02:00
id : machineNum ,
2025-07-01 17:03:36 +02:00
unit : "m3/h"
} ,
functionality : {
softwareType : "machine" ,
2025-09-23 11:19:22 +02:00
role : "rotationaldevicecontroller"
2025-07-01 17:03:36 +02:00
} ,
asset : {
2025-09-23 11:19:22 +02:00
category : "pump" ,
type : "centrifugal" ,
model : "hidrostal-h05k-s03r" ,
supplier : "hydrostal" ,
machineCurve : specs
2025-07-01 17:03:36 +02:00
} ,
mode : {
current : "auto" ,
allowedActions : {
auto : [ "execSequence" , "execMovement" , "statusCheck" ] ,
virtualControl : [ "execMovement" , "statusCheck" ] ,
fysicalControl : [ "statusCheck" ]
} ,
allowedSources : {
auto : [ "parent" , "GUI" ] ,
virtualControl : [ "GUI" ] ,
fysicalControl : [ "fysical" ]
}
} ,
sequences : {
startup : [ "starting" , "warmingup" , "operational" ] ,
shutdown : [ "stopping" , "coolingdown" , "idle" ] ,
emergencystop : [ "emergencystop" , "off" ] ,
boot : [ "idle" , "starting" , "warmingup" , "operational" ]
}
} ;
}
2025-10-02 17:08:41 +02:00
function createStateConfig ( ) {
return {
time : {
starting : 1 ,
stopping : 1 ,
warmingup : 1 ,
coolingdown : 1 ,
emergencystop : 1
} ,
movement : {
mode : "dynspeed" ,
speed : 100 ,
maxSpeed : 1000
}
}
} ;
2025-07-01 17:03:36 +02:00
function createBaseMachineGroupConfig ( name ) {
return {
general : {
logging : { enabled : true , logLevel : "debug" } ,
name : name
} ,
functionality : {
2025-09-23 11:19:22 +02:00
softwareType : "machinegroup" ,
role : "groupcontroller"
2025-07-01 17:03:36 +02:00
} ,
scaling : {
current : "normalized"
} ,
mode : {
current : "optimalControl"
}
} ;
}
2025-09-23 11:19:22 +02:00
const machineGroupConfig = createBaseMachineGroupConfig ( "testmachinegroup" ) ;
2025-10-02 17:08:41 +02:00
const stateConfigs = { } ;
2025-09-23 11:19:22 +02:00
const machineConfigs = { } ;
2025-10-02 17:08:41 +02:00
stateConfigs [ 1 ] = createStateConfig ( ) ;
stateConfigs [ 2 ] = createStateConfig ( ) ;
machineConfigs [ 1 ] = createBaseMachineConfig ( "asdfkj;asdf" , "testmachine" , specs ) ;
machineConfigs [ 2 ] = createBaseMachineConfig ( "asdfkj;asdf2" , "testmachine2" , specs ) ;
2025-09-23 11:19:22 +02:00
2025-07-01 17:03:36 +02:00
const ptConfig = {
general : {
logging : { enabled : true , logLevel : "debug" } ,
2025-09-23 11:19:22 +02:00
name : "testpt" ,
id : "0" ,
2025-07-01 17:03:36 +02:00
unit : "mbar" ,
} ,
functionality : {
softwareType : "measurement" ,
2025-09-23 11:19:22 +02:00
role : "sensor"
2025-07-01 17:03:36 +02:00
} ,
asset : {
2025-09-23 11:19:22 +02:00
category : "sensor" ,
type : "pressure" ,
model : "testmodel" ,
2025-07-01 17:03:36 +02:00
supplier : "vega"
} ,
scaling : {
absMin : 0 ,
absMax : 4000 ,
}
}
async function makeMachines ( ) {
const mg = new MachineGroup ( machineGroupConfig ) ;
const pt1 = new Measurement ( ptConfig ) ;
const numofMachines = 2 ;
2025-09-23 11:19:22 +02:00
for ( let i = 1 ; i <= numofMachines ; i ++ ) {
2025-10-02 17:08:41 +02:00
const machine = new Machine ( machineConfigs [ i ] , stateConfigs [ i ] ) ;
2025-07-01 17:03:36 +02:00
//mg.machines[i] = machine;
mg . childRegistrationUtils . registerChild ( machine , "downstream" ) ;
}
2025-10-02 17:08:41 +02:00
Object . keys ( mg . machines ) . forEach ( machineId => {
mg . machines [ machineId ] . childRegistrationUtils . registerChild ( pt1 , "downstream" ) ;
} ) ;
mg . setMode ( "prioritycontrol" ) ;
2025-07-01 17:03:36 +02:00
mg . setScaling ( "normalized" ) ;
const absMax = mg . dynamicTotals . flow . max ;
const absMin = mg . dynamicTotals . flow . min ;
const percMin = 0 ;
const percMax = 100 ;
try {
2025-10-02 17:08:41 +02:00
/ *
2025-07-01 17:03:36 +02:00
for ( let demand = mg . dynamicTotals . flow . min ; demand <= mg . dynamicTotals . flow . max ; demand += 2 ) {
//set pressure
console . log ( "------------------------------------" ) ;
await mg . handleInput ( "parent" , demand ) ;
pt1 . calculateInput ( 1400 ) ;
//await new Promise(resolve => setTimeout(resolve, 200));
console . log ( "------------------------------------" ) ;
}
for ( let demand = 240 ; demand >= mg . dynamicTotals . flow . min ; demand -= 40 ) {
//set pressure
console . log ( "------------------------------------" ) ;
await mg . handleInput ( "parent" , demand ) ;
pt1 . calculateInput ( 1400 ) ;
//await new Promise(resolve => setTimeout(resolve, 200));
console . log ( "------------------------------------" ) ;
}
2025-10-02 17:08:41 +02:00
//*//*
for ( let demand = 0 ; demand <= 50 ; demand += 1 ) {
2025-07-01 17:03:36 +02:00
//set pressure
2025-10-02 17:08:41 +02:00
console . log ( ` TESTING: processing demand of ${ demand } ` ) ;
2025-09-23 15:03:57 +02:00
2025-07-01 17:03:36 +02:00
await mg . handleInput ( "parent" , demand ) ;
2025-10-02 17:08:41 +02:00
Object . keys ( mg . machines ) . forEach ( machineId => {
console . log ( mg . machines [ machineId ] . state . getCurrentState ( ) ) ;
} ) ;
console . log ( ` updating pressure to 1400 mbar ` ) ;
2025-07-01 17:03:36 +02:00
pt1 . calculateInput ( 1400 ) ;
console . log ( "------------------------------------" ) ;
}
}
catch ( err ) {
console . log ( err ) ;
}
}
makeMachines ( ) ;
2025-09-23 11:19:22 +02:00
2025-09-23 15:03:57 +02:00
//*/