Compare commits
19 Commits
35eb965609
...
dev-Rene
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4cb329597 | ||
|
|
b49f0c3ed2 | ||
|
|
edcffade75 | ||
|
|
b6ffefc92b | ||
|
|
ed2cf4c23d | ||
|
|
e0526250c2 | ||
|
|
426d45890f | ||
|
|
8c59a921d5 | ||
|
|
15501e8b1d | ||
|
|
b4364094c6 | ||
|
|
a55c6bdbea | ||
|
|
ac9d1b4fdd | ||
|
|
cbc0840f0c | ||
|
|
c62071992d | ||
|
|
ffab553f7e | ||
|
|
078a0d80dc | ||
|
|
dc1fb500c0 | ||
|
|
de5652b73d | ||
|
|
2aeb876c0d |
102
LICENSE
102
LICENSE
@@ -1,15 +1,97 @@
|
|||||||
MIT License
|
OPENBARE LICENTIE VAN DE EUROPESE UNIE v. 1.2.
|
||||||
|
EUPL © Europese Unie 2007, 2016
|
||||||
|
Deze openbare licentie van de Europese Unie („EUPL”) is van toepassing op het werk (zoals hieronder gedefinieerd) dat onder de voorwaarden van deze licentie wordt verstrekt. Elk gebruik van het werk dat niet door deze licentie is toegestaan, is verboden (voor zover dit gebruik valt onder een recht van de houder van het auteursrecht op het werk). Het werk wordt verstrekt onder de voorwaarden van deze licentie wanneer de licentiegever (zoals hieronder gedefinieerd), direct volgend op de kennisgeving inzake het auteursrecht op het werk, de volgende kennisgeving opneemt:
|
||||||
|
In licentie gegeven krachtens de EUPL
|
||||||
|
of op een andere wijze zijn bereidheid te kennen heeft gegeven krachtens de EUPL in licentie te geven.
|
||||||
|
|
||||||
Copyright (c) 2025 Rene De Ren
|
1.Definities
|
||||||
|
In deze licentie wordt verstaan onder:
|
||||||
|
— „de licentie”:de onderhavige licentie;
|
||||||
|
— „het oorspronkelijke werk”:het werk dat of de software die door de licentiegever krachtens deze licentie wordt verspreid of medegedeeld, en dat/die beschikbaar is als broncode en, in voorkomend geval, ook als uitvoerbare code;
|
||||||
|
— „bewerkingen”:de werken of software die de licentiehouder kan creëren op grond van het oorspronkelijke werk of wijzigingen ervan. In deze licentie wordt niet gedefinieerd welke mate van wijziging of afhankelijkheid van het oorspronkelijke werk vereist is om een werk als een bewerking te kunnen aanmerken; dat wordt bepaald conform het auteursrecht dat van toepassing is in de in artikel 15 bedoelde staat;
|
||||||
|
— „het werk”:het oorspronkelijke werk of de bewerkingen ervan;
|
||||||
|
— „de broncode”:de voor mensen leesbare vorm van het werk, die het gemakkelijkste door mensen kan worden bestudeerd en gewijzigd;
|
||||||
|
— „de uitvoerbare code”:elke code die over het algemeen is gecompileerd en is bedoeld om door een computer als een programma te worden uitgevoerd;
|
||||||
|
— „de licentiegever”:de natuurlijke of rechtspersoon die het werk krachtens de licentie verspreidt of mededeelt;
|
||||||
|
— „bewerker(s)”:elke natuurlijke of rechtspersoon die het werk krachtens de licentie wijzigt of op een andere wijze bijdraagt tot de totstandkoming van een bewerking;
|
||||||
|
— „de licentiehouder” of „u”:elke natuurlijke of rechtspersoon die het werk onder de voorwaarden van de licentie gebruikt; — „verspreiding” of „mededeling”:het verkopen, geven, uitlenen, verhuren, verspreiden, mededelen, doorgeven, of op een andere wijze online of offline beschikbaar stellen van kopieën van het werk of het verlenen van toegang tot de essentiële functies ervan ten behoeve van andere natuurlijke of rechtspersonen.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
2.Draagwijdte van de uit hoofde van de licentie verleende rechten
|
||||||
of this software and associated documentation files (the "Software"), to use,
|
De licentiegever verleent u hierbij een wereldwijde, royaltyvrije, niet-exclusieve, voor een sublicentie in aanmerking komende licentie, om voor de duur van het aan het oorspronkelijke werk verbonden auteursrecht, het volgende te doen:
|
||||||
copy, modify, merge, publish, and distribute the Software for **personal, scientific, or educational purposes**, subject to the following conditions:
|
— het werk in alle omstandigheden en voor ongeacht welk doel te gebruiken;
|
||||||
|
— het werk te verveelvoudigen;
|
||||||
|
— het werk te wijzigen en op grond van het werk bewerkingen te ontwikkelen;
|
||||||
|
— het werk aan het publiek mede te delen, waaronder het recht om het werk of kopieën ervan aan het publiek ter beschikking te stellen of te vertonen, en het werk, in voorkomend geval, in het openbaar uit te voeren;
|
||||||
|
— het werk of kopieën ervan te verspreiden;
|
||||||
|
— het werk of kopieën ervan uit te lenen en te verhuren;
|
||||||
|
— de rechten op het werk of op kopieën ervan in sublicentie te geven.
|
||||||
|
Deze rechten kunnen worden uitgeoefend met gebruikmaking van alle thans bekende of nog uit te vinden media, dragers en formaten, voor zover het toepasselijke recht dit toestaat. In de landen waar immateriële rechten van toepassing zijn, doet de licentiegever afstand van zijn recht op uitoefening van zijn immateriële rechten in de mate die door het toepasselijke recht wordt toegestaan teneinde een doeltreffende uitoefening van de bovenvermelde in licentie gegeven economische rechten mogelijk te maken. De licentiegever verleent de licentiehouder een royaltyvrij, niet-exclusief gebruiksrecht op alle octrooien van de licentiegever, voor zover dit noodzakelijk is om de uit hoofde van deze licentie verleende rechten op het werk te gebruiken.
|
||||||
|
|
||||||
**Commercial use of the Software or any derivative work is explicitly prohibited without prior written consent from the authors.**
|
3.Mededeling van de broncode
|
||||||
This includes but is not limited to resale, inclusion in paid products or services, and monetized distribution.
|
De licentiegever kan het werk verstrekken in zijn broncode of als uitvoerbare code. Indien het werk als uitvoerbare code wordt verstrekt, verstrekt de licentiegever bij elke door hem verspreide kopie van het werk tevens een machinaal leesbare kopie van de broncode van het werk of geeft hij in een mededeling, volgende op de bij het werk gevoegde auteursrechtelijke kennisgeving, de plaats aan waar de broncode gemakkelijk en vrij toegankelijk is, zolang de licentiegever het werk blijft verspreiden of mededelen.
|
||||||
Any commercial usage must be governed by a shared license or explicit contractual agreement with the authors.
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
4.Beperkingen van het auteursrecht
|
||||||
|
Geen enkele bepaling in deze licentie heeft ten doel de licentiehouder het recht te ontnemen een beroep te doen op een uitzondering op of een beperking van de exclusieve rechten van de rechthebbenden op het werk, of op de uitputting van die rechten of andere toepasselijke beperkingen daarvan.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED...
|
5.Verplichtingen van de licentiehouder
|
||||||
|
De verlening van de bovenvermelde rechten is onderworpen aan een aantal aan de licentiehouder opgelegde beperkingen en verplichtingen. Het gaat om de onderstaande verplichtingen.
|
||||||
|
|
||||||
|
Attributierecht: de licentiehouder moet alle auteurs-, octrooi- of merkenrechtelijke kennisgevingen onverlet laten alsook alle kennisgevingen die naar de licentie en de afwijzing van garanties verwijzen. De licentiehouder moet een afschrift van deze kennisgevingen en een afschrift van de licentie bij elke kopie van het werk voegen die hij verspreidt of mededeelt. De licentiehouder moet in elke bewerking duidelijk aangeven dat het werk is gewijzigd, en eveneens de datum van wijziging vermelden.
|
||||||
|
|
||||||
|
Copyleftclausule: wanneer de licentiehouder kopieën van het oorspronkelijke werk of bewerkingen verspreidt of mededeelt, geschiedt die verspreiding of mededeling onder de voorwaarden van deze licentie of van een latere versie van deze licentie, tenzij het oorspronkelijke werk uitdrukkelijk alleen onder deze versie van de licentie wordt verspreid — bijvoorbeeld door de mededeling „alleen EUPL v. 1.2”. De licentiehouder (die licentiegever wordt) kan met betrekking tot het werk of de bewerkingen geen aanvullende bepalingen of voorwaarden opleggen of stellen die de voorwaarden van de licentie wijzigen of beperken.
|
||||||
|
|
||||||
|
Verenigbaarheidsclausule: wanneer de licentiehouder bewerkingen of kopieën ervan verspreidt of mededeelt die zijn gebaseerd op het werk en op een ander werk dat uit hoofde van een verenigbare licentie in licentie is gegeven, kan die verspreiding of mededeling geschieden onder de voorwaarden van deze verenigbare licentie. Voor de toepassing van deze clausule wordt onder „verenigbare licentie” verstaan, de licenties die in het aanhangsel bij deze licentie zijn opgesomd. Indien de verplichtingen van de licentiehouder uit hoofde van de verenigbare licentie in strijd zijn met diens verplichtingen uit hoofde van deze licentie, hebben de verplichtingen van de verenigbare licentie voorrang.
|
||||||
|
|
||||||
|
Verstrekking van de broncode: bij de verspreiding of mededeling van kopieën van het werk verstrekt de licentiehouder een machinaal leesbare kopie van de broncode of geeft hij aan waar deze broncode gemakkelijk en vrij toegankelijk is, zolang de licentiehouder het werk blijft verspreiden of mededelen.
|
||||||
|
|
||||||
|
Juridische bescherming: deze licentie verleent geen toestemming om handelsnamen, handelsmerken, dienstmerken of namen van de licentiegever te gebruiken, behalve wanneer dit op grond van een redelijk en normaal gebruik noodzakelijk is om de oorsprong van het werk te beschrijven en de inhoud van de auteursrechtelijke kennisgeving te herhalen.
|
||||||
|
|
||||||
|
6.Auteursketen
|
||||||
|
De oorspronkelijke licentiegever garandeert dat hij houder is van het hierbij verleende auteursrecht op het oorspronkelijke werk dan wel dat dit hem in licentie is gegeven en dat hij de bevoegdheid heeft de licentie te verlenen. Elke bewerker garandeert dat hij houder is van het auteursrecht op de door hem aan het werk aangebrachte wijzigingen dan wel dat dit hem in licentie is gegeven en dat hij de bevoegdheid heeft de licentie te verlenen. Telkens wanneer u de licentie aanvaardt, verlenen de oorspronkelijke licentiegever en de opeenvolgende bewerkers u een licentie op hun bijdragen aan het werk onder de voorwaarden van deze licentie.
|
||||||
|
|
||||||
|
7.Uitsluiting van garantie
|
||||||
|
Het werk is een werk in ontwikkeling, dat voortdurend door vele bewerkers wordt verbeterd. Het is een onvoltooid werk, dat bijgevolg nog tekortkomingen of programmeerfouten („bugs”) kan vertonen, die onlosmakelijk verbonden zijn met dit soort ontwikkeling. Om die reden wordt het werk op grond van de licentie verstrekt „zoals het is” en zonder enige garantie met betrekking tot het werk te geven, met inbegrip van, maar niet beperkt tot garanties met betrekking tot de verhandelbaarheid, de geschiktheid voor een specifiek doel, de afwezigheid van tekortkomingen of fouten, de nauwkeurigheid, de eerbiediging van andere intellectuele-eigendomsrechten dan het in artikel 6 van deze licentie bedoelde auteursrecht. Deze uitsluiting van garantie is een essentieel onderdeel van de licentie en een voorwaarde voor de verlening van rechten op het werk.
|
||||||
|
|
||||||
|
8.Uitsluiting van aansprakelijkheid
|
||||||
|
Behoudens in het geval van een opzettelijke fout of directe schade aan natuurlijke personen, is de licentiegever in geen enkel geval aansprakelijk voor ongeacht welke directe of indirecte, materiële of immateriële schade die voortvloeit uit de licentie of het gebruik van het werk, met inbegrip van, maar niet beperkt tot schade als gevolg van het verlies van goodwill, verloren werkuren, een computerdefect of computerfout, het verlies van gegevens, of enige andere commerciële schade, zelfs indien de licentiegever werd gewezen op de mogelijkheid van dergelijke schade. De licentiegever is echter aansprakelijk op grond van de wetgeving inzake productaansprakelijkheid, voor zover deze wetgeving op het werk van toepassing is.
|
||||||
|
|
||||||
|
9.Aanvullende overeenkomsten
|
||||||
|
Bij de verspreiding van het werk kunt u ervoor kiezen een aanvullende overeenkomst te sluiten, waarin de verplichtingen of diensten overeenkomstig deze licentie worden omschreven. Indien deze verplichtingen worden aanvaard, kunt u echter alleen in eigen naam en onder eigen verantwoordelijkheid handelen, en dus niet in naam van de oorspronkelijke licentiegever of een bewerker, en kunt u voorts alleen handelen indien u ermee instemt alle bewerkers schadeloos te stellen, te verdedigen of te vrijwaren met betrekking tot de aansprakelijkheid van of vorderingen tegen deze bewerkers op grond van het feit dat u een garantie of aanvullende aansprakelijkheid hebt aanvaard.
|
||||||
|
|
||||||
|
10.Aanvaarding van de licentie
|
||||||
|
De bepalingen van deze licentie kunnen worden aanvaard door te klikken op het pictogram „Ik ga akkoord”, dat zich bevindt onderaan het venster waarin de tekst van deze licentie is weergegeven, of door overeenkomstig de toepasselijke wetsbepalingen op een soortgelijke wijze met de licentie in te stemmen. Door op dat pictogram te klikken geeft u aan dat u deze licentie en alle voorwaarden ervan ondubbelzinnig en onherroepelijk aanvaardt. Evenzo aanvaardt u onherroepelijk deze licentie en alle voorwaarden ervan door uitoefening van de rechten die u in artikel 2 van deze licentie zijn verleend, zoals het gebruik van het werk, het creëren door u van een bewerking of de verspreiding of mededeling door u van het werk of kopieën ervan.
|
||||||
|
|
||||||
|
11.Voorlichting van het publiek
|
||||||
|
Indien u het werk verspreidt of mededeelt door middel van elektronische communicatiemiddelen (bijvoorbeeld door voor te stellen het werk op afstand te downloaden), moet het distributiekanaal of het medium (bijvoorbeeld een website) het publiek ten minste de gegevens verschaffen die door het toepasselijke recht zijn voorgeschreven met betrekking tot de licentiegever, de licentie en de wijze waarop deze kan worden geraadpleegd, gesloten, opgeslagen en gereproduceerd door de licentiehouder.
|
||||||
|
|
||||||
|
12.Einde van de licentie
|
||||||
|
De licentie en de uit hoofde daarvan verleende rechten eindigen automatisch bij elke inbreuk door de licentiehouder op de voorwaarden van de licentie. Dit einde beëindigt niet de licenties van personen die het werk van de licentiehouder krachtens de licentie hebben ontvangen, mits deze personen zich volledig aan de licentie houden.
|
||||||
|
|
||||||
|
13.Overige
|
||||||
|
Onverminderd artikel 9 vormt de licentie de gehele overeenkomst tussen de partijen met betrekking tot het werk. Indien een bepaling van de licentie volgens het toepasselijke recht ongeldig is of niet uitvoerbaar is, doet dit geen afbreuk aan de geldigheid of uitvoerbaarheid van de licentie in haar geheel. Deze bepaling dient zodanig te worden uitgelegd of gewijzigd dat zij geldig en uitvoerbaar wordt. De Europese Commissie kan, voor zover dit noodzakelijk en redelijk is, versies in andere talen of nieuwe versies van deze licentie of geactualiseerde versies van dit aanhangsel publiceren, zonder de draagwijdte van de uit hoofde van de licentie verleende rechten te beperken. Nieuwe versies van de licentie zullen worden gepubliceerd met een uniek versienummer. Alle door de Europese Commissie goedgekeurde taalversies van deze licentie hebben dezelfde waarde. De partijen kunnen zich beroepen op de taalversie van hun keuze.
|
||||||
|
|
||||||
|
14.Bevoegd gerecht
|
||||||
|
Onverminderd specifieke overeenkomsten tussen de partijen,
|
||||||
|
— vallen alle geschillen tussen de instellingen, organen en instanties van de Europese Unie, als licentiegeefster, en een licentiehouder in verband met de uitlegging van deze licentie onder de bevoegdheid van het Hof van Justitie van de Europese Unie, conform artikel 272 van het Verdrag betreffende de werking van de Europese Unie,
|
||||||
|
— vallen alle geschillen tussen andere partijen in verband met de uitlegging van deze licentie onder de uitsluitende bevoegdheid van het bevoegde gerecht van de plaats waar de licentiegever is gevestigd of zijn voornaamste activiteit uitoefent.
|
||||||
|
|
||||||
|
15.Toepasselijk recht
|
||||||
|
Onverminderd specifieke overeenkomsten tussen de partijen,
|
||||||
|
— wordt deze licentie beheerst door het recht van de lidstaat van de Europese Unie waar de licentiegever zijn statutaire zetel, verblijfplaats of hoofdkantoor heeft,
|
||||||
|
— wordt deze licentie beheerst door het Belgische recht indien de licentiegever geen statutaire zetel, verblijfplaats of hoofdkantoor heeft in een lidstaat van de Europese Unie.
|
||||||
|
|
||||||
|
|
||||||
|
Aanhangsel
|
||||||
|
„Verenigbare licenties” in de zin van artikel 5 EUPL zijn:
|
||||||
|
— GNU General Public License (GPL) v. 2, v. 3
|
||||||
|
— GNU Affero General Public License (AGPL) v. 3
|
||||||
|
— Open Software License (OSL) v. 2.1, v. 3.0
|
||||||
|
— Eclipse Public License (EPL) v. 1.0
|
||||||
|
— CeCILL v. 2.0, v. 2.1
|
||||||
|
— Mozilla Public Licence (MPL) v. 2
|
||||||
|
— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||||
|
— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) voor andere werken dan software
|
||||||
|
— European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||||
|
— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) of Strong Reciprocity (LiLiQ-R+).
|
||||||
|
De Europese Commissie kan dit aanhangsel actualiseren in geval van latere versies van de bovengenoemde licenties zonder dat er een nieuwe EUPL-versie wordt ontwikkeld, zolang die versies de uit hoofde van artikel 2 van deze licentie verleende rechten verlenen en ze de betrokken broncode beschermen tegen exclusieve toe-eigening.
|
||||||
|
Voor alle andere wijzigingen van of aanvullingen op dit aanhangsel is de ontwikkeling van een nieuwe EUPL-versie vereist.
|
||||||
1056
dependencies/machineGroup/machineGroup.js
vendored
1056
dependencies/machineGroup/machineGroup.js
vendored
File diff suppressed because it is too large
Load Diff
566
dependencies/machineGroup/machineGroup.test.js
vendored
566
dependencies/machineGroup/machineGroup.test.js
vendored
@@ -1,566 +0,0 @@
|
|||||||
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
188
dependencies/machineGroup/machineGroupConfig.json
vendored
@@ -1,188 +0,0 @@
|
|||||||
{
|
|
||||||
"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
137
dependencies/test.js
vendored
@@ -1,137 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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`);
|
|
||||||
}
|
|
||||||
46
mgc.html
46
mgc.html
@@ -1,15 +1,12 @@
|
|||||||
<!--
|
<!--
|
||||||
brabantse delta kleuren:
|
| S88-niveau | Primair (blokkleur) | Tekstkleur |
|
||||||
#eaf4f1
|
| ---------------------- | ------------------- | ---------- |
|
||||||
#86bbdd
|
| **Area** | `#0f52a5` | wit |
|
||||||
#bad33b
|
| **Process Cell** | `#0c99d9` | wit |
|
||||||
#0c99d9
|
| **Unit** | `#50a8d9` | zwart |
|
||||||
#a9daee
|
| **Equipment (Module)** | `#86bbdd` | zwart |
|
||||||
#0f52a5
|
| **Control Module** | `#a9daee` | zwart |
|
||||||
#50a8d9
|
|
||||||
#cade63
|
|
||||||
#4f8582
|
|
||||||
#c4cce0
|
|
||||||
-->
|
-->
|
||||||
<script src="/machineGroupControl/menu.js"></script> <!-- Load the menu script for dynamic dropdowns -->
|
<script src="/machineGroupControl/menu.js"></script> <!-- Load the menu script for dynamic dropdowns -->
|
||||||
<script src="/machineGroupControl/configData.js"></script> <!-- Load the config script for node information -->
|
<script src="/machineGroupControl/configData.js"></script> <!-- Load the config script for node information -->
|
||||||
@@ -17,7 +14,7 @@
|
|||||||
<script>
|
<script>
|
||||||
RED.nodes.registerType('machineGroupControl',{
|
RED.nodes.registerType('machineGroupControl',{
|
||||||
category: "EVOLV",
|
category: "EVOLV",
|
||||||
color: "#eaf4f1",
|
color: "#50a8d9",
|
||||||
defaults: {
|
defaults: {
|
||||||
// Define default properties
|
// Define default properties
|
||||||
name: { value: "" },
|
name: { value: "" },
|
||||||
@@ -26,16 +23,20 @@
|
|||||||
enableLog: { value: false },
|
enableLog: { value: false },
|
||||||
logLevel: { value: "error" },
|
logLevel: { value: "error" },
|
||||||
|
|
||||||
// Physical aspect
|
//physicalAspect
|
||||||
positionVsParent: { value: "" },
|
positionVsParent: { value: "" },
|
||||||
positionLabel: { value: "" },
|
|
||||||
positionIcon: { value: "" },
|
positionIcon: { value: "" },
|
||||||
|
hasDistance: { value: false },
|
||||||
|
distance: { value: 0 },
|
||||||
|
distanceUnit: { value: "m" },
|
||||||
|
distanceDescription: { value: "" }
|
||||||
|
|
||||||
},
|
},
|
||||||
inputs:1,
|
inputs:1,
|
||||||
outputs:3,
|
outputs:3,
|
||||||
inputLabels: ["Input"],
|
inputLabels: ["Input"],
|
||||||
outputLabels: ["process", "dbase", "parent"],
|
outputLabels: ["process", "dbase", "parent"],
|
||||||
icon: "font-awesome/fa-tachometer",
|
icon: "font-awesome/fa-cogs",
|
||||||
|
|
||||||
label: function () {
|
label: function () {
|
||||||
return this.positionIcon + " " + "machineGroup";
|
return this.positionIcon + " " + "machineGroup";
|
||||||
@@ -57,13 +58,13 @@
|
|||||||
const node = this;
|
const node = this;
|
||||||
|
|
||||||
// Validate logger properties using the logger menu
|
// Validate logger properties using the logger menu
|
||||||
if (window.EVOLV?.nodes?.measurement?.loggerMenu?.saveEditor) {
|
if (window.EVOLV?.nodes?.machineGroupControl?.loggerMenu?.saveEditor) {
|
||||||
success = window.EVOLV.nodes.measurement.loggerMenu.saveEditor(node);
|
success = window.EVOLV.nodes.machineGroupControl.loggerMenu.saveEditor(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
// save position field
|
// save position field
|
||||||
if (window.EVOLV?.nodes?.rotatingMachine?.positionMenu?.saveEditor) {
|
if (window.EVOLV?.nodes?.machineGroupControl?.positionMenu?.saveEditor) {
|
||||||
window.EVOLV.nodes.rotatingMachine.positionMenu.saveEditor(this);
|
window.EVOLV.nodes.machineGroupControl.positionMenu.saveEditor(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -79,13 +80,6 @@
|
|||||||
<!-- Position fields injected here -->
|
<!-- Position fields injected here -->
|
||||||
<div id="position-fields-placeholder"></div>
|
<div id="position-fields-placeholder"></div>
|
||||||
|
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<script type="text/html" data-help-name="machineGroupControl">
|
<script type="text/html" data-help-name="machineGroupControl">
|
||||||
|
|||||||
170
mgcOLD.js
170
mgcOLD.js
@@ -1,170 +0,0 @@
|
|||||||
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);
|
|
||||||
};
|
|
||||||
345
src/groupcontrol.test.js
Normal file
345
src/groupcontrol.test.js
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const MachineGroup = require('./specificClass');
|
||||||
|
const Machine = require('../../rotatingMachine/src/specificClass');
|
||||||
|
const Measurement = require('../../measurement/src/specificClass');
|
||||||
|
const baseCurve = require('../../generalFunctions/datasets/assetData/curves/hidrostal-H05K-S03R.json');
|
||||||
|
|
||||||
|
const CONTROL_MODES = ['optimalcontrol', 'prioritycontrol', 'prioritypercentagecontrol'];
|
||||||
|
const MODE_LABELS = {
|
||||||
|
optimalcontrol: 'OPT',
|
||||||
|
prioritycontrol: 'PRIO',
|
||||||
|
prioritypercentagecontrol: 'PERC'
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateConfig = {
|
||||||
|
time: { starting: 0, warmingup: 0, stopping: 0, coolingdown: 0, emergencystop: 0 },
|
||||||
|
movement: { speed: 1200, mode: 'staticspeed', maxSpeed: 1800 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const ptConfig = {
|
||||||
|
general: { logging: { enabled: false, logLevel: 'error' }, name: 'synthetic-pt', id: 'pt-1', unit: 'mbar' },
|
||||||
|
functionality: {
|
||||||
|
softwareType: 'measurement',
|
||||||
|
role: 'sensor',
|
||||||
|
positionVsParent: 'downstream'
|
||||||
|
},
|
||||||
|
asset: { category: 'sensor', type: 'pressure', model: 'synthetic-pt', supplier: 'lab', unit: 'mbar' },
|
||||||
|
scaling: { absMin: 0, absMax: 4000 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const scenarios = [
|
||||||
|
{
|
||||||
|
name: 'balanced_pair',
|
||||||
|
description: 'Two identical pumps validate equal-machine behaviour.',
|
||||||
|
machines: [
|
||||||
|
{ id: 'eq-1', label: 'equal-A', curveMods: { flowScale: 1, powerScale: 1 } },
|
||||||
|
{ id: 'eq-2', label: 'equal-B', curveMods: { flowScale: 1, powerScale: 1 } }
|
||||||
|
],
|
||||||
|
pressures: [900, 1300, 1700],
|
||||||
|
flowTargetsPercent: [0.1, 0.4, 0.7, 1],
|
||||||
|
flowMatchTolerance: 5,
|
||||||
|
priorityList: ['eq-1', 'eq-2']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mixed_trio',
|
||||||
|
description: 'High / mid / low efficiency pumps to stress unequal-machine behaviour.',
|
||||||
|
machines: [
|
||||||
|
{ id: 'hi', label: 'high-eff', curveMods: { flowScale: 1.25, powerScale: 0.82, flowTilt: 0.1, powerTilt: -0.05 } },
|
||||||
|
{ id: 'mid', label: 'mid-eff', curveMods: { flowScale: 1, powerScale: 1 } },
|
||||||
|
{ id: 'low', label: 'low-eff', curveMods: { flowScale: 0.7, powerScale: 1.35, flowTilt: -0.08, powerTilt: 0.15 } }
|
||||||
|
],
|
||||||
|
pressures: [800, 1200, 1600, 2000],
|
||||||
|
flowTargetsPercent: [0.1, 0.35, 0.7, 1],
|
||||||
|
flowMatchTolerance: 8,
|
||||||
|
priorityList: ['hi', 'mid', 'low']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function createGroupConfig(name) {
|
||||||
|
return {
|
||||||
|
general: { logging: { enabled: false, logLevel: 'error' }, name: `machinegroup-${name}` },
|
||||||
|
functionality: { softwareType: 'machinegroup', role: 'groupcontroller' },
|
||||||
|
scaling: { current: 'normalized' },
|
||||||
|
mode: { current: 'optimalcontrol' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setPressure(pt, value) {
|
||||||
|
const retries = 6;
|
||||||
|
for (let attempt = 0; attempt < retries; attempt += 1) {
|
||||||
|
try {
|
||||||
|
pt.calculateInput(value);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error?.message || String(error);
|
||||||
|
if (!message.toLowerCase().includes('coolprop is still warming up')) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
await sleep(50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to update pressure to ${value} mbar; CoolProp did not initialise in time.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepClone(obj) {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
function distortSeries(series = [], scale = 1, tilt = 0) {
|
||||||
|
if (!Array.isArray(series) || series.length === 0) {
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
const lastIndex = series.length - 1;
|
||||||
|
return series.map((value, index) => {
|
||||||
|
const gradient = lastIndex === 0 ? 0 : index / lastIndex - 0.5;
|
||||||
|
const distorted = value * scale * (1 + tilt * gradient);
|
||||||
|
return Number(Math.max(distorted, 0).toFixed(6));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSyntheticCurve(mods = {}) {
|
||||||
|
const { flowScale = 1, powerScale = 1, flowTilt = 0, powerTilt = 0 } = mods;
|
||||||
|
const curve = deepClone(baseCurve);
|
||||||
|
if (curve.nq) {
|
||||||
|
Object.values(curve.nq).forEach(set => {
|
||||||
|
set.y = distortSeries(set.y, flowScale, flowTilt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (curve.np) {
|
||||||
|
Object.values(curve.np).forEach(set => {
|
||||||
|
set.y = distortSeries(set.y, powerScale, powerTilt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return curve;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMachineConfig(id, label) {
|
||||||
|
return {
|
||||||
|
general: { logging: { enabled: false, logLevel: 'error' }, name: label, id, unit: 'm3/h' },
|
||||||
|
functionality: { softwareType: 'machine', role: 'rotationaldevicecontroller' },
|
||||||
|
asset: { category: 'pump', type: 'centrifugal', model: 'hidrostal-h05k-s03r', supplier: 'hidrostal', machineCurve: baseCurve },
|
||||||
|
mode: {
|
||||||
|
current: 'auto',
|
||||||
|
allowedActions: {
|
||||||
|
auto: ['execsequence', 'execmovement', 'flowmovement', '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']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootstrapScenarioMachines(scenario) {
|
||||||
|
const mg = new MachineGroup(createGroupConfig(scenario.name));
|
||||||
|
const pt = new Measurement(ptConfig);
|
||||||
|
|
||||||
|
for (const machineDef of scenario.machines) {
|
||||||
|
const machine = new Machine(createMachineConfig(machineDef.id, machineDef.label), stateConfig);
|
||||||
|
if (machineDef.curveMods) {
|
||||||
|
machine.updateCurve(createSyntheticCurve(machineDef.curveMods));
|
||||||
|
}
|
||||||
|
mg.childRegistrationUtils.registerChild(machine, 'downstream');
|
||||||
|
machine.childRegistrationUtils.registerChild(pt, 'downstream');
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(25);
|
||||||
|
return { mg, pt };
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureTotals(mg) {
|
||||||
|
const flow = mg.measurements.type('flow').variant('predicted').position('atequipment').getCurrentValue() || 0;
|
||||||
|
const power = mg.measurements.type('power').variant('predicted').position('atequipment').getCurrentValue() || 0;
|
||||||
|
const efficiency = mg.measurements.type('efficiency').variant('predicted').position('atequipment').getCurrentValue() || 0;
|
||||||
|
return { flow, power, efficiency };
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeAbsoluteTargets(dynamicTotals, percentages) {
|
||||||
|
const { flow } = dynamicTotals;
|
||||||
|
const min = Number.isFinite(flow.min) ? flow.min : 0;
|
||||||
|
const max = Number.isFinite(flow.max) ? flow.max : 0;
|
||||||
|
const span = Math.max(max - min, 1);
|
||||||
|
return percentages.map(percent => {
|
||||||
|
const pct = Math.max(0, Math.min(1, percent));
|
||||||
|
return min + pct * span;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function driveModeToFlow({ mg, pt, mode, pressure, targetFlow, priorityOrder }) {
|
||||||
|
await setPressure(pt, pressure);
|
||||||
|
await sleep(15);
|
||||||
|
|
||||||
|
mg.setMode(mode);
|
||||||
|
mg.setScaling('normalized'); // required for prioritypercentagecontrol, works for others too
|
||||||
|
|
||||||
|
const dynamic = mg.calcDynamicTotals();
|
||||||
|
const span = Math.max(dynamic.flow.max - dynamic.flow.min, 1);
|
||||||
|
const normalizedTarget = ((targetFlow - dynamic.flow.min) / span) * 100;
|
||||||
|
|
||||||
|
let low = 0;
|
||||||
|
let high = 100;
|
||||||
|
let demand = Math.max(0, Math.min(100, normalizedTarget || 0));
|
||||||
|
let best = { demand, flow: 0, power: 0, efficiency: 0, error: Infinity };
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
||||||
|
await mg.handleInput('parent', demand, Infinity, priorityOrder);
|
||||||
|
await sleep(30);
|
||||||
|
|
||||||
|
const totals = captureTotals(mg);
|
||||||
|
const error = Math.abs(totals.flow - targetFlow);
|
||||||
|
if (error < best.error) {
|
||||||
|
best = {
|
||||||
|
demand,
|
||||||
|
flow: totals.flow,
|
||||||
|
power: totals.power,
|
||||||
|
efficiency: totals.efficiency,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totals.flow > targetFlow) {
|
||||||
|
high = demand;
|
||||||
|
} else {
|
||||||
|
low = demand;
|
||||||
|
}
|
||||||
|
demand = (low + high) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEfficiencyRows(rows) {
|
||||||
|
return rows.map(row => {
|
||||||
|
const optimal = row.modes.optimalcontrol;
|
||||||
|
const priority = row.modes.prioritycontrol;
|
||||||
|
const percentage = row.modes.prioritypercentagecontrol;
|
||||||
|
return {
|
||||||
|
pressure: row.pressure,
|
||||||
|
targetFlow: Number(row.targetFlow.toFixed(1)),
|
||||||
|
[`${MODE_LABELS.optimalcontrol}_Flow`]: Number(optimal.flow.toFixed(1)),
|
||||||
|
[`${MODE_LABELS.optimalcontrol}_Eff`]: Number(optimal.efficiency.toFixed(3)),
|
||||||
|
[`${MODE_LABELS.prioritycontrol}_Flow`]: Number(priority.flow.toFixed(1)),
|
||||||
|
[`${MODE_LABELS.prioritycontrol}_Eff`]: Number(priority.efficiency.toFixed(3)),
|
||||||
|
[`Δ${MODE_LABELS.prioritycontrol}-OPT_Eff`]: Number(
|
||||||
|
(priority.efficiency - optimal.efficiency).toFixed(3)
|
||||||
|
),
|
||||||
|
[`${MODE_LABELS.prioritypercentagecontrol}_Flow`]: Number(percentage.flow.toFixed(1)),
|
||||||
|
[`${MODE_LABELS.prioritypercentagecontrol}_Eff`]: Number(percentage.efficiency.toFixed(3)),
|
||||||
|
[`Δ${MODE_LABELS.prioritypercentagecontrol}-OPT_Eff`]: Number(
|
||||||
|
(percentage.efficiency - optimal.efficiency).toFixed(3)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeEfficiency(rows) {
|
||||||
|
const map = new Map();
|
||||||
|
rows.forEach(row => {
|
||||||
|
CONTROL_MODES.forEach(mode => {
|
||||||
|
const key = `${row.scenario}-${mode}`;
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, {
|
||||||
|
scenario: row.scenario,
|
||||||
|
mode,
|
||||||
|
samples: 0,
|
||||||
|
avgFlowDiff: 0,
|
||||||
|
avgEfficiency: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const bucket = map.get(key);
|
||||||
|
const stats = row.modes[mode];
|
||||||
|
bucket.samples += 1;
|
||||||
|
bucket.avgFlowDiff += Math.abs(stats.flow - row.targetFlow);
|
||||||
|
bucket.avgEfficiency += stats.efficiency || 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Array.from(map.values()).map(item => ({
|
||||||
|
scenario: item.scenario,
|
||||||
|
mode: item.mode,
|
||||||
|
samples: item.samples,
|
||||||
|
avgFlowDiff: Number((item.avgFlowDiff / item.samples).toFixed(2)),
|
||||||
|
avgEfficiency: Number((item.avgEfficiency / item.samples).toFixed(3))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function evaluateScenario(scenario) {
|
||||||
|
console.log(`\nRunning scenario "${scenario.name}": ${scenario.description}`);
|
||||||
|
const { mg, pt } = await bootstrapScenarioMachines(scenario);
|
||||||
|
const priorityOrder =
|
||||||
|
scenario.priorityList && scenario.priorityList.length
|
||||||
|
? scenario.priorityList
|
||||||
|
: scenario.machines.map(machine => machine.id);
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
for (const pressure of scenario.pressures) {
|
||||||
|
await setPressure(pt, pressure);
|
||||||
|
await sleep(20);
|
||||||
|
|
||||||
|
const dynamicTotals = mg.calcDynamicTotals();
|
||||||
|
const targets = computeAbsoluteTargets(dynamicTotals, scenario.flowTargetsPercent || [0, 0.5, 1]);
|
||||||
|
|
||||||
|
for (let idx = 0; idx < targets.length; idx += 1) {
|
||||||
|
const targetFlow = targets[idx];
|
||||||
|
const row = {
|
||||||
|
scenario: scenario.name,
|
||||||
|
pressure,
|
||||||
|
targetFlow,
|
||||||
|
modes: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const mode of CONTROL_MODES) {
|
||||||
|
const stats = await driveModeToFlow({
|
||||||
|
mg,
|
||||||
|
pt,
|
||||||
|
mode,
|
||||||
|
pressure,
|
||||||
|
targetFlow,
|
||||||
|
priorityOrder
|
||||||
|
});
|
||||||
|
row.modes[mode] = stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Efficiency comparison table for scenario "${scenario.name}":`);
|
||||||
|
console.table(formatEfficiencyRows(rows));
|
||||||
|
|
||||||
|
return { rows };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const combinedRows = [];
|
||||||
|
|
||||||
|
for (const scenario of scenarios) {
|
||||||
|
const { rows } = await evaluateScenario(scenario);
|
||||||
|
combinedRows.push(...rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nEfficiency summary by scenario and control mode:');
|
||||||
|
console.table(summarizeEfficiency(combinedRows));
|
||||||
|
|
||||||
|
console.log('\nAll machine group control tests completed successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch(err => {
|
||||||
|
console.error('Machine group control test harness crashed:', err);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
@@ -58,28 +58,32 @@ class nodeClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateNodeStatus() {
|
_updateNodeStatus() {
|
||||||
|
//console.log('Updating node status...');
|
||||||
const mg = this.source;
|
const mg = this.source;
|
||||||
const mode = mg.mode;
|
const mode = mg.mode;
|
||||||
const scaling = mg.scaling;
|
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
|
// Add safety checks for measurements
|
||||||
const availableMachines = Object.values(mg.machines).filter((machine) => {
|
const totalFlow = mg.measurements
|
||||||
|
?.type("flow")
|
||||||
|
?.variant("predicted")
|
||||||
|
?.position("atequipment")
|
||||||
|
?.getCurrentValue('m3/h') || 0;
|
||||||
|
|
||||||
|
const totalPower = mg.measurements
|
||||||
|
?.type("power")
|
||||||
|
?.variant("predicted")
|
||||||
|
?.position("atEquipment")
|
||||||
|
?.getCurrentValue() || 0;
|
||||||
|
|
||||||
|
// Calculate total capacity based on available machines with safety checks
|
||||||
|
const availableMachines = Object.values(mg.machines || {}).filter((machine) => {
|
||||||
|
// Safety check: ensure machine and machine.state exist
|
||||||
|
if (!machine || !machine.state || typeof machine.state.getCurrentState !== 'function') {
|
||||||
|
console.warn(`Machine missing or invalid:`, machine?.config?.general?.id || 'unknown');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const state = machine.state.getCurrentState();
|
const state = machine.state.getCurrentState();
|
||||||
const mode = machine.currentMode;
|
const mode = machine.currentMode;
|
||||||
return !(
|
return !(
|
||||||
@@ -89,29 +93,27 @@ class nodeClass {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalCapacity = Math.round(mg.dynamicTotals.flow.max * 1) / 1;
|
const totalCapacity = Math.round((mg.dynamicTotals?.flow?.max || 0) * 1) / 1;
|
||||||
|
|
||||||
// Determine overall status based on available machines
|
// Determine overall status based on available machines
|
||||||
const status =
|
const status = availableMachines.length > 0
|
||||||
availableMachines.length > 0
|
? `${availableMachines.length} machine(s) connected`
|
||||||
? `${availableMachines.length} machines`
|
|
||||||
: "No machines";
|
: "No machines";
|
||||||
|
|
||||||
let scalingSymbol = "";
|
let scalingSymbol = "";
|
||||||
switch (scaling.toLowerCase()) {
|
switch ((scaling || "").toLowerCase()) {
|
||||||
case "absolute":
|
case "absolute":
|
||||||
scalingSymbol = "Ⓐ"; // Clear symbol for Absolute mode
|
scalingSymbol = "Ⓐ";
|
||||||
break;
|
break;
|
||||||
case "normalized":
|
case "normalized":
|
||||||
scalingSymbol = "Ⓝ"; // Clear symbol for Normalized mode
|
scalingSymbol = "Ⓝ";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
scalingSymbol = mode;
|
scalingSymbol = mode || "";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate status text in a single line
|
const text = ` ${mode || 'Unknown'} | ${scalingSymbol}: 💨=${Math.round(totalFlow)}/${totalCapacity} | ⚡=${Math.round(totalPower)} | ${status}`;
|
||||||
const text = ` ${mode} | ${scalingSymbol}: 💨=${totalFlow}/${totalCapacity} | ⚡=${totalPower} | ${status}`;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fill: availableMachines.length > 0 ? "green" : "red",
|
fill: availableMachines.length > 0 ? "green" : "red",
|
||||||
@@ -179,8 +181,8 @@ class nodeClass {
|
|||||||
*/
|
*/
|
||||||
_tick() {
|
_tick() {
|
||||||
const raw = this.source.getOutput();
|
const raw = this.source.getOutput();
|
||||||
const processMsg = this._output.formatMsg(raw, this.config, "process");
|
const processMsg = this._output.formatMsg(raw, this.source.config, "process");
|
||||||
const influxMsg = this._output.formatMsg(raw, this.config, "influxdb");
|
const influxMsg = this._output.formatMsg(raw, this.source.config, "influxdb");
|
||||||
|
|
||||||
// Send only updated outputs on ports 0 & 1
|
// Send only updated outputs on ports 0 & 1
|
||||||
this.node.send([processMsg, influxMsg]);
|
this.node.send([processMsg, influxMsg]);
|
||||||
@@ -192,27 +194,41 @@ class nodeClass {
|
|||||||
_attachInputHandler() {
|
_attachInputHandler() {
|
||||||
this.node.on(
|
this.node.on(
|
||||||
"input",
|
"input",
|
||||||
(msg, send, done) =>
|
async (msg, send, done) => {
|
||||||
async function () {
|
const mg = this.source;
|
||||||
|
const RED = this.RED;
|
||||||
switch (msg.topic) {
|
switch (msg.topic) {
|
||||||
case "registerChild":
|
case "registerChild":
|
||||||
|
//console.log(`Registering child in mgc: ${msg.payload}`);
|
||||||
const childId = msg.payload;
|
const childId = msg.payload;
|
||||||
const childObj = RED.nodes.getNode(childId);
|
const childObj = RED.nodes.getNode(childId);
|
||||||
|
|
||||||
|
// Debug: Check what we're getting
|
||||||
|
//console.log(`Child object:`, childObj ? 'found' : 'NOT FOUND');
|
||||||
|
//console.log(`Child source:`, childObj?.source ? 'exists' : 'MISSING');
|
||||||
|
if (childObj?.source) {
|
||||||
|
//console.log(`Child source type:`, childObj.source.constructor.name);
|
||||||
|
//console.log(`Child has state:`, !!childObj.source.state);
|
||||||
|
}
|
||||||
|
|
||||||
mg.childRegistrationUtils.registerChild(
|
mg.childRegistrationUtils.registerChild(
|
||||||
childObj.source,
|
childObj.source,
|
||||||
msg.positionVsParent
|
msg.positionVsParent
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Debug: Check machines after registration
|
||||||
|
//console.log(`Total machines after registration:`, Object.keys(mg.machines || {}).length);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "setMode":
|
case "setMode":
|
||||||
const mode = msg.payload;
|
const mode = msg.payload;
|
||||||
const source = "parent";
|
mg.setMode(mode);
|
||||||
mg.setMode(source, mode);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "setScaling":
|
case "setScaling":
|
||||||
const scaling = msg.payload;
|
const scaling = msg.payload;
|
||||||
mg.setScaling(scaling);
|
mg.setScaling(scaling);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "Qd":
|
case "Qd":
|
||||||
@@ -235,6 +251,7 @@ class nodeClass {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
// Handle unknown topics if needed
|
// Handle unknown topics if needed
|
||||||
|
mg.logger.warn(`Unknown topic: ${msg.topic}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
done();
|
done();
|
||||||
@@ -248,6 +265,7 @@ class nodeClass {
|
|||||||
_attachCloseHandler() {
|
_attachCloseHandler() {
|
||||||
this.node.on("close", (done) => {
|
this.node.on("close", (done) => {
|
||||||
clearInterval(this._tickInterval);
|
clearInterval(this._tickInterval);
|
||||||
|
clearInterval(this._statusInterval);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user