Compare commits
3 Commits
88945add81
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3391069ab0 | ||
|
|
f53f62a522 | ||
|
|
09f1c37125 |
98
LICENSE
98
LICENSE
@@ -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.
|
||||||
318
dependencies/heatExchanger/heatExchanger.js
vendored
Normal file
318
dependencies/heatExchanger/heatExchanger.js
vendored
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
/**
|
||||||
|
* @file heatExchanger.js
|
||||||
|
*
|
||||||
|
* Permission is hereby granted to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to use it for personal
|
||||||
|
* or non-commercial purposes, with the following restrictions:
|
||||||
|
*
|
||||||
|
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
|
||||||
|
* be copied, merged, distributed, sublicensed, or sold without explicit
|
||||||
|
* prior written permission from the author.
|
||||||
|
*
|
||||||
|
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
|
||||||
|
* a valid license, obtainable only with the explicit consent of the author.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* Ownership of this code remains solely with the original author. Unauthorized
|
||||||
|
* use of this Software is strictly prohibited.
|
||||||
|
*
|
||||||
|
* Author:
|
||||||
|
* - Rene De Ren
|
||||||
|
* Email:
|
||||||
|
* - rene@thegoldenbasket.nl
|
||||||
|
*
|
||||||
|
* Future Improvements:
|
||||||
|
* - Time-based stability checks
|
||||||
|
* - Warmup handling
|
||||||
|
* - Dynamic outlier detection thresholds
|
||||||
|
* - Dynamic smoothing window and methods
|
||||||
|
* - Alarm and threshold handling
|
||||||
|
* - Maintenance mode
|
||||||
|
* - Historical data and trend analysis
|
||||||
|
*/
|
||||||
|
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const Logger = require('../../../generalfunctions/helper/logger');
|
||||||
|
const defaultConfig = require('./heatExchangerConfig.json');
|
||||||
|
const ConfigUtils = require('../../../generalfunctions/helper/configUtils');
|
||||||
|
|
||||||
|
class heatExchanger {
|
||||||
|
constructor(config={}) {
|
||||||
|
|
||||||
|
this.emitter = new EventEmitter(); // Own EventEmitter
|
||||||
|
this.configUtils = new ConfigUtils(defaultConfig);
|
||||||
|
this.config = this.configUtils.initConfig(config);
|
||||||
|
|
||||||
|
// Init after config is set
|
||||||
|
this.logger = new Logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name);
|
||||||
|
this.measurements = {};
|
||||||
|
|
||||||
|
// defining all parameters of a heat exchanger
|
||||||
|
this.maxFlow = this.config.asset.maxFlowRate; // expressed in m3/h
|
||||||
|
this.volume = 0.36; // expressed in liters
|
||||||
|
this.tempRange = {min: -195 , max : 225}; // expressed in °C
|
||||||
|
this.pressureRange = {min: 0, max: 20}; // expressed in bar
|
||||||
|
this.maxPower = this.config.asset.maxPower ; // expressed in kW
|
||||||
|
this.type = this.config.asset.subType ; // type of heat exchanger
|
||||||
|
|
||||||
|
//keep track of last known values
|
||||||
|
this.mFlowRateHot = 0; // kg/s
|
||||||
|
this.mFlowRateCold = 0; // kg/s
|
||||||
|
this.tempInHot = 0; // °C
|
||||||
|
this.tempOutHot = 0; // °C
|
||||||
|
this.tempInCold = 0; // °C
|
||||||
|
this.tempOutCold = 0; // °C
|
||||||
|
this.Q_act = 0; // kW
|
||||||
|
|
||||||
|
//medium properties
|
||||||
|
this.inputMedium = this.config.medium.input; // type of medium
|
||||||
|
this.outputMedium = this.config.medium.output; // type of medium
|
||||||
|
//build lookup table for medium properties (to do later)
|
||||||
|
this.CpIn = 4.18; // specific heat capacity of water in kJ/kg°C
|
||||||
|
this.CpOut = 4.18; // specific heat capacity of water in kJ/kg°C
|
||||||
|
|
||||||
|
this.flowConfig = this.config.flowConfiguration; // Type of flow arrangement: "counterflow", "parallel", "crossflow", etc.
|
||||||
|
|
||||||
|
this.U = this.config.asset.U; // average for stainaless steel plate heat exchangers water to water - 2000-5000 W/m2K
|
||||||
|
this.A = this.config.asset.A; // heat exchanger area in m2 (for example 0.24m2)
|
||||||
|
this.UA = this.calcUA(this.U,this.A) / 1000; // heat exchanger UA value in kW/K
|
||||||
|
|
||||||
|
this.logger.debug(`heatExchanger id: ${this.config.general.id}, initialized successfully.`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//later add conversions for different units
|
||||||
|
setHotFlowRate(flowRate) {
|
||||||
|
this.mFlowRateHot = this.m3hToKgs(flowRate);
|
||||||
|
this.calcOutputTemps();
|
||||||
|
return this.mFlowRateHot;
|
||||||
|
}
|
||||||
|
|
||||||
|
setColdFlowRate(flowRate) {
|
||||||
|
if (flowRate === undefined || flowRate === null || isNaN(parseFloat(flowRate))){
|
||||||
|
this.logger.error(`Invalid flow rate value: ${flowRate}`);
|
||||||
|
this.mFlowRateCold = 0;
|
||||||
|
return this.mFlowRateCold;
|
||||||
|
}
|
||||||
|
this.mFlowRateCold = this.m3hToKgs(flowRate);
|
||||||
|
this.calcOutputTemps();
|
||||||
|
return this.mFlowRateCold;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHotInput(temp) {
|
||||||
|
this.tempInHot = temp;
|
||||||
|
this.calcOutputTemps();
|
||||||
|
return this.tempInHot;
|
||||||
|
}
|
||||||
|
|
||||||
|
setColdInput(temp) {
|
||||||
|
this.tempInCold = temp;
|
||||||
|
this.calcOutputTemps();
|
||||||
|
return this.tempInCold;
|
||||||
|
}
|
||||||
|
|
||||||
|
calcUA(U, A) {
|
||||||
|
// UA = U * A
|
||||||
|
const UA = U * A;
|
||||||
|
return UA;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHeatTransferRate(mFlowRate, Cp, deltaT) {
|
||||||
|
// Q heat transfer rate kW = m mass flow rate kg/s * Cp specific heat capacity kJ/kg°C * ΔT temperature difference °C
|
||||||
|
const Q = mFlowRate * Cp * deltaT;
|
||||||
|
return Q;
|
||||||
|
}
|
||||||
|
|
||||||
|
calcOutputTemps() {
|
||||||
|
// Calculate outlet temperatures based on last known input temperatures and heat transfer rate
|
||||||
|
const result = this.calcOutletTempsEffectivenessNTU(this.mFlowRateHot, this.tempInHot, this.mFlowRateCold, this.tempInCold);
|
||||||
|
this.tempOutHot = result.tempOutHot;
|
||||||
|
this.tempOutCold = result.tempOutCold;
|
||||||
|
this.Q_act = result.Q;
|
||||||
|
this.logger.debug(`Hot Outlet Temperature: ${this.tempOutHot.toFixed(2)} °C, Cold Outlet Temperature: ${this.tempOutCold.toFixed(2)} °C, Heat Transfer Rate: ${this.Q_act.toFixed(2)} kW`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
calcOutletTempsEffectivenessNTU(mFlowRateHot, tempInHot, mFlowRateCold, tempInCold) {
|
||||||
|
// Calculate heat capacity rates
|
||||||
|
const C_hot = mFlowRateHot * this.CpIn;
|
||||||
|
const C_cold = mFlowRateCold * this.CpOut;
|
||||||
|
const UA = this.UA;
|
||||||
|
const flowConfig = this.flowConfig;
|
||||||
|
|
||||||
|
// Find C_min and C_max
|
||||||
|
const C_min = Math.min(C_hot, C_cold);
|
||||||
|
const C_max = Math.max(C_hot, C_cold);
|
||||||
|
const Cr = C_min / C_max;
|
||||||
|
|
||||||
|
// Calculate NTU (Number of Transfer Units)
|
||||||
|
const NTU = (C_min > 0) ? UA / C_min : 0;
|
||||||
|
|
||||||
|
// Calculate effectiveness based on exchanger type
|
||||||
|
let epsilon;
|
||||||
|
switch (flowConfig) {
|
||||||
|
case "counterflow":
|
||||||
|
// Special case for Cr = 1 (balanced flow)
|
||||||
|
if (Math.abs(Cr - 1) < 0.00001) {
|
||||||
|
epsilon = NTU / (1 + NTU);
|
||||||
|
} else {
|
||||||
|
epsilon = (1 - Math.exp(-NTU * (1 - Cr))) / (1 - Cr * Math.exp(-NTU * (1 - Cr)));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "parallelflow":
|
||||||
|
epsilon = (1 - Math.exp(-NTU * (1 + Cr))) / (1 + Cr);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported flow configuration ${flowConfig}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
// Calculate maximum heat transfer in
|
||||||
|
const Q_max = C_min * (tempInHot - tempInCold);
|
||||||
|
|
||||||
|
// Calculate actual heat transfer
|
||||||
|
const Q = Math.min ( epsilon * Q_max, this.maxPower) ;
|
||||||
|
|
||||||
|
// Calculate outlet temperatures
|
||||||
|
const tempOutHot = C_hot > 0 ? ( tempInHot - Q / C_hot) : tempInHot;
|
||||||
|
const tempOutCold = C_cold > 0 ? ( tempInCold + Q / C_cold ) : tempInCold;
|
||||||
|
|
||||||
|
return { tempOutHot, tempOutCold, Q };
|
||||||
|
}
|
||||||
|
|
||||||
|
// mFlowRate in kg/s, Cp in kJ/kg°C, tempIn in °C without effectiveness
|
||||||
|
calcOutletTemps(mFlowRateHot, CpHot, tempInHot, mFlowRateCold, CpCold, tempInCold, Q) {
|
||||||
|
// Calculate hot outlet temperature (heat is lost)
|
||||||
|
const tempOutHot = tempInHot - (Q / (mFlowRateHot * CpHot));
|
||||||
|
|
||||||
|
// Calculate cold outlet temperature (heat is gained)
|
||||||
|
const tempOutCold = tempInCold + (Q / (mFlowRateCold * CpCold));
|
||||||
|
|
||||||
|
return { tempOutHot, tempOutCold };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Q to get the output temperature of the same stream temp in °C, mFlowRate in kg/s, Cp in kJ/kg°C
|
||||||
|
getTempOut(mFlowRate, Cp, tempIn, Q) {
|
||||||
|
// T_out = T_in + Q / (m * Cp)
|
||||||
|
const tempOut = tempIn + Q / (mFlowRate * Cp);
|
||||||
|
return tempOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
m3hToKgs(m3h) {
|
||||||
|
// 1 m3/h = 0.000277778 m3/s
|
||||||
|
const m3s = m3h * 0.000277778;
|
||||||
|
// 1 m3 = 1000 kg
|
||||||
|
const kgs = m3s * 1000;
|
||||||
|
return kgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// use Q to get the input temperature of the same stream
|
||||||
|
getTempIn(mFlowRate, Cp, tempOut, Q) {
|
||||||
|
// T_in = T_out - Q / (m * Cp)
|
||||||
|
const tempIn = tempOut - Q / (mFlowRate * Cp);
|
||||||
|
return tempIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeltaT(tempIn, tempOut) {
|
||||||
|
// ΔT = T_out - T_in
|
||||||
|
const deltaT = tempOut - tempIn;
|
||||||
|
return deltaT;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOutput() {
|
||||||
|
|
||||||
|
// Improved output object generation
|
||||||
|
const output = {};
|
||||||
|
//build the output object
|
||||||
|
if(Object.keys(this.measurements).length > 0) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
output["flowRateHot"] = this.mFlowRateHot;
|
||||||
|
output["flowRateCold"] = this.mFlowRateCold;
|
||||||
|
output["tempOutHot"] = this.tempOutHot;
|
||||||
|
output["tempOutCold"] = this.tempOutCold;
|
||||||
|
output["Q"] = this.Q_act;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = heatExchanger;
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Testing the class
|
||||||
|
const configuration = {
|
||||||
|
general: {
|
||||||
|
name: "he1",
|
||||||
|
logging: {
|
||||||
|
enabled: true,
|
||||||
|
logLevel: "debug",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const he = new heatExchanger(configuration);
|
||||||
|
|
||||||
|
he.logger.info(`heatExchanger created with config : ${JSON.stringify(he.config)}`);
|
||||||
|
|
||||||
|
he.logger.setLogLevel("debug");
|
||||||
|
|
||||||
|
// hot side (water)
|
||||||
|
const mFlowHot = he.m3hToKgs(1); // kg/s
|
||||||
|
const CpHot = 4.18; // kJ/kg.K (Water)
|
||||||
|
const Th_in = 100; // °C
|
||||||
|
|
||||||
|
// cold side (water)
|
||||||
|
const mFlowCold = he.m3hToKgs(1); // kg/s
|
||||||
|
const CpCold = 4.18; // kJ/kg.K (Water)
|
||||||
|
const Tc_in = 10; // °C
|
||||||
|
|
||||||
|
console.log(`UA: ${he.UA} W/K`);
|
||||||
|
|
||||||
|
const results = he.calcOutletTempsEffectivenessNTU(mFlowHot, CpHot, Th_in, mFlowCold, CpCold, Tc_in);
|
||||||
|
//loop over different temperatures and calculate the heat transfer rate
|
||||||
|
for(let i = 0; i < 10; i++) {
|
||||||
|
const tempOutHot = results.tempOutHot - i;
|
||||||
|
const tempOutCold = results.tempOutCold + i;
|
||||||
|
const Q = he.getHeatTransferRate(mFlowHot, CpHot, Th_in - tempOutHot);
|
||||||
|
const deltaT = he.getDeltaT(tempOutHot, tempOutCold);
|
||||||
|
console.log(`Hot Outlet Temperature: ${tempOutHot.toFixed(2)} °C, Cold Outlet Temperature: ${tempOutCold.toFixed(2)} °C, Heat Transfer Rate: ${Q.toFixed(2)} kW, ΔT: ${deltaT.toFixed(2)} °C`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`---------------------------------`);
|
||||||
|
console.log(`Hot Inlet Temperature: ${Th_in} °C`);
|
||||||
|
console.log(`Hot Outlet Temperature: ${results.tempOutHot.toFixed(2)} °C`);
|
||||||
|
console.log(`flow rate hot: ${mFlowHot} kg/s`);
|
||||||
|
console.log(`---------------------------------`);
|
||||||
|
console.log(`Cold Inlet Temperature: ${Tc_in} °C`);
|
||||||
|
console.log(`Cold Outlet Temperature: ${results.tempOutCold.toFixed(2)} °C`);
|
||||||
|
console.log(`flow rate cold: ${mFlowCold} kg/s`);
|
||||||
|
console.log(`---------------------------------`);
|
||||||
|
console.log(`Heat Transfer Rate: ${results.Q.toFixed(2)} kW`);
|
||||||
|
|
||||||
|
// */
|
||||||
471
dependencies/heatExchanger/heatExchanger.test.js
vendored
Normal file
471
dependencies/heatExchanger/heatExchanger.test.js
vendored
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
const HeatExchanger = require('./heatExchanger');
|
||||||
|
|
||||||
|
// Suppress console output from the actual class during testing
|
||||||
|
const silentLogger = {
|
||||||
|
error: () => {},
|
||||||
|
debug: () => {},
|
||||||
|
info: () => {},
|
||||||
|
warn: () => {},
|
||||||
|
setLogLevel: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base configuration for testing
|
||||||
|
const testConfig = {
|
||||||
|
general: {
|
||||||
|
name: "test_heat_exchanger",
|
||||||
|
id: "HX-001",
|
||||||
|
logging: {
|
||||||
|
enabled: false,
|
||||||
|
logLevel: "error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
maxFlowRate: 10, // m3/h
|
||||||
|
maxPower: 100, // kW
|
||||||
|
subType: "plate",
|
||||||
|
U: 3000, // W/m²K - Overall heat transfer coefficient
|
||||||
|
A: 0.24 // m² - Heat transfer area
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
input: "water",
|
||||||
|
output: "water"
|
||||||
|
},
|
||||||
|
flowConfiguration: "counterflow" // Default to counterflow
|
||||||
|
};
|
||||||
|
|
||||||
|
class HeatExchangerTester {
|
||||||
|
constructor() {
|
||||||
|
this.totalTests = 0;
|
||||||
|
this.passedTests = 0;
|
||||||
|
this.failedTests = 0;
|
||||||
|
this.heatExchanger = new HeatExchanger(testConfig);
|
||||||
|
|
||||||
|
// Override logger to prevent console output during tests
|
||||||
|
this.heatExchanger.logger = silentLogger;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(condition, message) {
|
||||||
|
this.totalTests++;
|
||||||
|
if (condition) {
|
||||||
|
console.log(`✓ PASS: ${message}`);
|
||||||
|
this.passedTests++;
|
||||||
|
} else {
|
||||||
|
console.log(`✗ FAIL: ${message}`);
|
||||||
|
this.failedTests++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertApproxEqual(actual, expected, tolerance = 0.01, message) {
|
||||||
|
const diff = Math.abs(actual - expected);
|
||||||
|
const relativeError = expected !== 0 ? diff / Math.abs(expected) : diff;
|
||||||
|
|
||||||
|
this.assert(
|
||||||
|
relativeError <= tolerance,
|
||||||
|
`${message} - Expected: ${expected.toFixed(3)}, Got: ${actual.toFixed(3)}, Relative Error: ${(relativeError * 100).toFixed(2)}%`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testM3hToKgsConversion() {
|
||||||
|
console.log("\nTesting m3/h to kg/s Conversion...");
|
||||||
|
|
||||||
|
// Standard water conversion
|
||||||
|
const m3h = 3.6; // 3.6 m³/h
|
||||||
|
const expectedKgs = 1.0; // 1.0 kg/s
|
||||||
|
|
||||||
|
const convertedValue = this.heatExchanger.m3hToKgs(m3h);
|
||||||
|
this.assertApproxEqual(convertedValue, expectedKgs, 0.001, "3.6 m³/h should convert to 1.0 kg/s");
|
||||||
|
|
||||||
|
// Zero case
|
||||||
|
this.assertApproxEqual(this.heatExchanger.m3hToKgs(0), 0, 0.001, "0 m³/h should convert to 0 kg/s");
|
||||||
|
|
||||||
|
// Large value
|
||||||
|
this.assertApproxEqual(this.heatExchanger.m3hToKgs(36), 10, 0.001, "36 m³/h should convert to 10 kg/s");
|
||||||
|
}
|
||||||
|
|
||||||
|
testUACalculation() {
|
||||||
|
console.log("\nTesting UA Calculation...");
|
||||||
|
|
||||||
|
// Simple multiplication
|
||||||
|
const U = 3; // kW/m²K
|
||||||
|
const A = 0.24; // m²
|
||||||
|
const expectedUA = 0.720; // kW/K
|
||||||
|
|
||||||
|
const calculatedUA = this.heatExchanger.calcUA(U, A);
|
||||||
|
this.assertApproxEqual(calculatedUA, expectedUA, 0.001, "UA should be U × A");
|
||||||
|
|
||||||
|
// Ensure class initialization sets UA correctly
|
||||||
|
this.assertApproxEqual(this.heatExchanger.UA, expectedUA, 0.001, "UA should be initialized correctly");
|
||||||
|
}
|
||||||
|
|
||||||
|
testHeatTransferRate() {
|
||||||
|
console.log("\nTesting Heat Transfer Rate Calculation...");
|
||||||
|
|
||||||
|
// Basic heat transfer equation: Q = ṁ × Cp × ΔT
|
||||||
|
const mFlowRate = 1.0; // kg/s
|
||||||
|
const Cp = 4.18; // kJ/kgK (water)
|
||||||
|
const deltaT = 10; // 10°C temperature difference
|
||||||
|
const expectedQ = 41.8; // kW
|
||||||
|
|
||||||
|
const calculatedQ = this.heatExchanger.getHeatTransferRate(mFlowRate, Cp, deltaT);
|
||||||
|
this.assertApproxEqual(calculatedQ, expectedQ, 0.001, "Heat transfer rate should match Q = ṁ × Cp × ΔT");
|
||||||
|
|
||||||
|
// Zero case
|
||||||
|
this.assertApproxEqual(this.heatExchanger.getHeatTransferRate(0, Cp, deltaT), 0, 0.001, "No flow should result in zero heat transfer");
|
||||||
|
this.assertApproxEqual(this.heatExchanger.getHeatTransferRate(mFlowRate, Cp, 0), 0, 0.001, "No temperature difference should result in zero heat transfer");
|
||||||
|
}
|
||||||
|
|
||||||
|
testDeltaTCalculation() {
|
||||||
|
console.log("\nTesting Delta T Calculation...");
|
||||||
|
|
||||||
|
const tempIn = 20; // °C
|
||||||
|
const tempOut = 30; // °C
|
||||||
|
const expectedDeltaT = 10; // °C
|
||||||
|
|
||||||
|
const calculatedDeltaT = this.heatExchanger.getDeltaT(tempIn, tempOut);
|
||||||
|
this.assertApproxEqual(calculatedDeltaT, expectedDeltaT, 0.001, "Delta T calculation should be correct");
|
||||||
|
|
||||||
|
// Negative delta (cooling)
|
||||||
|
this.assertApproxEqual(
|
||||||
|
this.heatExchanger.getDeltaT(50, 20),
|
||||||
|
-30,
|
||||||
|
0.001,
|
||||||
|
"Delta T should handle cooling correctly"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testCalcOutletTempsEffectivenessNTU_Counterflow() {
|
||||||
|
console.log("\nTesting Outlet Temperature Calculation (Counterflow)...");
|
||||||
|
|
||||||
|
// Create a specific instance for this test with counterflow
|
||||||
|
const config = JSON.parse(JSON.stringify(testConfig));
|
||||||
|
config.flowConfiguration = "counterflow";
|
||||||
|
const counterflowHE = new HeatExchanger(config);
|
||||||
|
counterflowHE.logger = silentLogger;
|
||||||
|
|
||||||
|
// Test case: balanced flow (C_hot = C_cold)
|
||||||
|
const mFlowRateHot = 1.0; // kg/s
|
||||||
|
const tempInHot = 100; // °C
|
||||||
|
const mFlowRateCold = 1.0; // kg/s
|
||||||
|
const tempInCold = 20; // °C
|
||||||
|
|
||||||
|
// UA = 720 W/K = 0.72 kW/K
|
||||||
|
// C_min = m * Cp = 1 * 4.18 = 4.18 kW/K
|
||||||
|
// NTU = UA/C_min = 0.72 / 4.18 = 0.172
|
||||||
|
// Cr = 1 (balanced flow)
|
||||||
|
// Effectiveness (counterflow) for Cr=1: ε = NTU/(1+NTU) = 0.172/(1+0.172) = 0.147
|
||||||
|
// Q_max = C_min * (Th_in - Tc_in) = 4.18 * (100 - 20) = 334.4 kW
|
||||||
|
// Q = ε * Q_max = 0.147 * 334.4 = 49.2 kW (but limited by maxPower = 100 kW)
|
||||||
|
// Th_out = Th_in - Q/C_hot = 100 - 49.2/4.18 = 88.2°C
|
||||||
|
// Tc_out = Tc_in + Q/C_cold = 20 + 49.2/4.18 = 31.8°C
|
||||||
|
|
||||||
|
const result = counterflowHE.calcOutletTempsEffectivenessNTU(
|
||||||
|
mFlowRateHot,
|
||||||
|
tempInHot,
|
||||||
|
mFlowRateCold,
|
||||||
|
tempInCold
|
||||||
|
);
|
||||||
|
|
||||||
|
// Using calculated effectiveness value to check outputs
|
||||||
|
this.assertApproxEqual(result.tempOutHot, 88.2, 0.1, "Hot outlet temperature should match for counterflow");
|
||||||
|
this.assertApproxEqual(result.tempOutCold, 31.8, 0.1, "Cold outlet temperature should match for counterflow");
|
||||||
|
this.assertApproxEqual(result.Q, 49.2, 0.3, "Heat transfer rate should match for counterflow");
|
||||||
|
|
||||||
|
// Check energy balance
|
||||||
|
const QHot = mFlowRateHot * counterflowHE.CpIn * (tempInHot - result.tempOutHot);
|
||||||
|
const QCold = mFlowRateCold * counterflowHE.CpOut * (result.tempOutCold - tempInCold);
|
||||||
|
this.assertApproxEqual(QHot, QCold, 0.01, "Energy balance should be maintained (Q_hot = Q_cold)");
|
||||||
|
}
|
||||||
|
|
||||||
|
testCalcOutletTempsEffectivenessNTU_Parallel() {
|
||||||
|
console.log("\nTesting Outlet Temperature Calculation (Parallel Flow)...");
|
||||||
|
|
||||||
|
// Create a specific instance for this test with parallel flow
|
||||||
|
const config = JSON.parse(JSON.stringify(testConfig));
|
||||||
|
config.flowConfiguration = "parallelflow";
|
||||||
|
const parallelHE = new HeatExchanger(config);
|
||||||
|
parallelHE.logger = silentLogger;
|
||||||
|
|
||||||
|
// Same inputs as the counterflow test for comparison
|
||||||
|
const mFlowRateHot = 1.0; // kg/s
|
||||||
|
const tempInHot = 100; // °C
|
||||||
|
const mFlowRateCold = 1.0; // kg/s
|
||||||
|
const tempInCold = 20; // °C
|
||||||
|
|
||||||
|
// Calculate expected values with the correct formula
|
||||||
|
// NTU = 0.172, Cr = 1
|
||||||
|
// Effectiveness (parallel flow): ε = (1 - exp(-NTU * (1 + Cr))) / (1 + Cr)
|
||||||
|
// ε = (1 - exp(-0.172 * 2)) / 2 ≈ 0.146
|
||||||
|
// Q = ε * Q_max = 0.146 * 334.4 ≈ 48.8 kW
|
||||||
|
// Th_out = Th_in - Q/C_hot = 100 - 48.8/4.18 ≈ 88.3°C
|
||||||
|
// Tc_out = Tc_in + Q/C_cold = 20 + 48.8/4.18 ≈ 31.7°C
|
||||||
|
|
||||||
|
const result = parallelHE.calcOutletTempsEffectivenessNTU(
|
||||||
|
mFlowRateHot,
|
||||||
|
tempInHot,
|
||||||
|
mFlowRateCold,
|
||||||
|
tempInCold
|
||||||
|
);
|
||||||
|
|
||||||
|
// Using corrected calculated effectiveness value to check outputs
|
||||||
|
this.assertApproxEqual(result.tempOutHot, 88.3, 0.1, "Hot outlet temperature should match for parallel flow");
|
||||||
|
this.assertApproxEqual(result.tempOutCold, 31.7, 0.1, "Cold outlet temperature should match for parallel flow");
|
||||||
|
this.assertApproxEqual(result.Q, 48.8, 0.3, "Heat transfer rate should match for parallel flow");
|
||||||
|
|
||||||
|
// Check energy balance
|
||||||
|
const QHot = mFlowRateHot * parallelHE.CpIn * (tempInHot - result.tempOutHot);
|
||||||
|
const QCold = mFlowRateCold * parallelHE.CpOut * (result.tempOutCold - tempInCold);
|
||||||
|
this.assertApproxEqual(QHot, QCold, 0.01, "Energy balance should be maintained (Q_hot = Q_cold)");
|
||||||
|
}
|
||||||
|
|
||||||
|
testMaxPowerLimit() {
|
||||||
|
console.log("\nTesting Maximum Power Limit...");
|
||||||
|
|
||||||
|
// Create a specific instance with low maximum power
|
||||||
|
const config = JSON.parse(JSON.stringify(testConfig));
|
||||||
|
config.asset.maxPower = 20; // kW - intentionally low
|
||||||
|
const limitedHE = new HeatExchanger(config);
|
||||||
|
limitedHE.logger = silentLogger;
|
||||||
|
|
||||||
|
// Set up a scenario that would exceed the maximum power
|
||||||
|
const mFlowRateHot = 2.0; // kg/s
|
||||||
|
const tempInHot = 100; // °C
|
||||||
|
const mFlowRateCold = 2.0; // kg/s
|
||||||
|
const tempInCold = 10; // °C
|
||||||
|
|
||||||
|
// With these parameters, the theoretical heat transfer would be higher,
|
||||||
|
// but should be limited to 20 kW by the maxPower setting
|
||||||
|
|
||||||
|
const result = limitedHE.calcOutletTempsEffectivenessNTU(
|
||||||
|
mFlowRateHot,
|
||||||
|
tempInHot,
|
||||||
|
mFlowRateCold,
|
||||||
|
tempInCold
|
||||||
|
);
|
||||||
|
|
||||||
|
this.assertApproxEqual(result.Q, 20, 0.01, "Heat transfer rate should be limited by maxPower");
|
||||||
|
|
||||||
|
// Calculate what the outlet temperatures should be with the limited power
|
||||||
|
// Th_out = Th_in - Q/C_hot = 100 - 20/(2*4.18) = 97.6°C
|
||||||
|
// Tc_out = Tc_in + Q/C_cold = 10 + 20/(2*4.18) = 12.4°C
|
||||||
|
|
||||||
|
this.assertApproxEqual(result.tempOutHot, 97.6, 0.1, "Hot outlet temperature should reflect power limit");
|
||||||
|
this.assertApproxEqual(result.tempOutCold, 12.4, 0.1, "Cold outlet temperature should reflect power limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
testImbalancedFlowRates() {
|
||||||
|
console.log("\nTesting Imbalanced Flow Rates...");
|
||||||
|
|
||||||
|
// Create a specific instance for this test
|
||||||
|
const imbalancedHE = new HeatExchanger(testConfig);
|
||||||
|
imbalancedHE.logger = silentLogger;
|
||||||
|
|
||||||
|
// Test case: C_hot < C_cold
|
||||||
|
const mFlowRateHot = 0.5; // kg/s
|
||||||
|
const tempInHot = 80; // °C
|
||||||
|
const mFlowRateCold = 2.0; // kg/s
|
||||||
|
const tempInCold = 15; // °C
|
||||||
|
|
||||||
|
// C_hot = 0.5 * 4.18 = 2.09 kW/K
|
||||||
|
// C_cold = 2.0 * 4.18 = 8.36 kW/K
|
||||||
|
// C_min = 2.09, C_max = 8.36
|
||||||
|
// Cr = 2.09/8.36 = 0.25
|
||||||
|
// NTU = UA/C_min = 0.72/2.09 = 0.344
|
||||||
|
// Effectiveness calculation for counterflow with Cr=0.25:
|
||||||
|
// ε = (1 - exp(-NTU * (1 - Cr))) / (1 - Cr * exp(-NTU * (1 - Cr)))
|
||||||
|
|
||||||
|
const result = imbalancedHE.calcOutletTempsEffectivenessNTU(
|
||||||
|
mFlowRateHot,
|
||||||
|
tempInHot,
|
||||||
|
mFlowRateCold,
|
||||||
|
tempInCold
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify reasonable outputs
|
||||||
|
this.assert(result.tempOutHot < tempInHot, "Hot outlet temperature should be less than inlet");
|
||||||
|
this.assert(result.tempOutCold > tempInCold, "Cold outlet temperature should be greater than inlet");
|
||||||
|
|
||||||
|
// Check energy balance
|
||||||
|
const QHot = mFlowRateHot * imbalancedHE.CpIn * (tempInHot - result.tempOutHot);
|
||||||
|
const QCold = mFlowRateCold * imbalancedHE.CpOut * (result.tempOutCold - tempInCold);
|
||||||
|
this.assertApproxEqual(QHot, QCold, 0.01, "Energy balance should be maintained with imbalanced flow rates");
|
||||||
|
this.assertApproxEqual(QHot, result.Q, 0.01, "Calculated Q should match energy extracted from hot stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
testZeroFlowRate() {
|
||||||
|
console.log("\nTesting Zero Flow Rate Handling...");
|
||||||
|
|
||||||
|
// Create a specific instance for this test
|
||||||
|
const zeroFlowHE = new HeatExchanger(testConfig);
|
||||||
|
zeroFlowHE.logger = silentLogger;
|
||||||
|
|
||||||
|
// Test with zero hot flow rate
|
||||||
|
const resultZeroHot = zeroFlowHE.calcOutletTempsEffectivenessNTU(
|
||||||
|
0, // kg/s - zero flow rate
|
||||||
|
80, // °C
|
||||||
|
1.0, // kg/s
|
||||||
|
15 // °C
|
||||||
|
);
|
||||||
|
|
||||||
|
this.assertApproxEqual(resultZeroHot.Q, 0, 0.001, "Zero hot flow rate should result in zero heat transfer");
|
||||||
|
this.assertApproxEqual(resultZeroHot.tempOutHot, 80, 0.001, "Zero hot flow should result in unchanged hot outlet temperature");
|
||||||
|
this.assertApproxEqual(resultZeroHot.tempOutCold, 15, 0.001, "Zero hot flow should result in unchanged cold outlet temperature");
|
||||||
|
|
||||||
|
// Test with zero cold flow rate
|
||||||
|
const resultZeroCold = zeroFlowHE.calcOutletTempsEffectivenessNTU(
|
||||||
|
1.0, // kg/s
|
||||||
|
80, // °C
|
||||||
|
0, // kg/s - zero flow rate
|
||||||
|
15 // °C
|
||||||
|
);
|
||||||
|
|
||||||
|
this.assertApproxEqual(resultZeroCold.Q, 0, 0.001, "Zero cold flow rate should result in zero heat transfer");
|
||||||
|
this.assertApproxEqual(resultZeroCold.tempOutHot, 80, 0.001, "Zero cold flow should result in unchanged hot outlet temperature");
|
||||||
|
this.assertApproxEqual(resultZeroCold.tempOutCold, 15, 0.001, "Zero cold flow should result in unchanged cold outlet temperature");
|
||||||
|
}
|
||||||
|
|
||||||
|
testSetter_HotFlowRate() {
|
||||||
|
console.log("\nTesting Hot Flow Rate Setter...");
|
||||||
|
|
||||||
|
const setterHE = new HeatExchanger(testConfig);
|
||||||
|
setterHE.logger = silentLogger;
|
||||||
|
|
||||||
|
// Set temperatures first to have a baseline
|
||||||
|
setterHE.setHotInput(80);
|
||||||
|
setterHE.setColdInput(20);
|
||||||
|
setterHE.setColdFlowRate(2);
|
||||||
|
|
||||||
|
// Initial calculation happens
|
||||||
|
const originalOutletHot = setterHE.tempOutHot;
|
||||||
|
|
||||||
|
// Now change hot flow rate
|
||||||
|
const m3h = 5;
|
||||||
|
const expectedKgs = 5 * 0.000277778 * 1000; // convert to kg/s
|
||||||
|
|
||||||
|
const result = setterHE.setHotFlowRate(m3h);
|
||||||
|
|
||||||
|
this.assertApproxEqual(result, expectedKgs, 0.001, "setHotFlowRate should return converted flow rate");
|
||||||
|
this.assertApproxEqual(setterHE.mFlowRateHot, expectedKgs, 0.001, "mFlowRateHot should be updated correctly");
|
||||||
|
|
||||||
|
// Outlet temperature should change with different flow rate
|
||||||
|
this.assert(setterHE.tempOutHot !== originalOutletHot, "Hot outlet temperature should change after flow rate update");
|
||||||
|
}
|
||||||
|
|
||||||
|
testSetter_HotInput() {
|
||||||
|
console.log("\nTesting Hot Input Temperature Setter...");
|
||||||
|
|
||||||
|
const setterHE = new HeatExchanger(testConfig);
|
||||||
|
setterHE.logger = silentLogger;
|
||||||
|
|
||||||
|
// Set other parameters first
|
||||||
|
setterHE.setColdInput(20);
|
||||||
|
setterHE.setHotFlowRate(3);
|
||||||
|
setterHE.setColdFlowRate(3);
|
||||||
|
|
||||||
|
const originalOutletCold = setterHE.tempOutCold;
|
||||||
|
|
||||||
|
// Now set hot input temperature
|
||||||
|
const newTemp = 90;
|
||||||
|
const result = setterHE.setHotInput(newTemp);
|
||||||
|
|
||||||
|
this.assertApproxEqual(result, newTemp, 0.001, "setHotInput should return the set temperature");
|
||||||
|
this.assertApproxEqual(setterHE.tempInHot, newTemp, 0.001, "tempInHot should be updated correctly");
|
||||||
|
|
||||||
|
// Cold outlet temperature should change with different hot input
|
||||||
|
this.assert(setterHE.tempOutCold !== originalOutletCold,
|
||||||
|
"Cold outlet temperature should change after hot input temperature update");
|
||||||
|
}
|
||||||
|
|
||||||
|
testPhysicalLimits() {
|
||||||
|
console.log("\nTesting Physical Limits and Constraints...");
|
||||||
|
|
||||||
|
// Test that extreme temperature differences don't break the model
|
||||||
|
const extremeHE = new HeatExchanger(testConfig);
|
||||||
|
extremeHE.logger = silentLogger;
|
||||||
|
|
||||||
|
// Extreme temperature difference
|
||||||
|
const result = extremeHE.calcOutletTempsEffectivenessNTU(
|
||||||
|
1.0, // kg/s
|
||||||
|
1000, // °C - extremely high temperature
|
||||||
|
1.0, // kg/s
|
||||||
|
0, // °C - very low temperature
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check energy balance is still maintained
|
||||||
|
const QHot = 1.0 * extremeHE.CpIn * (1000 - result.tempOutHot);
|
||||||
|
const QCold = 1.0 * extremeHE.CpOut * (result.tempOutCold - 0);
|
||||||
|
this.assertApproxEqual(QHot, QCold, 0.01, "Energy balance should be maintained even with extreme temperatures");
|
||||||
|
|
||||||
|
// Make sure temps are capped at maxPower
|
||||||
|
this.assert(result.Q <= extremeHE.maxPower, "Heat transfer rate should not exceed maxPower");
|
||||||
|
}
|
||||||
|
|
||||||
|
testOutputGeneration() {
|
||||||
|
console.log("\nTesting Output Generation...");
|
||||||
|
|
||||||
|
const outputHE = new HeatExchanger(testConfig);
|
||||||
|
outputHE.logger = silentLogger;
|
||||||
|
|
||||||
|
// Set some values
|
||||||
|
outputHE.setHotInput(80);
|
||||||
|
outputHE.setColdInput(20);
|
||||||
|
outputHE.setHotFlowRate(3);
|
||||||
|
outputHE.setColdFlowRate(2);
|
||||||
|
|
||||||
|
// Get output object
|
||||||
|
const output = outputHE.getOutput();
|
||||||
|
|
||||||
|
// Check that the output contains the right keys
|
||||||
|
this.assert(output.hasOwnProperty('flowRateHot'), "Output should contain flowRateHot");
|
||||||
|
this.assert(output.hasOwnProperty('flowRateCold'), "Output should contain flowRateCold");
|
||||||
|
this.assert(output.hasOwnProperty('tempOutHot'), "Output should contain tempOutHot");
|
||||||
|
this.assert(output.hasOwnProperty('tempOutCold'), "Output should contain tempOutCold");
|
||||||
|
this.assert(output.hasOwnProperty('Q'), "Output should contain Q");
|
||||||
|
|
||||||
|
// Check values match the internal state
|
||||||
|
this.assertApproxEqual(output.flowRateHot, outputHE.mFlowRateHot, 0.001, "Output flowRateHot should match internal state");
|
||||||
|
this.assertApproxEqual(output.tempOutHot, outputHE.tempOutHot, 0.001, "Output tempOutHot should match internal state");
|
||||||
|
this.assertApproxEqual(output.Q, outputHE.Q_act, 0.001, "Output Q should match internal state");
|
||||||
|
}
|
||||||
|
|
||||||
|
async runAllTests() {
|
||||||
|
console.log("\nStarting Heat Exchanger Tests...\n");
|
||||||
|
|
||||||
|
// Basic functionality tests
|
||||||
|
this.testM3hToKgsConversion();
|
||||||
|
this.testUACalculation();
|
||||||
|
this.testHeatTransferRate();
|
||||||
|
this.testDeltaTCalculation();
|
||||||
|
|
||||||
|
// Heat exchanger physics tests
|
||||||
|
this.testCalcOutletTempsEffectivenessNTU_Counterflow();
|
||||||
|
this.testCalcOutletTempsEffectivenessNTU_Parallel();
|
||||||
|
this.testMaxPowerLimit();
|
||||||
|
this.testImbalancedFlowRates();
|
||||||
|
this.testZeroFlowRate();
|
||||||
|
|
||||||
|
// API tests
|
||||||
|
this.testSetter_HotFlowRate();
|
||||||
|
this.testSetter_HotInput();
|
||||||
|
this.testPhysicalLimits();
|
||||||
|
this.testOutputGeneration();
|
||||||
|
|
||||||
|
console.log("\nTest Summary:");
|
||||||
|
console.log(`Total Tests: ${this.totalTests}`);
|
||||||
|
console.log(`Passed: ${this.passedTests}`);
|
||||||
|
console.log(`Failed: ${this.failedTests}`);
|
||||||
|
console.log(`Success Rate: ${((this.passedTests / this.totalTests) * 100).toFixed(1)}%`);
|
||||||
|
|
||||||
|
return this.failedTests === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all tests
|
||||||
|
const tester = new HeatExchangerTester();
|
||||||
|
tester.runAllTests()
|
||||||
|
.then(success => {
|
||||||
|
process.exit(success ? 0 : 1);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error running tests:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
258
dependencies/heatExchanger/heatExchangerConfig.json
vendored
Normal file
258
dependencies/heatExchanger/heatExchangerConfig.json
vendored
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "heatExchanger Configuration",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A human-readable name or label for this heatExchanger configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A unique identifier for this configuration. If not provided, defaults to null."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "unitless",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The unit of heatExchanger for this configuration (e.g., 'meters', 'seconds', 'unitless')."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"logLevel": {
|
||||||
|
"default": "info",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "debug",
|
||||||
|
"description": "Log messages are printed for debugging purposes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "info",
|
||||||
|
"description": "Informational messages are printed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "warn",
|
||||||
|
"description": "Warning messages are printed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "error",
|
||||||
|
"description": "Error messages are printed."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates whether logging is active. If true, log messages will be generated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"functionality": {
|
||||||
|
"softwareType": {
|
||||||
|
"default": "heatExchanger",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specified software type for this configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "Sensor",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Indicates the role this configuration plays (e.g., sensor, controller, etc.)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"asset": {
|
||||||
|
"uuid": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Asset tag number which is a universally unique identifier for this asset. May be null if not assigned."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tagCode":{
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Asset tag code which is a unique identifier for this asset. May be null if not assigned."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geoLocation": {
|
||||||
|
"default": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "An object representing the asset's physical coordinates or location.",
|
||||||
|
"schema": {
|
||||||
|
"x": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "X coordinate of the asset's location."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Y coordinate of the asset's location."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"z": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Z coordinate of the asset's location."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supplier": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The supplier or manufacturer of the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"default": "heatExchanger",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "heatExchanger",
|
||||||
|
"description": "A device used to transfer heat between two or more fluids."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"subType": {
|
||||||
|
"default": "plate",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A user-defined or manufacturer-defined subtype for the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"U": {
|
||||||
|
"default": 3000,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The overall heat transfer coefficient for the heat exchanger in W/m2K."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"A": {
|
||||||
|
"default": 0.24,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The heat transfer area for the heat exchanger in m2."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maxPower": {
|
||||||
|
"default": 44,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The maximum power that the heat exchanger can handle in kW."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maxFlowRate": {
|
||||||
|
"default": 4,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The maximum flow rate that the heat exchanger can handle in m3/h."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"medium": {
|
||||||
|
"input":{
|
||||||
|
"default": "water",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The fluid that is allowed to flow through the heat exchanger."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"output":{
|
||||||
|
"default": "water",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The fluid that is allowed to flow through the heat exchanger."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowed": {
|
||||||
|
"default": [
|
||||||
|
"water",
|
||||||
|
"glycol",
|
||||||
|
"oil",
|
||||||
|
"steam"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"description": "An array of allowed mediums for the heat exchanger."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flowConfiguration": {
|
||||||
|
"default": "counterflow",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values":[
|
||||||
|
{
|
||||||
|
"value": "counterflow",
|
||||||
|
"description": "The direction of the flow of the two fluids is opposite to each other."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "parallelflow",
|
||||||
|
"description": "The direction of the flow of the two fluids is the same."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "crossflow",
|
||||||
|
"description": "The direction of the flow of the two fluids is perpendicular to each other."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The direction of the flow of the two fluids."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"default": "plate",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values":[
|
||||||
|
{
|
||||||
|
"value": "shellAndTube",
|
||||||
|
"description": "A type of heat exchanger that consists of a shell (a large pressure vessel) with a bundle of tubes inside it."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "plate",
|
||||||
|
"description": "A type of heat exchanger that uses metal plates to transfer heat between two fluids."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "regenerative",
|
||||||
|
"description": "A type of heat exchanger that uses a rotating wheel to transfer heat between two fluids."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The type of heat exchanger."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
328
heatExchanger.html
Normal file
328
heatExchanger.html
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
<script type="module">
|
||||||
|
import * as menuUtils from "/generalfunctions/helper/menuUtils.js";
|
||||||
|
|
||||||
|
RED.nodes.registerType("heatExchanger", {
|
||||||
|
|
||||||
|
category: "digital twin",
|
||||||
|
color: "#e4a363",
|
||||||
|
|
||||||
|
defaults: {
|
||||||
|
|
||||||
|
// Define default properties
|
||||||
|
name: { value: "", required: true },
|
||||||
|
enableLog: { value: false },
|
||||||
|
logLevel: { value: "error" },
|
||||||
|
|
||||||
|
// Define specific properties
|
||||||
|
scaling: { value: false },
|
||||||
|
i_min: { value: 0, required: true },
|
||||||
|
i_max: { value: 0, required: true },
|
||||||
|
i_offset: { value: 0 },
|
||||||
|
o_min: { value: 0, required: true },
|
||||||
|
o_max: { value: 1, required: true },
|
||||||
|
simulator: { value: false },
|
||||||
|
unit: { value: "unit", required: true },
|
||||||
|
smooth_method: { value: "" },
|
||||||
|
count: { value: "10", required: true },
|
||||||
|
|
||||||
|
//define asset properties
|
||||||
|
supplier: { value: "" },
|
||||||
|
subType: { value: "" },
|
||||||
|
model: { value: "" },
|
||||||
|
unit: { value: "" },
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
inputs: 1,
|
||||||
|
outputs: 4,
|
||||||
|
inputLabels: ["heatExchanger Input"],
|
||||||
|
outputLabels: ["process", "dbase", "upstreamParent", "downstreamParent"],
|
||||||
|
icon: "font-awesome/fa-tachometer",
|
||||||
|
|
||||||
|
label: function () {
|
||||||
|
return this.name || "heatExchanger";
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
oneditprepare: function() {
|
||||||
|
|
||||||
|
const node = this;
|
||||||
|
|
||||||
|
// Define UI html elements
|
||||||
|
const elements = {
|
||||||
|
// Basic fields
|
||||||
|
name: document.getElementById("node-input-name"),
|
||||||
|
// specific fields
|
||||||
|
scalingCheckbox: document.getElementById("node-input-scaling"),
|
||||||
|
rowInputMin: document.getElementById("row-input-i_min"),
|
||||||
|
rowInputMax: document.getElementById("row-input-i_max"),
|
||||||
|
smoothMethod: document.getElementById("node-input-smooth_method"),
|
||||||
|
count: document.getElementById("node-input-count"),
|
||||||
|
iOffset: document.getElementById("node-input-i_offset"),
|
||||||
|
oMin: document.getElementById("node-input-o_min"),
|
||||||
|
oMax: document.getElementById("node-input-o_max"),
|
||||||
|
// Logging fields
|
||||||
|
logCheckbox: document.getElementById("node-input-enableLog"),
|
||||||
|
logLevelSelect: document.getElementById("node-input-logLevel"),
|
||||||
|
rowLogLevel: document.getElementById("row-logLevel"),
|
||||||
|
// Asset fields
|
||||||
|
supplier: document.getElementById("node-input-supplier"),
|
||||||
|
subType: document.getElementById("node-input-subType"),
|
||||||
|
model: document.getElementById("node-input-model"),
|
||||||
|
unit: document.getElementById("node-input-unit"),
|
||||||
|
};
|
||||||
|
|
||||||
|
//this needs to live somewhere and for now we add it to every node file for simplicity
|
||||||
|
const projecSettingstURL = "http://localhost:1880/generalfunctions/settings/projectSettings.json";
|
||||||
|
|
||||||
|
try{
|
||||||
|
|
||||||
|
// Fetch project settings
|
||||||
|
menuUtils.fetchProjectData(projecSettingstURL)
|
||||||
|
.then((projectSettings) => {
|
||||||
|
|
||||||
|
//assign to node vars
|
||||||
|
node.configUrls = projectSettings.configUrls;
|
||||||
|
|
||||||
|
const { cloudConfigURL, localConfigURL } = menuUtils.getSpecificConfigUrl("heatExchanger",node.configUrls.cloud.taggcodeAPI);
|
||||||
|
node.configUrls.cloud.config = cloudConfigURL; // first call
|
||||||
|
node.configUrls.local.config = localConfigURL; // backup call
|
||||||
|
|
||||||
|
node.locationId = projectSettings.locationId;
|
||||||
|
node.uuid = projectSettings.uuid;
|
||||||
|
|
||||||
|
// Gets the ID of the active workspace (Flow)
|
||||||
|
const activeFlowId = RED.workspaces.active(); //fetches active flow id
|
||||||
|
node.processId = 1;//activeFlowId;
|
||||||
|
|
||||||
|
|
||||||
|
// UI elements specific for node
|
||||||
|
menuUtils.initMeasurementToggles(elements);
|
||||||
|
menuUtils.populateSmoothingMethods(node.configUrls, elements, node);
|
||||||
|
// UI elements across all nodes
|
||||||
|
menuUtils.fetchAndPopulateDropdowns(node.configUrls, elements, node); // function for all assets
|
||||||
|
menuUtils.initBasicToggles(elements);
|
||||||
|
|
||||||
|
})
|
||||||
|
}catch(e){
|
||||||
|
console.log("Error fetching project settings", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(node.d){
|
||||||
|
//this means node is disabled
|
||||||
|
console.log("Current status of node is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
oneditsave: function () {
|
||||||
|
const node = this;
|
||||||
|
|
||||||
|
console.log(`------------ Saving changes to node ------------`);
|
||||||
|
console.log(`${node.uuid}`);
|
||||||
|
|
||||||
|
// Save basic properties
|
||||||
|
["name", "supplier", "subType", "model", "unit", "smooth_method"].forEach(
|
||||||
|
(field) => (node[field] = document.getElementById(`node-input-${field}`).value || "")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save numeric and boolean properties
|
||||||
|
["scaling", "enableLog", "simulator"].forEach(
|
||||||
|
(field) => (node[field] = document.getElementById(`node-input-${field}`).checked)
|
||||||
|
);
|
||||||
|
|
||||||
|
["i_min", "i_max", "i_offset", "o_min", "o_max", "count"].forEach(
|
||||||
|
(field) => (node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
node.logLevel = document.getElementById("node-input-logLevel").value || "info";
|
||||||
|
|
||||||
|
// Validation checks
|
||||||
|
if (node.scaling && (isNaN(node.i_min) || isNaN(node.i_max))) {
|
||||||
|
RED.notify("Scaling enabled, but input range is incomplete!", "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.unit) {
|
||||||
|
RED.notify("Unit selection is required.", "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.subType && !node.unit) {
|
||||||
|
RED.notify("Unit must be set when specifying a subtype.", "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("stored node modelData", node.modelMetadata);
|
||||||
|
console.log("------------ Changes saved to heatExchanger node preparing to save to API ------------");
|
||||||
|
|
||||||
|
try{
|
||||||
|
// Fetch project settings
|
||||||
|
menuUtils.apiCall(node,node.configUrls)
|
||||||
|
.then((response) => {
|
||||||
|
|
||||||
|
//save response to node information
|
||||||
|
node.assetTagCode = response.asset_tag_number;
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("Error during API call", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(e){
|
||||||
|
console.log("Error saving assetID and tagnumber", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Main UI -->
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="heatExchanger">
|
||||||
|
|
||||||
|
<!-- Node Name -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="node-input-name"
|
||||||
|
placeholder="heatExchanger Name"
|
||||||
|
style="width:70%;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scaling Checkbox -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-scaling"
|
||||||
|
><i class="fa fa-compress"></i> Scaling</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="node-input-scaling"
|
||||||
|
style="width:20px; vertical-align:baseline;"
|
||||||
|
/>
|
||||||
|
<span>Enable input scaling?</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Source Min/Max (only if scaling is true) -->
|
||||||
|
<div class="form-row" id="row-input-i_min">
|
||||||
|
<label for="node-input-i_min"
|
||||||
|
><i class="fa fa-arrow-down"></i> Source Min</label>
|
||||||
|
<input type="number" id="node-input-i_min" placeholder="0" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row" id="row-input-i_max">
|
||||||
|
<label for="node-input-i_max"
|
||||||
|
><i class="fa fa-arrow-up"></i> Source Max</label>
|
||||||
|
<input type="number" id="node-input-i_max" placeholder="3000" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Offset -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-i_offset"
|
||||||
|
><i class="fa fa-adjust"></i> Input Offset</label>
|
||||||
|
<input type="number" id="node-input-i_offset" placeholder="0" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Output / Process Min/Max -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-o_min"><i class="fa fa-tag"></i> Process Min</label>
|
||||||
|
<input type="number" id="node-input-o_min" placeholder="0" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-o_max"><i class="fa fa-tag"></i> Process Max</label>
|
||||||
|
<input type="number" id="node-input-o_max" placeholder="1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Simulator Checkbox -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-simulator"
|
||||||
|
><i class="fa fa-cog"></i> Simulator</label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="node-input-simulator"
|
||||||
|
style="width:20px; vertical-align:baseline;"
|
||||||
|
/>
|
||||||
|
<span>Activate internal simulation?</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Smoothing Method -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-smooth_method"
|
||||||
|
><i class="fa fa-line-chart"></i> Smoothing</label>
|
||||||
|
<select id="node-input-smooth_method" style="width:60%;">
|
||||||
|
<!-- Filled dynamically from heatExchangerConfig.json or fallback -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Smoothing Window -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-count">Window</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="node-input-count"
|
||||||
|
placeholder="10"
|
||||||
|
style="width:60px;"
|
||||||
|
/>
|
||||||
|
<div class="form-tips">Number of samples for smoothing</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Optional Extended Fields: supplier, type, subType, model -->
|
||||||
|
<hr />
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-supplier"
|
||||||
|
><i class="fa fa-industry"></i> Supplier</label>
|
||||||
|
<select id="node-input-supplier" style="width:60%;">
|
||||||
|
<option value="">(optional)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-subType"
|
||||||
|
><i class="fa fa-puzzle-piece"></i> SubType</label>
|
||||||
|
<select id="node-input-subType" style="width:60%;">
|
||||||
|
<option value="">(optional)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-model"><i class="fa fa-wrench"></i> Model</label>
|
||||||
|
<select id="node-input-model" style="width:60%;">
|
||||||
|
<option value="">(optional)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-unit"><i class="fa fa-balance-scale"></i> Unit</label>
|
||||||
|
<select id="node-input-unit" style="width:60%;"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<!-- loglevel checkbox -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-enableLog"
|
||||||
|
><i class="fa fa-cog"></i> Enable Log</label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="node-input-enableLog"
|
||||||
|
style="width:20px; vertical-align:baseline;"
|
||||||
|
/>
|
||||||
|
<span>Enable logging</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row" id="row-logLevel">
|
||||||
|
<label for="node-input-logLevel"><i class="fa fa-cog"></i> Log Level</label>
|
||||||
|
<select id="node-input-logLevel" style="width:60%;">
|
||||||
|
<option value="info">Info</option>
|
||||||
|
<option value="debug">Debug</option>
|
||||||
|
<option value="warn">Warn</option>
|
||||||
|
<option value="error">Error</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="heatExchanger">
|
||||||
|
<p><b>heatExchanger Node</b>: Scales, smooths, and simulates heatExchanger data.</p>
|
||||||
|
<p>Use this node to scale, smooth, and simulate heatExchanger data. The node can be configured to scale input data to a specified range, smooth the data using a variety of methods, and simulate data for testing purposes.</p>
|
||||||
|
|
||||||
|
</script>
|
||||||
147
heatExchanger.js
Normal file
147
heatExchanger.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
module.exports = function (RED) {
|
||||||
|
function heatExchanger(config) {
|
||||||
|
//create node
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
//call this => node so whenver you want to call a node function type node and the function behind it
|
||||||
|
var node = this;
|
||||||
|
|
||||||
|
try{
|
||||||
|
|
||||||
|
//fetch heatExchanger object from source.js
|
||||||
|
const heatExchanger = require("./dependencies/heatExchanger/heatExchanger");
|
||||||
|
const OutputUtils = require("../generalfunctions/helper/outputUtils");
|
||||||
|
|
||||||
|
//load user defined config in the node-red UI
|
||||||
|
const heConfig={
|
||||||
|
general: {
|
||||||
|
name: config.name,
|
||||||
|
id: node.id,
|
||||||
|
unit: config.unit,
|
||||||
|
logging:{
|
||||||
|
logLevel: config.logLevel,
|
||||||
|
enabled: config.enableLog,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
tagCode: config.assetTagCode,
|
||||||
|
supplier: config.supplier,
|
||||||
|
subType: config.subType,
|
||||||
|
model: config.model,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//make new heatExchanger on creation to work with.
|
||||||
|
const he = new heatExchanger(heConfig);
|
||||||
|
|
||||||
|
// put m on node memory as source
|
||||||
|
node.source = he;
|
||||||
|
|
||||||
|
//load output utils
|
||||||
|
const output = new OutputUtils();
|
||||||
|
|
||||||
|
function updateNodeStatus(val) {
|
||||||
|
//display status
|
||||||
|
node.status({ fill: "green", shape: "dot", text: val + " " + heConfig.general.unit });
|
||||||
|
}
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
try {
|
||||||
|
//const status = updateNodeStatus();
|
||||||
|
//node.status(status);
|
||||||
|
|
||||||
|
//get output
|
||||||
|
const classOutput = he.getOutput();
|
||||||
|
const dbOutput = output.formatMsg(classOutput, he.config, "influxdb");
|
||||||
|
const pOutput = output.formatMsg(classOutput, he.config, "process");
|
||||||
|
|
||||||
|
//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.intervalId = setInterval(function(){ tick() },1000)
|
||||||
|
},
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------->>what to do on input
|
||||||
|
node.on("input", function (msg,send,done) {
|
||||||
|
|
||||||
|
// augment this later with child connections
|
||||||
|
if(msg.topic == "TempHotInput")
|
||||||
|
{
|
||||||
|
if(msg.payload.mAbs){
|
||||||
|
he.setHotInput(msg.payload.mAbs);
|
||||||
|
console.log("HotInput: " + msg.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(msg.topic == "TempColdInput")
|
||||||
|
{
|
||||||
|
if(msg.payload.mAbs){
|
||||||
|
he.setColdInput(msg.payload.mAbs);
|
||||||
|
console.log("ColdInput: " + msg.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(msg.topic == "FlowRateHot"){
|
||||||
|
if(msg.payload.downstream_predicted_flow){
|
||||||
|
he.setHotFlowRate(msg.payload.downstream_predicted_flow);
|
||||||
|
console.log("FlowRateHot: " + msg.payload.downstream_predicted_flow);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(msg.topic == "FlowRateCold"){
|
||||||
|
if(msg.payload.downstream_predicted_flow){
|
||||||
|
he.setColdFlowRate(msg.payload.downstream_predicted_flow);
|
||||||
|
console.log("FlowRateCold: " + msg.payload.downstream_predicted_flow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// tidy up any async code here - shutdown connections and so on.
|
||||||
|
node.on('close', function(done) {
|
||||||
|
if (node.intervalId) clearTimeout(node.intervalId);
|
||||||
|
if (node.tickInterval) clearInterval(node.tickInterval);
|
||||||
|
if (done) done();
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(e){
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("heatExchanger", heatExchanger);
|
||||||
|
};
|
||||||
29
package.json
Normal file
29
package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "heatExchanger",
|
||||||
|
"version": "0.9.0",
|
||||||
|
"description": "Control module heatExchanger",
|
||||||
|
"main": "heatExchanger.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "heatExchanger.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.centraal.wbd-rd.nl/RnD/heatExchanger.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"heat exchanger",
|
||||||
|
"node-red"
|
||||||
|
],
|
||||||
|
"author": "Rene De Ren",
|
||||||
|
"license": "SEE LICENSE",
|
||||||
|
"dependencies": {
|
||||||
|
"generalfunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalfunctions.git",
|
||||||
|
"convert": "git+https://gitea.centraal.wbd-rd.nl/RnD/convert.git",
|
||||||
|
"predict": "git+https://gitea.centraal.wbd-rd.nl/RnD/predict.git"
|
||||||
|
},
|
||||||
|
"node-red": {
|
||||||
|
"nodes": {
|
||||||
|
"heatExchanger": "heatExchanger.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user