forked from RnD/machineGroupControl
updates to machinegroupcontrol to work in new gitea repo
This commit is contained in:
1056
dependencies/machineGroup/machineGroup.js
vendored
Normal file
1056
dependencies/machineGroup/machineGroup.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
566
dependencies/machineGroup/machineGroup.test.js
vendored
Normal file
566
dependencies/machineGroup/machineGroup.test.js
vendored
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
const MachineGroup = require('./machineGroup');
|
||||||
|
const Machine = require('../../../rotatingMachine/dependencies/machine/machine');
|
||||||
|
const specs = require('../../../generalFunctions/datasets/assetData/pumps/hydrostal/centrifugal pumps/models.json');
|
||||||
|
|
||||||
|
class MachineGroupTester {
|
||||||
|
constructor() {
|
||||||
|
this.totalTests = 0;
|
||||||
|
this.passedTests = 0;
|
||||||
|
this.failedTests = 0;
|
||||||
|
this.machineCurve = specs[0].machineCurve;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(condition, message) {
|
||||||
|
this.totalTests++;
|
||||||
|
if (condition) {
|
||||||
|
console.log(`✓ PASS: ${message}`);
|
||||||
|
this.passedTests++;
|
||||||
|
} else {
|
||||||
|
console.log(`✗ FAIL: ${message}`);
|
||||||
|
this.failedTests++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createBaseMachineConfig(name) {
|
||||||
|
return {
|
||||||
|
general: {
|
||||||
|
logging: { enabled: true, logLevel: "debug" },
|
||||||
|
name: name,
|
||||||
|
unit: "m3/h"
|
||||||
|
},
|
||||||
|
functionality: {
|
||||||
|
softwareType: "machine",
|
||||||
|
role: "RotationalDeviceController"
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
type: "pump",
|
||||||
|
subType: "Centrifugal",
|
||||||
|
model: "TestModel",
|
||||||
|
supplier: "Hydrostal",
|
||||||
|
machineCurve: this.machineCurve
|
||||||
|
},
|
||||||
|
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"]
|
||||||
|
},
|
||||||
|
calculationMode: "medium"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createBaseMachineGroupConfig(name) {
|
||||||
|
return {
|
||||||
|
general: {
|
||||||
|
logging: { enabled: true, logLevel: "debug" },
|
||||||
|
name: name
|
||||||
|
},
|
||||||
|
functionality: {
|
||||||
|
softwareType: "machineGroup",
|
||||||
|
role: "GroupController"
|
||||||
|
},
|
||||||
|
scaling: {
|
||||||
|
current: "normalized"
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
current: "optimalControl"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async testSingleMachineOperation() {
|
||||||
|
console.log('\nTesting Single Machine Operation...');
|
||||||
|
|
||||||
|
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup");
|
||||||
|
const machineConfig = this.createBaseMachineConfig("TestMachine1");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mg = new MachineGroup(machineGroupConfig);
|
||||||
|
const machine = new Machine(machineConfig);
|
||||||
|
|
||||||
|
// Register machine with group
|
||||||
|
mg.childRegistrationUtils.registerChild(machine, "downstream");
|
||||||
|
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
||||||
|
await machine.state.transitionToState("idle");
|
||||||
|
|
||||||
|
// Test 1: Basic initialization
|
||||||
|
this.assert(
|
||||||
|
Object.keys(mg.machines).length === 0,
|
||||||
|
'Machine group should have exactly zero machine'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 2: Calculate demand with single machine
|
||||||
|
await machine.handleInput("parent", "execSequence", "startup");
|
||||||
|
await mg.handleFlowInput(50);
|
||||||
|
this.assert(
|
||||||
|
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
|
||||||
|
'Total flow should be greater than 0 for demand of 50'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 3: Check machine mode handling
|
||||||
|
machine.setMode("virtualControl");
|
||||||
|
const {single, machineNum} = mg.singleMachine();
|
||||||
|
this.assert(
|
||||||
|
single === true,
|
||||||
|
'Should identify as single machine when in virtual control'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 4: Zero demand handling
|
||||||
|
await mg.handleFlowInput(0);
|
||||||
|
this.assert(
|
||||||
|
!mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() ||
|
||||||
|
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0,
|
||||||
|
'Total flow should be 0 for zero demand'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 5: Max demand handling
|
||||||
|
await mg.handleFlowInput(100);
|
||||||
|
this.assert(
|
||||||
|
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
|
||||||
|
'Total flow should be greater than 0 for max demand'
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed with error:', error);
|
||||||
|
this.failedTests++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async testMultipleMachineOperation() {
|
||||||
|
console.log('\nTesting Multiple Machine Operation...');
|
||||||
|
|
||||||
|
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mg = new MachineGroup(machineGroupConfig);
|
||||||
|
const machine1 = new Machine(this.createBaseMachineConfig("Machine1"));
|
||||||
|
const machine2 = new Machine(this.createBaseMachineConfig("Machine2"));
|
||||||
|
|
||||||
|
mg.childRegistrationUtils.registerChild(machine1, "downstream");
|
||||||
|
mg.childRegistrationUtils.registerChild(machine2, "downstream");
|
||||||
|
|
||||||
|
machine1.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
||||||
|
machine2.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
||||||
|
|
||||||
|
await machine1.state.transitionToState("idle");
|
||||||
|
await machine2.state.transitionToState("idle");
|
||||||
|
|
||||||
|
await machine1.handleInput("parent", "execSequence", "startup");
|
||||||
|
await machine2.handleInput("parent", "execSequence", "startup");
|
||||||
|
|
||||||
|
// Test 1: Multiple machine registration
|
||||||
|
this.assert(
|
||||||
|
Object.keys(mg.machines).length === 2,
|
||||||
|
'Machine group should have exactly two machines'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 1.1: Calculate demand with multiple machines
|
||||||
|
await mg.handleFlowInput(0); // Testing with higher demand for two machines
|
||||||
|
const machineOutputs = Object.keys(mg.machines).filter(id =>
|
||||||
|
mg.machines[id].measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
this.assert(
|
||||||
|
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0 &&
|
||||||
|
machineOutputs.length > 0,
|
||||||
|
'Should distribute load between machines'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 1.2: Calculate demand with multiple machines with an increment of 10
|
||||||
|
for(let i = 0; i < 100; i+=10){
|
||||||
|
await mg.handleFlowInput(i); // Testing with incrementing demand
|
||||||
|
const flowValue = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
|
||||||
|
this.assert(
|
||||||
|
flowValue !== undefined && !isNaN(flowValue),
|
||||||
|
`Should handle demand of ${i} units properly`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Calculate nonsense demands with multiple machines
|
||||||
|
await mg.handleFlowInput(150); // Testing with higher demand for two machines
|
||||||
|
this.assert(
|
||||||
|
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
|
||||||
|
'Should handle excessive demand gracefully'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 3: Force single machine mode
|
||||||
|
machine2.setMode("maintenance");
|
||||||
|
const {single} = mg.singleMachine();
|
||||||
|
this.assert(
|
||||||
|
single === true,
|
||||||
|
'Should identify as single machine when one machine is in maintenance'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed with error:', error);
|
||||||
|
this.failedTests++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async testDynamicTotals() {
|
||||||
|
console.log('\nTesting Dynamic Totals...');
|
||||||
|
|
||||||
|
const mg = new MachineGroup(this.createBaseMachineGroupConfig("TestMachineGroup"));
|
||||||
|
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
mg.childRegistrationUtils.registerChild(machine, "downstream");
|
||||||
|
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
||||||
|
await machine.state.transitionToState("idle");
|
||||||
|
await machine.handleInput("parent", "execSequence", "startup");
|
||||||
|
|
||||||
|
// Test 1: Dynamic totals initialization
|
||||||
|
const maxFlow = machine.predictFlow.currentFxyYMax;
|
||||||
|
const maxPower = machine.predictPower.currentFxyYMax;
|
||||||
|
|
||||||
|
this.assert(
|
||||||
|
mg.dynamicTotals.flow.max === maxFlow && mg.dynamicTotals.power.max === maxPower,
|
||||||
|
'Dynamic totals should reflect machine capabilities'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 2: Demand scaling
|
||||||
|
await mg.handleFlowInput(50); // 50% of max
|
||||||
|
const actualFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
|
||||||
|
this.assert(
|
||||||
|
actualFlow <= maxFlow * 0.6, // Allow some margin for interpolation
|
||||||
|
'Scaled demand should be approximately 50% of max flow'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed with error:', error);
|
||||||
|
this.failedTests++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async testInterpolation() {
|
||||||
|
console.log('\nTesting Interpolation...');
|
||||||
|
|
||||||
|
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup");
|
||||||
|
const machineConfig = this.createBaseMachineConfig("TestMachine");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mg = new MachineGroup(machineGroupConfig);
|
||||||
|
const machine = new Machine(machineConfig);
|
||||||
|
|
||||||
|
// Register machine and set initial state
|
||||||
|
mg.childRegistrationUtils.registerChild(machine, "downstream");
|
||||||
|
machine.measurements.type("pressure").variant("measured").position("downstream").value(1);
|
||||||
|
machine.state.transitionToState("idle");
|
||||||
|
|
||||||
|
// Test interpolation at different demand points
|
||||||
|
const testPoints = [0, 25, 50, 75, 100];
|
||||||
|
for (const demand of testPoints) {
|
||||||
|
await mg.handleFlowInput(demand);
|
||||||
|
const flowValue = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
|
||||||
|
const powerValue = mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue();
|
||||||
|
|
||||||
|
this.assert(
|
||||||
|
flowValue !== undefined && !isNaN(flowValue),
|
||||||
|
`Interpolation should produce valid flow value for demand ${demand}`
|
||||||
|
);
|
||||||
|
this.assert(
|
||||||
|
powerValue !== undefined && !isNaN(powerValue),
|
||||||
|
`Interpolation should produce valid power value for demand ${demand}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test interpolation between curve points
|
||||||
|
const interpolatedPoint = 45; // Should interpolate between 40 and 60
|
||||||
|
await mg.handleFlowInput(interpolatedPoint);
|
||||||
|
this.assert(
|
||||||
|
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
|
||||||
|
`Interpolation should handle non-exact point ${interpolatedPoint}`
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed with error:', error);
|
||||||
|
this.failedTests++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async testSingleMachineControlModes() {
|
||||||
|
console.log('\nTesting Single Machine Control Modes...');
|
||||||
|
|
||||||
|
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup");
|
||||||
|
const machineConfig = this.createBaseMachineConfig("TestMachine1");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mg = new MachineGroup(machineGroupConfig);
|
||||||
|
const machine = new Machine(machineConfig);
|
||||||
|
|
||||||
|
// Register machine and initialize
|
||||||
|
mg.childRegistrationUtils.registerChild(machine, "downstream");
|
||||||
|
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
||||||
|
await machine.state.transitionToState("idle");
|
||||||
|
await machine.handleInput("parent", "execSequence", "startup");
|
||||||
|
|
||||||
|
// Test 1: Virtual Control Mode
|
||||||
|
machine.setMode("virtualControl");
|
||||||
|
await mg.handleFlowInput(50);
|
||||||
|
this.assert(
|
||||||
|
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() !== undefined,
|
||||||
|
'Should handle virtual control mode'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 2: Physical Control Mode
|
||||||
|
machine.setMode("fysicalControl");
|
||||||
|
await mg.handleFlowInput(75);
|
||||||
|
this.assert(
|
||||||
|
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() !== undefined,
|
||||||
|
'Should handle physical control mode'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 3: Auto Mode Return
|
||||||
|
machine.setMode("auto");
|
||||||
|
await mg.handleFlowInput(60);
|
||||||
|
this.assert(
|
||||||
|
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
|
||||||
|
'Should return to normal operation in auto mode'
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed with error:', error);
|
||||||
|
this.failedTests++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async testMachinesOffNormalized() {
|
||||||
|
console.log('\nTesting Machines Off with Normalized Flow...');
|
||||||
|
|
||||||
|
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_OffNormalized");
|
||||||
|
// scaling is "normalized" by default
|
||||||
|
const mg = new MachineGroup(machineGroupConfig);
|
||||||
|
const machine = new Machine(this.createBaseMachineConfig("TestMachine_OffNormalized"));
|
||||||
|
|
||||||
|
mg.childRegistrationUtils.registerChild(machine, "downstream");
|
||||||
|
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
||||||
|
await machine.state.transitionToState("idle");
|
||||||
|
await machine.handleInput("parent", "execSequence", "startup");
|
||||||
|
|
||||||
|
// Turn machines off by setting demand to 0 with normalized scaling
|
||||||
|
await mg.handleFlowInput(-1);
|
||||||
|
this.assert(
|
||||||
|
!mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() ||
|
||||||
|
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0,
|
||||||
|
'Total flow should be 0 when demand is < 0 in normalized scaling'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async testMachinesOffAbsolute() {
|
||||||
|
console.log('\nTesting Machines Off with Absolute Flow...');
|
||||||
|
|
||||||
|
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_OffAbsolute");
|
||||||
|
// Switch scaling to "absolute"
|
||||||
|
machineGroupConfig.scaling.current = "absolute";
|
||||||
|
const mg = new MachineGroup(machineGroupConfig);
|
||||||
|
const machine = new Machine(this.createBaseMachineConfig("TestMachine_OffAbsolute"));
|
||||||
|
|
||||||
|
mg.childRegistrationUtils.registerChild(machine, "downstream");
|
||||||
|
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
||||||
|
await machine.state.transitionToState("idle");
|
||||||
|
await machine.handleInput("parent", "execSequence", "startup");
|
||||||
|
|
||||||
|
// Turn machines off by setting demand to 0 with absolute scaling
|
||||||
|
await mg.handleFlowInput(0);
|
||||||
|
this.assert(
|
||||||
|
!mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() ||
|
||||||
|
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0,
|
||||||
|
'Total flow should be 0 when demand is 0 in absolute scaling'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async testPriorityControl() {
|
||||||
|
console.log('\nTesting Priority Control...');
|
||||||
|
|
||||||
|
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_Priority");
|
||||||
|
const mg = new MachineGroup(machineGroupConfig);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create 3 machines with different configurations for clearer testing
|
||||||
|
const machines = [];
|
||||||
|
for(let i = 1; i <= 3; i++) {
|
||||||
|
const machineConfig = this.createBaseMachineConfig(`Machine${i}`);
|
||||||
|
const machine = new Machine(machineConfig);
|
||||||
|
machines.push(machine);
|
||||||
|
mg.childRegistrationUtils.registerChild(machine, "downstream");
|
||||||
|
|
||||||
|
// Set different max flows to make priority visible
|
||||||
|
machine.predictFlow = {
|
||||||
|
currentFxyYMin: 10 * i, // Different min flows
|
||||||
|
currentFxyYMax: 50 * i // Different max flows
|
||||||
|
};
|
||||||
|
|
||||||
|
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
||||||
|
await machine.state.transitionToState("idle");
|
||||||
|
await machine.handleInput("parent", "execSequence", "startup");
|
||||||
|
|
||||||
|
// Mock the inputFlowCalcPower method for testing
|
||||||
|
machine.inputFlowCalcPower = (flow) => flow * 2; // Simple mock function
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 1: Default priority (by machine ID)
|
||||||
|
// Use handleInput which routes to equalControl in prioritycontrol mode
|
||||||
|
await mg.handleInput("parent", 80);
|
||||||
|
const flowAfterDefaultPriority = Object.values(mg.machines).map(machine =>
|
||||||
|
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0
|
||||||
|
);
|
||||||
|
this.assert(
|
||||||
|
flowAfterDefaultPriority[0] > 0 && flowAfterDefaultPriority[1] > 0 && flowAfterDefaultPriority[2] === 0,
|
||||||
|
'Default priority should use machines in ID order until demand is met'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 2: Custom priority list
|
||||||
|
await mg.handleInput("parent", 120, Infinity, [3, 2, 1]);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
const flowAfterCustomPriority = Object.values(mg.machines).map(machine =>
|
||||||
|
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0
|
||||||
|
);
|
||||||
|
this.assert(
|
||||||
|
flowAfterCustomPriority[2] > 0 && flowAfterCustomPriority[1] > 0 && flowAfterCustomPriority[0] === 0,
|
||||||
|
'Custom priority should use machines in specified order until demand is met'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 3: Zero demand should shut down all machines
|
||||||
|
await mg.handleInput("parent", 0);
|
||||||
|
const noFlowCondition = Object.values(mg.machines).every(machine =>
|
||||||
|
!machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() ||
|
||||||
|
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0
|
||||||
|
);
|
||||||
|
this.assert(
|
||||||
|
noFlowCondition,
|
||||||
|
'Zero demand should result in no flow from any machine'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 4: Handling excessive demand (more than total capacity)
|
||||||
|
const totalMaxFlow = machines.reduce((sum, machine) => sum + machine.predictFlow.currentFxyYMax, 0);
|
||||||
|
await mg.handleInput("parent", totalMaxFlow + 100);
|
||||||
|
const totalActualFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
|
||||||
|
this.assert(
|
||||||
|
totalActualFlow <= totalMaxFlow && totalActualFlow > 0,
|
||||||
|
'Excessive demand should be capped to maximum possible flow'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test 5: Check all measurements are updated correctly
|
||||||
|
this.assert(
|
||||||
|
mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() > 0 &&
|
||||||
|
mg.measurements.type("efficiency").variant("predicted").position("downstream").getCurrentValue() > 0,
|
||||||
|
'All measurements should be updated after priority control'
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Priority control test failed with error:', error);
|
||||||
|
this.failedTests++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async runAllTests() {
|
||||||
|
console.log('Starting MachineGroup Tests...\n');
|
||||||
|
|
||||||
|
await this.testSingleMachineOperation();
|
||||||
|
await this.testMultipleMachineOperation();
|
||||||
|
await this.testDynamicTotals();
|
||||||
|
await this.testInterpolation();
|
||||||
|
await this.testSingleMachineControlModes();
|
||||||
|
await this.testMachinesOffNormalized();
|
||||||
|
await this.testMachinesOffAbsolute();
|
||||||
|
await this.testPriorityControl(); // Add the new test
|
||||||
|
await testCombinationIterations();
|
||||||
|
|
||||||
|
console.log('\nTest Summary:');
|
||||||
|
console.log(`Total Tests: ${this.totalTests}`);
|
||||||
|
console.log(`Passed: ${this.passedTests}`);
|
||||||
|
console.log(`Failed: ${this.failedTests}`);
|
||||||
|
|
||||||
|
// Return exit code based on test results
|
||||||
|
process.exit(this.failedTests > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a custom logger to capture debug logs during tests
|
||||||
|
class CapturingLogger {
|
||||||
|
constructor() {
|
||||||
|
this.logs = [];
|
||||||
|
}
|
||||||
|
debug(message) {
|
||||||
|
this.logs.push({ level: "debug", message });
|
||||||
|
console.debug(message);
|
||||||
|
}
|
||||||
|
info(message) {
|
||||||
|
this.logs.push({ level: "info", message });
|
||||||
|
console.info(message);
|
||||||
|
}
|
||||||
|
warn(message) {
|
||||||
|
this.logs.push({ level: "warn", message });
|
||||||
|
console.warn(message);
|
||||||
|
}
|
||||||
|
error(message) {
|
||||||
|
this.logs.push({ level: "error", message });
|
||||||
|
console.error(message);
|
||||||
|
}
|
||||||
|
getAll() {
|
||||||
|
return this.logs;
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
this.logs = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify one of the test functions to override the machineGroup logger
|
||||||
|
async function testCombinationIterations() {
|
||||||
|
console.log('\nTesting Combination Iterations Logging...');
|
||||||
|
|
||||||
|
const machineGroupConfig = tester.createBaseMachineGroupConfig("TestCombinationIterations");
|
||||||
|
const mg = new MachineGroup(machineGroupConfig);
|
||||||
|
|
||||||
|
// Override logger with a capturing logger
|
||||||
|
const customLogger = new CapturingLogger();
|
||||||
|
mg.logger = customLogger;
|
||||||
|
|
||||||
|
// Create one machine for simplicity (or two if you like)
|
||||||
|
const machine = new Machine(tester.createBaseMachineConfig("TestMachineForCombo"));
|
||||||
|
mg.childRegistrationUtils.registerChild(machine, "downstream");
|
||||||
|
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
||||||
|
await machine.state.transitionToState("idle");
|
||||||
|
await machine.handleInput("parent", "execSequence", "startup");
|
||||||
|
|
||||||
|
// For testing, force dynamic totals so that combination search is exercised
|
||||||
|
mg.dynamicTotals.flow = { min: 0, max: 200 }; // example totalling
|
||||||
|
// Call handleFlowInput with a demand that requires iterations
|
||||||
|
await mg.handleFlowInput(120);
|
||||||
|
|
||||||
|
// After running, output captured iteration debug logs
|
||||||
|
console.log("\n-- Captured Debug Logs for Combination Search Iterations --");
|
||||||
|
customLogger.getAll().forEach(log => {
|
||||||
|
if(log.level === "debug") {
|
||||||
|
console.log(log.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also output best result details if any needed for further improvement
|
||||||
|
console.log("\n-- Final Output --");
|
||||||
|
const totalFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
|
||||||
|
console.log("Total Flow: ", totalFlow);
|
||||||
|
|
||||||
|
// Get machine outputs by checking each machine's measurements
|
||||||
|
const machineOutputs = {};
|
||||||
|
Object.entries(mg.machines).forEach(([id, machine]) => {
|
||||||
|
const flow = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
|
||||||
|
if (flow) machineOutputs[id] = flow;
|
||||||
|
});
|
||||||
|
console.log("Machine Outputs: ", machineOutputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the tests
|
||||||
|
const tester = new MachineGroupTester();
|
||||||
|
tester.runAllTests().catch(console.error);
|
||||||
188
dependencies/machineGroup/machineGroupConfig.json
vendored
Normal file
188
dependencies/machineGroup/machineGroupConfig.json
vendored
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "Machine Group Configuration",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A human-readable name or label for this machine group configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A unique identifier for this configuration. If not provided, defaults to null."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"logLevel": {
|
||||||
|
"default": "info",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "debug",
|
||||||
|
"description": "Log messages are printed for debugging purposes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "info",
|
||||||
|
"description": "Informational messages are printed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "warn",
|
||||||
|
"description": "Warning messages are printed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "error",
|
||||||
|
"description": "Error messages are printed."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates whether logging is active. If true, log messages will be generated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"functionality": {
|
||||||
|
"softwareType": {
|
||||||
|
"default": "machineGroup",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Logical name identifying the software type."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "GroupController",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Controls a group of machines within the system."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"current": {
|
||||||
|
"default": "optimalControl",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "optimalControl",
|
||||||
|
"description": "The group controller selects the most optimal combination of machines based on their real-time performance curves."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "priorityControl",
|
||||||
|
"description": "Machines are controlled sequentially from minimum to maximum output until each is maxed out, then additional machines are added."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "prioritypercentagecontrol",
|
||||||
|
"description": "Machines are controlled sequentially from minimum to maximum output until each is maxed out, then additional machines are added based on a percentage of the total demand."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "maintenance",
|
||||||
|
"description": "The group is in maintenance mode with limited actions (monitoring only)."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The operational mode of the machine group controller."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedActions": {
|
||||||
|
"default": {},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"optimalControl": {
|
||||||
|
"default": ["statusCheck", "execOptimalCombination", "balanceLoad", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in optimalControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"priorityControl": {
|
||||||
|
"default": ["statusCheck", "execSequentialControl", "balanceLoad", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in priorityControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prioritypercentagecontrol": {
|
||||||
|
"default": ["statusCheck", "execSequentialControl", "balanceLoad", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in manualOverride mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maintenance": {
|
||||||
|
"default": ["statusCheck"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in maintenance mode."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Defines the actions available for each operational mode of the machine group controller."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedSources": {
|
||||||
|
"default": {},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"optimalcontrol": {
|
||||||
|
"default": ["parent", "GUI", "physical", "API"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Command sources allowed in optimalControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prioritycontrol": {
|
||||||
|
"default": ["parent", "GUI", "physical", "API"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Command sources allowed in priorityControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prioritypercentagecontrol": {
|
||||||
|
"default": ["parent", "GUI", "physical", "API"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Command sources allowed "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Specifies the valid command sources recognized by the machine group controller for each mode."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scaling": {
|
||||||
|
"current": {
|
||||||
|
"default": "normalized",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "normalized",
|
||||||
|
"description": "Scales the demand between 0–100% of the total flow capacity, interpolating to calculate the effective demand."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "absolute",
|
||||||
|
"description": "Uses the absolute demand value directly, capped between the min and max machine flow capacities."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The scaling mode for demand calculations."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
dependencies/test.js
vendored
Normal file
137
dependencies/test.js
vendored
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* This file implements a pump optimization algorithm that:
|
||||||
|
* 1. Models different pumps with efficiency characteristics
|
||||||
|
* 2. Determines all possible pump combinations that can meet a demand flow
|
||||||
|
* 3. Finds the optimal combination that minimizes power consumption
|
||||||
|
* 4. Tests the algorithm with different demand levels
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pump Class
|
||||||
|
* Represents a pump with specific operating characteristics including:
|
||||||
|
* - Maximum flow capacity
|
||||||
|
* - Center of Gravity (CoG) for efficiency
|
||||||
|
* - Efficiency curve mapping flow percentages to power consumption
|
||||||
|
*/
|
||||||
|
class Pump {
|
||||||
|
constructor(name, maxFlow, cog, efficiencyCurve) {
|
||||||
|
this.name = name;
|
||||||
|
this.maxFlow = maxFlow; // Maximum flow at a given pressure
|
||||||
|
this.CoG = cog; // Efficiency center of gravity percentage
|
||||||
|
this.efficiencyCurve = efficiencyCurve; // Flow % -> Power usage mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns pump flow at a given pressure
|
||||||
|
* Currently assumes constant flow regardless of pressure
|
||||||
|
*/
|
||||||
|
getFlow(pressure) {
|
||||||
|
return this.maxFlow; // Assume constant flow at a given pressure
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates power consumption based on flow and pressure
|
||||||
|
* Uses the efficiency curve when available, otherwise uses linear approximation
|
||||||
|
*/
|
||||||
|
getPowerConsumption(flow, pressure) {
|
||||||
|
let flowPercent = flow / this.maxFlow;
|
||||||
|
return this.efficiencyCurve[flowPercent] || (1.2 * flow); // Default linear approximation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test pump definitions
|
||||||
|
* Three pump models with different flow capacities and efficiency characteristics
|
||||||
|
*/
|
||||||
|
const pumps = [
|
||||||
|
new Pump("Pump A", 100, 0.6, {0.6: 50, 0.8: 70, 1.0: 100}),
|
||||||
|
new Pump("Pump B", 120, 0.7, {0.6: 55, 0.8: 75, 1.0: 110}),
|
||||||
|
new Pump("Pump C", 90, 0.5, {0.5: 40, 0.7: 60, 1.0: 90}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const pressure = 1.0; // Assume constant pressure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all valid pump combinations that meet the required demand flow (Qd)
|
||||||
|
*
|
||||||
|
* @param {Array} pumps - Available pump array
|
||||||
|
* @param {Number} Qd - Required demand flow
|
||||||
|
* @param {Number} pressure - System pressure
|
||||||
|
* @returns {Array} Array of valid pump combinations that can meet or exceed the demand
|
||||||
|
*
|
||||||
|
* This function:
|
||||||
|
* 1. Generates all possible subsets of pumps (power set)
|
||||||
|
* 2. Filters for non-empty subsets that can meet or exceed demand flow
|
||||||
|
*/
|
||||||
|
function getValidPumpCombinations(pumps, Qd, pressure) {
|
||||||
|
let subsets = [[]];
|
||||||
|
for (let pump of pumps) {
|
||||||
|
let newSubsets = subsets.map(set => [...set, pump]);
|
||||||
|
subsets = subsets.concat(newSubsets);
|
||||||
|
}
|
||||||
|
return subsets.filter(subset => subset.length > 0 &&
|
||||||
|
subset.reduce((sum, p) => sum + p.getFlow(pressure), 0) >= Qd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the optimal pump combination that minimizes power consumption
|
||||||
|
*
|
||||||
|
* @param {Array} pumps - Available pump array
|
||||||
|
* @param {Number} Qd - Required demand flow
|
||||||
|
* @param {Number} pressure - System pressure
|
||||||
|
* @returns {Object} Object containing the best pump combination and its power consumption
|
||||||
|
*
|
||||||
|
* This function:
|
||||||
|
* 1. Gets all valid pump combinations that meet demand
|
||||||
|
* 2. For each combination, distributes flow based on CoG proportions
|
||||||
|
* 3. Calculates total power consumption for each distribution
|
||||||
|
* 4. Returns the combination with minimum power consumption
|
||||||
|
*/
|
||||||
|
function optimizePumpSelection(pumps, Qd, pressure) {
|
||||||
|
let validCombinations = getValidPumpCombinations(pumps, Qd, pressure);
|
||||||
|
let bestCombination = null;
|
||||||
|
let minPower = Infinity;
|
||||||
|
|
||||||
|
validCombinations.forEach(combination => {
|
||||||
|
let totalFlow = combination.reduce((sum, pump) => sum + pump.getFlow(pressure), 0);
|
||||||
|
let totalCoG = combination.reduce((sum, pump) => sum + pump.CoG, 0);
|
||||||
|
|
||||||
|
// Distribute flow based on CoG proportions
|
||||||
|
let flowDistribution = combination.map(pump => ({
|
||||||
|
pump,
|
||||||
|
flow: (pump.CoG / totalCoG) * Qd
|
||||||
|
}));
|
||||||
|
|
||||||
|
let totalPower = flowDistribution.reduce((sum, { pump, flow }) =>
|
||||||
|
sum + pump.getPowerConsumption(flow, pressure), 0);
|
||||||
|
|
||||||
|
if (totalPower < minPower) {
|
||||||
|
minPower = totalPower;
|
||||||
|
bestCombination = flowDistribution;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { bestCombination, minPower };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test function that runs optimization for different demand levels
|
||||||
|
* Tests from 0% to 100% of total available flow in 10% increments
|
||||||
|
* Outputs the selected pumps, flow allocation, and power consumption for each scenario
|
||||||
|
*/
|
||||||
|
console.log("Pump Optimization Results:");
|
||||||
|
const totalAvailableFlow = pumps.reduce((sum, pump) => sum + pump.getFlow(pressure), 0);
|
||||||
|
|
||||||
|
for (let i = 0; i <= 10; i++) {
|
||||||
|
let Qd = (i / 10) * totalAvailableFlow; // Incremental flow demand
|
||||||
|
let { bestCombination, minPower } = optimizePumpSelection(pumps, Qd, pressure);
|
||||||
|
|
||||||
|
console.log(`\nTotal Demand Flow: ${Qd.toFixed(2)}`);
|
||||||
|
console.log("Selected Pumps and Allocated Flow:");
|
||||||
|
|
||||||
|
bestCombination.forEach(({ pump, flow }) => {
|
||||||
|
console.log(` ${pump.name}: ${flow.toFixed(2)} units`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Total Power Consumption: ${minPower.toFixed(2)} kW`);
|
||||||
|
}
|
||||||
154
mgc.html
Normal file
154
mgc.html
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<!--
|
||||||
|
brabantse delta kleuren:
|
||||||
|
#eaf4f1
|
||||||
|
#86bbdd
|
||||||
|
#bad33b
|
||||||
|
#0c99d9
|
||||||
|
#a9daee
|
||||||
|
#0f52a5
|
||||||
|
#50a8d9
|
||||||
|
#cade63
|
||||||
|
#4f8582
|
||||||
|
#c4cce0
|
||||||
|
-->
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
RED.nodes.registerType('machineGroupControl',{
|
||||||
|
|
||||||
|
category: 'digital twin',
|
||||||
|
color: '#eaf4f1',
|
||||||
|
defaults: {
|
||||||
|
name: {value:""},
|
||||||
|
enableLog: { value: false },
|
||||||
|
logLevel: { value: "error" },
|
||||||
|
},
|
||||||
|
inputs:1,
|
||||||
|
outputs:4,
|
||||||
|
inputLabels: "Usage see manual",
|
||||||
|
outputLabels: ["process", "dbase", "upstreamParent", "downstreamParent"],
|
||||||
|
icon: "font-awesome/fa-tachometer",
|
||||||
|
//define label function
|
||||||
|
label: function() {
|
||||||
|
return this.name || "MachineGroup controller";
|
||||||
|
},
|
||||||
|
|
||||||
|
oneditprepare: function() {
|
||||||
|
const node = this;
|
||||||
|
|
||||||
|
console.log("Rotating Machine Node: Edit Prepare");
|
||||||
|
|
||||||
|
const elements = {
|
||||||
|
// Basic fields
|
||||||
|
name: document.getElementById("node-input-name"),
|
||||||
|
|
||||||
|
// Logging fields
|
||||||
|
logCheckbox: document.getElementById("node-input-enableLog"),
|
||||||
|
logLevelSelect: document.getElementById("node-input-logLevel"),
|
||||||
|
rowLogLevel: document.getElementById("row-logLevel"),
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const projecSettingstURL = "http://localhost:1880/generalFunctions/settings/projectSettings.json";
|
||||||
|
|
||||||
|
try{
|
||||||
|
|
||||||
|
// Fetch project settings
|
||||||
|
menuUtils.fetchProjectData(projecSettingstURL)
|
||||||
|
.then((projectSettings) => {
|
||||||
|
|
||||||
|
//assign to node vars
|
||||||
|
node.configUrls = projectSettings.configUrls;
|
||||||
|
|
||||||
|
const { cloudConfigURL, localConfigURL } = menuUtils.getSpecificConfigUrl("machineGroupControl",node.configUrls.cloud.taggcodeAPI);
|
||||||
|
node.configUrls.cloud.config = cloudConfigURL; // first call
|
||||||
|
node.configUrls.local.config = localConfigURL; // backup call
|
||||||
|
|
||||||
|
|
||||||
|
// Gets the ID of the active workspace (Flow)
|
||||||
|
const activeFlowId = RED.workspaces.active(); //fetches active flow id
|
||||||
|
node.processId = activeFlowId;
|
||||||
|
|
||||||
|
// UI elements
|
||||||
|
menuUtils.initBasicToggles(elements);
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}catch(e){
|
||||||
|
console.log("Error fetching project settings", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(node.d){
|
||||||
|
//this means node is disabled
|
||||||
|
console.log("Current status of node is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
oneditsave: function(){
|
||||||
|
|
||||||
|
const node = this;
|
||||||
|
|
||||||
|
//save basic properties
|
||||||
|
["name"].forEach(
|
||||||
|
(field) => {
|
||||||
|
const element = document.getElementById(`node-input-${field}`);
|
||||||
|
if (element) {
|
||||||
|
node[field] = element.value || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const logLevelElement = document.getElementById("node-input-logLevel");
|
||||||
|
node.logLevel = logLevelElement ? logLevelElement.value || "info" : "info";
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="machineGroupControl">
|
||||||
|
|
||||||
|
<!-------------------------------------------INPUT NAME / TYPE ----------------------------------------------->
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i>Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<!-- loglevel checkbox -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-enableLog"
|
||||||
|
><i class="fa fa-cog"></i> Enable Log</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="node-input-enableLog"
|
||||||
|
style="width:20px; vertical-align:baseline;"
|
||||||
|
/>
|
||||||
|
<span>Enable logging</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row" id="row-logLevel">
|
||||||
|
<label for="node-input-logLevel"><i class="fa fa-cog"></i> Log Level</label>
|
||||||
|
<select id="node-input-logLevel" style="width:60%;">
|
||||||
|
<option value="info">Info</option>
|
||||||
|
<option value="debug">Debug</option>
|
||||||
|
<option value="warn">Warn</option>
|
||||||
|
<option value="error">Error</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-------------------------------------------INPUT TRANSLATION TO OUTPUT ----------------------------------------------->
|
||||||
|
<hr />
|
||||||
|
<div class="form-tips"></div>
|
||||||
|
<b>Tip:</b> Ensure that the "Name" field is unique to easily identify the node.
|
||||||
|
Enable logging if you need detailed information for debugging purposes.
|
||||||
|
Choose the appropriate log level based on the verbosity required.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="machineGroupControl">
|
||||||
|
<p>A machineGroupControl node</p>
|
||||||
|
</script>
|
||||||
171
mgc.js
Normal file
171
mgc.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
module.exports = function (RED) {
|
||||||
|
function machineGroupControl(config) {
|
||||||
|
//create node
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
|
||||||
|
//call this => node so whenver you want to call a node function type node and the function behind it
|
||||||
|
var node = this;
|
||||||
|
|
||||||
|
//fetch machine object from machine.js
|
||||||
|
const MachineGroup = require('./dependencies/machineGroup/machineGroup');
|
||||||
|
const OutputUtils = require("../generalFunctions/helper/outputUtils");
|
||||||
|
|
||||||
|
const mgConfig = config = {
|
||||||
|
general: {
|
||||||
|
name: config.name,
|
||||||
|
id : config.id,
|
||||||
|
logging: {
|
||||||
|
enabled: config.loggingEnabled,
|
||||||
|
logLevel: config.logLevel,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
//make new class on creation to work with.
|
||||||
|
const mg = new MachineGroup(mgConfig);
|
||||||
|
|
||||||
|
// put mg on node memory as source
|
||||||
|
node.source = mg;
|
||||||
|
|
||||||
|
//load output utils
|
||||||
|
const output = new OutputUtils();
|
||||||
|
|
||||||
|
//update node status
|
||||||
|
function updateNodeStatus(mg) {
|
||||||
|
|
||||||
|
const mode = mg.mode;
|
||||||
|
const scaling = mg.scaling;
|
||||||
|
const totalFlow = Math.round(mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() * 1) / 1;
|
||||||
|
const totalPower = Math.round(mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() * 1) / 1;
|
||||||
|
|
||||||
|
// Calculate total capacity based on available machines
|
||||||
|
const availableMachines = Object.values(mg.machines).filter(machine => {
|
||||||
|
const state = machine.state.getCurrentState();
|
||||||
|
const mode = machine.currentMode;
|
||||||
|
return !(state === "off" || state === "maintenance" || mode === "maintenance");
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalCapacity = Math.round(mg.dynamicTotals.flow.max * 1) / 1;
|
||||||
|
|
||||||
|
// Determine overall status based on available machines
|
||||||
|
const status = availableMachines.length > 0
|
||||||
|
? `${availableMachines.length} machines`
|
||||||
|
: "No machines";
|
||||||
|
|
||||||
|
let scalingSymbol = '';
|
||||||
|
switch (scaling.toLowerCase()) {
|
||||||
|
case 'absolute':
|
||||||
|
scalingSymbol = 'Ⓐ'; // Clear symbol for Absolute mode
|
||||||
|
break;
|
||||||
|
case 'normalized':
|
||||||
|
scalingSymbol = 'Ⓝ'; // Clear symbol for Normalized mode
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
scalingSymbol = mode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Generate status text in a single line
|
||||||
|
const text = ` ${mode} | ${scalingSymbol}: 💨=${totalFlow}/${totalCapacity} | ⚡=${totalPower} | ${status}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
fill: availableMachines.length > 0 ? "green" : "red",
|
||||||
|
shape: "dot",
|
||||||
|
text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//never ending functions
|
||||||
|
function tick(){
|
||||||
|
//source.tick();
|
||||||
|
const status = updateNodeStatus(mg);
|
||||||
|
node.status(status);
|
||||||
|
|
||||||
|
//get output
|
||||||
|
const classOutput = mg.getOutput();
|
||||||
|
const dbOutput = output.formatMsg(classOutput, mg.config, "influxdb");
|
||||||
|
const pOutput = output.formatMsg(classOutput, mg.config, "process");
|
||||||
|
|
||||||
|
//only send output on values that changed
|
||||||
|
let msgs = [];
|
||||||
|
msgs[0] = pOutput;
|
||||||
|
msgs[1] = dbOutput;
|
||||||
|
|
||||||
|
node.send(msgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// register child on first output this timeout is needed because of node - red stuff
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
|
||||||
|
/*---execute code on first start----*/
|
||||||
|
let msgs = [];
|
||||||
|
|
||||||
|
msgs[2] = { topic : "registerChild" , payload: node.id, positionVsParent: "upstream" };
|
||||||
|
msgs[3] = { topic : "registerChild" , payload: node.id, positionVsParent: "downstream" };
|
||||||
|
|
||||||
|
//send msg
|
||||||
|
this.send(msgs);
|
||||||
|
},
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
//declare refresh interval internal node
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
/*---execute code on first start----*/
|
||||||
|
this.interval_id = setInterval(function(){ tick() },1000)
|
||||||
|
},
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------->>what to do on input
|
||||||
|
node.on("input", async function (msg,send,done) {
|
||||||
|
|
||||||
|
if(msg.topic == 'registerChild'){
|
||||||
|
const childId = msg.payload;
|
||||||
|
const childObj = RED.nodes.getNode(childId);
|
||||||
|
mg.childRegistrationUtils.registerChild(childObj.source,msg.positionVsParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(msg.topic == 'setMode'){
|
||||||
|
const mode = msg.payload;
|
||||||
|
const source = "parent";
|
||||||
|
mg.setMode(source,mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(msg.topic == 'setScaling'){
|
||||||
|
const scaling = msg.payload;
|
||||||
|
mg.setScaling(scaling);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(msg.topic == 'Qd'){
|
||||||
|
const Qd = parseFloat(msg.payload);
|
||||||
|
const source = "parent";
|
||||||
|
|
||||||
|
if (isNaN(Qd)) {
|
||||||
|
return mg.logger.error(`Invalid demand value: ${Qd}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
try{
|
||||||
|
|
||||||
|
await mg.handleInput(source,Qd);
|
||||||
|
msg.topic = mg.config.general.name;
|
||||||
|
msg.payload = "done";
|
||||||
|
send(msg);
|
||||||
|
}catch(e){
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tidy up any async code here - shutdown connections and so on.
|
||||||
|
node.on('close', function() {
|
||||||
|
clearTimeout(this.interval_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("machineGroupControl", machineGroupControl);
|
||||||
|
};
|
||||||
28
package.json
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "machineGroupControl",
|
||||||
|
"version": "0.9.0",
|
||||||
|
"description": "Control module machineGroupControl",
|
||||||
|
"main": "mgc.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node mgc.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.centraal.wbd-rd.nl/RnD/machineGroupControl.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"machineGroupControl",
|
||||||
|
"node-red"
|
||||||
|
],
|
||||||
|
"author": "Rene De Ren",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git",
|
||||||
|
"predict": "git+https://gitea.centraal.wbd-rd.nl/RnD/predict.git"
|
||||||
|
},
|
||||||
|
"node-red": {
|
||||||
|
"nodes": {
|
||||||
|
"machineGroupControl": "mgc.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user