Compare commits

...

31 Commits

Author SHA1 Message Date
znetsixe
108d2e23ca bug fixes 2025-11-30 09:24:37 +01:00
znetsixe
446ef81f24 adjusted input for measurement container 2025-11-28 09:59:51 +01:00
znetsixe
966ba06faa some minor addons to measurement container 2025-11-27 17:46:56 +01:00
znetsixe
e8c96c4b1e removed useless parameter 2025-11-25 16:19:23 +01:00
znetsixe
f083e7596a update 2025-11-20 22:29:24 +01:00
znetsixe
6ca6e536a5 fixed dropdown speed selection 2025-11-20 11:09:44 +01:00
znetsixe
fb75fb8a11 Removed error when machine doesnt have curve so node-red doesnt crash when you dont select a machine 2025-11-13 19:39:05 +01:00
znetsixe
6528c966d8 added default liquid temp and atm pressure, added nhyd - specific flow and specific energy consumption 2025-11-12 17:40:38 +01:00
znetsixe
994cf641a3 removed some old comments 2025-11-07 15:10:46 +01:00
znetsixe
6ae622b6bf fixed bugs with db output formatting 2025-11-06 11:19:08 +01:00
znetsixe
4b5ec33c1d fixed bugs for rotating machine execSequence 2025-11-05 17:15:47 +01:00
znetsixe
51f966cfb9 Added sanitizing of input for handleInput for rotating machine 2025-11-05 15:47:39 +01:00
znetsixe
4ae6beba37 updated measurement node to match selected units from user and convert it properly 2025-10-31 18:35:40 +01:00
znetsixe
d2a0274eb3 changed colours and icon based on s88 2025-10-14 13:52:39 +02:00
znetsixe
2073207df1 removed old readme 2025-10-07 18:10:45 +02:00
Rene De ren
bc916c0165 log updates 2025-10-03 15:41:53 +02:00
znetsixe
5357290b41 Several fixes, especially measurement and pressure updates and triggers where flawed 2025-10-02 17:09:24 +02:00
znetsixe
000bee7190 changed some old comment 2025-09-23 15:51:16 +02:00
znetsixe
b0c18e7bae added warning when model is not specified so user knows. 2025-09-23 15:11:06 +02:00
znetsixe
38408c7bc3 updates 2025-09-22 16:06:18 +02:00
znetsixe
ddfc612894 physicalPosition 1D update 2025-09-05 16:20:27 +02:00
znetsixe
f4696618a6 updated registration of measurements 2025-09-04 17:07:29 +02:00
znetsixe
82bb55e10f updated child registration and measurement process 2025-08-08 14:29:15 +02:00
znetsixe
7820bd2ad2 improvement on business handler of specific class child handling. 2025-08-07 15:58:59 +02:00
znetsixe
d9c2699566 license update and enhancements to measurement functionality + child parent relationship 2025-08-07 13:52:06 +02:00
znetsixe
fa7c59fcab updates 2025-07-24 13:15:33 +02:00
znetsixe
3fc8dbefe8 bug fix 2025-07-02 17:07:19 +02:00
znetsixe
f24a5fb90b bug fixes 2025-07-02 16:00:52 +02:00
znetsixe
8d2a3b80e7 bug fixes 2025-07-02 10:53:03 +02:00
znetsixe
b2eb8fe900 minor updates 2025-07-01 17:02:51 +02:00
znetsixe
a6dfbec5d0 generic updates completed for now 2025-07-01 15:25:07 +02:00
10 changed files with 541 additions and 1619 deletions

98
LICENSE
View File

@@ -1,9 +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 RnD
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 of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
2.Draagwijdte van de uit hoofde van de licentie verleende rechten
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:
— 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.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
3.Mededeling van de broncode
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.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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.
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.

View File

@@ -1,814 +0,0 @@
/**
* @file machine.js
*
* Permission is hereby granted to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to use it for personal
* or non-commercial purposes, with the following restrictions:
*
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
* be copied, merged, distributed, sublicensed, or sold without explicit
* prior written permission from the author.
*
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
* a valid license, obtainable only with the explicit consent of the author.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Ownership of this code remains solely with the original author. Unauthorized
* use of this Software is strictly prohibited.
*
* @summary A class to interact and manipulate machines with a non-euclidian curve
* @description A class to interact and manipulate machines with a non-euclidian curve
* @module machine
* @exports machine
* @version 0.1.0
* @since 0.1.0
*
* Author:
* - Rene De Ren
* Email:
* - rene@thegoldenbasket.nl
*
* Add functionality later
// -------- Operational Metrics -------- //
maintenanceAlert: this.state.checkMaintenanceStatus()
*/
//load local dependencies
const EventEmitter = require('events');
const Logger = require('../../../generalFunctions/helper/logger');
const State = require('../../../generalFunctions/helper/state/state');
const Predict = require('../../../predict/dependencies/predict/predict_class');
const { MeasurementContainer } = require('../../../generalFunctions/helper/measurements/index');
const Interpolation = require('../../../predict/dependencies/predict/interpolation');
//load all config modules
const defaultConfig = require('../rotatingMachine/rotatingMachineConfig.json');
const ConfigUtils = require('../../../generalFunctions/helper/configUtils');
//load registration utility
const ChildRegistrationUtils = require('../../../generalFunctions/helper/childRegistrationUtils');
const ErrorMetrics = require('../../../generalFunctions/helper/nrmse/errorMetrics');
class Machine {
/*------------------- Construct and set vars -------------------*/
constructor(machineConfig = {}, stateConfig = {}, errorMetricsConfig = {}) {
//basic setup
this.emitter = new EventEmitter(); // Own EventEmitter
this.configUtils = new ConfigUtils(defaultConfig);
this.config = this.configUtils.initConfig(machineConfig);
// Initialize measurements
this.measurements = new MeasurementContainer();
this.interpolation = new Interpolation();
this.child = {}; // object to hold child information so we know on what to subscribe
this.flowDrift = null;
// Init after config is set
this.logger = new Logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, this.config.general.name);
this.state = new State(stateConfig, this.logger); // Init State manager and pass logger
this.errorMetrics = new ErrorMetrics(errorMetricsConfig, this.logger);
this.predictFlow = new Predict({ curve: this.config.asset.machineCurve.nq }); // load nq (x : ctrl , y : flow relationship)
this.predictPower = new Predict({ curve: this.config.asset.machineCurve.np }); // load np (x : ctrl , y : power relationship)
this.predictCtrl = new Predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship)
this.currentMode = this.config.mode.current;
this.currentEfficiencyCurve = {};
this.cog = 0;
this.NCog = 0;
this.cogIndex = 0;
this.minEfficiency = 0;
this.absDistFromPeak = 0;
this.relDistFromPeak = 0;
this.state.emitter.on("positionChange", (data) => {
this.logger.debug(`Position change detected: ${data}`);
this.updatePosition();
});
//this.calcCog();
this.childRegistrationUtils = new ChildRegistrationUtils(this); // Child registration utility
}
// Method to assess drift using errorMetrics
assessDrift(measurement, processMin, processMax) {
this.logger.debug(`Assessing drift for measurement: ${measurement} processMin: ${processMin} processMax: ${processMax}`);
const predictedMeasurement = this.measurements.type(measurement).variant("predicted").position("downstream").getAllValues().values;
const measuredMeasurement = this.measurements.type(measurement).variant("measured").position("downstream").getAllValues().values;
if (!predictedMeasurement || !measuredMeasurement) return null;
return this.errorMetrics.assessDrift(
predictedMeasurement,
measuredMeasurement,
processMin,
processMax
);
}
reverseCurve(curve) {
const reversedCurve = {};
for (const [pressure, values] of Object.entries(curve)) {
reversedCurve[pressure] = {
x: [...values.y], // Previous y becomes new x
y: [...values.x] // Previous x becomes new y
};
}
return reversedCurve;
}
// -------- Config -------- //
updateConfig(newConfig) {
this.config = this.configUtils.updateConfig(this.config, newConfig);
}
// -------- Mode and Input Management -------- //
isValidSourceForMode(source, mode) {
const allowedSourcesSet = this.config.mode.allowedSources[mode] || [];
return allowedSourcesSet.has(source);
}
isValidActionForMode(action, mode) {
const allowedActionsSet = this.config.mode.allowedActions[mode] || [];
return allowedActionsSet.has(action);
}
async handleInput(source, action, parameter) {
if (!this.isValidSourceForMode(source, this.currentMode)) {
let warningTxt = `Source '${source}' is not valid for mode '${this.currentMode}'.`;
this.logger.warn(warningTxt);
return {status : false , feedback: warningTxt};
}
this.logger.info(`Handling input from source '${source}' with action '${action}' in mode '${this.currentMode}'.`);
try {
switch (action) {
case "execSequence":
await this.executeSequence(parameter);
//recalc flow and power
this.updatePosition();
break;
case "execMovement":
await this.setpoint(parameter);
break;
case "flowMovement":
// Calculate the control value for a desired flow
const pos = this.calcCtrl(parameter);
// Move to the desired setpoint
await this.setpoint(pos);
break;
case "emergencyStop":
this.logger.warn(`Emergency stop activated by '${source}'.`);
await this.executeSequence("emergencyStop");
break;
case "statusCheck":
this.logger.info(`Status Check: Mode = '${this.currentMode}', Source = '${source}'.`);
break;
default:
this.logger.warn(`Action '${action}' is not implemented.`);
break;
}
this.logger.debug(`Action '${action}' successfully executed`);
return {status : true , feedback: `Action '${action}' successfully executed.`};
} catch (error) {
this.logger.error(`Error handling input: ${error}`);
}
}
setMode(newMode) {
const availableModes = defaultConfig.mode.current.rules.values.map(v => v.value);
if (!availableModes.includes(newMode)) {
this.logger.warn(`Invalid mode '${newMode}'. Allowed modes are: ${availableModes.join(', ')}`);
return;
}
this.currentMode = newMode;
this.logger.info(`Mode successfully changed to '${newMode}'.`);
}
// -------- Sequence Handlers -------- //
async executeSequence(sequenceName) {
const sequence = this.config.sequences[sequenceName];
if (!sequence || sequence.size === 0) {
this.logger.warn(`Sequence '${sequenceName}' not defined.`);
return;
}
if (this.state.getCurrentState() == "operational" && sequenceName == "shutdown") {
this.logger.info(`Machine will ramp down to position 0 before performing ${sequenceName} sequence`);
await this.setpoint(0);
}
this.logger.info(` --------- Executing sequence: ${sequenceName} -------------`);
for (const state of sequence) {
try {
await this.state.transitionToState(state);
// Update measurements after state change
} catch (error) {
this.logger.error(`Error during sequence '${sequenceName}': ${error}`);
break; // Exit sequence execution on error
}
}
}
async setpoint(setpoint) {
try {
// Validate setpoint
if (typeof setpoint !== 'number' || setpoint < 0) {
throw new Error("Invalid setpoint: Setpoint must be a non-negative number.");
}
// Move to the desired setpoint
await this.state.moveTo(setpoint);
} catch (error) {
console.error(`Error setting setpoint: ${error}`);
}
}
// Calculate flow based on current pressure and position
calcFlow(x) {
const state = this.state.getCurrentState();
if (!["operational", "accelerating", "decelerating"].includes(state)) {
this.measurements.type("flow").variant("predicted").position("downstream").value(0);
this.logger.debug(`Machine is not operational. Setting predicted flow to 0.`);
return 0;
}
//this.predictFlow.currentX = x; Decrepated
const cFlow = this.predictFlow.y(x);
this.measurements.type("flow").variant("predicted").position("downstream").value(cFlow);
//this.logger.debug(`Calculated flow: ${cFlow} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cFlow;
}
// Calculate power based on current pressure and position
calcPower(x) {
const state = this.state.getCurrentState();
if (!["operational", "accelerating", "decelerating"].includes(state)) {
this.measurements.type("power").variant("predicted").position('upstream').value(0);
this.logger.debug(`Machine is not operational. Setting predicted power to 0.`);
return 0;
}
//this.predictPower.currentX = x; Decrepated
const cPower = this.predictPower.y(x);
this.measurements.type("power").variant("predicted").position('upstream').value(cPower);
//this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cPower;
}
// calculate the power consumption using only flow and pressure
inputFlowCalcPower(flow) {
this.predictCtrl.currentX = flow;
const cCtrl = this.predictCtrl.y(flow);
this.predictPower.currentX = cCtrl;
const cPower = this.predictPower.y(cCtrl);
return cPower;
}
// Function to predict control value for a desired flow
calcCtrl(x) {
this.predictCtrl.currentX = x;
const cCtrl = this.predictCtrl.y(x);
this.measurements.type("ctrl").variant("predicted").position('upstream').value(cCtrl);
//this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cCtrl;
}
// this function returns the pressure for calculations
getMeasuredPressure() {
const pressureDiff = this.measurements.type('pressure').variant('measured').difference();
// Both upstream & downstream => differential
if (pressureDiff != null) {
this.logger.debug(`Pressure differential: ${pressureDiff.value}`);
this.predictFlow.fDimension = pressureDiff.value;
this.predictPower.fDimension = pressureDiff.value;
this.predictCtrl.fDimension = pressureDiff.value;
//update the cog
const { cog, minEfficiency } = this.calcCog();
// calc efficiency
const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted");
//update the distance from peak
this.calcDistanceBEP(efficiency,cog,minEfficiency);
return pressureDiff.value;
}
// get downstream
const downstreamPressure = this.measurements.type('pressure').variant('measured').position('downstream').getCurrentValue();
// Only downstream => use it, warn that it's partial
if (downstreamPressure != null) {
this.logger.warn(`Using downstream pressure only for prediction: ${downstreamPressure} `);
this.predictFlow.fDimension = downstreamPressure;
this.predictPower.fDimension = downstreamPressure;
this.predictCtrl.fDimension = downstreamPressure;
//update the cog
const { cog, minEfficiency } = this.calcCog();
// calc efficiency
const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted");
//update the distance from peak
this.calcDistanceBEP(efficiency,cog,minEfficiency);
return downstreamPressure;
}
this.logger.error(`No valid pressure measurements available to calculate prediction using last known pressure`);
//set default at 0 => lowest pressure possible
this.predictFlow.fDimension = 0;
this.predictPower.fDimension = 0;
this.predictCtrl.fDimension = 0;
//update the cog
const { cog, minEfficiency } = this.calcCog();
// calc efficiency
const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted");
//update the distance from peak
this.calcDistanceBEP(efficiency,cog,minEfficiency);
return 0;
}
handleMeasuredFlow() {
const flowDiff = this.measurements.type('flow').variant('measured').difference();
// If both are present
if (flowDiff != null) {
// In theory, mass flow in = mass flow out, so they should match or be close.
if (flowDiff.value < 0.001) {
// flows match within tolerance
this.logger.debug(`Flow match: ${flowDiff.value}`);
return flowDiff.value;
} else {
// Mismatch => decide how to handle. Maybe take the average?
// Or bail out with an error. Example: we bail out here.
this.logger.error(`Something wrong with down or upstream flow measurement. Bailing out!`);
return null;
}
}
// get
const upstreamFlow = this.measurements.type('pressure').variant('measured').position('upstream').getCurrentValue();
// Only upstream => might still accept it, but warn
if (upstreamFlow != null) {
this.logger.warn(`Only upstream flow is present. Using it but results may be incomplete!`);
return upstreamFlow;
}
// get
const downstreamFlow = this.measurements.type('pressure').variant('measured').position('downstream').getCurrentValue();
// Only downstream => might still accept it, but warn
if (downstreamFlow != null) {
this.logger.warn(`Only downstream flow is present. Using it but results may be incomplete!`);
return downstreamFlow;
}
// Neither => error
this.logger.error(`No upstream or downstream flow measurement. Bailing out!`);
return null;
}
handleMeasuredPower() {
const power = this.measurements.type("power").variant("measured").position("upstream").getCurrentValue();
// If your system calls it "upstream" or just a single "value", adjust accordingly
if (power != null) {
this.logger.debug(`Measured power: ${power}`);
return power;
} else {
this.logger.error(`No measured power found. Bailing out!`);
return null;
}
}
updatePressure(variant,value,position) {
switch (variant) {
case ("measured"):
//only update when machine is in a state where it can be used
if (this.state.getCurrentState() == "operational" || this.state.getCurrentState() == "accelerating" || this.state.getCurrentState() == "decelerating") {
// put value in measurements
this.measurements.type("pressure").variant("measured").position(position).value(value);
//when measured pressure gets updated we need some logic to fetch the relevant value which could be downstream or differential pressure
const pressure = this.getMeasuredPressure();
//update the flow power and cog
this.updatePosition();
this.logger.debug(`Measured pressure: ${pressure}`);
}
break;
default:
this.logger.warn(`Unrecognized variant '${variant}' for pressure update.`);
break;
}
}
updateFlow(variant,value,position) {
switch (variant) {
case ("measured"):
// put value in measurements
this.measurements.type("flow").variant("measured").position(position).value(value);
//when measured flow gets updated we need to push the last known value in the prediction measurements to keep them synced
this.measurements.type("flow").variant("predicted").position("downstream").value(this.predictFlow.outputY);
break;
case ("predicted"):
this.logger.debug('not doing anythin yet');
break;
default:
this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
break;
}
}
updateMeasurement(variant, subType, value, position) {
this.logger.debug(`---------------------- updating ${subType} ------------------ `);
switch (subType) {
case "pressure":
// Update pressure measurement
this.updatePressure(variant,value,position);
break;
case "flow":
this.updateFlow(variant,value,position);
// Update flow measurement
this.flowDrift = this.assessDrift("flow", this.predictFlow.currentFxyYMin , this.predictFlow.currentFxyYMax);
this.logger.debug(`---------------------------------------- `);
break;
case "power":
// Update power measurement
break;
default:
this.logger.error(`Type '${type}' not recognized for measured update.`);
return;
}
}
//what is the internal functions that need updating when something changes that has influence on this.
updatePosition() {
if (this.state.getCurrentState() == "operational" || this.state.getCurrentState() == "accelerating" || this.state.getCurrentState() == "decelerating") {
const currentPosition = this.state.getCurrentPosition();
// Update the predicted values based on the new position
const { cPower, cFlow } = this.calcFlowPower(currentPosition);
// Calc predicted efficiency
const efficiency = this.calcEfficiency(cPower, cFlow, "predicted");
//update the cog
const { cog, minEfficiency } = this.calcCog();
//update the distance from peak
this.calcDistanceBEP(efficiency,cog,minEfficiency);
}
}
calcDistanceFromPeak(currentEfficiency,peakEfficiency){
return Math.abs(currentEfficiency - peakEfficiency);
}
calcRelativeDistanceFromPeak(currentEfficiency,maxEfficiency,minEfficiency){
let distance = 1;
if(currentEfficiency != null){
distance = this.interpolation.interpolate_lin_single_point(currentEfficiency,maxEfficiency, minEfficiency, 0, 1);
}
return distance;
}
// Calculate the center of gravity for current pressure
calcCog() {
//fetch current curve data for power and flow
const { powerCurve, flowCurve } = this.getCurrentCurves();
const {efficiencyCurve, peak, peakIndex, minEfficiency } = this.calcEfficiencyCurve(powerCurve, flowCurve);
// Calculate the normalized center of gravity
const NCog = (flowCurve.y[peakIndex] - this.predictFlow.currentFxyYMin) / (this.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin);
//store in object for later retrieval
this.currentEfficiencyCurve = efficiencyCurve;
this.cog = peak;
this.cogIndex = peakIndex;
this.NCog = NCog;
this.minEfficiency = minEfficiency;
return { cog: peak, cogIndex: peakIndex, NCog: NCog, minEfficiency: minEfficiency };
}
calcEfficiencyCurve(powerCurve, flowCurve) {
const efficiencyCurve = [];
let peak = 0;
let peakIndex = 0;
let minEfficiency = 0;
// Calculate efficiency curve based on power and flow curves
powerCurve.y.forEach((power, index) => {
// Get flow for the current power
const flow = flowCurve.y[index];
// higher efficiency is better
efficiencyCurve.push( Math.round( ( flow / power ) * 100 ) / 100);
// Keep track of peak efficiency
peak = Math.max(peak, efficiencyCurve[index]);
peakIndex = peak == efficiencyCurve[index] ? index : peakIndex;
minEfficiency = Math.min(...efficiencyCurve);
});
return { efficiencyCurve, peak, peakIndex, minEfficiency };
}
//calc flow power based on pressure and current position
calcFlowPower(x) {
// Calculate flow and power
const cFlow = this.calcFlow(x);
const cPower = this.calcPower(x);
return { cPower, cFlow };
}
calcEfficiency(power, flow, variant) {
if (power != 0 && flow != 0) {
// Calculate efficiency after measurements update
this.measurements.type("efficiency").variant(variant).position('downstream').value((flow / power));
} else {
this.measurements.type("efficiency").variant(variant).position('downstream').value(null);
}
return this.measurements.type("efficiency").variant(variant).position('downstream').getCurrentValue();
}
updateCurve(newCurve) {
this.logger.info(`Updating machine curve`);
const newConfig = { asset: { machineCurve: newCurve } };
//validate input of new curve fed to the machine
this.config = this.configUtils.updateConfig(this.config, newConfig);
//After we passed validation load the curves into their predictors
this.predictFlow.updateCurve(this.config.asset.machineCurve.nq);
this.predictPower.updateCurve(this.config.asset.machineCurve.np);
this.predictCtrl.updateCurve(this.reverseCurve(this.config.asset.machineCurve.nq));
}
getCompleteCurve() {
const powerCurve = this.predictPower.inputCurveData;
const flowCurve = this.predictFlow.inputCurveData;
return { powerCurve, flowCurve };
}
getCurrentCurves() {
const powerCurve = this.predictPower.currentFxyCurve[this.predictPower.currentF];
const flowCurve = this.predictFlow.currentFxyCurve[this.predictFlow.currentF];
return { powerCurve, flowCurve };
}
calcDistanceBEP(efficiency,maxEfficiency,minEfficiency) {
const absDistFromPeak = this.calcDistanceFromPeak(efficiency,maxEfficiency);
const relDistFromPeak = this.calcRelativeDistanceFromPeak(efficiency,maxEfficiency,minEfficiency);
//store internally
this.absDistFromPeak = absDistFromPeak ;
this.relDistFromPeak = relDistFromPeak;
return { absDistFromPeak: absDistFromPeak, relDistFromPeak: relDistFromPeak };
}
getOutput() {
// Improved output object generation
const output = {};
//build the output object
this.measurements.getTypes().forEach(type => {
this.measurements.getVariants(type).forEach(variant => {
const downstreamVal = this.measurements.type(type).variant(variant).position("downstream").getCurrentValue();
const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue();
if (downstreamVal != null) {
output[`downstream_${variant}_${type}`] = downstreamVal;
}
if (upstreamVal != null) {
output[`upstream_${variant}_${type}`] = upstreamVal;
}
if (downstreamVal != null && upstreamVal != null) {
const diffVal = this.measurements.type(type).variant(variant).difference().value;
output[`differential_${variant}_${type}`] = diffVal;
}
});
});
//fill in the rest of the output object
output["state"] = this.state.getCurrentState();
output["runtime"] = this.state.getRunTimeHours();
output["ctrl"] = this.state.getCurrentPosition();
output["moveTimeleft"] = this.state.getMoveTimeLeft();
output["mode"] = this.currentMode;
output["cog"] = this.cog; // flow / power efficiency
output["NCog"] = this.NCog; // normalized cog
output["NCogPercent"] = Math.round(this.NCog * 100 * 100) / 100 ;
if(this.flowDrift != null){
const flowDrift = this.flowDrift;
output["flowNrmse"] = flowDrift.nrmse;
output["flowLongterNRMSD"] = flowDrift.longTermNRMSD;
output["flowImmediateLevel"] = flowDrift.immediateLevel;
output["flowLongTermLevel"] = flowDrift.longTermLevel;
}
//should this all go in the container of measurements?
output["effDistFromPeak"] = this.absDistFromPeak;
output["effRelDistFromPeak"] = this.relDistFromPeak;
//this.logger.debug(`Output: ${JSON.stringify(output)}`);
return output;
}
} // end of class
module.exports = Machine;
/*------------------- Testing -------------------*/
/*
curve = require('C:/Users/zn375/.node-red/public/fallbackData.json');
//import a child
const Child = require('../../../measurement/dependencies/measurement/measurement');
console.log(`Creating child...`);
const PT1 = new Child(config={
general:{
name:"PT1",
logging:{
enabled:true,
logLevel:"debug",
},
},
functionality:{
softwareType:"measurement",
},
asset:{
type:"sensor",
subType:"pressure",
},
});
const PT2 = new Child(config={
general:{
name:"PT2",
logging:{
enabled:true,
logLevel:"debug",
},
},
functionality:{
softwareType:"measurement",
},
asset:{
type:"sensor",
subType:"pressure",
},
});
//create a machine
console.log(`Creating machine...`);
const machineConfig = {
general: {
name: "Hydrostal",
logging: {
enabled: true,
logLevel: "debug",
}
},
asset: {
supplier: "Hydrostal",
type: "pump",
subType: "centrifugal",
model: "H05K-S03R+HGM1X-X280KO", // Ensure this field is present.
machineCurve: curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"],
}
}
const stateConfig = {
general: {
logging: {
enabled: true,
logLevel: "debug",
},
},
// Your custom config here (or leave empty for defaults)
movement: {
speed: 1,
},
time: {
starting: 2,
warmingup: 3,
stopping: 2,
coolingdown: 3,
},
};
const machine = new Machine(machineConfig, stateConfig);
//machine.logger.info(JSON.stringify(curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"]));
machine.logger.info(`Registering child...`);
machine.childRegistrationUtils.registerChild(PT1, "upstream");
machine.childRegistrationUtils.registerChild(PT2, "downstream");
//feed curve to the machine class
//machine.updateCurve(curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"]);
PT1.logger.info(`Enable sim...`);
PT1.toggleSimulation();
PT2.logger.info(`Enable sim...`);
PT2.toggleSimulation();
machine.getOutput();
//manual test
//machine.handleInput("parent", "execSequence", "startup");
machine.measurements.type("pressure").variant("measured").position('upstream').value(-200);
machine.measurements.type("pressure").variant("measured").position('downstream').value(1000);
testingSequences();
const tickLoop = setInterval(changeInput,1000);
function changeInput(){
PT1.logger.info(`tick...`);
PT1.tick();
PT2.tick();
}
async function testingSequences(){
try{
console.log(` ********** Testing sequence startup... **********`);
await machine.handleInput("parent", "execSequence", "startup");
console.log(` ********** Testing movement to 15... **********`);
await machine.handleInput("parent", "execMovement", 15);
machine.getOutput();
console.log(` ********** Testing sequence shutdown... **********`);
await machine.handleInput("parent", "execSequence", "shutdown");
console.log(`********** Testing moving to setpoint 10... while in idle **********`);
await machine.handleInput("parent", "execMovement", 10);
console.log(` ********** Testing sequence emergencyStop... **********`);
await machine.handleInput("parent", "execSequence", "emergencystop");
console.log(`********** Testing sequence boot... **********`);
await machine.handleInput("parent", "execSequence", "boot");
}catch(error){
console.error(`Error: ${error}`);
}
}
//*/

View File

@@ -1,297 +0,0 @@
const Machine = require('./machine');
const specs = require('../../../generalFunctions/datasets/assetData/pumps/hydrostal/centrifugal pumps/models.json');
class MachineTester {
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"
};
}
async testBasicOperations() {
console.log('\nTesting Basic Machine Operations...');
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
try {
// Test 1: Initialization
this.assert(
machine.currentMode === "auto",
'Machine should initialize in auto mode'
);
// Test 2: Set pressure measurement
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
const pressure = machine.handleMeasuredPressure();
this.assert(
pressure === 800,
'Should correctly handle pressure measurement'
);
// Test 3: State transition
await machine.state.transitionToState("idle");
this.assert(
machine.state.getCurrentState() === "idle",
'Should transition to idle state'
);
// Test 4: Mode change
machine.setMode("virtualControl");
this.assert(
machine.currentMode === "virtualControl",
'Should change mode to virtual control'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testPredictions() {
console.log('\nTesting Machine Predictions...');
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
try {
// Test 1: Flow prediction
const flow = machine.calcFlow(50);
this.assert(
flow > 0 && !isNaN(flow),
'Should calculate valid flow for control value'
);
// Test 2: Power prediction
const power = machine.calcPower(50);
this.assert(
power > 0 && !isNaN(power),
'Should calculate valid power for control value'
);
// Test 3: Control prediction
const ctrl = machine.calcCtrl(100);
this.assert(
ctrl >= 0 && ctrl <= 100,
'Should calculate valid control value for desired flow'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testSequenceExecution() {
console.log('\nTesting Machine Sequences...');
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
try {
// Test 1: Startup sequence
await machine.handleInput("parent", "execSequence", "startup");
this.assert(
machine.state.getCurrentState() === "operational",
'Should complete startup sequence'
);
// Test 2: Movement after startup
await machine.handleInput("parent", "execMovement", 50);
this.assert(
machine.state.getCurrentPosition() === 50,
'Should move to specified position'
);
// Test 3: Shutdown sequence
await machine.handleInput("parent", "execSequence", "shutdown");
this.assert(
machine.state.getCurrentState() === "idle",
'Should complete shutdown sequence'
);
// Test 4: Emergency stop
await machine.handleInput("parent", "execSequence", "emergencystop");
this.assert(
machine.state.getCurrentState() === "off",
'Should execute emergency stop'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testMeasurementHandling() {
console.log('\nTesting Measurement Handling...');
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
try {
// Test 1: Pressure measurement
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
machine.measurements.type("pressure").variant("measured").setUpstream(1000);
const pressure = machine.handleMeasuredPressure();
this.assert(
pressure === 200,
'Should calculate correct differential pressure'
);
// Test 2: Flow measurement
machine.measurements.type("flow").variant("measured").position("downstream").value(100);
const flow = machine.handleMeasuredFlow();
this.assert(
flow === 100,
'Should handle flow measurement correctly'
);
// Test 3: Power measurement
machine.measurements.type("power").variant("measured").setUpstream(75);
const power = machine.handleMeasuredPower();
this.assert(
power === 75,
'Should handle power measurement correctly'
);
// Test 4: Efficiency calculation
machine.calcEfficiency();
const efficiency = machine.measurements.type("efficiency").variant("measured").getDownstream();
this.assert(
efficiency > 0 && !isNaN(efficiency),
'Should calculate valid efficiency'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testCurveHandling() {
console.log('\nTesting Machine Curve Handling...');
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
try {
// Test 1: Curve initialization
const curves = machine.showCurve();
this.assert(
curves.powerCurve && curves.flowCurve,
'Should properly initialize power and flow curves'
);
// Test 2: Test reverse curve creation
const reversedCurve = machine.reverseCurve(this.machineCurve.nq);
this.assert(
reversedCurve["1"].x[0] === this.machineCurve.nq["1"].y[0] &&
reversedCurve["1"].y[0] === this.machineCurve.nq["1"].x[0],
'Should correctly reverse x and y values in curve'
);
// Test 3: Update curve dynamically
const newCurve = {
nq: {
"1": {
x: [0, 25, 50, 75, 100],
y: [0, 125, 250, 375, 500]
}
},
np: {
"1": {
x: [0, 25, 50, 75, 100],
y: [0, 75, 150, 225, 300]
}
}
};
machine.updateCurve(newCurve);
const updatedCurves = machine.showCurve();
this.assert(
updatedCurves.flowCurve["1"].y[2] === 250,
'Should update curve with new values'
);
// Test 4: Verify curve interpolation
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
const midpointCtrl = machine.calcCtrl(250); // Should interpolate between points
const calculatedFlow = machine.calcFlow(midpointCtrl);
this.assert(
Math.abs(calculatedFlow - 250) < 1, // Allow small numerical error
'Should accurately interpolate between curve points'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async runAllTests() {
console.log('Starting Machine Tests...\n');
await this.testBasicOperations();
await this.testPredictions();
await this.testSequenceExecution();
await this.testMeasurementHandling();
await this.testCurveHandling();
console.log('\nTest Summary:');
console.log(`Total Tests: ${this.totalTests}`);
console.log(`Passed: ${this.passedTests}`);
console.log(`Failed: ${this.failedTests}`);
process.exit(this.failedTests > 0 ? 1 : 0);
}
}
// Run the tests
const tester = new MachineTester();
tester.runAllTests().catch(console.error);

View File

@@ -1,247 +0,0 @@
module.exports = function (RED) {
function rotatingMachine(config) {
RED.nodes.createNode(this, config);
var node = this;
try {
// Load Machine class and curve data
const Machine = require("./dependencies/machine/machine");
const OutputUtils = require("../generalFunctions/helper/outputUtils");
const machineConfig = {
general: {
name: config.name || "Default Machine",
id: node.id,
logging: {
enabled: config.eneableLog,
logLevel: config.logLevel
}
},
asset: {
supplier: config.supplier || "Unknown",
type: config.machineType || "generic",
subType: config.subType || "generic",
model: config.model || "generic",
machineCurve: config.machineCurve
}
};
const stateConfig = {
general: {
logging: {
enabled: config.eneableLog,
logLevel: config.logLevel
}
},
movement: {
speed: Number(config.speed)
},
time: {
starting: Number(config.startup),
warmingup: Number(config.warmup),
stopping: Number(config.shutdown),
coolingdown: Number(config.cooldown)
}
};
// Create machine instance
const m = new Machine(machineConfig, stateConfig);
// put m on node memory as source
node.source = m;
//load output utils
const output = new OutputUtils();
function updateNodeStatus() {
try {
const mode = m.currentMode;
const state = m.state.getCurrentState();
const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').getCurrentValue());
const power = Math.round(m.measurements.type("power").variant("predicted").position('upstream').getCurrentValue());
let symbolState;
switch(state){
case "off":
symbolState = "⬛";
break;
case "idle":
symbolState = "⏸️";
break;
case "operational":
symbolState = "⏵️";
break;
case "starting":
symbolState = "⏯️";
break;
case "warmingup":
symbolState = "🔄";
break;
case "accelerating":
symbolState = "⏩";
break;
case "stopping":
symbolState = "⏹️";
break;
case "coolingdown":
symbolState = "❄️";
break;
case "decelerating":
symbolState = "⏪";
break;
}
const position = m.state.getCurrentPosition();
const roundedPosition = Math.round(position * 100) / 100;
let status;
switch (state) {
case "off":
status = { fill: "red", shape: "dot", text: `${mode}: OFF` };
break;
case "idle":
status = { fill: "blue", shape: "dot", text: `${mode}: ${symbolState}` };
break;
case "operational":
status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` };
break;
case "starting":
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
break;
case "warmingup":
status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` };
break;
case "accelerating":
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}%| 💨${flow}m³/h | ⚡${power}kW` };
break;
case "stopping":
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
break;
case "coolingdown":
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
break;
case "decelerating":
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} - ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` };
break;
default:
status = { fill: "grey", shape: "dot", text: `${mode}: ${symbolState}` };
}
return status;
} catch (error) {
node.error("Error in updateNodeStatus: " + error.message);
return { fill: "red", shape: "ring", text: "Status Error" };
}
}
function tick() {
try {
const status = updateNodeStatus();
node.status(status);
//get output
const classOutput = m.getOutput();
const dbOutput = output.formatMsg(classOutput, m.config, "influxdb");
const pOutput = output.formatMsg(classOutput, m.config, "process");
//console.log(pOutput);
//only send output on values that changed
let msgs = [];
msgs[0] = pOutput;
msgs[1] = dbOutput;
node.send(msgs);
} catch (error) {
node.error("Error in tick function: " + error);
node.status({ fill: "red", shape: "ring", text: "Tick Error" });
}
}
// 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
);
node.on("input", function(msg, send, done) {
try {
/* Update to complete event based node by putting the tick function after an input event */
switch(msg.topic) {
case 'registerChild':
const childId = msg.payload;
const childObj = RED.nodes.getNode(childId);
m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
break;
case 'setMode':
m.setMode(msg.payload);
break;
case 'execSequence':
const { source, action, parameter } = msg.payload;
m.handleInput(source, action, parameter);
break;
case 'execMovement':
const { source: mvSource, action: mvAction, setpoint } = msg.payload;
m.handleInput(mvSource, mvAction, Number(setpoint));
break;
case 'flowMovement':
const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload;
m.handleInput(fmSource, fmAction, Number(fmSetpoint));
break;
case 'emergencystop':
const { source: esSource, action: esAction } = msg.payload;
m.handleInput(esSource, esAction);
break;
case 'showCompleteCurve':
m.showCompleteCurve();
send({ topic : "Showing curve" , payload: m.showCompleteCurve() });
break;
case 'CoG':
m.showCoG();
send({ topic : "Showing CoG" , payload: m.showCoG() });
break;
}
if (done) done();
} catch (error) {
node.error("Error processing input: " + error.message);
if (done) done(error);
}
});
node.on('close', function(done) {
if (node.interval_id) clearTimeout(node.interval_id);
if (node.tick_interval) clearInterval(node.tick_interval);
if (done) done();
});
} catch (error) {
node.error("Fatal error in node initialization: " + error.stack);
node.status({fill: "red", shape: "ring", text: "Fatal Error"});
}
}
RED.nodes.registerType("rotatingMachine", rotatingMachine);
};

View File

@@ -1,3 +1,13 @@
<!--
| S88-niveau | Primair (blokkleur) | Tekstkleur |
| ---------------------- | ------------------- | ---------- |
| **Area** | `#0f52a5` | wit |
| **Process Cell** | `#0c99d9` | wit |
| **Unit** | `#50a8d9` | zwart |
| **Equipment (Module)** | `#86bbdd` | zwart |
| **Control Module** | `#a9daee` | zwart |
-->
<!-- Load the dynamic menu & config endpoints -->
<script src="/rotatingMachine/menu.js"></script>
<script src="/rotatingMachine/configData.js"></script>
@@ -5,16 +15,16 @@
<script>
RED.nodes.registerType("rotatingMachine", {
category: "EVOLV",
color: "#4f8582",
color: "#86bbdd",
defaults: {
// Define default properties
name: { value: "", required: true }, // use asset category as name ?
// Define specific properties
speed: { value: 1, required: true },
startup: { value: 0 },
warmup: { value: 0 },
shutdown: { value: 0 },
cooldown: { value: 0 },
movementMode : { value: "staticspeed" }, // static or dynamic
machineCurve : { value: {}},
//define asset properties
@@ -31,16 +41,21 @@
//physicalAspect
positionVsParent: { value: "" },
positionIcon: { value: "" },
hasDistance: { value: false },
distance: { value: 0 },
distanceUnit: { value: "m" },
distanceDescription: { value: "" }
},
inputs: 1,
outputs: 3,
inputLabels: ["Input"],
outputLabels: ["process", "dbase", "parent"],
icon: "font-awesome/fa-tachometer",
icon: "font-awesome/fa-cog",
label: function() {
return this.name || "rotatingMachine";
label: function () {
return this.positionIcon + " " + this.category || "Machine";
},
oneditprepare: function() {
@@ -55,15 +70,20 @@
waitForMenuData();
// your existing projectsettings & asset dropdown logic can remain here
document.getElementById("node-input-speed"),
document.getElementById("node-input-startup"),
document.getElementById("node-input-warmup"),
document.getElementById("node-input-shutdown"),
document.getElementById("node-input-cooldown")
document.getElementById("node-input-speed");
document.getElementById("node-input-startup");
document.getElementById("node-input-warmup");
document.getElementById("node-input-shutdown");
document.getElementById("node-input-cooldown");
const movementMode = document.getElementById("node-input-movementMode");
if (movementMode) {
movementMode.value = this.movementMode || "staticspeed";
}
},
oneditsave: function() {
const node = this;
// save asset fields
if (window.EVOLV?.nodes?.rotatingMachine?.assetMenu?.saveEditor) {
window.EVOLV.nodes.rotatingMachine.assetMenu.saveEditor(this);
@@ -76,10 +96,16 @@
if (window.EVOLV?.nodes?.rotatingMachine?.positionMenu?.saveEditor) {
window.EVOLV.nodes.rotatingMachine.positionMenu.saveEditor(this);
}
// …plus any custom saves for speed, startup, etc.
["speed", "startup", "warmup", "shutdown", "cooldown"].forEach(
(field) => (node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0)
);
["speed", "startup", "warmup", "shutdown", "cooldown"].forEach((field) => {
const element = document.getElementById(`node-input-${field}`);
const value = parseFloat(element?.value) || 0;
console.log(`----------------> Saving ${field}: ${value}`);
node[field] = value;
});
node.movementMode = document.getElementById("node-input-movementMode").value;
console.log(`----------------> Saving movementMode: ${node.movementMode}`);
}
});
@@ -109,6 +135,13 @@
<label for="node-input-cooldown"><i class="fa fa-clock-o"></i> Cooldown Time</label>
<input type="number" id="node-input-cooldown" style="width:60%;" />
</div>
<div class="form-row">
<label for="node-input-movementMode"><i class="fa fa-exchange"></i> Movement Mode</label>
<select id="node-input-movementMode" style="width:60%;">
<option value="staticspeed">Static</option>
<option value="dynspeed">Dynamic</option>
</select>
</div>
<!-- Asset fields injected here -->
<div id="asset-fields-placeholder"></div>

View File

@@ -1,12 +1,12 @@
const nameOfNode = 'rotatingMachine';
const NodeClass = require('./src/nodeClass.js');
const nodeClass = require('./src/nodeClass.js');
const { MenuManager, configManager } = require('generalFunctions');
module.exports = function(RED) {
// 1) Register the node type and delegate to your class
RED.nodes.registerType(nameOfNode, function(config) {
RED.nodes.createNode(this, config);
this.nodeClass = new NodeClass(config, RED, this, nameOfNode);
this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
});
// 2) Setup the dynamic menu & config endpoints

View File

@@ -1,5 +1,5 @@
/**
* measurement.class.js
* node class.js
*
* Encapsulates all node logic in a reusable class. In future updates we can split this into multiple generic classes and use the config to specifiy which ones to use.
* This allows us to keep the Node-RED node clean and focused on wiring up the UI and event handlers.
@@ -7,9 +7,6 @@
const { outputUtils, configManager } = require('generalFunctions');
const Specific = require("./specificClass");
/**
* Class representing a Node-RED node.
*/
class nodeClass {
/**
* Create a Node.
@@ -23,12 +20,13 @@ class nodeClass {
this.RED = RED; // This is the Node-RED runtime API, we can use this to create endpoints if needed
this.name = nameOfNode; // This is the name of the node, it should match the file name and the node type in Node-RED
this.source = null; // Will hold the specific class instance
this.config = null; // Will hold the merged configuration
// Load default & UI config
this._loadConfig(uiConfig,this.node);
// Instantiate core Measurement class
this._setupSpecificClass();
// Instantiate core class
this._setupSpecificClass(uiConfig);
// Wire up event and lifecycle handlers
this._bindEvents();
@@ -43,13 +41,10 @@ class nodeClass {
* @param {object} uiConfig - Raw config from Node-RED UI.
*/
_loadConfig(uiConfig,node) {
const cfgMgr = new configManager();
this.defaultConfig = cfgMgr.getConfig(this.name);
// Merge UI config over defaults
this.config = {
general: {
name: uiConfig.name,
id: node.id, // node.id is for the child registration process
unit: uiConfig.unit, // add converter options later to convert to default units (need like a model that defines this which units we are going to use and then conver to those standards)
logging: {
@@ -67,7 +62,7 @@ class nodeClass {
unit: uiConfig.unit
},
functionality: {
positionVsParent: uiConfig.positionVsParent || 'atEquipment', // Default to 'atEquipment' if not specified
positionVsParent: uiConfig.positionVsParent
}
};
@@ -78,37 +73,43 @@ class nodeClass {
/**
* Instantiate the core Measurement logic and store as source.
*/
_setupSpecificClass() {
_setupSpecificClass(uiConfig) {
const machineConfig = this.config;
console.log(`----------------> Loaded movementMode in nodeClass: ${uiConfig.movementMode}`);
// need extra state for this
const stateConfig = {
general: {
logging: {
enabled: config.eneableLog,
logLevel: config.logLevel
enabled: machineConfig.eneableLog,
logLevel: machineConfig.logLevel
}
},
movement: {
speed: Number(config.speed)
speed: Number(uiConfig.speed),
mode: uiConfig.movementMode
},
time: {
starting: Number(config.startup),
warmingup: Number(config.warmup),
stopping: Number(config.shutdown),
coolingdown: Number(config.cooldown)
starting: Number(uiConfig.startup),
warmingup: Number(uiConfig.warmup),
stopping: Number(uiConfig.shutdown),
coolingdown: Number(uiConfig.cooldown)
}
};
this.source = new Specific(this.config, stateConfig);
this.source = new Specific(machineConfig, stateConfig);
//store in node
this.node.source = this.source; // Store the source in the node instance for easy access
}
/**
* Bind Measurement events to Node-RED status updates. Using internal emitter. --> REMOVE LATER WE NEED ONLY COMPLETE CHILDS AND THEN CHECK FOR UPDATES
* Bind events to Node-RED status updates. Using internal emitter. --> REMOVE LATER WE NEED ONLY COMPLETE CHILDS AND THEN CHECK FOR UPDATES
*/
_bindEvents() {
this.source.emitter.on('mAbs', (val) => {
this.node.status({ fill: 'green', shape: 'dot', text: `${val} ${this.config.general.unit}` });
});
}
_updateNodeStatus() {
@@ -116,8 +117,8 @@ class nodeClass {
try {
const mode = m.currentMode;
const state = m.state.getCurrentState();
const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').getCurrentValue());
const power = Math.round(m.measurements.type("power").variant("predicted").position('upstream').getCurrentValue());
const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').getCurrentValue('m3/h'));
const power = Math.round(m.measurements.type("power").variant("predicted").position('atequipment').getCurrentValue('kW'));
let symbolState;
switch(state){
case "off":
@@ -147,6 +148,9 @@ class nodeClass {
case "decelerating":
symbolState = "⏪";
break;
case "maintenance":
symbolState = "🔧";
break;
}
const position = m.state.getCurrentPosition();
const roundedPosition = Math.round(position * 100) / 100;
@@ -204,7 +208,7 @@ class nodeClass {
}
/**
* Start the periodic tick loop to drive the Measurement class.
* Start the periodic tick loop.
*/
_startTickLoop() {
setTimeout(() => {
@@ -223,11 +227,11 @@ class nodeClass {
* Execute a single tick: update measurement, format and send outputs.
*/
_tick() {
this.source.tick();
//this.source.tick();
const raw = this.source.getOutput();
const processMsg = this._output.formatMsg(raw, this.config, 'process');
const influxMsg = this._output.formatMsg(raw, this.config, 'influxdb');
const processMsg = this._output.formatMsg(raw, this.source.config, 'process');
const influxMsg = this._output.formatMsg(raw, this.source.config, 'influxdb');
// Send only updated outputs on ports 0 & 1
this.node.send([processMsg, influxMsg]);
@@ -242,6 +246,7 @@ class nodeClass {
const m = this.source;
switch(msg.topic) {
case 'registerChild':
// Register this node as a child of the parent node
const childId = msg.payload;
const childObj = this.RED.nodes.getNode(childId);
m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
@@ -266,9 +271,9 @@ class nodeClass {
const { source: esSource, action: esAction } = msg.payload;
m.handleInput(esSource, esAction);
break;
case 'showCompleteCurve':
m.showCompleteCurve();
send({ topic : "Showing curve" , payload: m.showCompleteCurve() });
case 'showWorkingCurves':
m.showWorkingCurves();
send({ topic : "Showing curve" , payload: m.showWorkingCurves() });
break;
case 'CoG':
m.showCoG();

View File

@@ -1,50 +1,5 @@
/**
* @file machine.js
*
* Permission is hereby granted to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to use it for personal
* or non-commercial purposes, with the following restrictions:
*
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
* be copied, merged, distributed, sublicensed, or sold without explicit
* prior written permission from the author.
*
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
* a valid license, obtainable only with the explicit consent of the author.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Ownership of this code remains solely with the original author. Unauthorized
* use of this Software is strictly prohibited.
*
* @summary A class to interact and manipulate machines with a non-euclidian curve
* @description A class to interact and manipulate machines with a non-euclidian curve
* @module machine
* @exports machine
* @version 0.1.0
* @since 0.1.0
*
* Author:
* - Rene De Ren
* Email:
* - r.de.ren@brabantsedelta.nl
*
* Add functionality later
// -------- Operational Metrics -------- //
maintenanceAlert: this.state.checkMaintenanceStatus()
*/
//load local dependencies
const EventEmitter = require('events');
const {logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils} = require('generalFunctions');
const {loadCurve,gravity,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils,coolprop} = require('generalFunctions');
class Machine {
@@ -53,28 +8,58 @@ class Machine {
//basic setup
this.emitter = new EventEmitter(); // Own EventEmitter
this.logger = new logger(machineConfig.general.logging.enabled,machineConfig.general.logging.logLevel, machineConfig.general.name);
this.configManager = new configManager();
this.defaultConfig = this.configManager.getConfig('rotatingMachine'); // Load default config for rotating machine
this.defaultConfig = this.configManager.getConfig('rotatingMachine'); // Load default config for rotating machine ( use software type name ? )
this.configUtils = new configUtils(this.defaultConfig);
// Load a specific curve
this.model = machineConfig.asset.model; // Get the model from the machineConfig
this.curve = this.model ? loadCurve(this.model) : null; // we need to convert the curve and add units to the curve information
//Init config and check if it is valid
this.config = this.configUtils.initConfig(machineConfig);
// Init after config is set
this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name);
//add unique name for this node.
this.config = this.configUtils.updateConfig(this.config, {general:{name: this.config.functionality?.softwareType + "_" + machineConfig.general.id}}); // add unique name if not present
if (!this.model || !this.curve) {
this.logger.error(`${!this.model ? 'Model not specified' : 'Curve not found for model ' + this.model} in machineConfig. Cannot make predictions.`);
// Set prediction objects to null to prevent method calls
this.predictFlow = null;
this.predictPower = null;
this.predictCtrl = null;
this.hasCurve = false;
}
else{
this.hasCurve = true;
this.config = this.configUtils.updateConfig(this.config, { asset: { ...this.config.asset, machineCurve: this.curve } });
//machineConfig = { ...machineConfig, asset: { ...machineConfig.asset, machineCurve: this.curve } }; // Merge curve into machineConfig
this.predictFlow = new predict({ curve: this.config.asset.machineCurve.nq }); // load nq (x : ctrl , y : flow relationship)
this.predictPower = new predict({ curve: this.config.asset.machineCurve.np }); // load np (x : ctrl , y : power relationship)
this.predictCtrl = new predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship)
}
this.state = new state(stateConfig, this.logger); // Init State manager and pass logger
this.errorMetrics = new nrmse(errorMetricsConfig, this.logger);
// Initialize measurements
this.measurements = new MeasurementContainer();
this.measurements = new MeasurementContainer({
autoConvert: true,
windowSize: 50,
defaultUnits: {
pressure: 'mbar',
flow: this.config.general.unit,
power: 'kW',
temperature: 'C'
}
});
this.interpolation = new interpolation();
this.child = {}; // object to hold child information so we know on what to subscribe
this.flowDrift = null;
this.predictFlow = new predict({ curve: this.config.asset.machineCurve.nq }); // load nq (x : ctrl , y : flow relationship)
this.predictPower = new predict({ curve: this.config.asset.machineCurve.np }); // load np (x : ctrl , y : power relationship)
this.predictCtrl = new predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship)
this.currentMode = this.config.mode.current;
this.currentEfficiencyCurve = {};
this.cog = 0;
@@ -84,17 +69,105 @@ class Machine {
this.absDistFromPeak = 0;
this.relDistFromPeak = 0;
// When position state changes, update position
this.state.emitter.on("positionChange", (data) => {
this.logger.debug(`Position change detected: ${data}`);
this.updatePosition();
});
//this.calcCog();
//When state changes look if we need to do other updates
this.state.emitter.on("stateChange", (newState) => {
this.logger.debug(`State change detected: ${newState}`);
this._updateState();
});
//perform init for certain values
this._init();
this.child = {}; // object to hold child information so we know on what to subscribe
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
}
_init(){
//assume standard temperature is 20degrees
this.measurements.type('temperature').variant('measured').position('atEquipment').value(15).unit('C');
//assume standard atm pressure is at sea level
this.measurements.type('atmPressure').variant('measured').position('atEquipment').value(101325).unit('Pa');
//populate min and max
const flowunit = this.config.general.unit;
this.measurements.type('flow').variant('predicted').position('max').value(this.predictFlow.currentFxyYMax, Date.now() , flowunit)
this.measurements.type('flow').variant('predicted').position('min').value(this.predictFlow.currentFxyYMin).unit(this.config.general.unit);
}
_updateState(){
const isOperational = this._isOperationalState();
if(!isOperational){
//overrule the last prediction this should be 0 now
this.measurements.type("flow").variant("predicted").position("downstream").value(0,Date.now(),this.config.general.unit);
this.measurements.type("flow").variant("predicted").position("atEquipment").value(0,Date.now(),this.config.general.unit);
}
}
/*------------------- Register child events -------------------*/
registerChild(child, softwareType) {
this.logger.debug('Setting up child event for softwaretype ' + softwareType);
if(softwareType === "measurement"){
const position = child.config.functionality.positionVsParent;
const distance = child.config.functionality.distanceVsParent || 0;
const measurementType = child.config.asset.type;
const key = `${measurementType}_${position}`;
//rebuild to measurementype.variant no position and then switch based on values not strings or names.
const eventName = `${measurementType}.measured.${position}`;
this.logger.debug(`Setting up listener for ${eventName} from child ${child.config.general.name}`);
// Register event listener for measurement updates
child.measurements.emitter.on(eventName, (eventData) => {
this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
this.logger.debug(` Emitting... ${eventName} with data:`);
// Store directly in parent's measurement container
this.measurements
.type(measurementType)
.variant("measured")
.position(position)
.value(eventData.value, eventData.timestamp, eventData.unit);
// Call the appropriate handler
this._callMeasurementHandler(measurementType, eventData.value, position, eventData);
});
}
}
// Centralized handler dispatcher
_callMeasurementHandler(measurementType, value, position, context) {
switch (measurementType) {
case 'pressure':
this.updateMeasuredPressure(value, position, context);
break;
case 'flow':
this.updateMeasuredFlow(value, position, context);
break;
case 'temperature':
this.updateMeasuredTemperature(value, position, context);
break;
default:
this.logger.warn(`No handler for measurement type: ${measurementType}`);
// Generic handler - just update position
this.updatePosition();
break;
}
}
//---------------- END child stuff -------------//
// Method to assess drift using errorMetrics
assessDrift(measurement, processMin, processMax) {
this.logger.debug(`Assessing drift for measurement: ${measurement} processMin: ${processMin} processMax: ${processMax}`);
@@ -130,46 +203,68 @@ class Machine {
// -------- Mode and Input Management -------- //
isValidSourceForMode(source, mode) {
const allowedSourcesSet = this.config.mode.allowedSources[mode] || [];
return allowedSourcesSet.has(source);
const allowed = allowedSourcesSet.has(source);
allowed?
this.logger.debug(`source is allowed proceeding with ${source} for mode ${mode}`) :
this.logger.warn(`${source} is not allowed in mode ${mode}`);
return allowed;
}
isValidActionForMode(action, mode) {
const allowedActionsSet = this.config.mode.allowedActions[mode] || [];
return allowedActionsSet.has(action);
const allowed = allowedActionsSet.has(action);
allowed ?
this.logger.debug(`Action is allowed proceeding with ${action} for mode ${mode}`) :
this.logger.warn(`${action} is not allowed in mode ${mode}`);
return allowed;
}
async handleInput(source, action, parameter) {
if (!this.isValidSourceForMode(source, this.currentMode)) {
let warningTxt = `Source '${source}' is not valid for mode '${this.currentMode}'.`;
this.logger.warn(warningTxt);
return {status : false , feedback: warningTxt};
}
//sanitize input
if( typeof action !== 'string'){this.logger.error(`Action must be string`); return;}
//convert to lower case to avoid to many mistakes in commands
action = action.toLowerCase();
// check for validity of the request
if(!this.isValidActionForMode(action,this.currentMode)){return ;}
if (!this.isValidSourceForMode(source, this.currentMode)) {return ;}
this.logger.info(`Handling input from source '${source}' with action '${action}' in mode '${this.currentMode}'.`);
try {
switch (action) {
case "execSequence":
await this.executeSequence(parameter);
//recalc flow and power
this.updatePosition();
break;
case "execMovement":
await this.setpoint(parameter);
break;
case "flowMovement":
case "execsequence":
return await this.executeSequence(parameter);
case "execmovement":
return await this.setpoint(parameter);
case "entermaintenance":
return await this.executeSequence(parameter);
case "exitmaintenance":
return await this.executeSequence(parameter);
case "flowmovement":
// Calculate the control value for a desired flow
const pos = this.calcCtrl(parameter);
// Move to the desired setpoint
await this.setpoint(pos);
break;
case "emergencyStop":
return await this.setpoint(pos);
case "emergencystop":
this.logger.warn(`Emergency stop activated by '${source}'.`);
await this.executeSequence("emergencyStop");
break;
case "statusCheck":
return await this.executeSequence("emergencyStop");
case "statuscheck":
this.logger.info(`Status Check: Mode = '${this.currentMode}', Source = '${source}'.`);
break;
default:
this.logger.warn(`Action '${action}' is not implemented.`);
break;
@@ -179,10 +274,17 @@ class Machine {
} catch (error) {
this.logger.error(`Error handling input: ${error}`);
}
}
abortMovement(reason = "group override") {
if (this.state?.abortCurrentMovement) {
this.state.abortCurrentMovement(reason);
}
}
setMode(newMode) {
const availableModes = defaultConfig.mode.current.rules.values.map(v => v.value);
const availableModes = this.defaultConfig.mode.current.rules.values.map(v => v.value);
if (!availableModes.includes(newMode)) {
this.logger.warn(`Invalid mode '${newMode}'. Allowed modes are: ${availableModes.join(', ')}`);
return;
@@ -219,6 +321,9 @@ class Machine {
break; // Exit sequence execution on error
}
}
//recalc flow and power
this.updatePosition();
}
async setpoint(setpoint) {
@@ -229,6 +334,8 @@ class Machine {
throw new Error("Invalid setpoint: Setpoint must be a non-negative number.");
}
this.logger.info(`Setting setpoint to ${setpoint}. Current position: ${this.state.getCurrentPosition()}`);
// Move to the desired setpoint
await this.state.moveTo(setpoint);
@@ -239,64 +346,98 @@ class Machine {
// Calculate flow based on current pressure and position
calcFlow(x) {
const state = this.state.getCurrentState();
if(this.hasCurve) {
if (!this._isOperationalState()) {
this.measurements.type("flow").variant("predicted").position("downstream").value(0,Date.now(),this.config.general.unit);
this.measurements.type("flow").variant("predicted").position("atEquipment").value(0,Date.now(),this.config.general.unit);
this.logger.debug(`Machine is not operational. Setting predicted flow to 0.`);
return 0;
}
if (!["operational", "accelerating", "decelerating"].includes(state)) {
this.measurements.type("flow").variant("predicted").position("downstream").value(0);
this.logger.debug(`Machine is not operational. Setting predicted flow to 0.`);
return 0;
}
//this.predictFlow.currentX = x; Decrepated
const cFlow = this.predictFlow.y(x);
this.measurements.type("flow").variant("predicted").position("downstream").value(cFlow);
this.measurements.type("flow").variant("predicted").position("downstream").value(cFlow,Date.now(),this.config.general.unit);
this.measurements.type("flow").variant("predicted").position("atEquipment").value(cFlow,Date.now(),this.config.general.unit);
//this.logger.debug(`Calculated flow: ${cFlow} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cFlow;
}
// If no curve data is available, log a warning and return 0
this.logger.warn(`No curve data available for flow calculation. Returning 0.`);
this.measurements.type("flow").variant("predicted").position("downstream").value(0, Date.now(),this.config.general.unit);
this.measurements.type("flow").variant("predicted").position("atEquipment").value(0, Date.now(),this.config.general.unit);
return 0;
}
// Calculate power based on current pressure and position
calcPower(x) {
const state = this.state.getCurrentState();
if (!["operational", "accelerating", "decelerating"].includes(state)) {
this.measurements.type("power").variant("predicted").position('upstream').value(0);
this.logger.debug(`Machine is not operational. Setting predicted power to 0.`);
return 0;
}
if(this.hasCurve) {
if (!this._isOperationalState()) {
this.measurements.type("power").variant("predicted").position('atEquipment').value(0);
this.logger.debug(`Machine is not operational. Setting predicted power to 0.`);
return 0;
}
//this.predictPower.currentX = x; Decrepated
const cPower = this.predictPower.y(x);
this.measurements.type("power").variant("predicted").position('upstream').value(cPower);
this.measurements.type("power").variant("predicted").position('atEquipment').value(cPower);
//this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cPower;
}
// If no curve data is available, log a warning and return 0
this.logger.warn(`No curve data available for power calculation. Returning 0.`);
this.measurements.type("power").variant("predicted").position('atEquipment').value(0);
return 0;
}
// calculate the power consumption using only flow and pressure
inputFlowCalcPower(flow) {
this.predictCtrl.currentX = flow;
const cCtrl = this.predictCtrl.y(flow);
this.predictPower.currentX = cCtrl;
const cPower = this.predictPower.y(cCtrl);
return cPower;
if(this.hasCurve) {
this.predictCtrl.currentX = flow;
const cCtrl = this.predictCtrl.y(flow);
this.predictPower.currentX = cCtrl;
const cPower = this.predictPower.y(cCtrl);
return cPower;
}
// If no curve data is available, log a warning and return 0
this.logger.warn(`No curve data available for power calculation. Returning 0.`);
this.measurements.type("power").variant("predicted").position('atEquipment').value(0);
return 0;
}
// Function to predict control value for a desired flow
calcCtrl(x) {
if(this.hasCurve) {
this.predictCtrl.currentX = x;
const cCtrl = this.predictCtrl.y(x);
this.measurements.type("ctrl").variant("predicted").position('atEquipment').value(cCtrl);
//this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cCtrl;
}
this.predictCtrl.currentX = x;
const cCtrl = this.predictCtrl.y(x);
this.measurements.type("ctrl").variant("predicted").position('upstream').value(cCtrl);
//this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cCtrl;
// If no curve data is available, log a warning and return 0
this.logger.warn(`No curve data available for control calculation. Returning 0.`);
this.measurements.type("ctrl").variant("predicted").position('atEquipment').value(0);
return 0;
}
// this function returns the pressure for calculations
// returns the best available pressure measurement to use in the prediction calculation
// this will be either the differential pressure, downstream or upstream pressure
getMeasuredPressure() {
if(this.hasCurve === false){
this.logger.error(`No valid curve available to calculate prediction using last known pressure`);
return 0;
}
const pressureDiff = this.measurements.type('pressure').variant('measured').difference();
// Both upstream & downstream => differential
if (pressureDiff != null) {
if (pressureDiff) {
this.logger.debug(`Pressure differential: ${pressureDiff.value}`);
this.predictFlow.fDimension = pressureDiff.value;
this.predictPower.fDimension = pressureDiff.value;
@@ -316,7 +457,7 @@ class Machine {
// Only downstream => use it, warn that it's partial
if (downstreamPressure != null) {
this.logger.warn(`Using downstream pressure only for prediction: ${downstreamPressure} `);
this.logger.warn(`Using downstream pressure only for prediction: ${downstreamPressure} This is less acurate!!`);
this.predictFlow.fDimension = downstreamPressure;
this.predictPower.fDimension = downstreamPressure;
this.predictCtrl.fDimension = downstreamPressure;
@@ -341,6 +482,9 @@ class Machine {
const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted");
//update the distance from peak
this.calcDistanceBEP(efficiency,cog,minEfficiency);
//place min and max flow capabilities in containerthis.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin
this.measurements.type('flow').variant('predicted').position('max').value(this.predictFlow.currentFxyYMax).unit(this.config.general.unit);
this.measurements.type('flow').variant('predicted').position('min').value(this.predictFlow.currentFxyYMin).unit(this.config.general.unit);
return 0;
}
@@ -363,7 +507,7 @@ class Machine {
}
// get
const upstreamFlow = this.measurements.type('pressure').variant('measured').position('upstream').getCurrentValue();
const upstreamFlow = this.measurements.type('flow').variant('measured').position('upstream').getCurrentValue();
// Only upstream => might still accept it, but warn
if (upstreamFlow != null) {
@@ -372,7 +516,7 @@ class Machine {
}
// get
const downstreamFlow = this.measurements.type('pressure').variant('measured').position('downstream').getCurrentValue();
const downstreamFlow = this.measurements.type('flow').variant('measured').position('downstream').getCurrentValue();
// Only downstream => might still accept it, but warn
if (downstreamFlow != null) {
@@ -386,7 +530,7 @@ class Machine {
}
handleMeasuredPower() {
const power = this.measurements.type("power").variant("measured").position("upstream").getCurrentValue();
const power = this.measurements.type("power").variant("measured").position("atEquipment").getCurrentValue();
// If your system calls it "upstream" or just a single "value", adjust accordingly
if (power != null) {
@@ -398,73 +542,51 @@ class Machine {
}
}
updatePressure(variant,value,position) {
// context handler for pressure updates
updateMeasuredPressure(value, position, context = {}) {
switch (variant) {
case ("measured"):
//only update when machine is in a state where it can be used
if (this.state.getCurrentState() == "operational" || this.state.getCurrentState() == "accelerating" || this.state.getCurrentState() == "decelerating") {
// put value in measurements
this.measurements.type("pressure").variant("measured").position(position).value(value);
//when measured pressure gets updated we need some logic to fetch the relevant value which could be downstream or differential pressure
const pressure = this.getMeasuredPressure();
//update the flow power and cog
this.updatePosition();
this.logger.debug(`Measured pressure: ${pressure}`);
}
break;
this.logger.debug(`Pressure update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`);
default:
this.logger.warn(`Unrecognized variant '${variant}' for pressure update.`);
break;
// Store in parent's measurement container
this.measurements.type("pressure").variant("measured").position(position).value(value, context.timestamp, context.unit);
// Determine what kind of value to use as pressure (upstream , downstream or difference)
const pressure = this.getMeasuredPressure();
this.updatePosition();
this.logger.debug(`Using pressure: ${pressure} for calculations`);
}
// NEW: Flow handler
updateMeasuredFlow(value, position, context = {}) {
if (!this._isOperationalState()) {
this.logger.warn(`Machine not operational, skipping flow update from ${context.childName || 'unknown'}`);
return;
}
this.logger.debug(`Flow update: ${value} at ${position} from ${context.childName || 'child'}`);
// Store in parent's measurement container
this.measurements.type("flow").variant("measured").position(position).value(value, context.timestamp, context.unit);
// Update predicted flow if you have prediction capability
if (this.predictFlow) {
this.measurements.type("flow").variant("predicted").position("downstream").value(this.predictFlow.outputY || 0);
this.measurements.type("flow").variant("predicted").position("atEquipment").value(this.predictFlow.outputY || 0);
}
}
updateFlow(variant,value,position) {
switch (variant) {
case ("measured"):
// put value in measurements
this.measurements.type("flow").variant("measured").position(position).value(value);
//when measured flow gets updated we need to push the last known value in the prediction measurements to keep them synced
this.measurements.type("flow").variant("predicted").position("downstream").value(this.predictFlow.outputY);
break;
case ("predicted"):
this.logger.debug('not doing anythin yet');
break;
default:
this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
break;
}
}
updateMeasurement(variant, subType, value, position) {
this.logger.debug(`---------------------- updating ${subType} ------------------ `);
switch (subType) {
case "pressure":
// Update pressure measurement
this.updatePressure(variant,value,position);
break;
case "flow":
this.updateFlow(variant,value,position);
// Update flow measurement
this.flowDrift = this.assessDrift("flow", this.predictFlow.currentFxyYMin , this.predictFlow.currentFxyYMax);
this.logger.debug(`---------------------------------------- `);
break;
case "power":
// Update power measurement
break;
default:
this.logger.error(`Type '${type}' not recognized for measured update.`);
return;
}
// Helper method for operational state check
_isOperationalState() {
const state = this.state.getCurrentState();
this.logger.debug(`Checking operational state ${this.state.getCurrentState()} ? ${["operational", "accelerating", "decelerating"].includes(state)}`);
return ["operational", "accelerating", "decelerating"].includes(state);
}
//what is the internal functions that need updating when something changes that has influence on this.
updatePosition() {
if (this.state.getCurrentState() == "operational" || this.state.getCurrentState() == "accelerating" || this.state.getCurrentState() == "decelerating") {
if (this._isOperationalState()) {
const currentPosition = this.state.getCurrentPosition();
@@ -481,6 +603,7 @@ class Machine {
this.calcDistanceBEP(efficiency,cog,minEfficiency);
}
}
calcDistanceFromPeak(currentEfficiency,peakEfficiency){
@@ -495,6 +618,22 @@ class Machine {
return distance;
}
showWorkingCurves() {
// Show the current curves for debugging
const { powerCurve, flowCurve } = this.getCurrentCurves();
return {
powerCurve: powerCurve,
flowCurve: flowCurve,
cog: this.cog,
cogIndex: this.cogIndex,
NCog: this.NCog,
minEfficiency: this.minEfficiency,
currentEfficiencyCurve: this.currentEfficiencyCurve,
absDistFromPeak: this.absDistFromPeak,
relDistFromPeak: this.relDistFromPeak
};
}
// Calculate the center of gravity for current pressure
calcCog() {
@@ -504,7 +643,7 @@ class Machine {
const {efficiencyCurve, peak, peakIndex, minEfficiency } = this.calcEfficiencyCurve(powerCurve, flowCurve);
// Calculate the normalized center of gravity
const NCog = (flowCurve.y[peakIndex] - this.predictFlow.currentFxyYMin) / (this.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin);
const NCog = (flowCurve.y[peakIndex] - this.predictFlow.currentFxyYMin) / (this.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin); //
//store in object for later retrieval
this.currentEfficiencyCurve = efficiencyCurve;
@@ -554,16 +693,39 @@ class Machine {
return { cPower, cFlow };
}
calcEfficiency(power, flow, variant) {
calcEfficiency(power,flow,variant) {
const pressureDiff = this.measurements.type('pressure').variant('measured').difference('Pa');
const g = gravity.getStandardGravity();
const temp = this.measurements.type('temperature').variant('measured').position('atEquipment').getCurrentValue('K');
const atmPressure = this.measurements.type('atmPressure').variant('measured').position('atEquipment').getCurrentValue('Pa');
console.log(`--------------------calc efficiency : Pressure diff:${pressureDiff},${temp}, ${g} `);
const rho = coolprop.PropsSI('D', 'T', temp, 'P', atmPressure, 'WasteWater');
this.logger.debug(`temp: ${temp} atmPressure : ${atmPressure} rho : ${rho} pressureDiff: ${pressureDiff?.value || 0}`);
const flowM3s = this.measurements.type('flow').variant('predicted').position('atEquipment').getCurrentValue('m3/s');
const powerWatt = this.measurements.type('power').variant('predicted').position('atEquipment').getCurrentValue('W');
this.logger.debug(`Flow : ${flowM3s} power: ${powerWatt}`);
if (power != 0 && flow != 0) {
// Calculate efficiency after measurements update
this.measurements.type("efficiency").variant(variant).position('downstream').value((flow / power));
} else {
this.measurements.type("efficiency").variant(variant).position('downstream').value(null);
const specificFlow = flow / power;
const specificEnergyConsumption = power / flow;
this.measurements.type("efficiency").variant(variant).position('atEquipment').value(specificFlow);
this.measurements.type("specificEnergyConsumption").variant(variant).position('atEquipment').value(specificEnergyConsumption);
if(pressureDiff?.value != null && flowM3s != null && powerWatt != null){
const meterPerBar = pressureDiff.value / rho * g;
const nHydraulicEfficiency = rho * g * flowM3s * (pressureDiff.value * meterPerBar ) / powerWatt;
this.measurements.type("nHydraulicEfficiency").variant(variant).position('atEquipment').value(nHydraulicEfficiency);
}
}
return this.measurements.type("efficiency").variant(variant).position('downstream').getCurrentValue();
//change this to nhydrefficiency ?
return this.measurements.type("efficiency").variant(variant).position('atEquipment').getCurrentValue();
}
@@ -609,26 +771,8 @@ class Machine {
getOutput() {
// Improved output object generation
const output = {};
//build the output object
this.measurements.getTypes().forEach(type => {
this.measurements.getVariants(type).forEach(variant => {
const downstreamVal = this.measurements.type(type).variant(variant).position("downstream").getCurrentValue();
const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue();
if (downstreamVal != null) {
output[`downstream_${variant}_${type}`] = downstreamVal;
}
if (upstreamVal != null) {
output[`upstream_${variant}_${type}`] = upstreamVal;
}
if (downstreamVal != null && upstreamVal != null) {
const diffVal = this.measurements.type(type).variant(variant).difference().value;
output[`differential_${variant}_${type}`] = diffVal;
}
});
});
const output = this.measurements.getFlattenedOutput();
//fill in the rest of the output object
output["state"] = this.state.getCurrentState();
@@ -639,6 +783,7 @@ class Machine {
output["cog"] = this.cog; // flow / power efficiency
output["NCog"] = this.NCog; // normalized cog
output["NCogPercent"] = Math.round(this.NCog * 100 * 100) / 100 ;
output["maintenanceTime"] = this.state.getMaintenanceTimeHours();
if(this.flowDrift != null){
const flowDrift = this.flowDrift;
@@ -667,7 +812,7 @@ module.exports = Machine;
curve = require('C:/Users/zn375/.node-red/public/fallbackData.json');
//import a child
const Child = require('../../../measurement/dependencies/measurement/measurement');
const Child = require('../../measurement/src/specificClass');
console.log(`Creating child...`);
const PT1 = new Child(config={
@@ -680,11 +825,16 @@ const PT1 = new Child(config={
},
functionality:{
softwareType:"measurement",
positionVsParent:"upstream",
},
asset:{
type:"sensor",
subType:"pressure",
supplier:"Vega",
category:"sensor",
type:"pressure",
model:"Vegabar 82",
unit: "mbar"
},
});
const PT2 = new Child(config={
@@ -697,10 +847,14 @@ const PT2 = new Child(config={
},
functionality:{
softwareType:"measurement",
positionVsParent:"upstream",
},
asset:{
type:"sensor",
subType:"pressure",
supplier:"Vega",
category:"sensor",
type:"pressure",
model:"Vegabar 82",
unit: "mbar"
},
});
@@ -718,7 +872,7 @@ const machineConfig = {
asset: {
supplier: "Hydrostal",
type: "pump",
subType: "centrifugal",
category: "centrifugal",
model: "H05K-S03R+HGM1X-X280KO", // Ensure this field is present.
machineCurve: curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"],
}