Compare commits
81 Commits
5ca7889af1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 858189d6da | |||
| ec42ebcb25 | |||
| f4629e5fcc | |||
| dafe4c5336 | |||
| 5439d5111a | |||
| 1e5ef47a4d | |||
| 2b87c67876 | |||
| 0db90c0e4b | |||
| 1e07093101 | |||
| ce25ee930a | |||
| a293e0286a | |||
| 012b8a7ff6 | |||
|
|
9610e7138d | ||
| d5d078413c | |||
| 17662ef7cb | |||
| 9d8da15d0e | |||
| d503cf5dc9 | |||
|
|
48a227d519 | ||
| f653a1e98c | |||
|
|
1725c5b0e9 | ||
|
|
d7cb8e1072 | ||
| 9b7a8ae2c8 | |||
|
|
dc50432ee8 | ||
|
|
c99d24e4c6 | ||
|
|
f9d1348fd0 | ||
|
|
428c611ec6 | ||
| 2fb73e6713 | |||
|
|
cffbd51d92 | ||
|
|
de0b947c56 | ||
|
|
d99561fa80 | ||
|
|
44033da15d | ||
| 3886277616 | |||
| 83018fabe0 | |||
| e72579e5d0 | |||
| 0fb42865ff | |||
| b2b811e802 | |||
| bde2dcf7d8 | |||
| 76570280bc | |||
| d7017b5d33 | |||
| f93603c182 | |||
| c261335df5 | |||
| a41f053d5d | |||
| 8d7d98f126 | |||
| 3f90685834 | |||
| efc97d6cd1 | |||
|
|
d72bfd5560 | ||
| 6d30e25daa | |||
| 16e202e841 | |||
|
|
241ed1d3cb | ||
| 3876f86530 | |||
| 56be0f1840 | |||
|
|
a30f2c90f4 | ||
| 302e122387 | |||
|
|
50f99fa642 | ||
| 494a688583 | |||
| c512c96636 | |||
|
|
eb15da2a63 | ||
| 6dcd3c3d26 | |||
| 958ec2269c | |||
|
|
83ca429bf5 | ||
|
|
222d0f56fc | ||
|
|
e1c6124cf0 | ||
|
|
e87f9da4bf | ||
| 0bccad05f8 | |||
| 7191e57aea | |||
| aec2d3692d | |||
|
|
7061d6a539 | ||
| 71643375fc | |||
|
|
2540d19b76 | ||
| f13ee68938 | |||
| 475caa90db | |||
| 9aa38f9000 | |||
| 4a6273b037 | |||
| 8c9301b128 | |||
| 30908365ba | |||
| 7cdfc87c83 | |||
| 839ae2f3da | |||
| 950ca2b6b4 | |||
| 0a9d4b1dda | |||
|
|
4665949c88 | ||
|
|
a2018509ef |
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.
|
||||||
@@ -24,11 +24,24 @@
|
|||||||
"models": [
|
"models": [
|
||||||
{
|
{
|
||||||
"name": "VegaPressure 10",
|
"name": "VegaPressure 10",
|
||||||
"units": ["bar", "m WC", "psi"]
|
"units": ["bar", "mbar", "psi"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "VegaPressure 20",
|
"name": "VegaPressure 20",
|
||||||
"units": ["bar", "m WC", "psi"]
|
"units": ["bar", "mbar", "psi"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Flow",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "VegaFlow 10",
|
||||||
|
"units": ["m³/h", "gpm", "l/min"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VegaFlow 20",
|
||||||
|
"units": ["m³/h", "gpm", "l/min"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -44,6 +57,42 @@
|
|||||||
"units": ["m", "ft", "mm"]
|
"units": ["m", "ft", "mm"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Quantity (oxygen)",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "VegaOxySense 10",
|
||||||
|
"units": ["g/m³", "mol/m³"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Quantity (Ammonium)",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "VegaAmmoniaSense 10",
|
||||||
|
"units": ["g/m³", "mol/m³"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Quantity (NOx)",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "VegaNOxSense 10",
|
||||||
|
"units": ["g/m³", "mol/m³"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Quantity (TSS)",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "VegaSolidsProbe",
|
||||||
|
"units": ["g/m³"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -60,10 +109,43 @@
|
|||||||
"models": [
|
"models": [
|
||||||
{
|
{
|
||||||
"id": "hidrostal-pump-001",
|
"id": "hidrostal-pump-001",
|
||||||
"name": "H05K-S03R+HGM1X-X280KO",
|
"name": "hidrostal-H05K-S03R",
|
||||||
"units": ["m³/h", "gpm", "l/min"],
|
"units": ["l/s"]
|
||||||
"hasCurveData": true,
|
},
|
||||||
"curveDataFile": "/data/curves/hidrostal-H05K-S03R.json"
|
{
|
||||||
|
"id": "hidrostal-pump-002",
|
||||||
|
"name": "hidrostal-C5-D03R-SHN1",
|
||||||
|
"units": ["l/s"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Binder Engineering",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Valves",
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"name": "Gate",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "binder-valve-001",
|
||||||
|
"name": "ECDV",
|
||||||
|
"units": ["m³/h", "gpm", "l/min"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jet",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "binder-valve-002",
|
||||||
|
"name": "JCV",
|
||||||
|
"units": ["m³/h", "gpm", "l/min"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
16
datasets/assetData/curves/ECDV.json
Normal file
16
datasets/assetData/curves/ECDV.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"1.204": {
|
||||||
|
"125": {
|
||||||
|
"x": [0,10,20,30,40,50,60,70,80,90,100],
|
||||||
|
"y": [0,18,50,95,150,216,337,564,882,1398,1870]
|
||||||
|
},
|
||||||
|
"150": {
|
||||||
|
"x": [0,10,20,30,40,50,60,70,80,90,100],
|
||||||
|
"y": [0,25,73,138,217,314,490,818,1281,2029,2715]
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"x": [0,10,20,30,40,50,60,70,80,90,100],
|
||||||
|
"y": [0,155,443,839,1322,1911,2982,4980,7795,12349,16524]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
838
datasets/assetData/curves/hidrostal-C5-D03R-SHN1.json
Normal file
838
datasets/assetData/curves/hidrostal-C5-D03R-SHN1.json
Normal file
@@ -0,0 +1,838 @@
|
|||||||
|
{
|
||||||
|
"np": {
|
||||||
|
"400": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5953611390998625,
|
||||||
|
1.6935085477165994,
|
||||||
|
3.801139124304824,
|
||||||
|
7.367829525776738,
|
||||||
|
12.081735423116616
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.8497068236812997,
|
||||||
|
3.801139124304824,
|
||||||
|
7.367829525776738,
|
||||||
|
12.081735423116616
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"600": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.7497197821018213,
|
||||||
|
3.801139124304824,
|
||||||
|
7.367829525776738,
|
||||||
|
12.081735423116616
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"700": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.788320579602724,
|
||||||
|
3.9982668237045984,
|
||||||
|
7.367829525776738,
|
||||||
|
12.081735423116616
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"800": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.7824519364844427,
|
||||||
|
3.9885060367793064,
|
||||||
|
7.367829525776738,
|
||||||
|
12.081735423116616
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"900": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6934482683506376,
|
||||||
|
3.9879559558537054,
|
||||||
|
7.367829525776738,
|
||||||
|
12.081735423116616
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1000": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6954385513069579,
|
||||||
|
4.0743508382926795,
|
||||||
|
7.422392692482345,
|
||||||
|
12.081735423116616
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1100": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
4.160745720731654,
|
||||||
|
7.596626714476177,
|
||||||
|
12.081735423116616
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1200": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
4.302551231007837,
|
||||||
|
7.637247864947884,
|
||||||
|
12.081735423116616
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1300": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
4.37557913990704,
|
||||||
|
7.773442147000839,
|
||||||
|
12.081735423116616
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1400": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
4.334434337766139,
|
||||||
|
7.940911352646818,
|
||||||
|
12.081735423116616
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1500": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
4.2327206586037995,
|
||||||
|
8.005238800611183,
|
||||||
|
12.254836577088351
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1600": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
4.195405588464695,
|
||||||
|
7.991827302945298,
|
||||||
|
12.423663269044452
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1700": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
14.255458319309813,
|
||||||
|
8.096768422220196,
|
||||||
|
12.584668380908582
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1800": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
3.8742719210788685,
|
||||||
|
31.54620347513727,
|
||||||
|
12.637080520201405
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1900": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
3.8742719210788685,
|
||||||
|
8.148423429611098,
|
||||||
|
12.74916725120127
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2000": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
3.8742719210788685,
|
||||||
|
8.146439484120116,
|
||||||
|
12.905178964345618
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2100": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
3.8742719210788685,
|
||||||
|
8.149576025637684,
|
||||||
|
13.006940917309247
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2200": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
3.8742719210788685,
|
||||||
|
8.126246430368305,
|
||||||
|
13.107503837410825
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2300": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
3.8742719210788685,
|
||||||
|
8.104379361635342,
|
||||||
|
13.223235973280122
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2400": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
3.8742719210788685,
|
||||||
|
8.135190080423746,
|
||||||
|
13.36128347785936
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2500": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
3.8742719210788685,
|
||||||
|
7.981219508598527,
|
||||||
|
13.473697427231842
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2600": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
3.8742719210788685,
|
||||||
|
7.863899404441271,
|
||||||
|
13.50303289156837
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2700": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
3.8742719210788685,
|
||||||
|
7.658860522528131,
|
||||||
|
13.485230880073107
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2800": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
3.8742719210788685,
|
||||||
|
7.44407948309266,
|
||||||
|
13.446135725634615
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2900": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
0.5522732775894703,
|
||||||
|
1.6920721090317592,
|
||||||
|
3.8742719210788685,
|
||||||
|
7.44407948309266,
|
||||||
|
13.413693596332184
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nq": {
|
||||||
|
"400": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
7.6803204433986965,
|
||||||
|
25.506609120436963,
|
||||||
|
35.4,
|
||||||
|
44.4,
|
||||||
|
52.5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
22.622804921188227,
|
||||||
|
35.4,
|
||||||
|
44.4,
|
||||||
|
52.5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"600": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
19.966301579194372,
|
||||||
|
35.4,
|
||||||
|
44.4,
|
||||||
|
52.5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"700": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
17.430763940163832,
|
||||||
|
33.79508340848005,
|
||||||
|
44.4,
|
||||||
|
52.5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"800": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
14.752921911234477,
|
||||||
|
31.71885034449889,
|
||||||
|
44.4,
|
||||||
|
52.5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"900": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
11.854693031181021,
|
||||||
|
29.923046639543475,
|
||||||
|
44.4,
|
||||||
|
52.5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1000": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.549433913822687,
|
||||||
|
26.734189128096668,
|
||||||
|
43.96760750800311,
|
||||||
|
52.5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1100": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
26.26933164936586,
|
||||||
|
42.23523193272671,
|
||||||
|
52.5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1200": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
24.443114637042832,
|
||||||
|
40.57167959798151,
|
||||||
|
52.5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1300": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
22.41596168949836,
|
||||||
|
39.04561852479495,
|
||||||
|
52.5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1400": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
20.276864821170303,
|
||||||
|
37.557663261443224,
|
||||||
|
52.252852231224054
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1500": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
18.252772588147742,
|
||||||
|
35.9974418607538,
|
||||||
|
50.68604059588987
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1600": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
16.31441663648616,
|
||||||
|
34.51170378091407,
|
||||||
|
49.20153034100798
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1700": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
14.255458319309813,
|
||||||
|
33.043410795291045,
|
||||||
|
47.820213744181245
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1800": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
12.7,
|
||||||
|
31.54620347513727,
|
||||||
|
46.51705619739449
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1900": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
12.7,
|
||||||
|
29.986013742375484,
|
||||||
|
45.29506741639918
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2000": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
12.7,
|
||||||
|
28.432646044605782,
|
||||||
|
44.107822395271945
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2100": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
12.7,
|
||||||
|
26.892634464336055,
|
||||||
|
42.758175515158776
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2200": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
12.7,
|
||||||
|
25.270679127870263,
|
||||||
|
41.467063889795895
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2300": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
12.7,
|
||||||
|
23.531132157718837,
|
||||||
|
40.293041104955826
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2400": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
12.7,
|
||||||
|
21.815645106750623,
|
||||||
|
39.03109248860755
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2500": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
12.7,
|
||||||
|
20.34997949463564,
|
||||||
|
37.71320701654063
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2600": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
12.7,
|
||||||
|
18.81710568651804,
|
||||||
|
36.35563657017404
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2700": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
12.7,
|
||||||
|
17.259072160217805,
|
||||||
|
35.02979557646653
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2800": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
12.7,
|
||||||
|
16,
|
||||||
|
33.74372254979665
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2900": {
|
||||||
|
"x": [
|
||||||
|
0,
|
||||||
|
25.510204081632654,
|
||||||
|
51.020408163265309,
|
||||||
|
76.530612244897952,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
6.4,
|
||||||
|
9.500000000000002,
|
||||||
|
12.7,
|
||||||
|
16,
|
||||||
|
32.54934541379723
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"curve": {
|
|
||||||
"np": {
|
"np": {
|
||||||
"700": {
|
"700": {
|
||||||
"x": [
|
"x": [
|
||||||
@@ -139,7 +138,7 @@
|
|||||||
],
|
],
|
||||||
"y": [
|
"y": [
|
||||||
11.498673648884504,
|
11.498673648884504,
|
||||||
20.996631954252725,
|
20.996631954252724,
|
||||||
31.954252725886462,
|
31.954252725886462,
|
||||||
45.54353714625641,
|
45.54353714625641,
|
||||||
63.22528016894755
|
63.22528016894755
|
||||||
@@ -1060,5 +1059,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
1
datasets/get_all_assets.php
Normal file
1
datasets/get_all_assets.php
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Database connection failed: SQLSTATE[28000] [1045] Access denied for user 'pimmoe1q_rdlab'@'localhost' (using password: YES)
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,207 +0,0 @@
|
|||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "Product modellen succesvol opgehaald.",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "Macbook Air 12",
|
|
||||||
"product_model_subtype_id": "1",
|
|
||||||
"product_model_description": null,
|
|
||||||
"vendor_id": "1",
|
|
||||||
"product_model_status": null,
|
|
||||||
"vendor_name": "Apple",
|
|
||||||
"product_subtype_name": "Laptop",
|
|
||||||
"product_model_meta": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"name": "Macbook Air 13",
|
|
||||||
"product_model_subtype_id": "1",
|
|
||||||
"product_model_description": null,
|
|
||||||
"vendor_id": "1",
|
|
||||||
"product_model_status": null,
|
|
||||||
"vendor_name": "Apple",
|
|
||||||
"product_subtype_name": "Laptop",
|
|
||||||
"product_model_meta": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3",
|
|
||||||
"name": "AirMac 1 128 GB White",
|
|
||||||
"product_model_subtype_id": "2",
|
|
||||||
"product_model_description": null,
|
|
||||||
"vendor_id": "1",
|
|
||||||
"product_model_status": null,
|
|
||||||
"vendor_name": "Apple",
|
|
||||||
"product_subtype_name": "Desktop",
|
|
||||||
"product_model_meta": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4",
|
|
||||||
"name": "AirMac 2 256 GB Black",
|
|
||||||
"product_model_subtype_id": "2",
|
|
||||||
"product_model_description": null,
|
|
||||||
"vendor_id": "1",
|
|
||||||
"product_model_status": null,
|
|
||||||
"vendor_name": "Apple",
|
|
||||||
"product_subtype_name": "Desktop",
|
|
||||||
"product_model_meta": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "5",
|
|
||||||
"name": "AirMac 2 256 GB White",
|
|
||||||
"product_model_subtype_id": "2",
|
|
||||||
"product_model_description": null,
|
|
||||||
"vendor_id": "1",
|
|
||||||
"product_model_status": null,
|
|
||||||
"vendor_name": "Apple",
|
|
||||||
"product_subtype_name": "Desktop",
|
|
||||||
"product_model_meta": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "6",
|
|
||||||
"name": "Vegabar 14",
|
|
||||||
"product_model_subtype_id": "3",
|
|
||||||
"product_model_description": "vegabar 14",
|
|
||||||
"vendor_id": "4",
|
|
||||||
"product_model_status": "Actief",
|
|
||||||
"vendor_name": "vega",
|
|
||||||
"product_subtype_name": "pressure",
|
|
||||||
"product_model_meta": {
|
|
||||||
"machineCurve": {
|
|
||||||
"np": {
|
|
||||||
"700": {
|
|
||||||
"x": [
|
|
||||||
0,
|
|
||||||
24.59,
|
|
||||||
49.18,
|
|
||||||
73.77,
|
|
||||||
100
|
|
||||||
],
|
|
||||||
"y": [
|
|
||||||
12.962460720759278,
|
|
||||||
20.65443723573673,
|
|
||||||
31.029351002816465,
|
|
||||||
44.58926412111886,
|
|
||||||
62.87460150792057
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"800": {
|
|
||||||
"x": [
|
|
||||||
0,
|
|
||||||
24.59,
|
|
||||||
49.18,
|
|
||||||
73.77,
|
|
||||||
100
|
|
||||||
],
|
|
||||||
"y": [
|
|
||||||
13.035157335397209,
|
|
||||||
20.74906989186132,
|
|
||||||
31.029351002816465,
|
|
||||||
44.58926412111886,
|
|
||||||
62.87460150792057
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"900": {
|
|
||||||
"x": [
|
|
||||||
0,
|
|
||||||
24.59,
|
|
||||||
49.18,
|
|
||||||
73.77,
|
|
||||||
100
|
|
||||||
],
|
|
||||||
"y": [
|
|
||||||
13.064663380158798,
|
|
||||||
20.927197054134297,
|
|
||||||
31.107126521989933,
|
|
||||||
44.58926412111886,
|
|
||||||
62.87460150792057
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1000": {
|
|
||||||
"x": [
|
|
||||||
0,
|
|
||||||
24.59,
|
|
||||||
49.18,
|
|
||||||
73.77,
|
|
||||||
100
|
|
||||||
],
|
|
||||||
"y": [
|
|
||||||
13.039271391128953,
|
|
||||||
21.08680188366637,
|
|
||||||
31.30899920405947,
|
|
||||||
44.58926412111886,
|
|
||||||
62.87460150792057
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1100": {
|
|
||||||
"x": [
|
|
||||||
0,
|
|
||||||
24.59,
|
|
||||||
49.18,
|
|
||||||
73.77,
|
|
||||||
100
|
|
||||||
],
|
|
||||||
"y": [
|
|
||||||
12.940075520572446,
|
|
||||||
21.220547481589954,
|
|
||||||
31.51468295656385,
|
|
||||||
44.621326083982,
|
|
||||||
62.87460150792057
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nq": {
|
|
||||||
"700": {
|
|
||||||
"x": [
|
|
||||||
0,
|
|
||||||
24.59,
|
|
||||||
49.18,
|
|
||||||
73.77,
|
|
||||||
100
|
|
||||||
],
|
|
||||||
"y": [
|
|
||||||
119.13938764447377,
|
|
||||||
150.12178608265387,
|
|
||||||
178.82698019104356,
|
|
||||||
202.3699313222398,
|
|
||||||
227.06382297856618
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"800": {
|
|
||||||
"x": [
|
|
||||||
0,
|
|
||||||
24.59,
|
|
||||||
49.18,
|
|
||||||
73.77,
|
|
||||||
100
|
|
||||||
],
|
|
||||||
"y": [
|
|
||||||
112.59072109293984,
|
|
||||||
148.15847460389205,
|
|
||||||
178.82698019104356,
|
|
||||||
202.3699313222398,
|
|
||||||
227.06382297856618
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"900": {
|
|
||||||
"x": [
|
|
||||||
0,
|
|
||||||
24.59,
|
|
||||||
49.18,
|
|
||||||
73.77,
|
|
||||||
100
|
|
||||||
],
|
|
||||||
"y": [
|
|
||||||
105.6217241180404,
|
|
||||||
144.00502117747064,
|
|
||||||
177.15212647335034,
|
|
||||||
202.3699313222398,
|
|
||||||
227.06382297856618
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
11
index.js
11
index.js
@@ -12,18 +12,20 @@ const outputUtils = require('./src/helper/outputUtils.js');
|
|||||||
const logger = require('./src/helper/logger.js');
|
const logger = require('./src/helper/logger.js');
|
||||||
const validation = require('./src/helper/validationUtils.js');
|
const validation = require('./src/helper/validationUtils.js');
|
||||||
const configUtils = require('./src/helper/configUtils.js');
|
const configUtils = require('./src/helper/configUtils.js');
|
||||||
|
const assertions = require('./src/helper/assertionUtils.js')
|
||||||
|
const coolprop = require('./src/coolprop-node/src/index.js');
|
||||||
|
|
||||||
// Domain-specific modules
|
// Domain-specific modules
|
||||||
const { MeasurementContainer } = require('./src/measurements/index.js');
|
const { MeasurementContainer } = require('./src/measurements/index.js');
|
||||||
const configManager = require('./src/configs/index.js');
|
const configManager = require('./src/configs/index.js');
|
||||||
const nrmse = require('./src/nrmse/ErrorMetrics.js');
|
const nrmse = require('./src/nrmse/errorMetrics.js');
|
||||||
const state = require('./src/state/state.js');
|
const state = require('./src/state/state.js');
|
||||||
const convert = require('./src/convert/index.js');
|
const convert = require('./src/convert/index.js');
|
||||||
const MenuManager = require('./src/menu/index.js');
|
const MenuManager = require('./src/menu/index.js');
|
||||||
const predict = require('./src/predict/predict_class.js');
|
const predict = require('./src/predict/predict_class.js');
|
||||||
const interpolation = require('./src/predict/interpolation.js');
|
const interpolation = require('./src/predict/interpolation.js');
|
||||||
const childRegistrationUtils = require('./src/helper/childRegistrationUtils.js');
|
const childRegistrationUtils = require('./src/helper/childRegistrationUtils.js');
|
||||||
|
const { loadCurve } = require('./datasets/assetData/curves/index.js');
|
||||||
|
|
||||||
// Export everything
|
// Export everything
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -34,10 +36,13 @@ module.exports = {
|
|||||||
configUtils,
|
configUtils,
|
||||||
logger,
|
logger,
|
||||||
validation,
|
validation,
|
||||||
|
assertions,
|
||||||
MeasurementContainer,
|
MeasurementContainer,
|
||||||
nrmse,
|
nrmse,
|
||||||
state,
|
state,
|
||||||
|
coolprop,
|
||||||
convert,
|
convert,
|
||||||
MenuManager,
|
MenuManager,
|
||||||
childRegistrationUtils
|
childRegistrationUtils,
|
||||||
|
loadCurve
|
||||||
};
|
};
|
||||||
216
src/configs/machineGroupControl.json
Normal file
216
src/configs/machineGroupControl.json
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "Machine Group Configuration",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A human-readable name or label for this machine group configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A unique identifier for this configuration. If not provided, defaults to null."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "m3/h",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The default measurement unit 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": "machineGroup",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Logical name identifying the software type."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "GroupController",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Controls a group of machines within the system."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"positionVsParent":{
|
||||||
|
"default":"atEquipment",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "atEquipment",
|
||||||
|
"description": "The node is connected at the equipment level and is responsible for controlling or monitoring the equipment as a whole."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "upstream",
|
||||||
|
"description": "The node is connected in a downstream position, indicating it is responsible for monitoring or controlling processes that occur after the equipment's operation, such as product flow or output."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "downstream",
|
||||||
|
"description": "The node is connected in an upstream position, indicating it is responsible for monitoring or controlling processes that occur before the equipment's operation, such as input flow or supply."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Defines the position of the measurement relative to its parent equipment or system."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"current": {
|
||||||
|
"default": "optimalControl",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "optimalControl",
|
||||||
|
"description": "The group controller selects the most optimal combination of machines based on their real-time performance curves."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "priorityControl",
|
||||||
|
"description": "Machines are controlled sequentially from minimum to maximum output until each is maxed out, then additional machines are added."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "prioritypercentagecontrol",
|
||||||
|
"description": "Machines are controlled sequentially from minimum to maximum output until each is maxed out, then additional machines are added based on a percentage of the total demand."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "maintenance",
|
||||||
|
"description": "The group is in maintenance mode with limited actions (monitoring only)."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The operational mode of the machine group controller."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedActions": {
|
||||||
|
"default": {},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"optimalControl": {
|
||||||
|
"default": ["statusCheck", "execOptimalCombination", "balanceLoad", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in optimalControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"priorityControl": {
|
||||||
|
"default": ["statusCheck", "execSequentialControl", "balanceLoad", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in priorityControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prioritypercentagecontrol": {
|
||||||
|
"default": ["statusCheck", "execSequentialControl", "balanceLoad", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in manualOverride mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maintenance": {
|
||||||
|
"default": ["statusCheck"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in maintenance mode."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Defines the actions available for each operational mode of the machine group controller."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedSources": {
|
||||||
|
"default": {},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"optimalcontrol": {
|
||||||
|
"default": ["parent", "GUI", "physical", "API"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Command sources allowed in optimalControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prioritycontrol": {
|
||||||
|
"default": ["parent", "GUI", "physical", "API"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Command sources allowed in priorityControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prioritypercentagecontrol": {
|
||||||
|
"default": ["parent", "GUI", "physical", "API"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Command sources allowed "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Specifies the valid command sources recognized by the machine group controller for each mode."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scaling": {
|
||||||
|
"current": {
|
||||||
|
"default": "normalized",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "normalized",
|
||||||
|
"description": "Scales the demand between 0–100% of the total flow capacity, interpolating to calculate the effective demand."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "absolute",
|
||||||
|
"description": "Uses the absolute demand value directly, capped between the min and max machine flow capacities."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The scaling mode for demand calculations."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"general": {
|
"general": {
|
||||||
"name": {
|
"name": {
|
||||||
"default": "Measurement Configuration",
|
"default": "Sensor",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A human-readable name or label for this measurement configuration."
|
"description": "A human-readable name or label for this measurement configuration."
|
||||||
@@ -91,6 +91,13 @@
|
|||||||
],
|
],
|
||||||
"description": "Defines the position of the measurement relative to its parent equipment or system."
|
"description": "Defines the position of the measurement relative to its parent equipment or system."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"distance":{
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Defines the position of the measurement relative to its parent equipment or system."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"asset": {
|
"asset": {
|
||||||
|
|||||||
694
src/configs/pumpingStation.json
Normal file
694
src/configs/pumpingStation.json
Normal file
@@ -0,0 +1,694 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "Pumping Station",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A human-readable name or label for this pumping station configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A unique identifier for this pumping station configuration. If not provided, defaults to null."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "m3/h",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The default flow unit used for reporting station throughput."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"logLevel": {
|
||||||
|
"default": "info",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "debug",
|
||||||
|
"description": "Log verbose diagnostic messages that aid in troubleshooting the station."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "info",
|
||||||
|
"description": "Log general informational messages about station behavior."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "warn",
|
||||||
|
"description": "Log warnings when station behavior deviates from expected ranges."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "error",
|
||||||
|
"description": "Log only error level messages for critical failures."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Defines the minimum severity that will be written to the log."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If true, logging is active for the pumping station node."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"functionality": {
|
||||||
|
"softwareType": {
|
||||||
|
"default": "pumpingStation",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specified software type used to locate the proper default configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "StationController",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Describes the station's function within the EVOLV ecosystem."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"positionVsParent": {
|
||||||
|
"default": "atEquipment",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"description": "Defines how the station is positioned relative to its parent process or site.",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "atEquipment",
|
||||||
|
"description": "The station is controlled at the equipment level and represents the primary pumping asset."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "upstream",
|
||||||
|
"description": "The station governs flows entering upstream of the parent asset."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "downstream",
|
||||||
|
"description": "The station influences conditions downstream of the parent asset, such as discharge or transfer."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tickIntervalMs": {
|
||||||
|
"default": 1000,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 100,
|
||||||
|
"description": "Interval in milliseconds between internal evaluation cycles and output refreshes."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supportsSimulation": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates whether the station can operate using simulated inflow and level data."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supportedChildSoftwareTypes": {
|
||||||
|
"default": [
|
||||||
|
"measurement"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "List of child node software types that may register with the station."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"asset": {
|
||||||
|
"uuid": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Asset tag number which is a universally unique identifier for this pumping station."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tagCode": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Asset tag code which uniquely identifies the pumping station. May be null if not assigned."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"default": "station",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "station",
|
||||||
|
"description": "Represents a dedicated pumping station asset."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "High level classification for asset reporting."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"default": "pumpingStation",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specific asset type used to identify this configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Manufacturer or integrator model designation for the station."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supplier": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Primary supplier or maintainer responsible for the station."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geoLocation": {
|
||||||
|
"default": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Coordinate reference for locating the pumping station.",
|
||||||
|
"schema": {
|
||||||
|
"x": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "X coordinate in meters or site units."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Y coordinate in meters or site units."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"z": {
|
||||||
|
"default": 0,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Z coordinate in meters or site units."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"basin": {
|
||||||
|
"volume": {
|
||||||
|
"default": "1",
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Total volume of empty basin in m3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"default": "1",
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Total height of basin in m"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"levelUnit": {
|
||||||
|
"default": "m",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unit used for level related setpoints and thresholds."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"heightInlet": {
|
||||||
|
"default": 2,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Height of the inlet pipe measured from the basin floor (m)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"heightOutlet": {
|
||||||
|
"default": 0.2,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Height of the outlet pipe measured from the basin floor (m)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"heightOverflow": {
|
||||||
|
"default": 2.5,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Height of the overflow point measured from the basin floor (m)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inletPipeDiameter": {
|
||||||
|
"default": 0.4,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Nominal inlet pipe diameter (m)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outletPipeDiameter": {
|
||||||
|
"default": 0.4,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Nominal outlet pipe diameter (m)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hydraulics": {
|
||||||
|
"maxInflowRate": {
|
||||||
|
"default": 200,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Maximum expected inflow during peak events (m3/h)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"refHeight": {
|
||||||
|
"default": "NAP",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "NAP",
|
||||||
|
"description": "NAP (Normaal Amsterdams Peil)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "EVRF",
|
||||||
|
"description": "EVRF (European Vertical Reference Frame)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "EGM2008",
|
||||||
|
"description": "EGM2008 / EGM96 (satellietmetingen) Geopotentieel model earth "
|
||||||
|
}
|
||||||
|
|
||||||
|
],
|
||||||
|
"description": "Reference height to use to identify the height vs other basins with. This will say something more about the expected pressure loss in m head"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"staticHead": {
|
||||||
|
"default": 12,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Static head between station suction and discharge point (m)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maxDischargeHead": {
|
||||||
|
"default": 24,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Maximum allowable discharge head before calling for alarms (m)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pipelineLength": {
|
||||||
|
"default": 80,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Length of the discharge pipeline considered in calculations (m)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultFluid": {
|
||||||
|
"default": "wastewater",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "wastewater",
|
||||||
|
"description": "The wet well is primarily cylindrical."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "water",
|
||||||
|
"description": "The wet well is rectangular or box shaped."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"temperatureReferenceDegC": {
|
||||||
|
"default": 15,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Reference fluid temperature for property lookups (degC)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"controlStrategy": {
|
||||||
|
"default": "levelBased",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "levelBased",
|
||||||
|
"description": "Lead and lag pumps are controlled by basin level thresholds."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "pressureBased",
|
||||||
|
"description": "Pumps target a discharge pressure setpoint."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "flowTracking",
|
||||||
|
"description": "Pumps modulate to match measured inflow or downstream demand."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "manual",
|
||||||
|
"description": "Pumps are operated manually or by an external controller."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Primary control philosophy for pump actuation."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"levelSetpoints": {
|
||||||
|
"default": {
|
||||||
|
"startLeadPump": 1.2,
|
||||||
|
"stopLeadPump": 0.8,
|
||||||
|
"startLagPump": 1.8,
|
||||||
|
"stopLagPump": 1.4,
|
||||||
|
"alarmHigh": 2.3,
|
||||||
|
"alarmLow": 0.3
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Level thresholds that govern pump staging and alarms (m).",
|
||||||
|
"schema": {
|
||||||
|
"startLeadPump": {
|
||||||
|
"default": 1.2,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Level that starts the lead pump."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stopLeadPump": {
|
||||||
|
"default": 0.8,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Level that stops the lead pump."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"startLagPump": {
|
||||||
|
"default": 1.8,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Level that starts the lag pump."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stopLagPump": {
|
||||||
|
"default": 1.4,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Level that stops the lag pump."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"alarmHigh": {
|
||||||
|
"default": 2.3,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "High level alarm threshold."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"alarmLow": {
|
||||||
|
"default": 0.3,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Low level alarm threshold."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pressureSetpoint": {
|
||||||
|
"default": 250,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Target discharge pressure when operating in pressure control (kPa)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"alarmDebounceSeconds": {
|
||||||
|
"default": 10,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Time a condition must persist before raising an alarm (seconds)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"equalizationTargetPercent": {
|
||||||
|
"default": 60,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"description": "Target fill percentage of the basin when operating in equalization mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoRestartAfterPowerLoss": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If true, pumps resume based on last known state after power restoration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manualOverrideTimeoutMinutes": {
|
||||||
|
"default": 30,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Duration after which a manual override expires automatically (minutes)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flowBalanceTolerance": {
|
||||||
|
"default": 5,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Allowable error between inflow and outflow before adjustments are triggered (m3/h)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"alarms": {
|
||||||
|
"default": {
|
||||||
|
"highLevel": {
|
||||||
|
"enabled": true,
|
||||||
|
"threshold": 2.3,
|
||||||
|
"delaySeconds": 30,
|
||||||
|
"severity": "critical",
|
||||||
|
"acknowledgmentRequired": true
|
||||||
|
},
|
||||||
|
"lowLevel": {
|
||||||
|
"enabled": true,
|
||||||
|
"threshold": 0.2,
|
||||||
|
"delaySeconds": 15,
|
||||||
|
"severity": "warning",
|
||||||
|
"acknowledgmentRequired": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Alarm configuration for the pumping station.",
|
||||||
|
"schema": {
|
||||||
|
"highLevel": {
|
||||||
|
"default": {
|
||||||
|
"enabled": true,
|
||||||
|
"threshold": 2.3,
|
||||||
|
"delaySeconds": 30,
|
||||||
|
"severity": "critical",
|
||||||
|
"acknowledgmentRequired": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"enabled": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable or disable the high level alarm."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"default": 2.3,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Level threshold that triggers the high level alarm (m)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delaySeconds": {
|
||||||
|
"default": 30,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Delay before issuing the high level alarm (seconds)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"severity": {
|
||||||
|
"default": "critical",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "info",
|
||||||
|
"description": "Informational notification."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "warning",
|
||||||
|
"description": "Warning condition requiring attention."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "critical",
|
||||||
|
"description": "Critical alarm requiring immediate intervention."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Severity associated with the high level alarm."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"acknowledgmentRequired": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If true, this alarm must be acknowledged by an operator."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lowLevel": {
|
||||||
|
"default": {
|
||||||
|
"enabled": true,
|
||||||
|
"threshold": 0.2,
|
||||||
|
"delaySeconds": 15,
|
||||||
|
"severity": "warning",
|
||||||
|
"acknowledgmentRequired": false
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"enabled": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable or disable the low level alarm."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"default": 0.2,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Level threshold that triggers the low level alarm (m)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delaySeconds": {
|
||||||
|
"default": 15,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"min": 0,
|
||||||
|
"description": "Delay before issuing the low level alarm (seconds)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"severity": {
|
||||||
|
"default": "warning",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "info",
|
||||||
|
"description": "Informational notification."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "warning",
|
||||||
|
"description": "Warning condition requiring attention."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "critical",
|
||||||
|
"description": "Critical alarm requiring immediate intervention."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Severity associated with the low level alarm."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"acknowledgmentRequired": {
|
||||||
|
"default": false,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If true, this alarm must be acknowledged by an operator."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"simulation": {
|
||||||
|
"enabled": {
|
||||||
|
"default": false,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If true, the station operates in simulation mode using generated inflow and level data."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"default": "diurnal",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "static",
|
||||||
|
"description": "Use constant inflow and level conditions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "diurnal",
|
||||||
|
"description": "Use a typical diurnal inflow curve to drive simulation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "storm",
|
||||||
|
"description": "Use an elevated inflow profile representing a storm event."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Defines which synthetic profile drives the simulation."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seed": {
|
||||||
|
"default": 42,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Seed used for pseudo-random components in simulation."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"applyRandomNoise": {
|
||||||
|
"default": true,
|
||||||
|
"rules": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If true, adds small noise to simulated measurements."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inflowProfile": {
|
||||||
|
"default": [
|
||||||
|
80,
|
||||||
|
110,
|
||||||
|
160,
|
||||||
|
120,
|
||||||
|
90
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"type": "array",
|
||||||
|
"itemType": "number",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Relative inflow profile used when mode is set to diurnal or storm (percentage of design inflow)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,6 +102,14 @@
|
|||||||
"description": "A universally unique identifier for this asset. May be null if not assigned."
|
"description": "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": {
|
"geoLocation": {
|
||||||
"default": {},
|
"default": {},
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -139,14 +147,14 @@
|
|||||||
"description": "The supplier or manufacturer of the asset."
|
"description": "The supplier or manufacturer of the asset."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": {
|
"category": {
|
||||||
"default": "pump",
|
"default": "pump",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A general classification of the asset tied to the specific software. This is not chosen from the asset dropdown menu."
|
"description": "A general classification of the asset tied to the specific software. This is not chosen from the asset dropdown menu."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"subType": {
|
"type": {
|
||||||
"default": "Centrifugal",
|
"default": "Centrifugal",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -160,6 +168,13 @@
|
|||||||
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "unitless",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The unit of measurement for this asset (e.g., 'meters', 'seconds', 'unitless')."
|
||||||
|
}
|
||||||
|
},
|
||||||
"accuracy": {
|
"accuracy": {
|
||||||
"default": null,
|
"default": null,
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -397,6 +412,14 @@
|
|||||||
],
|
],
|
||||||
"description": "The frequency at which calculations are performed."
|
"description": "The frequency at which calculations are performed."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"flowNumber": {
|
||||||
|
"default": 1,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": false,
|
||||||
|
"description": "Defines which effluent flow of the parent node to handle."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
387
src/configs/valve.json
Normal file
387
src/configs/valve.json
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "valve",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A human-readable name or label for this machine configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A unique identifier for this configuration. If not provided, defaults to null."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "m3/h",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The default measurement unit 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": "valve",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specified software type for this configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "controller",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Indicates the role this configuration plays within the system."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"positionVsParent":{
|
||||||
|
"default":"atEquipment",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "atEquipment",
|
||||||
|
"description": "The node is connected at the equipment level and is responsible for controlling or monitoring the equipment as a whole."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "upstream",
|
||||||
|
"description": "The node is connected in a downstream position, indicating it is responsible for monitoring or controlling processes that occur after the equipment's operation, such as product flow or output."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "downstream",
|
||||||
|
"description": "The node is connected in an upstream position, indicating it is responsible for monitoring or controlling processes that occur before the equipment's operation, such as input flow or supply."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Defines the position of the measurement relative to its parent equipment or system."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"asset": {
|
||||||
|
"uuid": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "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": {},
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"default": "valve",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A general classification of the asset tied to the specific software. This is not chosen from the asset dropdown menu."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"default": "gate",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A more specific classification within 'type'. For example, 'centrifugal' for a centrifugal pump."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"default": "unitless",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The unit of measurement for this asset (e.g., 'meters', 'seconds', 'unitless')."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accuracy": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The accuracy of the machine or sensor, typically as a percentage or absolute value."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"valveCurve": {
|
||||||
|
"default": {
|
||||||
|
"1.204": {
|
||||||
|
"1": {
|
||||||
|
"x": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
|
||||||
|
"y": [0, 18, 50, 95, 150, 216, 337, 564, 882, 1398, 1870]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "valveCurve",
|
||||||
|
"description": "the first parameter is kg (usually according to 1 normal cubic meter per hour acc. to din norm ) and the second parameter is the diameter in mm. The x values are the opening of the valve in percent and the y values are the KV values in m3/h. The KV value is the flow rate of water at a temperature of 20 degrees Celsius through the valve when it is fully open."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"current": {
|
||||||
|
"default": "auto",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "auto",
|
||||||
|
"description": "Machine accepts setpoints from a parent controller and runs autonomously."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "virtualControl",
|
||||||
|
"description": "Controlled via GUI setpoints; ignores parent commands."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "fysicalControl",
|
||||||
|
"description": "Controlled via physical buttons or switches; ignores external automated commands."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "maintenance",
|
||||||
|
"description": "No active control from auto, virtual, or fysical sources."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The operational mode of the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedActions":{
|
||||||
|
"default":{},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema":{
|
||||||
|
"auto": {
|
||||||
|
"default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in auto mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"virtualControl": {
|
||||||
|
"default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in virtualControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fysicalControl": {
|
||||||
|
"default": ["statusCheck", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in fysicalControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maintenance": {
|
||||||
|
"default": ["statusCheck"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in maintenance mode."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Information about valid command sources recognized by the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedSources":{
|
||||||
|
"default": {},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema":{
|
||||||
|
"auto": {
|
||||||
|
"default": ["parent", "GUI", "fysical"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sources allowed in auto mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"virtualControl": {
|
||||||
|
"default": ["GUI", "fysical"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sources allowed in virtualControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fysicalControl": {
|
||||||
|
"default": ["fysical"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sources allowed in fysicalControl mode."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Information about valid command sources recognized by the machine."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"default": "parent",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "parent",
|
||||||
|
"description": "Commands are received from a parent controller."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "GUI",
|
||||||
|
"description": "Commands are received from a graphical user interface."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "fysical",
|
||||||
|
"description": "Commands are received from physical buttons or switches."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Information about valid command sources recognized by the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sequences":{
|
||||||
|
"default":{},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"startup": {
|
||||||
|
"default": ["starting","warmingup","operational"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for starting up the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shutdown": {
|
||||||
|
"default": ["stopping","coolingdown","idle"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for shutting down the machine."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"emergencystop": {
|
||||||
|
"default": ["emergencystop","off"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for an emergency stop."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boot": {
|
||||||
|
"default": ["idle","starting","warmingup","operational"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for booting up the machine."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Predefined sequences of states for the machine."
|
||||||
|
|
||||||
|
},
|
||||||
|
"calculationMode": {
|
||||||
|
"default": "medium",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "low",
|
||||||
|
"description": "Calculations run at fixed intervals (time-based)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "medium",
|
||||||
|
"description": "Calculations run when new setpoints arrive or measured changes occur (event-driven)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "high",
|
||||||
|
"description": "Calculations run on all event-driven info, including every movement."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The frequency at which calculations are performed."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
371
src/configs/valveGroupControl.json
Normal file
371
src/configs/valveGroupControl.json
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"name": {
|
||||||
|
"default": "ValveGroupControl",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A human-readable name or label for this valveGroupControl 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 default measurement unit 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": "valveGroupControl",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specified software type for this configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"default": "ValveGroupController",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Indicates the role this configuration plays within the system."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"asset": {
|
||||||
|
"uuid": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "A universally unique identifier for this asset. May be null if not assigned."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geoLocation": {
|
||||||
|
"default": {},
|
||||||
|
"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": "valve",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A general classification of the asset tied to the specific software. This is not chosen from the asset dropdown menu."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"subType": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A more specific classification within 'type'. For example, 'centrifugal' for a centrifugal pump."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"default": "Unknown",
|
||||||
|
"rules": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accuracy": {
|
||||||
|
"default": null,
|
||||||
|
"rules": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The accuracy of the valve or sensor, typically as a percentage or absolute value."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"current": {
|
||||||
|
"default": "auto",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "auto",
|
||||||
|
"description": "ValveGroupController accepts inputs from a parents and childs and runs autonomously."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "virtualControl",
|
||||||
|
"description": "Controlled via GUI setpoints; ignores parent commands."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "fysicalControl",
|
||||||
|
"description": "Controlled via physical buttons or switches; ignores external automated commands."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "maintenance",
|
||||||
|
"description": "No active control from auto, virtual, or fysical sources."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The operational mode of the valveGroupControl."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedActions":{
|
||||||
|
"default":{},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema":{
|
||||||
|
"auto": {
|
||||||
|
"default": ["statusCheck", "execSequence", "emergencyStop", "valvePositionChange", "totalFlowChange", "valveDeltaPchange"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in auto mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"virtualControl": {
|
||||||
|
"default": ["statusCheck", "execSequence", "emergencyStop", "valvePositionChange", "totalFlowChange", "valveDeltaPchange"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in virtualControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fysicalControl": {
|
||||||
|
"default": ["statusCheck", "emergencyStop"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in fysicalControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maintenance": {
|
||||||
|
"default": ["statusCheck"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Actions allowed in maintenance mode."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Information about valid command sources recognized by the valve."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedSources":{
|
||||||
|
"default": {},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema":{
|
||||||
|
"auto": {
|
||||||
|
"default": ["parent", "GUI", "fysical"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sources allowed in auto mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"virtualControl": {
|
||||||
|
"default": ["GUI", "fysical"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sources allowed in virtualControl mode."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fysicalControl": {
|
||||||
|
"default": ["fysical"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sources allowed in fysicalControl mode."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Information about valid command sources recognized by the valveGroupControl."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"default": "parent",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "parent",
|
||||||
|
"description": "Commands are received from a parent controller."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "GUI",
|
||||||
|
"description": "Commands are received from a graphical user interface."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "fysical",
|
||||||
|
"description": "Commands are received from physical buttons or switches."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Information about valid command sources recognized by the valveGroupControl."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"default": "statusCheck",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "statusCheck",
|
||||||
|
"description": "Checks the valveGroupControl's state (mode, submode, operational status)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "valvePositionChange",
|
||||||
|
"description": "If child valve position change, the new flow for each child valve is determined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "execSequence",
|
||||||
|
"description": "Allows execution of sequences through auto or GUI controls."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "totalFlowChange",
|
||||||
|
"description": "If total flow change, the new flow for each child valve is determined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "valveDeltaPchange",
|
||||||
|
"description": "If deltaP change, the deltaPmax is determined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "emergencyStop",
|
||||||
|
"description": "Overrides all commands and stops the valveGroupControl immediately (safety scenarios)."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Defines the possible actions that can be performed on the valveGroupControl."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sequences":{
|
||||||
|
"default":{},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"schema": {
|
||||||
|
"startup": {
|
||||||
|
"default": ["starting","warmingup","operational"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for starting up the valve."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shutdown": {
|
||||||
|
"default": ["stopping","coolingdown","idle"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for shutting down the valveGroupControl."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"emergencystop": {
|
||||||
|
"default": ["emergencystop","off"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for an emergency stop."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boot": {
|
||||||
|
"default": ["idle","starting","warmingup","operational"],
|
||||||
|
"rules": {
|
||||||
|
"type": "set",
|
||||||
|
"itemType": "string",
|
||||||
|
"description": "Sequence of states for booting up the valveGroupControl."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Predefined sequences of states for the valveGroupControl."
|
||||||
|
|
||||||
|
},
|
||||||
|
"calculationMode": {
|
||||||
|
"default": "medium",
|
||||||
|
"rules": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"value": "low",
|
||||||
|
"description": "Calculations run at fixed intervals (time-based)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "medium",
|
||||||
|
"description": "Calculations run when new setpoints arrive or measured changes occur (event-driven)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "high",
|
||||||
|
"description": "Calculations run on all event-driven info, including every movement."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The frequency at which calculations are performed."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/coolprop-node/.gitattributes
vendored
Normal file
2
src/coolprop-node/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
21
src/coolprop-node/LICENSE
Normal file
21
src/coolprop-node/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Craig Zych
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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.
|
||||||
253
src/coolprop-node/README.md
Normal file
253
src/coolprop-node/README.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# CoolProp-Node
|
||||||
|
|
||||||
|
A Node.js wrapper for CoolProp providing an easy-to-use interface for thermodynamic calculations and refrigerant properties. Unlike all the other CoolProp npm packages I've seen, this one should actually work. Please report any issues.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install coolprop-node
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Easy-to-use async interface for CoolProp
|
||||||
|
- Unit conversion support (Temperature: K/C/F, Pressure: Pa/kPa/bar/psi)
|
||||||
|
- Automatic initialization
|
||||||
|
- Configurable defaults
|
||||||
|
- Comprehensive error handling
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
No External Dependencies, as CoolProp.js and CoolProp.wasm are bundled with the package.
|
||||||
|
- [CoolProp](https://github.com/CoolProp/CoolProp) for the powerful thermodynamic library
|
||||||
|
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
```javascript
|
||||||
|
const nodeprop = require('coolprop-node');
|
||||||
|
async function example() {
|
||||||
|
// Initialize with defaults (optional)
|
||||||
|
await nodeprop.init({
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
// Calculate superheat
|
||||||
|
const result = await nodeprop.calculateSuperheat({
|
||||||
|
temperature: 25, // 25°C
|
||||||
|
pressure: 10, // 10 bar
|
||||||
|
refrigerant: 'R404A' // optional if set in init
|
||||||
|
});
|
||||||
|
console.log(result);
|
||||||
|
|
||||||
|
// expected output:
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
superheat: 5.2,
|
||||||
|
saturationTemperature: 19.8,
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
units: {
|
||||||
|
temperature: 'C',
|
||||||
|
pressure: 'bar'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
example();
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### nodeprop.init(config)
|
||||||
|
Initializes the wrapper with optional configuration.
|
||||||
|
###### Note: Calling `init()` is optional. The library will initialize automatically when you make your first call to any function, but you must provide a `refrigerant` parameter in that first call.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await nodeprop.init({
|
||||||
|
refrigerant: 'R404A', // Required on first init
|
||||||
|
tempUnit: 'C', // Optional, defaults to 'K'
|
||||||
|
pressureUnit: 'bar' // Optional, defaults to 'Pa'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### nodeprop.calculateSuperheat(input)
|
||||||
|
Calculates superheat for a given refrigerant.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const result = await nodeprop.calculateSuperheat({
|
||||||
|
temperature: 25, // 25°C
|
||||||
|
pressure: 10, // 10 bar
|
||||||
|
refrigerant: 'R404A' // optional if set in init
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
returns:
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
superheat: 5.2,
|
||||||
|
saturationTemperature: 19.8,
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
units: {
|
||||||
|
temperature: 'C',
|
||||||
|
pressure: 'bar'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### nodeprop.getSaturationTemperature(input)
|
||||||
|
Calculates saturation temperature for a given refrigerant.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const result = await nodeprop.calculateSaturationTemperature({
|
||||||
|
temperature: 25, // 25°C
|
||||||
|
pressure: 10, // 10 bar
|
||||||
|
refrigerant: 'R404A' // optional if set in init
|
||||||
|
});
|
||||||
|
|
||||||
|
returns:
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
temperature: 19.8,
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
units: {
|
||||||
|
temperature: 'C',
|
||||||
|
pressure: 'bar'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### nodeprop.getSaturationPressure(input)
|
||||||
|
Calculates saturation pressure for a given refrigerant.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const result = await nodeprop.calculateSaturationPressure({
|
||||||
|
temperature: 25, // 25°C
|
||||||
|
refrigerant: 'R404A' // optional if set in init
|
||||||
|
});
|
||||||
|
|
||||||
|
returns:
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
pressure: 10,
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
units: {
|
||||||
|
temperature: 'C',
|
||||||
|
pressure: 'bar'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### nodeprop.calculateSubcooling(input)
|
||||||
|
Calculates subcooling for a given refrigerant.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const result = await nodeprop.calculateSubcooling({
|
||||||
|
temperature: 25, // 25°C
|
||||||
|
pressure: 10, // 10 bar
|
||||||
|
refrigerant: 'R404A' // optional if set in init
|
||||||
|
});
|
||||||
|
|
||||||
|
returns:
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
subcooling: 5.2,
|
||||||
|
saturationTemperature: 19.8,
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
units: {
|
||||||
|
temperature: 'C',
|
||||||
|
pressure: 'bar'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### nodeprop.calculateSuperheat(input)
|
||||||
|
Calculates superheat for a given refrigerant.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const result = await nodeprop.calculateSuperheat({
|
||||||
|
temperature: 25, // 25°C
|
||||||
|
pressure: 10, // 10 bar
|
||||||
|
refrigerant: 'R404A' // optional if set in init
|
||||||
|
});
|
||||||
|
|
||||||
|
returns:
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
superheat: 5.2,
|
||||||
|
saturationTemperature: 19.8,
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
units: {
|
||||||
|
temperature: 'C',
|
||||||
|
pressure: 'bar'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### nodeprop.getProperties(input)
|
||||||
|
Gets all properties for a given refrigerant.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const result = await nodeprop.getProperties({
|
||||||
|
temperature: 25, // 25°C
|
||||||
|
pressure: 10, // 10 bar
|
||||||
|
refrigerant: 'R404A' // optional if set in init
|
||||||
|
});
|
||||||
|
|
||||||
|
returns:
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
properties: {
|
||||||
|
temperature: 25, // in configured temperature unit (e.g., °C)
|
||||||
|
pressure: 10, // in configured pressure unit (e.g., bar)
|
||||||
|
density: 1234.56, // in kg/m³
|
||||||
|
enthalpy: 400000, // in J/kg
|
||||||
|
entropy: 1750, // in J/kg/K
|
||||||
|
quality: 1, // dimensionless (0-1)
|
||||||
|
conductivity: 0.013, // in W/m/K
|
||||||
|
viscosity: 1.2e-5, // in Pa·s
|
||||||
|
specificHeat: 850 // in J/kg/K
|
||||||
|
},
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
units: {
|
||||||
|
temperature: 'C',
|
||||||
|
pressure: 'bar',
|
||||||
|
density: 'kg/m³',
|
||||||
|
enthalpy: 'J/kg',
|
||||||
|
entropy: 'J/kg/K',
|
||||||
|
quality: 'dimensionless',
|
||||||
|
conductivity: 'W/m/K',
|
||||||
|
viscosity: 'Pa·s',
|
||||||
|
specificHeat: 'J/kg/K'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### nodeprop.PropsSI
|
||||||
|
Direct access to CoolProp's PropsSI function.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const PropsSI = await nodeprop.getPropsSI();
|
||||||
|
const result = PropsSI('H', 'T', 298.15, 'P', 101325, 'R134a');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const result = await nodeprop.calculateSuperheat({
|
||||||
|
temperature: 25, // 25°C
|
||||||
|
pressure: 10, // 10 bar
|
||||||
|
refrigerant: 'R404' // Invalid refrigerant. Must be supported by CoolProp, but R404 is not even a valid refrigerant.
|
||||||
|
});
|
||||||
|
|
||||||
|
returns:
|
||||||
|
{
|
||||||
|
type: 'error',
|
||||||
|
message: 'Invalid refrigerant'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Acknowledgements
|
||||||
|
|
||||||
|
- [CoolProp](https://github.com/CoolProp/CoolProp) for the powerful thermodynamic library
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
80
src/coolprop-node/benchmark.js
Normal file
80
src/coolprop-node/benchmark.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
const coolprop = require('./src/index.js');
|
||||||
|
|
||||||
|
// Function to generate random number between min and max
|
||||||
|
function getRandomNumber(min, max) {
|
||||||
|
return min + Math.random() * (max - min);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate 1000 combinations of temperature and pressure
|
||||||
|
function generateCombinations(count) {
|
||||||
|
const combinations = [];
|
||||||
|
|
||||||
|
// For R744 (CO2), using realistic ranges from test files
|
||||||
|
// Temperature range: -40°F to 32°F
|
||||||
|
// Pressure range: 131 psig to 491 psig
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const temperature = getRandomNumber(-40, 32);
|
||||||
|
const pressure = getRandomNumber(131, 491);
|
||||||
|
|
||||||
|
combinations.push({
|
||||||
|
temperature,
|
||||||
|
pressure,
|
||||||
|
refrigerant: 'R744',
|
||||||
|
tempUnit: 'F',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinations;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBenchmark() {
|
||||||
|
console.log('Generating 1000 temperature and pressure combinations...');
|
||||||
|
const combinations = generateCombinations(1000);
|
||||||
|
console.log('Combinations generated.');
|
||||||
|
|
||||||
|
// Pre-initialize the library
|
||||||
|
console.log('Initializing library...');
|
||||||
|
await coolprop.init({
|
||||||
|
refrigerant: 'R744',
|
||||||
|
tempUnit: 'F',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
console.log('Library initialized.');
|
||||||
|
|
||||||
|
// Run benchmark
|
||||||
|
console.log('Starting benchmark...');
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (let i = 0; i < combinations.length; i++) {
|
||||||
|
const result = await coolprop.calculateSuperheat(combinations[i]);
|
||||||
|
results.push(result);
|
||||||
|
|
||||||
|
// Show progress every 100 calculations
|
||||||
|
if ((i + 1) % 100 === 0) {
|
||||||
|
console.log(`Processed ${i + 1} / ${combinations.length} calculations`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTime = performance.now();
|
||||||
|
const totalTime = endTime - startTime;
|
||||||
|
const avgTime = totalTime / combinations.length;
|
||||||
|
|
||||||
|
// Report results
|
||||||
|
console.log('\nBenchmark Results:');
|
||||||
|
console.log(`Total time: ${totalTime.toFixed(2)} ms`);
|
||||||
|
console.log(`Average time per calculation: ${avgTime.toFixed(2)} ms`);
|
||||||
|
console.log(`Calculations per second: ${(1000 / avgTime).toFixed(2)}`);
|
||||||
|
|
||||||
|
// Count success and error results
|
||||||
|
const successful = results.filter(r => r.type === 'success').length;
|
||||||
|
const failed = results.filter(r => r.type === 'error').length;
|
||||||
|
console.log(`\nSuccessful calculations: ${successful}`);
|
||||||
|
console.log(`Failed calculations: ${failed}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the benchmark
|
||||||
|
runBenchmark().catch(error => {
|
||||||
|
console.error('Benchmark failed:', error);
|
||||||
|
});
|
||||||
1
src/coolprop-node/coolprop/coolprop.js
Normal file
1
src/coolprop-node/coolprop/coolprop.js
Normal file
File diff suppressed because one or more lines are too long
BIN
src/coolprop-node/coolprop/coolprop.wasm
Normal file
BIN
src/coolprop-node/coolprop/coolprop.wasm
Normal file
Binary file not shown.
31
src/coolprop-node/package.json
Normal file
31
src/coolprop-node/package.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "coolprop-node",
|
||||||
|
"version": "1.0.20",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"coolprop",
|
||||||
|
"thermodynamics",
|
||||||
|
"fluid properties",
|
||||||
|
"refrigerant",
|
||||||
|
"refrigeration",
|
||||||
|
"refprop"
|
||||||
|
],
|
||||||
|
"author": "Craig Zych",
|
||||||
|
"license": "MIT",
|
||||||
|
"description": "A Node.js wrapper for CoolProp providing an easy-to-use interface for thermodynamic calculations and refrigerant properties. Unlike all the other CoolProp npm packages I've seen, this one should actually work. Please report any issues. ",
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^29.7.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"verbose": true
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Craigzyc/coolprop-node.git"
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/coolprop-node/src/cp.js
Normal file
92
src/coolprop-node/src/cp.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// Load and configure the CoolProp module
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const vm = require('vm');
|
||||||
|
|
||||||
|
// Mock XMLHttpRequest
|
||||||
|
class XMLHttpRequest {
|
||||||
|
open(method, url) {
|
||||||
|
this.method = method;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
send() {
|
||||||
|
try {
|
||||||
|
// Convert the URL to a local file path
|
||||||
|
const localPath = path.join(__dirname, '..', 'coolprop', path.basename(this.url));
|
||||||
|
const data = fs.readFileSync(localPath);
|
||||||
|
|
||||||
|
this.status = 200;
|
||||||
|
this.response = data;
|
||||||
|
this.responseType = 'arraybuffer';
|
||||||
|
|
||||||
|
if (this.onload) {
|
||||||
|
this.onload();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.onerror) {
|
||||||
|
this.onerror(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the coolprop.js file
|
||||||
|
const coolpropJs = fs.readFileSync(path.join(__dirname, '../coolprop/coolprop.js'), 'utf8');
|
||||||
|
|
||||||
|
// Create a context for the module
|
||||||
|
const context = {
|
||||||
|
window: {},
|
||||||
|
self: {},
|
||||||
|
Module: {
|
||||||
|
onRuntimeInitialized: function() {
|
||||||
|
context.Module.initialized = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
importScripts: () => {},
|
||||||
|
console: console,
|
||||||
|
location: {
|
||||||
|
href: 'file://' + __dirname,
|
||||||
|
pathname: __dirname,
|
||||||
|
},
|
||||||
|
document: {
|
||||||
|
currentScript: { src: '' }
|
||||||
|
},
|
||||||
|
XMLHttpRequest: XMLHttpRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make self reference the context itself
|
||||||
|
context.self = context;
|
||||||
|
// Make window reference the context itself
|
||||||
|
context.window = context;
|
||||||
|
|
||||||
|
// Execute coolprop.js in our custom context
|
||||||
|
vm.createContext(context);
|
||||||
|
vm.runInContext(coolpropJs, context);
|
||||||
|
|
||||||
|
// Wait for initialization
|
||||||
|
function waitForInit(timeout = 5000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const start = Date.now();
|
||||||
|
const check = () => {
|
||||||
|
if (context.Module.initialized) {
|
||||||
|
resolve(context.Module);
|
||||||
|
} else if (Date.now() - start > timeout) {
|
||||||
|
reject(new Error('CoolProp initialization timed out'));
|
||||||
|
} else {
|
||||||
|
setTimeout(check, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
init: () => waitForInit(),
|
||||||
|
PropsSI: (...args) => {
|
||||||
|
if (!context.Module.initialized) {
|
||||||
|
throw new Error('CoolProp not initialized. Call init() first');
|
||||||
|
}
|
||||||
|
return context.Module.PropsSI(...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
419
src/coolprop-node/src/index.js
Normal file
419
src/coolprop-node/src/index.js
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
const coolprop = require('./cp.js');
|
||||||
|
const customRefs = require('./refData.js');
|
||||||
|
|
||||||
|
class CoolPropWrapper {
|
||||||
|
constructor() {
|
||||||
|
this.initialized = false;
|
||||||
|
this.defaultRefrigerant = null;
|
||||||
|
this.defaultTempUnit = 'K'; // K, C, F
|
||||||
|
this.defaultPressureUnit = 'Pa' // Pa, kPa, bar, psi
|
||||||
|
this.customRef = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temperature conversion helpers
|
||||||
|
_convertTempToK(value, unit = this.defaultTempUnit) {
|
||||||
|
switch(unit.toUpperCase()) {
|
||||||
|
case 'K': return value;
|
||||||
|
case 'C': return value + 273.15;
|
||||||
|
case 'F': return (value + 459.67) * 5/9;
|
||||||
|
default: throw new Error('Unsupported temperature unit');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_convertTempFromK(value, unit = this.defaultTempUnit) {
|
||||||
|
switch(unit.toUpperCase()) {
|
||||||
|
case 'K': return value;
|
||||||
|
case 'C': return value - 273.15;
|
||||||
|
case 'F': return value * 9/5 - 459.67;
|
||||||
|
default: throw new Error('Unsupported temperature unit');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_convertDeltaTempFromK(value, unit = this.defaultTempUnit) {
|
||||||
|
switch(unit.toUpperCase()) {
|
||||||
|
case 'K': return value;
|
||||||
|
case 'C': return value;
|
||||||
|
case 'F': return (value * 1.8);
|
||||||
|
default: throw new Error('Unsupported temperature unit');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pressure conversion helpers
|
||||||
|
_convertPressureToPa(value, unit = this.defaultPressureUnit) {
|
||||||
|
switch(unit.toUpperCase()) {
|
||||||
|
case 'PAA': return value; // Absolute Pascal
|
||||||
|
case 'PAG':
|
||||||
|
case 'PA': return value + 101325; // Gauge Pascal
|
||||||
|
case 'KPAA': return value * 1000; // Absolute kiloPascal
|
||||||
|
case 'KPAG':
|
||||||
|
case 'KPA': return value * 1000 + 101325; // Gauge kiloPascal
|
||||||
|
case 'BARA': return value * 100000; // Absolute bar
|
||||||
|
case 'BARG':
|
||||||
|
case 'BAR': return value * 100000 + 101325; // Gauge bar
|
||||||
|
case 'PSIA': return value * 6894.76; // Absolute PSI
|
||||||
|
case 'PSIG':
|
||||||
|
case 'PSI': return value * 6894.76 + 101325;// Gauge PSI
|
||||||
|
default: throw new Error('Unsupported pressure unit');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_convertPressureFromPa(value, unit = this.defaultPressureUnit) {
|
||||||
|
switch(unit.toUpperCase()) {
|
||||||
|
case 'PAA': return value; // Absolute Pascal
|
||||||
|
case 'PAG':
|
||||||
|
case 'PA': return value - 101325; // Gauge Pascal
|
||||||
|
case 'KPAA': return value / 1000; // Absolute kiloPascal
|
||||||
|
case 'KPAG':
|
||||||
|
case 'KPA': return (value - 101325) / 1000; // Gauge kiloPascal
|
||||||
|
case 'BARA': return value / 100000; // Absolute bar
|
||||||
|
case 'BARG':
|
||||||
|
case 'BAR': return (value - 101325) / 100000;// Gauge bar
|
||||||
|
case 'PSIA': return value / 6894.76; // Absolute PSI
|
||||||
|
case 'PSIG':
|
||||||
|
case 'PSI': return (value - 101325) / 6894.76;// Gauge PSI
|
||||||
|
default: throw new Error('Unsupported pressure unit');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(config = {}) {
|
||||||
|
try {
|
||||||
|
// If already initialized, only update defaults if provided
|
||||||
|
if (this.initialized) {
|
||||||
|
if (config.refrigerant) this.defaultRefrigerant = config.refrigerant;
|
||||||
|
if (config.tempUnit) {
|
||||||
|
if (!['K', 'C', 'F'].includes(config.tempUnit.toUpperCase())) {
|
||||||
|
return { type: 'error', message: 'Invalid temperature unit. Must be K, C, or F' };
|
||||||
|
}
|
||||||
|
this.defaultTempUnit = config.tempUnit;
|
||||||
|
}
|
||||||
|
if (config.pressureUnit) {
|
||||||
|
if (!['PA', 'PAA', 'KPA', 'KPAA', 'BAR', 'BARA', 'PSI', 'PSIA'].includes(config.pressureUnit.toUpperCase())) {
|
||||||
|
return { type: 'error', message: 'Invalid pressure unit. Must be Pa, Paa, kPa, kPaa, bar, bara, psi, or psia' };
|
||||||
|
}
|
||||||
|
this.defaultPressureUnit = config.pressureUnit;
|
||||||
|
}
|
||||||
|
return { type: 'success', message: 'Default settings updated' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// First time initialization
|
||||||
|
if (!config.refrigerant) {
|
||||||
|
throw new Error('Refrigerant must be specified during initialization');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate temperature unit if provided
|
||||||
|
if (config.tempUnit && !['K', 'C', 'F'].includes(config.tempUnit.toUpperCase())) {
|
||||||
|
throw new Error('Invalid temperature unit. Must be K, C, or F');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate pressure unit if provided
|
||||||
|
if (config.pressureUnit && !['PA', 'PAA', 'KPA', 'KPAA', 'BAR', 'BARA', 'PSI', 'PSIA'].includes(config.pressureUnit.toUpperCase())) {
|
||||||
|
throw new Error('Invalid pressure unit. Must be Pa, Paa, kPa, kPaa, bar, bara, psi, or psia');
|
||||||
|
}
|
||||||
|
|
||||||
|
await coolprop.init();
|
||||||
|
this.initialized = true;
|
||||||
|
this.defaultRefrigerant = config.refrigerant;
|
||||||
|
this.defaultTempUnit = config.tempUnit || this.defaultTempUnit;
|
||||||
|
this.defaultPressureUnit = config.pressureUnit || this.defaultPressureUnit;
|
||||||
|
return { type: 'success', message: 'Initialized successfully' };
|
||||||
|
} catch (error) {
|
||||||
|
return { type: 'error', message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _ensureInit(config = {}) {
|
||||||
|
// Initialize CoolProp if not already done
|
||||||
|
if (!this.initialized) {
|
||||||
|
if (!config.refrigerant && !this.defaultRefrigerant) {
|
||||||
|
throw new Error('Refrigerant must be specified either during initialization or in the method call');
|
||||||
|
}
|
||||||
|
await coolprop.init();
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate temperature unit if provided
|
||||||
|
if (config.tempUnit && !['K', 'C', 'F'].includes(config.tempUnit.toUpperCase())) {
|
||||||
|
throw new Error('Invalid temperature unit. Must be K, C, or F');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate pressure unit if provided
|
||||||
|
if (config.pressureUnit && !['PA', 'PAA', 'PAG', 'KPA', 'KPAA', 'KPAG', 'BAR', 'BARA', 'BARG', 'PSI', 'PSIA', 'PSIG'].includes(config.pressureUnit.toUpperCase())) {
|
||||||
|
throw new Error('Invalid pressure unit. Must be Pa, Paa, Pag, kPa, kPaa, kPag, bar, bara, barg, psi, psia, or psig');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate refrigerant if provided
|
||||||
|
if (config.refrigerant && typeof config.refrigerant !== 'string') {
|
||||||
|
throw new Error('Invalid refrigerant type');
|
||||||
|
}
|
||||||
|
if (config.refrigerant && Object.keys(customRefs).includes(config.refrigerant)) {
|
||||||
|
this.customRef = true;
|
||||||
|
this.defaultRefrigerant = config.refrigerant;
|
||||||
|
//console.log(`Using custom refrigerant flag for ${this.defaultRefrigerant}`);
|
||||||
|
}else if(this.customRef && config.refrigerant){
|
||||||
|
this.customRef = false;
|
||||||
|
//console.log(`Cleared custom refrigerant flag`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update instance variables with new config values if provided
|
||||||
|
if (config.refrigerant) this.defaultRefrigerant = config.refrigerant;
|
||||||
|
if (config.tempUnit) this.defaultTempUnit = config.tempUnit.toUpperCase();
|
||||||
|
if (config.pressureUnit) this.defaultPressureUnit = config.pressureUnit.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfig() {
|
||||||
|
return {
|
||||||
|
refrigerant: this.defaultRefrigerant,
|
||||||
|
tempUnit: this.defaultTempUnit,
|
||||||
|
pressureUnit: this.defaultPressureUnit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async setConfig(config) {
|
||||||
|
await this.init(config);
|
||||||
|
return {
|
||||||
|
type: 'success',
|
||||||
|
message: 'Config updated successfully',
|
||||||
|
config: await this.getConfig()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method for linear interpolation/extrapolation
|
||||||
|
_interpolateSaturationTemperature(pressurePa, saturationData, pressureType = 'liquid') {
|
||||||
|
const data = saturationData.sort((a, b) => a[pressureType] - b[pressureType]); // Sort by specified pressure type
|
||||||
|
|
||||||
|
// If pressure is below the lowest data point, extrapolate using first two points
|
||||||
|
if (pressurePa <= data[0][pressureType]) {
|
||||||
|
if (data.length < 2) return data[0].K;
|
||||||
|
const p1 = data[0], p2 = data[1];
|
||||||
|
const slope = (p2.K - p1.K) / (p2[pressureType] - p1[pressureType]);
|
||||||
|
return p1.K + slope * (pressurePa - p1[pressureType]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If pressure is above the highest data point, extrapolate using last two points
|
||||||
|
if (pressurePa >= data[data.length - 1][pressureType]) {
|
||||||
|
if (data.length < 2) return data[data.length - 1].K;
|
||||||
|
const p1 = data[data.length - 2], p2 = data[data.length - 1];
|
||||||
|
const slope = (p2.K - p1.K) / (p2[pressureType] - p1[pressureType]);
|
||||||
|
return p1.K + slope * (pressurePa - p1[pressureType]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the two adjacent points for interpolation
|
||||||
|
for (let i = 0; i < data.length - 1; i++) {
|
||||||
|
if (pressurePa >= data[i][pressureType] && pressurePa <= data[i + 1][pressureType]) {
|
||||||
|
const p1 = data[i], p2 = data[i + 1];
|
||||||
|
|
||||||
|
// Linear interpolation
|
||||||
|
const slope = (p2.K - p1.K) / (p2[pressureType] - p1[pressureType]);
|
||||||
|
return p1.K + slope * (pressurePa - p1[pressureType]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback (shouldn't reach here)
|
||||||
|
return data[0].K;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method for linear interpolation/extrapolation of saturation pressure
|
||||||
|
_interpolateSaturationPressure(tempK, saturationData, pressureType = 'liquid') {
|
||||||
|
const data = saturationData.sort((a, b) => a.K - b.K); // Sort by temperature
|
||||||
|
|
||||||
|
// If temperature is below the lowest data point, extrapolate using first two points
|
||||||
|
if (tempK <= data[0].K) {
|
||||||
|
if (data.length < 2) return data[0][pressureType];
|
||||||
|
const p1 = data[0], p2 = data[1];
|
||||||
|
const slope = (p2[pressureType] - p1[pressureType]) / (p2.K - p1.K);
|
||||||
|
return p1[pressureType] + slope * (tempK - p1.K);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If temperature is above the highest data point, extrapolate using last two points
|
||||||
|
if (tempK >= data[data.length - 1].K) {
|
||||||
|
if (data.length < 2) return data[data.length - 1][pressureType];
|
||||||
|
const p1 = data[data.length - 2], p2 = data[data.length - 1];
|
||||||
|
const slope = (p2[pressureType] - p1[pressureType]) / (p2.K - p1.K);
|
||||||
|
return p1[pressureType] + slope * (tempK - p1.K);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the two adjacent points for interpolation
|
||||||
|
for (let i = 0; i < data.length - 1; i++) {
|
||||||
|
if (tempK >= data[i].K && tempK <= data[i + 1].K) {
|
||||||
|
const p1 = data[i], p2 = data[i + 1];
|
||||||
|
|
||||||
|
// Linear interpolation
|
||||||
|
const slope = (p2[pressureType] - p1[pressureType]) / (p2.K - p1.K);
|
||||||
|
return p1[pressureType] + slope * (tempK - p1.K);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback (shouldn't reach here)
|
||||||
|
return data[0][pressureType];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSaturationTemperature({ pressure, refrigerant = this.defaultRefrigerant, pressureUnit = this.defaultPressureUnit, tempUnit = this.defaultTempUnit }) {
|
||||||
|
try {
|
||||||
|
await this._ensureInit({ refrigerant, pressureUnit, tempUnit });
|
||||||
|
const pressurePa = this._convertPressureToPa(pressure, pressureUnit);
|
||||||
|
let tempK;
|
||||||
|
if(this.customRef){
|
||||||
|
tempK = this._interpolateSaturationTemperature(pressurePa, customRefs[refrigerant].saturation);
|
||||||
|
}else{
|
||||||
|
tempK = coolprop.PropsSI('T', 'P', pressurePa, 'Q', 0, this.customRefString || refrigerant);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'success',
|
||||||
|
temperature: this._convertTempFromK(tempK, tempUnit),
|
||||||
|
refrigerant,
|
||||||
|
units: {
|
||||||
|
temperature: tempUnit,
|
||||||
|
pressure: pressureUnit
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { type: 'error', message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async getSaturationPressure({ temperature, refrigerant = this.defaultRefrigerant, tempUnit = this.defaultTempUnit, pressureUnit = this.defaultPressureUnit }) {
|
||||||
|
try {
|
||||||
|
await this._ensureInit({ refrigerant, tempUnit, pressureUnit });
|
||||||
|
const tempK = this._convertTempToK(temperature, tempUnit);
|
||||||
|
let pressurePa;
|
||||||
|
|
||||||
|
if(this.customRef){
|
||||||
|
pressurePa = this._interpolateSaturationPressure(tempK, customRefs[refrigerant].saturation);
|
||||||
|
}else{
|
||||||
|
pressurePa = coolprop.PropsSI('P', 'T', tempK, 'Q', 0, this.customRefString || refrigerant);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'success',
|
||||||
|
pressure: this._convertPressureFromPa(pressurePa, pressureUnit),
|
||||||
|
refrigerant,
|
||||||
|
units: {
|
||||||
|
temperature: tempUnit,
|
||||||
|
pressure: pressureUnit
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { type: 'error', message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async calculateSubcooling({ temperature, pressure, refrigerant = this.defaultRefrigerant, tempUnit = this.defaultTempUnit, pressureUnit = this.defaultPressureUnit }) {
|
||||||
|
try {
|
||||||
|
await this._ensureInit({ refrigerant, tempUnit, pressureUnit });
|
||||||
|
const tempK = this._convertTempToK(temperature, tempUnit);
|
||||||
|
const pressurePa = this._convertPressureToPa(pressure, pressureUnit);
|
||||||
|
let satTempK;
|
||||||
|
if(this.customRef){
|
||||||
|
// Use liquid pressure for subcooling
|
||||||
|
satTempK = this._interpolateSaturationTemperature(pressurePa, customRefs[refrigerant].saturation, 'liquid');
|
||||||
|
}else{
|
||||||
|
satTempK = coolprop.PropsSI('T', 'P', pressurePa, 'Q', 0, this.customRefString || refrigerant);
|
||||||
|
}
|
||||||
|
const subcooling = satTempK - tempK;
|
||||||
|
const result = {
|
||||||
|
type: 'success',
|
||||||
|
subcooling: Math.max(0, this._convertDeltaTempFromK(subcooling, tempUnit)), // can't have less than 0 degrees subcooling
|
||||||
|
saturationTemperature: this._convertTempFromK(satTempK, tempUnit),
|
||||||
|
refrigerant,
|
||||||
|
units: {
|
||||||
|
temperature: tempUnit,
|
||||||
|
pressure: pressureUnit
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if(result.subcooling == Infinity && result.saturationTemperature == Infinity) {
|
||||||
|
return { type: 'error', message: 'Subcooling is infinity', note: 'If the pressures are in an expected range that this should work, please check your refrigerant type works in coolprop. "R507" for example is not supported, as it needs to be "R507a"'};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
return { type: 'error', message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async calculateSuperheat({ temperature, pressure, refrigerant = this.defaultRefrigerant, tempUnit = this.defaultTempUnit, pressureUnit = this.defaultPressureUnit }) {
|
||||||
|
try {
|
||||||
|
await this._ensureInit({ refrigerant, tempUnit, pressureUnit });
|
||||||
|
const tempK = this._convertTempToK(temperature, tempUnit);
|
||||||
|
const pressurePa = this._convertPressureToPa(pressure, pressureUnit);
|
||||||
|
//console.log(`In calculateSuperheat, pressurePa: ${pressurePa}, pressure: ${pressure}, pressureUnit: ${pressureUnit}, refrigerant: ${this.customRefString || refrigerant}`);
|
||||||
|
let satTempK;
|
||||||
|
if(this.customRef){
|
||||||
|
// Use vapor pressure for superheat
|
||||||
|
satTempK = this._interpolateSaturationTemperature(pressurePa, customRefs[refrigerant].saturation, 'vapor');
|
||||||
|
}else{
|
||||||
|
satTempK = coolprop.PropsSI('T', 'P', pressurePa, 'Q', 1, this.customRefString || refrigerant);
|
||||||
|
}
|
||||||
|
const superheat = tempK - satTempK;
|
||||||
|
//console.log(`superheat: ${superheat}, calculatedSuperheat: ${this._convertDeltaTempFromK(superheat, tempUnit)}, calculatedSatTempK: ${this._convertTempFromK(satTempK, tempUnit)}, tempK: ${tempK}, tempUnit: ${tempUnit}, pressurePa: ${pressurePa}, pressureUnit: ${pressureUnit}`);
|
||||||
|
const result = {
|
||||||
|
type: 'success',
|
||||||
|
superheat: Math.max(0, this._convertDeltaTempFromK(superheat, tempUnit)), // can't have less than 0 degrees superheat
|
||||||
|
saturationTemperature: this._convertTempFromK(satTempK, tempUnit),
|
||||||
|
refrigerant,
|
||||||
|
units: {
|
||||||
|
temperature: tempUnit,
|
||||||
|
pressure: pressureUnit
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if(result.superheat == Infinity && result.saturationTemperature == Infinity) {
|
||||||
|
return { type: 'error', message: 'Superheat is infinity', note: 'If the pressures are in an expected range that this should work, please check your refrigerant type works in coolprop. "R507" for example is not supported, as it needs to be "R507a"'};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
return { type: 'error', message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProperties({ temperature, pressure, refrigerant = this.defaultRefrigerant, tempUnit = this.defaultTempUnit, pressureUnit = this.defaultPressureUnit }) {
|
||||||
|
try {
|
||||||
|
await this._ensureInit({ refrigerant, tempUnit, pressureUnit });
|
||||||
|
const tempK = this._convertTempToK(temperature, tempUnit);
|
||||||
|
const pressurePa = this._convertPressureToPa(pressure, pressureUnit);
|
||||||
|
if(this.customRef){
|
||||||
|
return { type: 'error', message: 'Custom refrigerants are not supported for getProperties' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
temperature: this._convertTempFromK(tempK, tempUnit),
|
||||||
|
pressure: this._convertPressureFromPa(pressurePa, pressureUnit),
|
||||||
|
density: coolprop.PropsSI('D', 'T', tempK, 'P', pressurePa, this.customRefString || refrigerant),
|
||||||
|
enthalpy: coolprop.PropsSI('H', 'T', tempK, 'P', pressurePa, this.customRefString || refrigerant),
|
||||||
|
entropy: coolprop.PropsSI('S', 'T', tempK, 'P', pressurePa, this.customRefString || refrigerant),
|
||||||
|
quality: coolprop.PropsSI('Q', 'T', tempK, 'P', pressurePa, this.customRefString || refrigerant),
|
||||||
|
conductivity: coolprop.PropsSI('L', 'T', tempK, 'P', pressurePa, this.customRefString || refrigerant),
|
||||||
|
viscosity: coolprop.PropsSI('V', 'T', tempK, 'P', pressurePa, this.customRefString || refrigerant),
|
||||||
|
specificHeat: coolprop.PropsSI('C', 'T', tempK, 'P', pressurePa, this.customRefString || refrigerant)
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'success',
|
||||||
|
properties: props,
|
||||||
|
refrigerant,
|
||||||
|
units: {
|
||||||
|
temperature: tempUnit,
|
||||||
|
pressure: pressureUnit,
|
||||||
|
density: 'kg/m³',
|
||||||
|
enthalpy: 'J/kg',
|
||||||
|
entropy: 'J/kg/K',
|
||||||
|
quality: 'dimensionless',
|
||||||
|
conductivity: 'W/m/K',
|
||||||
|
viscosity: 'Pa·s',
|
||||||
|
specificHeat: 'J/kg/K'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { type: 'error', message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct access to CoolProp functions
|
||||||
|
async getPropsSI() {
|
||||||
|
if(!this.initialized) {
|
||||||
|
await coolprop.init();
|
||||||
|
}
|
||||||
|
return coolprop.PropsSI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new CoolPropWrapper();
|
||||||
308
src/coolprop-node/src/refData.js
Normal file
308
src/coolprop-node/src/refData.js
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
module.exports.R448a = {
|
||||||
|
saturation: [{
|
||||||
|
//values in kelvin, pascal
|
||||||
|
"K": 233.15,
|
||||||
|
"liquid": 135137.24,
|
||||||
|
"vapor": 101352.93
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 238.71,
|
||||||
|
"liquid": 173058.40,
|
||||||
|
"vapor": 131689.86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 244.26,
|
||||||
|
"liquid": 218563.80,
|
||||||
|
"vapor": 168921.55
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 249.82,
|
||||||
|
"liquid": 273032.38,
|
||||||
|
"vapor": 214426.94
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 255.37,
|
||||||
|
"liquid": 337153.62,
|
||||||
|
"vapor": 268895.52
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 260.93,
|
||||||
|
"liquid": 412306.47,
|
||||||
|
"vapor": 333016.76
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 266.48,
|
||||||
|
"liquid": 499869.88,
|
||||||
|
"vapor": 408859.09
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 272.04,
|
||||||
|
"liquid": 599843.86,
|
||||||
|
"vapor": 496422.50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 277.59,
|
||||||
|
"liquid": 714986.30,
|
||||||
|
"vapor": 598464.91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 283.15,
|
||||||
|
"liquid": 845986.68,
|
||||||
|
"vapor": 714986.30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 288.71,
|
||||||
|
"liquid": 990776.58,
|
||||||
|
"vapor": 845986.68
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 294.26,
|
||||||
|
"liquid": 1163145.51,
|
||||||
|
"vapor": 997671.34
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 299.82,
|
||||||
|
"liquid": 1349303.94,
|
||||||
|
"vapor": 1170040.26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 305.37,
|
||||||
|
"liquid": 1556146.65,
|
||||||
|
"vapor": 1363093.46
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 310.93,
|
||||||
|
"liquid": 1783673.64,
|
||||||
|
"vapor": 1576830.93
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 316.48,
|
||||||
|
"liquid": 2038779.64,
|
||||||
|
"vapor": 1818147.42
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 322.04,
|
||||||
|
"liquid": 2314569.92,
|
||||||
|
"vapor": 2087042.94
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 327.59,
|
||||||
|
"liquid": 2617939.23,
|
||||||
|
"vapor": 2383517.49
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 333.15,
|
||||||
|
"liquid": 2955782.33,
|
||||||
|
"vapor": 2714465.83
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 338.71,
|
||||||
|
"liquid": 3321204.45,
|
||||||
|
"vapor": 3086782.71
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports.R448A = module.exports.R448a;
|
||||||
|
|
||||||
|
module.exports.R449A = {
|
||||||
|
saturation: [
|
||||||
|
{
|
||||||
|
// values in kelvin, pascal
|
||||||
|
"K": 233.15,
|
||||||
|
"liquid": 134447.82,
|
||||||
|
"vapor": 101352.97
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 235.93,
|
||||||
|
"liquid": 152374.20,
|
||||||
|
"vapor": 115121.57
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 238.71,
|
||||||
|
"liquid": 171679.52,
|
||||||
|
"vapor": 131689.92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 241.48,
|
||||||
|
"liquid": 193052.21,
|
||||||
|
"vapor": 148949.73
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 244.26,
|
||||||
|
"liquid": 216503.85,
|
||||||
|
"vapor": 168255.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 247.04,
|
||||||
|
"liquid": 242702.42,
|
||||||
|
"vapor": 189627.74
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 249.82,
|
||||||
|
"liquid": 270979.90,
|
||||||
|
"vapor": 213768.86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 252.59,
|
||||||
|
"liquid": 301336.31,
|
||||||
|
"vapor": 240051.48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 255.37,
|
||||||
|
"liquid": 334440.63,
|
||||||
|
"vapor": 267609.92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 258.15,
|
||||||
|
"liquid": 370292.86,
|
||||||
|
"vapor": 298655.80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 260.93,
|
||||||
|
"liquid": 408892.90,
|
||||||
|
"vapor": 331760.12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 263.71,
|
||||||
|
"liquid": 450240.76,
|
||||||
|
"vapor": 367612.35
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 266.48,
|
||||||
|
"liquid": 495036.08,
|
||||||
|
"vapor": 406831.32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 269.26,
|
||||||
|
"liquid": 542579.32,
|
||||||
|
"vapor": 448868.64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 272.04,
|
||||||
|
"liquid": 594279.82,
|
||||||
|
"vapor": 493663.96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 274.82,
|
||||||
|
"liquid": 649728.18,
|
||||||
|
"vapor": 542579.32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 277.59,
|
||||||
|
"liquid": 708053.32,
|
||||||
|
"vapor": 594969.28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 280.37,
|
||||||
|
"liquid": 770873.08,
|
||||||
|
"vapor": 650767.64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 283.15,
|
||||||
|
"liquid": 839126.92,
|
||||||
|
"vapor": 710801.16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 285.93,
|
||||||
|
"liquid": 912814.72,
|
||||||
|
"vapor": 774989.44
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 288.71,
|
||||||
|
"liquid": 983940.92,
|
||||||
|
"vapor": 845977.32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 291.48,
|
||||||
|
"liquid": 1066606.52,
|
||||||
|
"vapor": 914889.32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 294.26,
|
||||||
|
"liquid": 1151351.00,
|
||||||
|
"vapor": 990835.62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 297.04,
|
||||||
|
"liquid": 1238843.30,
|
||||||
|
"vapor": 1073501.22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 299.82,
|
||||||
|
"liquid": 1335552.20,
|
||||||
|
"vapor": 1165089.32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 302.59,
|
||||||
|
"liquid": 1432261.10,
|
||||||
|
"vapor": 1256677.42
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 305.37,
|
||||||
|
"liquid": 1535864.72,
|
||||||
|
"vapor": 1357134.12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 308.15,
|
||||||
|
"liquid": 1646363.00,
|
||||||
|
"vapor": 1457590.92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 310.93,
|
||||||
|
"liquid": 1763756.02,
|
||||||
|
"vapor": 1568089.12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 313.71,
|
||||||
|
"liquid": 1887043.62,
|
||||||
|
"vapor": 1678587.32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 316.48,
|
||||||
|
"liquid": 2017225.92,
|
||||||
|
"vapor": 1802217.02
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 319.26,
|
||||||
|
"liquid": 2147408.22,
|
||||||
|
"vapor": 1934952.12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 322.04,
|
||||||
|
"liquid": 2291329.82,
|
||||||
|
"vapor": 2072621.52
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 324.82,
|
||||||
|
"liquid": 2435251.42,
|
||||||
|
"vapor": 2217185.62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 327.59,
|
||||||
|
"liquid": 2592912.32,
|
||||||
|
"vapor": 2368644.42
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 330.37,
|
||||||
|
"liquid": 2750573.22,
|
||||||
|
"vapor": 2526305.32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 333.15,
|
||||||
|
"liquid": 2925424.52,
|
||||||
|
"vapor": 2690860.82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 335.93,
|
||||||
|
"liquid": 3100275.92,
|
||||||
|
"vapor": 2871668.52
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"K": 338.71,
|
||||||
|
"liquid": 3288922.02,
|
||||||
|
"vapor": 3059370.92
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
|
||||||
|
module.exports.R449a = module.exports.R449A;
|
||||||
94
src/coolprop-node/test/R448a.test.js
Normal file
94
src/coolprop-node/test/R448a.test.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
const coolprop = require('../src/index.js');
|
||||||
|
|
||||||
|
describe('R448a Real Values', () => {
|
||||||
|
it('should calculate superheat correctly at -40°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: -35, // 5K above saturation temp of -40°C
|
||||||
|
pressure: 0, // saturation pressure at -40°C (from chart)
|
||||||
|
refrigerant: 'R448a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat - 5)).toBeLessThan(0.2); // Should be ~5K superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate superheat correctly at -20°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: -15, // 5K above saturation temp of -20°C
|
||||||
|
pressure: 21.0, // saturation pressure at -20°C (from chart)
|
||||||
|
refrigerant: 'R448a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
//console.log(result);
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat - 5)).toBeLessThan(0.2); // Should be ~5K superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate subcooling correctly at 30°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 25, // 5K below saturation temp of 30°C
|
||||||
|
pressure: 198.1, // saturation pressure at 30°C (from chart)
|
||||||
|
refrigerant: 'R448a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling - 5)).toBeLessThan(0.2); // Should be ~5K subcooling
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate subcooling correctly at 40°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 35, // 5K below saturation temp of 40°C
|
||||||
|
pressure: 258.0, // saturation pressure at 40°C (from chart)
|
||||||
|
refrigerant: 'R448a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling - 5)).toBeLessThan(0.2); // Should be ~5K subcooling
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate zero superheat at saturation point', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 0, // Exact saturation temperature
|
||||||
|
pressure: 60.1, // Matching saturation pressure from chart
|
||||||
|
refrigerant: 'R448a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat)).toBeLessThan(0.2); // Should be ~0K superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate zero subcooling at saturation point', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 20, // Exact saturation temperature
|
||||||
|
pressure: 148.5, // Matching saturation pressure from chart
|
||||||
|
refrigerant: 'R448a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling)).toBeLessThan(0.2); // Should be ~0K subcooling
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('It should also work with R448A (capital A)', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 20, // Exact saturation temperature
|
||||||
|
pressure: 148.5, // Matching saturation pressure from chart
|
||||||
|
refrigerant: 'R448A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling)).toBeLessThan(0.2); // Should be ~0K subcooling
|
||||||
|
});
|
||||||
|
});
|
||||||
94
src/coolprop-node/test/R449a.test.js
Normal file
94
src/coolprop-node/test/R449a.test.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
const coolprop = require('../src/index.js');
|
||||||
|
|
||||||
|
describe('R449a Real Values', () => {
|
||||||
|
it('should calculate superheat correctly at -40°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: -35, // 5K above saturation temp of -40°C
|
||||||
|
pressure: 0, // saturation pressure at -40°C (from chart)
|
||||||
|
refrigerant: 'R449a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat - 5)).toBeLessThan(0.2); // Should be ~5K superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate superheat correctly at -20°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: -15, // 5K above saturation temp of -20°C
|
||||||
|
pressure: 20.96, // saturation pressure at -20°C (from chart)
|
||||||
|
refrigerant: 'R449a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
//console.log(result);
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat - 5)).toBeLessThan(0.2); // Should be ~5K superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate subcooling correctly at 30°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 25, // 5K below saturation temp of 30°C
|
||||||
|
pressure: 195, // saturation pressure at 30°C (from chart)
|
||||||
|
refrigerant: 'R449a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling - 5)).toBeLessThan(0.2); // Should be ~5K subcooling
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate subcooling correctly at 40°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 35, // 5K below saturation temp of 40°C
|
||||||
|
pressure: 254.2, // saturation pressure at 40°C (from chart)
|
||||||
|
refrigerant: 'R449a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling - 5)).toBeLessThan(0.2); // Should be ~5K subcooling
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate zero superheat at saturation point', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 0, // Exact saturation temperature
|
||||||
|
pressure: 74.05, // Matching saturation pressure from chart
|
||||||
|
refrigerant: 'R449a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat)).toBeLessThan(0.2); // Should be ~0K superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate zero subcooling at saturation point', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 20, // Exact saturation temperature
|
||||||
|
pressure: 146.0, // Matching saturation pressure from chart
|
||||||
|
refrigerant: 'R449a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling)).toBeLessThan(0.2); // Should be ~0K subcooling
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('It should also work with R449A (capital A)', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 20, // Exact saturation temperature
|
||||||
|
pressure: 146.0, // Matching saturation pressure from chart
|
||||||
|
refrigerant: 'R449A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling)).toBeLessThan(0.2); // Should be ~0K subcooling
|
||||||
|
});
|
||||||
|
});
|
||||||
97
src/coolprop-node/test/R507.test.js
Normal file
97
src/coolprop-node/test/R507.test.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
const coolprop = require('../src/index.js');
|
||||||
|
|
||||||
|
describe('R507 Real Values', () => {
|
||||||
|
it('should calculate superheat correctly at -40°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: -35, // 5K above saturation temp of -40°C
|
||||||
|
pressure: 5.4, // saturation pressure at -40°C (from chart)
|
||||||
|
refrigerant: 'R507a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat - 5)).toBeLessThan(0.1); // Should be ~5K superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate superheat correctly at -20°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: -15, // 5K above saturation temp of -20°C
|
||||||
|
pressure: 30.9, // saturation pressure at -20°C (from chart)
|
||||||
|
refrigerant: 'R507a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat - 5)).toBeLessThan(0.1); // Should be ~5K superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate subcooling correctly at 30°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 25, // 5K below saturation temp of 30°C
|
||||||
|
pressure: 196.9, // saturation pressure at 30°C (from chart)
|
||||||
|
refrigerant: 'R507a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling - 5)).toBeLessThan(0.1); // Should be ~5K subcooling
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
it('should calculate subcooling correctly at 40°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 35, // 5K below saturation temp of 40°C
|
||||||
|
pressure: 256.2, // saturation pressure at 40°C (from chart)
|
||||||
|
refrigerant: 'R507a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling - 5)).toBeLessThan(0.1); // Should be ~5K subcooling
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate zero superheat at saturation point', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 0, // Exact saturation temperature
|
||||||
|
pressure: 75.8, // Matching saturation pressure from chart
|
||||||
|
refrigerant: 'R507a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat)).toBeLessThan(0.1); // Should be ~0K superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate zero subcooling at saturation point', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 20, // Exact saturation temperature
|
||||||
|
pressure: 148, // Matching saturation pressure from chart
|
||||||
|
refrigerant: 'R507a',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling)).toBeLessThan(0.1); // Should be ~0K subcooling
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate subcooling correctly at 30°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 25, // 5K below saturation temp of 30°C
|
||||||
|
pressure: 196.9, // saturation pressure at 30°C (from chart)
|
||||||
|
refrigerant: 'R507',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('error');
|
||||||
|
expect(result.message).toBe('Subcooling is infinity');
|
||||||
|
expect(result.note).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
55
src/coolprop-node/test/R744.C..test.js
Normal file
55
src/coolprop-node/test/R744.C..test.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
const coolprop = require('../src/index.js');
|
||||||
|
|
||||||
|
describe('R744 (CO2) Real Values', () => {
|
||||||
|
it('should calculate superheat correctly at -40°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: -35, // 5K above saturation temp of -40°C
|
||||||
|
pressure: 9.03, // saturation pressure at -40°C (from chart)
|
||||||
|
refrigerant: 'R744',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat - 5)).toBeLessThan(0.1); // Should be ~5K superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate subcooling correctly at 0°C saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: -5, // 5K below saturation temp of 0°C
|
||||||
|
pressure: 33.84, // saturation pressure at 0°C (from chart)
|
||||||
|
refrigerant: 'R744',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling - 5)).toBeLessThan(0.1); // Should be ~5K subcooling
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate zero superheat at saturation point', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: -20, // Exact saturation temperature
|
||||||
|
pressure: 18.68, // Matching saturation pressure from chart
|
||||||
|
refrigerant: 'R744',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat)).toBeLessThan(0.1); // Should be ~0K superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate zero subcooling at saturation point', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 10, // Exact saturation temperature
|
||||||
|
pressure: 44.01, // Matching saturation pressure from chart
|
||||||
|
refrigerant: 'R744',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling)).toBeLessThan(0.1); // Should be ~0K subcooling
|
||||||
|
});
|
||||||
|
});
|
||||||
55
src/coolprop-node/test/R744.F.test.js
Normal file
55
src/coolprop-node/test/R744.F.test.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
const coolprop = require('../src/index.js');
|
||||||
|
|
||||||
|
describe('R744 (CO2) Real Values', () => {
|
||||||
|
it('should calculate superheat correctly at -40°F saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: -35, // 5°F above saturation temp of -40°F
|
||||||
|
pressure: 131, // saturation pressure at -40°F (from chart)
|
||||||
|
refrigerant: 'R744',
|
||||||
|
tempUnit: 'F', // Changed to F
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat - 5)).toBeLessThan(0.1); // Should be ~5°F superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate subcooling correctly at 32°F saturation', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 27, // 5°F below saturation temp of 32°F
|
||||||
|
pressure: 490.8, // saturation pressure at 32°F (from chart)
|
||||||
|
refrigerant: 'R744',
|
||||||
|
tempUnit: 'F', // Changed to F
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling - 5)).toBeLessThan(0.1); // Should be ~5°F subcooling
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate zero superheat at saturation point', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 32, // Exact saturation temperature
|
||||||
|
pressure: 490.8, // Matching saturation pressure from chart
|
||||||
|
refrigerant: 'R744',
|
||||||
|
tempUnit: 'F', // Changed to F
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.superheat)).toBeLessThan(0.1); // Should be ~0°F superheat
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate zero subcooling at saturation point', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 32, // Exact saturation temperature
|
||||||
|
pressure: 490.8, // Matching saturation pressure from chart
|
||||||
|
refrigerant: 'R744',
|
||||||
|
tempUnit: 'F', // Changed to F
|
||||||
|
pressureUnit: 'psig'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(Math.abs(result.subcooling)).toBeLessThan(0.1); // Should be ~0°F subcooling
|
||||||
|
});
|
||||||
|
});
|
||||||
296
src/coolprop-node/test/nodeprop.test.js
Normal file
296
src/coolprop-node/test/nodeprop.test.js
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
const coolprop = require('../src/index.js');
|
||||||
|
|
||||||
|
describe('CoolProp Wrapper', () => {
|
||||||
|
describe('Initialization', () => {
|
||||||
|
it('should fail without refrigerant', async () => {
|
||||||
|
const result = await coolprop.init({});
|
||||||
|
expect(result.type).toBe('error');
|
||||||
|
expect(result.message).toContain('Refrigerant must be specified');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail with invalid temperature unit', async () => {
|
||||||
|
const result = await coolprop.init({ refrigerant: 'R404A', tempUnit: 'X' });
|
||||||
|
expect(result.type).toBe('error');
|
||||||
|
expect(result.message).toContain('Invalid temperature unit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail with invalid pressure unit', async () => {
|
||||||
|
const result = await coolprop.init({ refrigerant: 'R404A', pressureUnit: 'X' });
|
||||||
|
expect(result.type).toBe('error');
|
||||||
|
expect(result.message).toContain('Invalid pressure unit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should succeed with valid config', async () => {
|
||||||
|
const result = await coolprop.init({
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
console.log(result);
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Auto-initialization', () => {
|
||||||
|
it('should work without explicit init', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 25,
|
||||||
|
pressure: 10,
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(result.superheat).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Unit Conversions', () => {
|
||||||
|
it('should correctly convert temperature units', async () => {
|
||||||
|
const resultC = await coolprop.getSaturationTemperature({
|
||||||
|
pressure: 10,
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
pressureUnit: 'bar',
|
||||||
|
tempUnit: 'C'
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultF = await coolprop.getSaturationTemperature({
|
||||||
|
pressure: 10,
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
pressureUnit: 'bar',
|
||||||
|
tempUnit: 'F'
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultK = await coolprop.getSaturationTemperature({
|
||||||
|
pressure: 10,
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
pressureUnit: 'bar',
|
||||||
|
tempUnit: 'K'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Math.abs((resultC.temperature * 9/5 + 32) - resultF.temperature)).toBeLessThan(0.01);
|
||||||
|
expect(Math.abs((resultC.temperature + 273.15) - resultK.temperature)).toBeLessThan(0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly convert pressure units', async () => {
|
||||||
|
const resultBar = await coolprop.getSaturationPressure({
|
||||||
|
temperature: 25,
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultPsi = await coolprop.getSaturationPressure({
|
||||||
|
temperature: 25,
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'psi'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Math.abs((resultBar.pressure * 14.5038) - resultPsi.pressure)).toBeLessThan(0.1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Refrigerant Calculations', () => {
|
||||||
|
const refrigerants = ['R404A', 'R134a', 'R507A', 'R744'];
|
||||||
|
|
||||||
|
refrigerants.forEach(refrigerant => {
|
||||||
|
describe(refrigerant, () => {
|
||||||
|
it('should calculate superheat', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 25,
|
||||||
|
pressure: 10,
|
||||||
|
refrigerant,
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(result.superheat).toBeDefined();
|
||||||
|
expect(result.refrigerant).toBe(refrigerant);
|
||||||
|
expect(result.units).toEqual(expect.objectContaining({
|
||||||
|
temperature: 'C',
|
||||||
|
pressure: 'bar'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate subcooling', async () => {
|
||||||
|
const result = await coolprop.calculateSubcooling({
|
||||||
|
temperature: 20,
|
||||||
|
pressure: 20,
|
||||||
|
refrigerant,
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(result.subcooling).toBeDefined();
|
||||||
|
expect(result.refrigerant).toBe(refrigerant);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get all properties', async () => {
|
||||||
|
const result = await coolprop.getProperties({
|
||||||
|
temperature: 25,
|
||||||
|
pressure: 10,
|
||||||
|
refrigerant,
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
expect(result.type).toBe('success');
|
||||||
|
expect(result.properties).toBeDefined();
|
||||||
|
expect(result.refrigerant).toBe(refrigerant);
|
||||||
|
|
||||||
|
// Check all required properties exist
|
||||||
|
const requiredProps = [
|
||||||
|
'temperature', 'pressure', 'density', 'enthalpy',
|
||||||
|
'entropy', 'quality', 'conductivity', 'viscosity', 'specificHeat'
|
||||||
|
];
|
||||||
|
requiredProps.forEach(prop => {
|
||||||
|
expect(result.properties[prop]).toBeDefined();
|
||||||
|
expect(typeof result.properties[prop]).toBe('number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Default Override Behavior', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await coolprop.init({
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use defaults when no overrides provided', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 25,
|
||||||
|
pressure: 10
|
||||||
|
});
|
||||||
|
expect(result.refrigerant).toBe('R404A');
|
||||||
|
expect(result.units.temperature).toBe('C');
|
||||||
|
expect(result.units.pressure).toBe('bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow refrigerant override', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 25,
|
||||||
|
pressure: 10,
|
||||||
|
refrigerant: 'R134a'
|
||||||
|
});
|
||||||
|
expect(result.refrigerant).toBe('R134a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow unit overrides', async () => {
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 77,
|
||||||
|
pressure: 145,
|
||||||
|
tempUnit: 'F',
|
||||||
|
pressureUnit: 'psi'
|
||||||
|
});
|
||||||
|
expect(result.units.temperature).toBe('F');
|
||||||
|
expect(result.units.pressure).toBe('psi');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Default Settings Management', () => {
|
||||||
|
it('should allow updating defaults after initialization', async () => {
|
||||||
|
// Initial setup
|
||||||
|
await coolprop.init({
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update defaults
|
||||||
|
const updateResult = await coolprop.init({
|
||||||
|
refrigerant: 'R134a',
|
||||||
|
tempUnit: 'F',
|
||||||
|
pressureUnit: 'psi'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateResult.type).toBe('success');
|
||||||
|
expect(updateResult.message).toBe('Default settings updated');
|
||||||
|
|
||||||
|
// Verify new defaults are used
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 77,
|
||||||
|
pressure: 145
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.refrigerant).toBe('R134a');
|
||||||
|
expect(result.units.temperature).toBe('F');
|
||||||
|
expect(result.units.pressure).toBe('psi');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the coolprop instance if refrigerant is changed', async () => {
|
||||||
|
// Set initial defaults
|
||||||
|
await coolprop.init({
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = await coolprop.getConfig();
|
||||||
|
|
||||||
|
// First call with overrides
|
||||||
|
const result1 = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 25,
|
||||||
|
pressure: 10,
|
||||||
|
refrigerant: 'R507A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Second call using defaults
|
||||||
|
const result2 = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 25,
|
||||||
|
pressure: 10
|
||||||
|
});
|
||||||
|
const config2 = await coolprop.getConfig();
|
||||||
|
expect(config.refrigerant).toBe('R404A');
|
||||||
|
expect(config2.refrigerant).toBe('R507A');
|
||||||
|
expect(result1.refrigerant).toBe('R507A');
|
||||||
|
expect(result2.refrigerant).toBe('R507A');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow partial updates of defaults', async () => {
|
||||||
|
// Initial setup
|
||||||
|
await coolprop.init({
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update only temperature unit
|
||||||
|
await coolprop.init({
|
||||||
|
tempUnit: 'F'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await coolprop.calculateSuperheat({
|
||||||
|
temperature: 77,
|
||||||
|
pressure: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.refrigerant).toBe('R404A'); // unchanged
|
||||||
|
expect(result.units.temperature).toBe('F'); // updated
|
||||||
|
expect(result.units.pressure).toBe('bar'); // unchanged
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate units when updating defaults', async () => {
|
||||||
|
await coolprop.init({
|
||||||
|
refrigerant: 'R404A',
|
||||||
|
tempUnit: 'C',
|
||||||
|
pressureUnit: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await coolprop.init({
|
||||||
|
tempUnit: 'X' // invalid unit
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.type).toBe('error');
|
||||||
|
expect(result.message).toContain('Invalid temperature unit');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
58
src/coolprop-node/test/pressure-conversions.test.js
Normal file
58
src/coolprop-node/test/pressure-conversions.test.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const coolprop = require('../src/index.js');
|
||||||
|
|
||||||
|
describe('Pressure Conversion Chain Tests', () => {
|
||||||
|
|
||||||
|
test('bar -> pa -> bara -> pa -> bar conversion chain', () => {
|
||||||
|
const startValue = 2; // 2 bar gauge
|
||||||
|
|
||||||
|
const toPa = coolprop._convertPressureToPa(startValue, 'bar');
|
||||||
|
// console.log('bar to Pa:', toPa);
|
||||||
|
|
||||||
|
const toBara = coolprop._convertPressureFromPa(toPa, 'bara');
|
||||||
|
// console.log('Pa to bara:', toBara);
|
||||||
|
|
||||||
|
const backToPa = coolprop._convertPressureToPa(toBara, 'bara');
|
||||||
|
// console.log('bara to Pa:', backToPa);
|
||||||
|
|
||||||
|
const backToBar = coolprop._convertPressureFromPa(backToPa, 'bar');
|
||||||
|
// console.log('Pa to bar:', backToBar);
|
||||||
|
|
||||||
|
expect(Math.round(backToBar * 1000) / 1000).toBe(startValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('psi -> pa -> psia -> pa -> psi conversion chain', () => {
|
||||||
|
const startValue = 30; // 30 psi gauge
|
||||||
|
|
||||||
|
const toPa = coolprop._convertPressureToPa(startValue, 'psi');
|
||||||
|
// console.log('psi to Pa:', toPa);
|
||||||
|
|
||||||
|
const toPsia = coolprop._convertPressureFromPa(toPa, 'psia');
|
||||||
|
// console.log('Pa to psia:', toPsia);
|
||||||
|
|
||||||
|
const backToPa = coolprop._convertPressureToPa(toPsia, 'psia');
|
||||||
|
// console.log('psia to Pa:', backToPa);
|
||||||
|
|
||||||
|
const backToPsi = coolprop._convertPressureFromPa(backToPa, 'psi');
|
||||||
|
// console.log('Pa to psi:', backToPsi);
|
||||||
|
|
||||||
|
expect(Math.round(backToPsi * 1000) / 1000).toBe(startValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('kpa -> pa -> kpaa -> pa -> kpa conversion chain', () => {
|
||||||
|
const startValue = 200; // 200 kPa gauge
|
||||||
|
|
||||||
|
const toPa = coolprop._convertPressureToPa(startValue, 'kpa');
|
||||||
|
// console.log('kpa to Pa:', toPa);
|
||||||
|
|
||||||
|
const toKpaa = coolprop._convertPressureFromPa(toPa, 'kpaa');
|
||||||
|
// console.log('Pa to kpaa:', toKpaa);
|
||||||
|
|
||||||
|
const backToPa = coolprop._convertPressureToPa(toKpaa, 'kpaa');
|
||||||
|
// console.log('kpaa to Pa:', backToPa);
|
||||||
|
|
||||||
|
const backToKpa = coolprop._convertPressureFromPa(backToPa, 'kpa');
|
||||||
|
// console.log('Pa to kpa:', backToKpa);
|
||||||
|
|
||||||
|
expect(Math.round(backToKpa * 1000) / 1000).toBe(startValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
50
src/coolprop-node/test/propsSI.test.js
Normal file
50
src/coolprop-node/test/propsSI.test.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
const coolProp = require('../src/index.js');
|
||||||
|
|
||||||
|
describe('PropsSI Direct Access', () => {
|
||||||
|
let PropsSI;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Get the PropsSI function
|
||||||
|
PropsSI = await coolProp.getPropsSI();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should initialize and return PropsSI function', async () => {
|
||||||
|
expect(typeof PropsSI).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should calculate saturation temperature of R134a at 1 bar', () => {
|
||||||
|
const pressure = 100000; // 1 bar in Pa
|
||||||
|
const temp = PropsSI('T', 'P', pressure, 'Q', 0, 'R134a');
|
||||||
|
expect(temp).toBeCloseTo(246.79, 1); // ~246.79 K at 1 bar
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should calculate density of R134a at specific conditions', () => {
|
||||||
|
const temp = 300; // 300 K
|
||||||
|
const pressure = 100000; // 1 bar in Pa
|
||||||
|
const density = PropsSI('D', 'T', temp, 'P', pressure, 'R134a');
|
||||||
|
expect(density).toBeGreaterThan(0)
|
||||||
|
expect(density).toBeLessThan(Infinity);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error for invalid refrigerant', () => {
|
||||||
|
const temp = 300;
|
||||||
|
const pressure = 100000;
|
||||||
|
expect(() => {
|
||||||
|
let result = PropsSI('D', 'T', temp, 'P', pressure, 'INVALID_REFRIGERANT');
|
||||||
|
if(result == Infinity) {
|
||||||
|
throw new Error('Infinity due to invalid refrigerant');
|
||||||
|
}
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error for invalid input parameter', () => {
|
||||||
|
const temp = 300;
|
||||||
|
const pressure = 100000;
|
||||||
|
expect(() => {
|
||||||
|
let result = PropsSI('INVALID_PARAM', 'T', temp, 'P', pressure, 'R134a');
|
||||||
|
if(result == Infinity) {
|
||||||
|
throw new Error('Infinity due to invalid input parameter');
|
||||||
|
}
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
128
src/coolprop-node/test/temperature-conversions.test.js
Normal file
128
src/coolprop-node/test/temperature-conversions.test.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
const coolprop = require('../src/index.js');
|
||||||
|
|
||||||
|
describe('Temperature Conversion Tests', () => {
|
||||||
|
|
||||||
|
describe('Regular Temperature Conversions', () => {
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
startUnit: 'C',
|
||||||
|
startValue: 25,
|
||||||
|
expectedK: 298.15,
|
||||||
|
conversions: {
|
||||||
|
F: 77,
|
||||||
|
K: 298.15,
|
||||||
|
C: 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startUnit: 'F',
|
||||||
|
startValue: 77,
|
||||||
|
expectedK: 298.15,
|
||||||
|
conversions: {
|
||||||
|
F: 77,
|
||||||
|
K: 298.15,
|
||||||
|
C: 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startUnit: 'K',
|
||||||
|
startValue: 298.15,
|
||||||
|
expectedK: 298.15,
|
||||||
|
conversions: {
|
||||||
|
F: 77,
|
||||||
|
K: 298.15,
|
||||||
|
C: 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ startUnit, startValue, expectedK, conversions }) => {
|
||||||
|
test(`${startValue}${startUnit} conversion chain`, () => {
|
||||||
|
// First convert to Kelvin
|
||||||
|
const toK = coolprop._convertTempToK(startValue, startUnit);
|
||||||
|
expect(Math.round(toK * 100) / 100).toBe(expectedK);
|
||||||
|
|
||||||
|
// Then convert from Kelvin to each unit
|
||||||
|
Object.entries(conversions).forEach(([unit, expected]) => {
|
||||||
|
const converted = coolprop._convertTempFromK(toK, unit);
|
||||||
|
expect(Math.round(converted * 100) / 100).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Delta Temperature Conversions', () => {
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
startValue: 10, // 10K temperature difference
|
||||||
|
expected: {
|
||||||
|
K: 10,
|
||||||
|
C: 10,
|
||||||
|
F: 18 // 10K = 18°F difference
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ startValue, expected }) => {
|
||||||
|
test(`${startValue}K delta conversion to all units`, () => {
|
||||||
|
Object.entries(expected).forEach(([unit, expectedValue]) => {
|
||||||
|
const converted = coolprop._convertDeltaTempFromK(startValue, unit);
|
||||||
|
expect(Math.round(converted * 100) / 100).toBe(expectedValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Common Temperature Points', () => {
|
||||||
|
const commonPoints = [
|
||||||
|
{
|
||||||
|
description: 'Water freezing point',
|
||||||
|
C: 0,
|
||||||
|
F: 32,
|
||||||
|
K: 273.15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Water boiling point',
|
||||||
|
C: 100,
|
||||||
|
F: 212,
|
||||||
|
K: 373.15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Room temperature',
|
||||||
|
C: 20,
|
||||||
|
F: 68,
|
||||||
|
K: 293.15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Typical refrigeration evaporator',
|
||||||
|
C: 5,
|
||||||
|
F: 41,
|
||||||
|
K: 278.15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Typical refrigeration condenser',
|
||||||
|
C: 35,
|
||||||
|
F: 95,
|
||||||
|
K: 308.15
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
commonPoints.forEach(point => {
|
||||||
|
test(`${point.description} conversions`, () => {
|
||||||
|
// Test conversion to Kelvin from each unit
|
||||||
|
const fromC = coolprop._convertTempToK(point.C, 'C');
|
||||||
|
const fromF = coolprop._convertTempToK(point.F, 'F');
|
||||||
|
|
||||||
|
expect(Math.round(fromC * 100) / 100).toBe(point.K);
|
||||||
|
expect(Math.round(fromF * 100) / 100).toBe(point.K);
|
||||||
|
|
||||||
|
// Test conversion from Kelvin to each unit
|
||||||
|
const toC = coolprop._convertTempFromK(point.K, 'C');
|
||||||
|
const toF = coolprop._convertTempFromK(point.K, 'F');
|
||||||
|
|
||||||
|
expect(Math.round(toC * 100) / 100).toBe(point.C);
|
||||||
|
expect(Math.round(toF * 100) / 100).toBe(point.F);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
29
src/helper/assertionUtils.js
Normal file
29
src/helper/assertionUtils.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* @file assertionUtils.js
|
||||||
|
*
|
||||||
|
* Utility functions for assertions and throwing errors in EVOLV.
|
||||||
|
*
|
||||||
|
* @description This module provides functions to assert conditions and throw errors when those conditions are not met.
|
||||||
|
* @exports ValidationUtils
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Assertions {
|
||||||
|
/**
|
||||||
|
* Assert that no NaN values are present in an array.
|
||||||
|
* @param {Array} arr - The array to check for NaN values.
|
||||||
|
* @param {string} label - Array label to indicate where the error occurs.
|
||||||
|
*/
|
||||||
|
assertNoNaN(arr, label = "array") {
|
||||||
|
if (Array.isArray(arr)) {
|
||||||
|
for (const el of arr) {
|
||||||
|
assertNoNaN(el, label);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Number.isNaN(arr)) {
|
||||||
|
throw new Error(`NaN detected in ${label}!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Assertions;
|
||||||
@@ -1,243 +1,98 @@
|
|||||||
// ChildRegistrationUtils.js
|
|
||||||
class ChildRegistrationUtils {
|
class ChildRegistrationUtils {
|
||||||
constructor(mainClass) {
|
constructor(mainClass) {
|
||||||
this.mainClass = mainClass; // Reference to the main class
|
this.mainClass = mainClass;
|
||||||
this.logger = mainClass.logger;
|
this.logger = mainClass.logger;
|
||||||
|
this.registeredChildren = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerChild(child, positionVsParent) {
|
async registerChild(child, positionVsParent, distance) {
|
||||||
const { softwareType } = child.config.functionality;
|
const { softwareType } = child.config.functionality;
|
||||||
const { name, id, unit } = child.config.general;
|
const { name, id } = child.config.general;
|
||||||
const { type = "", subType = "" } = child.config.asset || {};
|
|
||||||
const emitter = child.emitter;
|
|
||||||
|
|
||||||
//define position vs parent in child
|
this.logger.debug(`Registering child: ${name} (${id}) as ${softwareType} at ${positionVsParent}`);
|
||||||
child.positionVsParent = positionVsParent;
|
|
||||||
child.parent = this.mainClass;
|
|
||||||
|
|
||||||
if (!this.mainClass.child) this.mainClass.child = {};
|
|
||||||
if (!this.mainClass.child[softwareType])
|
|
||||||
this.mainClass.child[softwareType] = {};
|
|
||||||
if (!this.mainClass.child[softwareType][type])
|
|
||||||
this.mainClass.child[softwareType][type] = {};
|
|
||||||
if (!this.mainClass.child[softwareType][type][subType])
|
|
||||||
this.mainClass.child[softwareType][type][subType] = {};
|
|
||||||
|
|
||||||
// Use an array to handle multiple subtypes
|
|
||||||
if (!Array.isArray(this.mainClass.child[softwareType][type][subType])) {
|
|
||||||
this.mainClass.child[softwareType][type][subType] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the child in the cloud when available and supply the new child on base of tagcode OLIFANT WE NEED TO FIX THIS SO ITS DYNAMIC!
|
|
||||||
/*
|
|
||||||
try{
|
|
||||||
const url = "https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api/asset/create_asset.php?";
|
|
||||||
const TagCode = child.config.asset.tagCode;
|
|
||||||
//console.log(`Register child => ${TagCode}`);
|
|
||||||
const completeURL = url + `asset_product_model_id=1&asset_product_model_uuid=123456789&asset_name=AssetNaam&asset_description=Beschrijving&asset_status=actief&asset_profile_id=1&asset_location_id=1&asset_process_id=11&asset_tag_number=${TagCode}&child_assets=[L6616]`;
|
|
||||||
|
|
||||||
await fetch(completeURL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}catch(e){
|
|
||||||
console.log("Error saving assetID and tagnumber", e);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// Push the new child to the array of the mainclass so we can track the childs
|
|
||||||
this.mainClass.child[softwareType][type][subType].push({
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
unit,
|
|
||||||
emitter,
|
|
||||||
});
|
|
||||||
|
|
||||||
//then connect the child depending on the type subtype etc..
|
|
||||||
this.connectChild(
|
|
||||||
id,
|
|
||||||
softwareType,
|
|
||||||
emitter,
|
|
||||||
type,
|
|
||||||
child,
|
|
||||||
subType,
|
|
||||||
positionVsParent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
connectChild(
|
|
||||||
id,
|
|
||||||
softwareType,
|
|
||||||
emitter,
|
|
||||||
type,
|
|
||||||
child,
|
|
||||||
subType,
|
|
||||||
positionVsParent
|
|
||||||
) {
|
|
||||||
this.logger.debug(
|
|
||||||
`Connecting child id=${id}: desc=${softwareType}, type=${type},subType=${subType}, position=${positionVsParent}`
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (softwareType) {
|
|
||||||
case "measurement":
|
|
||||||
this.logger.debug(
|
|
||||||
`Registering measurement child: ${id} with type=${type}`
|
|
||||||
);
|
|
||||||
this.connectMeasurement(child, subType, positionVsParent);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "machine":
|
|
||||||
this.logger.debug(`Registering complete machine child: ${id}`);
|
|
||||||
this.connectMachine(child);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "valve":
|
|
||||||
this.logger.debug(`Registering complete valve child: ${id}`);
|
|
||||||
this.connectValve(child);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "actuator":
|
|
||||||
this.logger.debug(`Registering linear actuator child: ${id}`);
|
|
||||||
this.connectActuator(child,positionVsParent);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
this.logger.error(`Child registration unrecognized desc: ${desc}`);
|
|
||||||
this.logger.error(`Unrecognized softwareType: ${softwareType}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectMeasurement(child, subType, position) {
|
|
||||||
this.logger.debug(
|
|
||||||
`Connecting measurement child: ${subType} with position=${position}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if subType is valid
|
|
||||||
if (!subType) {
|
|
||||||
this.logger.error(`Invalid subType for measurement: ${subType}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize the measurement to a number - logging each step for debugging
|
|
||||||
try {
|
|
||||||
this.logger.debug(
|
|
||||||
`Initializing measurement: ${subType}, position: ${position} value: 0`
|
|
||||||
);
|
|
||||||
const typeResult = this.mainClass.measurements.type(subType);
|
|
||||||
const variantResult = typeResult.variant("measured");
|
|
||||||
const positionResult = variantResult.position(position);
|
|
||||||
positionResult.value(0);
|
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
`Subscribing on mAbs event for measurement: ${subType}, position: ${position}`
|
|
||||||
);
|
|
||||||
// Listen for the mAbs event and update the measurement
|
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
`Successfully initialized measurement: ${subType}, position: ${position}`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to initialize measurement: ${error.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
child.emitter.on("mAbs", (value) => {
|
|
||||||
// Use the same method chaining approach that worked during initialization
|
|
||||||
this.mainClass.measurements
|
|
||||||
.type(subType)
|
|
||||||
.variant("measured")
|
|
||||||
.position(position)
|
|
||||||
.value(value);
|
|
||||||
this.mainClass.updateMeasurement("measured", subType, value, position);
|
|
||||||
//this.logger.debug(`--------->>>>>>>>>Updated measurement: ${subType}, value: ${value}, position: ${position}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
connectMachine(machine) {
|
|
||||||
if (!machine) {
|
|
||||||
this.logger.error("Invalid machine provided.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const machineId = Object.keys(this.mainClass.machines).length + 1;
|
|
||||||
this.mainClass.machines[machineId] = machine;
|
|
||||||
|
|
||||||
this.logger.info(
|
|
||||||
`Setting up pressureChange listener for machine ${machineId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
machine.emitter.on("pressureChange", () =>
|
|
||||||
this.mainClass.handlePressureChange(machine)
|
|
||||||
);
|
|
||||||
|
|
||||||
//update of child triggers the handler
|
|
||||||
this.mainClass.handleChildChange();
|
|
||||||
|
|
||||||
this.logger.info(`Machine ${machineId} registered successfully.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
connectValve(valve) {
|
|
||||||
if (!valve) {
|
|
||||||
this.logger.warn("Invalid valve provided.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const valveId = Object.keys(this.mainClass.valves).length + 1;
|
|
||||||
this.mainClass.valves[valveId] = valve; // Gooit valve object in de valves attribute met valve objects
|
|
||||||
|
|
||||||
valve.state.emitter.on("positionChange", (data) => {
|
|
||||||
//ValveGroupController abboneren op klepstand verandering
|
|
||||||
this.mainClass.logger.debug(`Position change of valve detected: ${data}`);
|
|
||||||
this.mainClass.calcValveFlows();
|
|
||||||
}); //bepaal nieuwe flow per valve
|
|
||||||
valve.emitter.on("deltaPChange", () => {
|
|
||||||
this.mainClass.logger.debug("DeltaP change of valve detected");
|
|
||||||
this.mainClass.calcMaxDeltaP();
|
|
||||||
}); //bepaal nieuwe max deltaP
|
|
||||||
|
|
||||||
this.logger.info(`Valve ${valveId} registered successfully.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
connectActuator(actuator, positionVsParent) {
|
|
||||||
if (!actuator) {
|
|
||||||
this.logger.warn("Invalid actuator provided.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Special case gateGroupControl
|
|
||||||
if (
|
|
||||||
this.mainClass.config.functionality.softwareType == "gateGroupControl"
|
|
||||||
) {
|
|
||||||
if (Object.keys(this.mainClass.actuators).length < 2) {
|
|
||||||
if (positionVsParent == "downstream") {
|
|
||||||
this.mainClass.actuators[0] = actuator;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (positionVsParent == "upstream") {
|
|
||||||
this.mainClass.actuators[1] = actuator;
|
|
||||||
}
|
|
||||||
//define emitters
|
|
||||||
actuator.state.emitter.on("positionChange", (data) => {
|
|
||||||
this.mainClass.logger.debug(`Position change of actuator detected: ${data}`);
|
|
||||||
this.mainClass.eventUpdate();
|
|
||||||
});
|
|
||||||
|
|
||||||
//define emitters
|
|
||||||
actuator.state.emitter.on("stateChange", (data) => {
|
|
||||||
this.mainClass.logger.debug(`State change of actuator detected: ${data}`);
|
|
||||||
this.mainClass.eventUpdate();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Enhanced child setup - multiple parents
|
||||||
|
if (Array.isArray(child.parent)) {
|
||||||
|
child.parent.push(this.mainClass);
|
||||||
} else {
|
} else {
|
||||||
this.logger.error(
|
child.parent = [this.mainClass];
|
||||||
"Too many actuators registered. Only two are allowed."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
child.positionVsParent = positionVsParent;
|
||||||
|
|
||||||
|
// Enhanced measurement container with rich context
|
||||||
|
if (child.measurements) {
|
||||||
|
child.measurements.setChildId(id);
|
||||||
|
child.measurements.setChildName(name);
|
||||||
|
child.measurements.setParentRef(this.mainClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
//wanneer hij deze ontvangt is deltaP van een van de valves veranderd (kan ook zijn niet child zijn, maar dat maakt niet uit)
|
// Store child in your expected structure
|
||||||
|
this._storeChild(child, softwareType);
|
||||||
|
|
||||||
|
// Track registration for utilities
|
||||||
|
this.registeredChildren.set(id, {
|
||||||
|
child,
|
||||||
|
softwareType,
|
||||||
|
position: positionVsParent,
|
||||||
|
registeredAt: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// IMPORTANT: Only call parent registration - no automatic handling and if parent has this function then try to register this child
|
||||||
|
if (typeof this.mainClass.registerChild === 'function') {
|
||||||
|
this.mainClass.registerChild(child, softwareType);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`✅ Child ${name} registered successfully`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_storeChild(child, softwareType) {
|
||||||
|
// Maintain your existing structure
|
||||||
|
if (!this.mainClass.child) this.mainClass.child = {};
|
||||||
|
if (!this.mainClass.child[softwareType]) this.mainClass.child[softwareType] = {};
|
||||||
|
|
||||||
|
const { category = "sensor" } = child.config.asset || {};
|
||||||
|
if (!this.mainClass.child[softwareType][category]) {
|
||||||
|
this.mainClass.child[softwareType][category] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mainClass.child[softwareType][category].push(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Utility methods for parent to use
|
||||||
|
getChildrenOfType(softwareType, category = null) {
|
||||||
|
if (!this.mainClass.child[softwareType]) return [];
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
return this.mainClass.child[softwareType][category] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all children of this software type
|
||||||
|
return Object.values(this.mainClass.child[softwareType]).flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildById(childId) {
|
||||||
|
return this.registeredChildren.get(childId)?.child || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllChildren() {
|
||||||
|
return Array.from(this.registeredChildren.values()).map(r => r.child);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Debugging utilities
|
||||||
|
logChildStructure() {
|
||||||
|
this.logger.debug('Current child structure:', JSON.stringify(
|
||||||
|
Object.keys(this.mainClass.child).reduce((acc, softwareType) => {
|
||||||
|
acc[softwareType] = Object.keys(this.mainClass.child[softwareType]).reduce((catAcc, category) => {
|
||||||
|
catAcc[category] = this.mainClass.child[softwareType][category].map(c => ({
|
||||||
|
id: c.config.general.id,
|
||||||
|
name: c.config.general.name
|
||||||
|
}));
|
||||||
|
return catAcc;
|
||||||
|
}, {});
|
||||||
|
return acc;
|
||||||
|
}, {}), null, 2
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ChildRegistrationUtils;
|
module.exports = ChildRegistrationUtils;
|
||||||
260
src/helper/childRegistrationUtils_DEPRECATED.js
Normal file
260
src/helper/childRegistrationUtils_DEPRECATED.js
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
// ChildRegistrationUtils.js
|
||||||
|
class ChildRegistrationUtils {
|
||||||
|
constructor(mainClass) {
|
||||||
|
this.mainClass = mainClass; // Reference to the main class
|
||||||
|
this.logger = mainClass.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerChild(child, positionVsParent) {
|
||||||
|
|
||||||
|
this.logger.debug(`Registering child: ${child.id} with position=${positionVsParent}`);
|
||||||
|
const { softwareType } = child.config.functionality;
|
||||||
|
const { name, id, unit } = child.config.general;
|
||||||
|
const { category = "", type = "" } = child.config.asset || {};
|
||||||
|
console.log(`Registering child: ${name}, id: ${id}, softwareType: ${softwareType}, category: ${category}, type: ${type}, positionVsParent: ${positionVsParent}` );
|
||||||
|
const emitter = child.emitter;
|
||||||
|
|
||||||
|
//define position vs parent in child
|
||||||
|
child.positionVsParent = positionVsParent;
|
||||||
|
child.parent = this.mainClass;
|
||||||
|
|
||||||
|
if (!this.mainClass.child) this.mainClass.child = {};
|
||||||
|
if (!this.mainClass.child[softwareType])
|
||||||
|
this.mainClass.child[softwareType] = {};
|
||||||
|
if (!this.mainClass.child[softwareType][category])
|
||||||
|
this.mainClass.child[softwareType][category] = {};
|
||||||
|
if (!this.mainClass.child[softwareType][category][type])
|
||||||
|
this.mainClass.child[softwareType][category][type] = {};
|
||||||
|
|
||||||
|
// Use an array to handle multiple categories
|
||||||
|
if (!Array.isArray(this.mainClass.child[softwareType][category][type])) {
|
||||||
|
this.mainClass.child[softwareType][category][type] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the new child to the array of the mainclass so we can track the childs
|
||||||
|
this.mainClass.child[softwareType][category][type].push({
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
unit,
|
||||||
|
emitter,
|
||||||
|
});
|
||||||
|
|
||||||
|
//then connect the child depending on the type type etc..
|
||||||
|
this.connectChild(
|
||||||
|
id,
|
||||||
|
softwareType,
|
||||||
|
emitter,
|
||||||
|
category,
|
||||||
|
child,
|
||||||
|
type,
|
||||||
|
positionVsParent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectChild(
|
||||||
|
id,
|
||||||
|
softwareType,
|
||||||
|
emitter,
|
||||||
|
category,
|
||||||
|
child,
|
||||||
|
type,
|
||||||
|
positionVsParent
|
||||||
|
) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Connecting child id=${id}: desc=${softwareType}, category=${category},type=${type}, position=${positionVsParent}`
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (softwareType) {
|
||||||
|
case "measurement":
|
||||||
|
this.logger.debug(
|
||||||
|
`Registering measurement child: ${id} with category=${category}`
|
||||||
|
);
|
||||||
|
this.connectMeasurement(child, type, positionVsParent);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "machine":
|
||||||
|
this.logger.debug(`Registering complete machine child: ${id}`);
|
||||||
|
this.connectMachine(child);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "valve":
|
||||||
|
this.logger.debug(`Registering complete valve child: ${id}`);
|
||||||
|
this.connectValve(child);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "machineGroup":
|
||||||
|
this.logger.debug(`Registering complete machineGroup child: ${id}`);
|
||||||
|
this.connectMachineGroup(child);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "actuator":
|
||||||
|
this.logger.debug(`Registering linear actuator child: ${id}`);
|
||||||
|
this.connectActuator(child,positionVsParent);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.logger.error(`Child registration unrecognized desc: ${desc}`);
|
||||||
|
this.logger.error(`Unrecognized softwareType: ${softwareType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectMeasurement(child, type, position) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Connecting measurement child: ${type} with position=${position}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if type is valid
|
||||||
|
if (!type) {
|
||||||
|
this.logger.error(`Invalid type for measurement: ${type}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize the measurement to a number - logging each step for debugging
|
||||||
|
try {
|
||||||
|
this.logger.debug(
|
||||||
|
`Initializing measurement: ${type}, position: ${position} value: 0`
|
||||||
|
);
|
||||||
|
const typeResult = this.mainClass.measurements.type(type);
|
||||||
|
const variantResult = typeResult.variant("measured");
|
||||||
|
const positionResult = variantResult.position(position);
|
||||||
|
positionResult.value(0);
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Subscribing on mAbs event for measurement: ${type}, position: ${position}`
|
||||||
|
);
|
||||||
|
// Listen for the mAbs event and update the measurement
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Successfully initialized measurement: ${type}, position: ${position}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to initialize measurement: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//testing new emitter strategy
|
||||||
|
child.measurements.emitter.on("newValue", (data) => {
|
||||||
|
this.logger.warn(
|
||||||
|
`Value change event received for measurement: ${type}, position: ${position}, value: ${data.value}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.emitter.on("mAbs", (value) => {
|
||||||
|
// Use the same method chaining approach that worked during initialization
|
||||||
|
this.mainClass.measurements
|
||||||
|
.type(type)
|
||||||
|
.variant("measured")
|
||||||
|
.position(position)
|
||||||
|
.value(value);
|
||||||
|
this.mainClass.updateMeasurement("measured", type, value, position);
|
||||||
|
//this.logger.debug(`--------->>>>>>>>>Updated measurement: ${type}, value: ${value}, position: ${position}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
connectMachine(machine) {
|
||||||
|
if (!machine) {
|
||||||
|
this.logger.error("Invalid machine provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const machineId = Object.keys(this.mainClass.machines).length + 1;
|
||||||
|
this.mainClass.machines[machineId] = machine;
|
||||||
|
|
||||||
|
this.logger.info(
|
||||||
|
`Setting up pressureChange listener for machine ${machineId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
machine.emitter.on("pressureChange", () =>
|
||||||
|
this.mainClass.handlePressureChange(machine)
|
||||||
|
);
|
||||||
|
|
||||||
|
//update of child triggers the handler
|
||||||
|
this.mainClass.handleChildChange();
|
||||||
|
|
||||||
|
this.logger.info(`Machine ${machineId} registered successfully.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectValve(valve) {
|
||||||
|
if (!valve) {
|
||||||
|
this.logger.warn("Invalid valve provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const valveId = Object.keys(this.mainClass.valves).length + 1;
|
||||||
|
this.mainClass.valves[valveId] = valve; // Gooit valve object in de valves attribute met valve objects
|
||||||
|
|
||||||
|
valve.state.emitter.on("positionChange", (data) => {
|
||||||
|
//ValveGroupController abboneren op klepstand verandering
|
||||||
|
this.mainClass.logger.debug(`Position change of valve detected: ${data}`);
|
||||||
|
this.mainClass.calcValveFlows();
|
||||||
|
}); //bepaal nieuwe flow per valve
|
||||||
|
valve.emitter.on("deltaPChange", () => {
|
||||||
|
this.mainClass.logger.debug("DeltaP change of valve detected");
|
||||||
|
this.mainClass.calcMaxDeltaP();
|
||||||
|
}); //bepaal nieuwe max deltaP
|
||||||
|
|
||||||
|
this.logger.info(`Valve ${valveId} registered successfully.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectMachineGroup(machineGroup) {
|
||||||
|
if (!machineGroup) {
|
||||||
|
this.logger.warn("Invalid machineGroup provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const machineGroupId = Object.keys(this.mainClass.machineGroups).length + 1;
|
||||||
|
this.mainClass.machineGroups[machineGroupId] = machineGroup;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Skip machinegroup connnection: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
machineGroup.emitter.on("totalFlowChange", (data) => {
|
||||||
|
this.mainClass.logger.debug('Total flow change of machineGroup detected');
|
||||||
|
this.mainClass.handleInput("parent", "totalFlowChange", data)}); //Geef nieuwe totale flow door aan valveGrouControl
|
||||||
|
|
||||||
|
this.logger.info(`MachineGroup ${machineGroup.config.general.name} registered successfully.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectActuator(actuator, positionVsParent) {
|
||||||
|
if (!actuator) {
|
||||||
|
this.logger.warn("Invalid actuator provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Special case gateGroupControl
|
||||||
|
if (
|
||||||
|
this.mainClass.config.functionality.softwareType == "gateGroupControl"
|
||||||
|
) {
|
||||||
|
if (Object.keys(this.mainClass.actuators).length < 2) {
|
||||||
|
if (positionVsParent == "downstream") {
|
||||||
|
this.mainClass.actuators[0] = actuator;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positionVsParent == "upstream") {
|
||||||
|
this.mainClass.actuators[1] = actuator;
|
||||||
|
}
|
||||||
|
//define emitters
|
||||||
|
actuator.state.emitter.on("positionChange", (data) => {
|
||||||
|
this.mainClass.logger.debug(`Position change of actuator detected: ${data}`);
|
||||||
|
this.mainClass.eventUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
//define emitters
|
||||||
|
actuator.state.emitter.on("stateChange", (data) => {
|
||||||
|
this.mainClass.logger.debug(`State change of actuator detected: ${data}`);
|
||||||
|
this.mainClass.eventUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.logger.error(
|
||||||
|
"Too many actuators registered. Only two are allowed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//wanneer hij deze ontvangt is deltaP van een van de valves veranderd (kan ook zijn niet child zijn, maar dat maakt niet uit)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ChildRegistrationUtils;
|
||||||
@@ -390,6 +390,12 @@ class ValidationUtils {
|
|||||||
return fieldSchema.default;
|
return fieldSchema.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for uppercase characters and convert to lowercase if present
|
||||||
|
if (newConfigValue !== newConfigValue.toLowerCase()) {
|
||||||
|
this.logger.warn(`${name}.${key} contains uppercase characters. Converting to lowercase: ${newConfigValue} -> ${newConfigValue.toLowerCase()}`);
|
||||||
|
newConfigValue = newConfigValue.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
return newConfigValue;
|
return newConfigValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
const convertModule = require('../convert/index');
|
const convertModule = require('../convert/index');
|
||||||
|
|
||||||
class Measurement {
|
class Measurement {
|
||||||
constructor(type, variant, position, windowSize) {
|
constructor(type, variant, position, windowSize, distance = null) {
|
||||||
this.type = type; // e.g. 'pressure', 'flow', etc.
|
this.type = type; // e.g. 'pressure', 'flow', etc.
|
||||||
this.variant = variant; // e.g. 'predicted' or 'measured', etc..
|
this.variant = variant; // e.g. 'predicted' or 'measured', etc..
|
||||||
this.position = position; // Downstream or upstream of parent object
|
this.position = position; // Downstream or upstream of parent object
|
||||||
this.windowSize = windowSize; // Rolling window size
|
this.windowSize = windowSize; // Rolling window size
|
||||||
|
this.distance = distance; // Distance from parent, if applicable
|
||||||
|
|
||||||
// Place all data inside an array
|
// Place all data inside an array
|
||||||
this.values = []; // Array to store all values
|
this.values = []; // Array to store all values
|
||||||
@@ -36,13 +37,12 @@ class Measurement {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
setValue(value, timestamp = Date.now()) {
|
setDistance(distance) {
|
||||||
/*
|
this.distance = distance;
|
||||||
if (value === undefined || value === null) {
|
return this;
|
||||||
value = null ;
|
|
||||||
//throw new Error('Value cannot be null or undefined');
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
setValue(value, timestamp = Date.now()) {
|
||||||
|
|
||||||
//shift the oldest value
|
//shift the oldest value
|
||||||
if(this.values.length >= this.windowSize){
|
if(this.values.length >= this.windowSize){
|
||||||
@@ -68,6 +68,23 @@ class Measurement {
|
|||||||
return this.values[this.values.length - 1];
|
return this.values[this.values.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLaggedValue(lag){
|
||||||
|
if(this.values.length <= lag) return null;
|
||||||
|
return this.values[this.values.length - lag];
|
||||||
|
}
|
||||||
|
|
||||||
|
getLaggedSample(lag){
|
||||||
|
if (lag < 0) throw new Error('lag must be >= 0');
|
||||||
|
const index = this.values.length - 1 - lag;
|
||||||
|
if (index < 0) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: this.values[index],
|
||||||
|
timestamp: this.timestamps[index],
|
||||||
|
unit: this.unit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
getAverage() {
|
getAverage() {
|
||||||
if (this.values.length === 0) return null;
|
if (this.values.length === 0) return null;
|
||||||
const sum = this.values.reduce((acc, val) => acc + val, 0);
|
const sum = this.values.reduce((acc, val) => acc + val, 0);
|
||||||
@@ -131,7 +148,6 @@ class Measurement {
|
|||||||
const downIndex = downValues.timestamps.indexOf(upTimestamp);
|
const downIndex = downValues.timestamps.indexOf(upTimestamp);
|
||||||
|
|
||||||
if (downIndex !== -1) {
|
if (downIndex !== -1) {
|
||||||
|
|
||||||
const diff = upValues.values[i] - downValues.values[downIndex];
|
const diff = upValues.values[i] - downValues.values[downIndex];
|
||||||
diffMeasurement.setValue(diff, upTimestamp);
|
diffMeasurement.setValue(diff, upTimestamp);
|
||||||
}
|
}
|
||||||
@@ -169,7 +185,8 @@ class Measurement {
|
|||||||
this.type,
|
this.type,
|
||||||
this.variant,
|
this.variant,
|
||||||
this.position,
|
this.position,
|
||||||
this.windowSize
|
this.windowSize,
|
||||||
|
this.distance
|
||||||
);
|
);
|
||||||
|
|
||||||
// Copy values and timestamps
|
// Copy values and timestamps
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class MeasurementBuilder {
|
|||||||
this.type = null;
|
this.type = null;
|
||||||
this.variant = null;
|
this.variant = null;
|
||||||
this.position = null;
|
this.position = null;
|
||||||
|
this.distance = null;
|
||||||
this.windowSize = 10; // Default window size
|
this.windowSize = 10; // Default window size
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +33,11 @@ class MeasurementBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDistance(distance) {
|
||||||
|
this.distance = distance;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!this.type) {
|
if (!this.type) {
|
||||||
@@ -43,13 +49,17 @@ class MeasurementBuilder {
|
|||||||
if (!this.position) {
|
if (!this.position) {
|
||||||
throw new Error('Measurement position is required');
|
throw new Error('Measurement position is required');
|
||||||
}
|
}
|
||||||
|
// distance is not a requirement as it can be derived from position
|
||||||
|
|
||||||
return new Measurement(
|
return new Measurement(
|
||||||
this.type,
|
this.type,
|
||||||
this.variant,
|
this.variant,
|
||||||
this.position,
|
this.position,
|
||||||
this.windowSize
|
this.windowSize,
|
||||||
|
this.distance
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
const MeasurementBuilder = require('./MeasurementBuilder');
|
const MeasurementBuilder = require('./MeasurementBuilder');
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const convertModule = require('../convert/index');
|
||||||
|
|
||||||
class MeasurementContainer {
|
class MeasurementContainer {
|
||||||
constructor(options = {}, logger) {
|
constructor(options = {},logger) {
|
||||||
this.logger = logger;
|
this.emitter = new EventEmitter();
|
||||||
this.measurements = {};
|
this.measurements = {};
|
||||||
this.windowSize = options.windowSize || 10; // Default window size
|
this.windowSize = options.windowSize || 10; // Default window size
|
||||||
|
|
||||||
@@ -10,6 +12,64 @@ class MeasurementContainer {
|
|||||||
this._currentType = null;
|
this._currentType = null;
|
||||||
this._currentVariant = null;
|
this._currentVariant = null;
|
||||||
this._currentPosition = null;
|
this._currentPosition = null;
|
||||||
|
this._currentDistance = null;
|
||||||
|
this._unit = null;
|
||||||
|
|
||||||
|
// Default units for each measurement type
|
||||||
|
this.defaultUnits = {
|
||||||
|
pressure: 'mbar',
|
||||||
|
flow: 'm3/h',
|
||||||
|
power: 'kW',
|
||||||
|
temperature: 'C',
|
||||||
|
volume: 'm3',
|
||||||
|
length: 'm',
|
||||||
|
...options.defaultUnits // Allow override
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-conversion settings
|
||||||
|
this.autoConvert = options.autoConvert !== false; // Default to true
|
||||||
|
this.preferredUnits = options.preferredUnits || {}; // Per-measurement overrides
|
||||||
|
|
||||||
|
// For chaining context
|
||||||
|
this._currentType = null;
|
||||||
|
this._currentVariant = null;
|
||||||
|
this._currentPosition = null;
|
||||||
|
this._unit = null;
|
||||||
|
|
||||||
|
// NEW: Enhanced child identification
|
||||||
|
this.childId = null;
|
||||||
|
this.childName = null;
|
||||||
|
this.parentRef = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Methods to set child context
|
||||||
|
setChildId(childId) {
|
||||||
|
this.childId = childId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setChildName(childName) {
|
||||||
|
this.childName = childName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setParentRef(parent) {
|
||||||
|
this.parentRef = parent;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method to set preferred units
|
||||||
|
setPreferredUnit(measurementType, unit) {
|
||||||
|
this.preferredUnits[measurementType] = unit;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the target unit for a measurement type
|
||||||
|
_getTargetUnit(measurementType) {
|
||||||
|
return this.preferredUnits[measurementType] ||
|
||||||
|
this.defaultUnits[measurementType] ||
|
||||||
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chainable methods
|
// Chainable methods
|
||||||
@@ -29,28 +89,137 @@ class MeasurementContainer {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
position(positionName) {
|
position(positionValue) {
|
||||||
if (!this._currentVariant) {
|
if (!this._currentVariant) {
|
||||||
throw new Error('Variant must be specified before position');
|
throw new Error('Variant must be specified before position');
|
||||||
}
|
}
|
||||||
this._currentPosition = positionName;
|
|
||||||
|
|
||||||
|
this._currentPosition = positionValue.toString().toLowerCase();;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core methods that complete the chain
|
distance(distance) {
|
||||||
value(val, timestamp = Date.now()) {
|
// If distance is not provided, derive from positionVsParent
|
||||||
|
if(distance === null) {
|
||||||
|
distance = this._convertPositionStr2Num(this._currentPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._currentDistance = distance;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENHANCED: Update your existing value method
|
||||||
|
value(val, timestamp = Date.now(), sourceUnit = null) {
|
||||||
if (!this._ensureChainIsValid()) return this;
|
if (!this._ensureChainIsValid()) return this;
|
||||||
|
|
||||||
const measurement = this._getOrCreateMeasurement();
|
const measurement = this._getOrCreateMeasurement();
|
||||||
measurement.setValue(val, timestamp);
|
const targetUnit = this._getTargetUnit(this._currentType);
|
||||||
|
|
||||||
|
let convertedValue = val;
|
||||||
|
let finalUnit = sourceUnit || targetUnit;
|
||||||
|
|
||||||
|
// Auto-convert if enabled and units are specified
|
||||||
|
if (this.autoConvert && sourceUnit && targetUnit && sourceUnit !== targetUnit) {
|
||||||
|
try {
|
||||||
|
convertedValue = convertModule(val).from(sourceUnit).to(targetUnit);
|
||||||
|
finalUnit = targetUnit;
|
||||||
|
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.debug(`Auto-converted ${val} ${sourceUnit} to ${convertedValue} ${targetUnit}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.warn(`Auto-conversion failed from ${sourceUnit} to ${targetUnit}: ${error.message}`);
|
||||||
|
}
|
||||||
|
convertedValue = val;
|
||||||
|
finalUnit = sourceUnit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
measurement.setValue(convertedValue, timestamp);
|
||||||
|
|
||||||
|
if (finalUnit && !measurement.unit) {
|
||||||
|
measurement.setUnit(finalUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENHANCED: Emit event with rich context
|
||||||
|
const eventData = {
|
||||||
|
value: convertedValue,
|
||||||
|
originalValue: val,
|
||||||
|
unit: finalUnit,
|
||||||
|
sourceUnit: sourceUnit,
|
||||||
|
timestamp,
|
||||||
|
position: this._currentPosition,
|
||||||
|
distance: this._currentDistance,
|
||||||
|
variant: this._currentVariant,
|
||||||
|
type: this._currentType,
|
||||||
|
// NEW: Enhanced context
|
||||||
|
childId: this.childId,
|
||||||
|
childName: this.childName,
|
||||||
|
parentRef: this.parentRef,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit the exact event your parent expects
|
||||||
|
this.emitter.emit(`${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
|
||||||
|
//console.log(`Emitted event: ${this._currentType}.${this._currentVariant}.${this._currentPosition}`, eventData);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a measurement series exists.
|
||||||
|
*
|
||||||
|
* You can rely on the current chain (type/variant/position already set via
|
||||||
|
* type().variant().position()), or pass them explicitly via the options.
|
||||||
|
*
|
||||||
|
* @param {object} options
|
||||||
|
* @param {string} [options.type] Override the current type
|
||||||
|
* @param {string} [options.variant] Override the current variant
|
||||||
|
* @param {string} [options.position] Override the current position
|
||||||
|
* @param {boolean} [options.requireValues=false]
|
||||||
|
* When true, the series must contain at least one stored value.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
exists({ type, variant, position, requireValues = false } = {}) {
|
||||||
|
const typeKey = type ?? this._currentType;
|
||||||
|
if (!typeKey) return false;
|
||||||
|
|
||||||
|
const variantKey = variant ?? this._currentVariant;
|
||||||
|
if (!variantKey) return false;
|
||||||
|
|
||||||
|
const positionKey = position ?? this._currentPosition;
|
||||||
|
|
||||||
|
const typeBucket = this.measurements[typeKey];
|
||||||
|
if (!typeBucket) return false;
|
||||||
|
|
||||||
|
const variantBucket = typeBucket[variantKey];
|
||||||
|
if (!variantBucket) return false;
|
||||||
|
|
||||||
|
if (!positionKey) {
|
||||||
|
// No specific position requested – just check the variant bucket.
|
||||||
|
return requireValues
|
||||||
|
? Object.values(variantBucket).some(m => m?.values?.length > 0)
|
||||||
|
: Object.keys(variantBucket).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const measurement = variantBucket[positionKey];
|
||||||
|
if (!measurement) return false;
|
||||||
|
|
||||||
|
return requireValues ? measurement.values?.length > 0 : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
unit(unitName) {
|
unit(unitName) {
|
||||||
if (!this._ensureChainIsValid()) return this;
|
if (!this._ensureChainIsValid()) return this;
|
||||||
|
|
||||||
const measurement = this._getOrCreateMeasurement();
|
const measurement = this._getOrCreateMeasurement();
|
||||||
measurement.setUnit(unitName);
|
measurement.setUnit(unitName);
|
||||||
|
this._unit = unitName;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,14 +229,52 @@ class MeasurementContainer {
|
|||||||
return this._getOrCreateMeasurement();
|
return this._getOrCreateMeasurement();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentValue() {
|
getCurrentValue(requestedUnit = null) {
|
||||||
const measurement = this.get();
|
const measurement = this.get();
|
||||||
return measurement ? measurement.getCurrentValue() : null;
|
if (!measurement) return null;
|
||||||
|
|
||||||
|
const value = measurement.getCurrentValue();
|
||||||
|
if (value === null) return null;
|
||||||
|
|
||||||
|
// Return as-is if no unit conversion requested
|
||||||
|
if (!requestedUnit) {
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAverage() {
|
// Convert if needed
|
||||||
|
if (measurement.unit && requestedUnit !== measurement.unit) {
|
||||||
|
try {
|
||||||
|
return convertModule(value).from(measurement.unit).to(requestedUnit);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error(`Unit conversion failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
return value; // Return original value if conversion fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAverage(requestedUnit = null) {
|
||||||
const measurement = this.get();
|
const measurement = this.get();
|
||||||
return measurement ? measurement.getAverage() : null;
|
if (!measurement) return null;
|
||||||
|
|
||||||
|
const avgValue = measurement.getAverage();
|
||||||
|
if (avgValue === null) return null;
|
||||||
|
|
||||||
|
if (!requestedUnit || !measurement.unit || requestedUnit === measurement.unit) {
|
||||||
|
return avgValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return convertModule(avgValue).from(measurement.unit).to(requestedUnit);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error(`Unit conversion failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
return avgValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMin() {
|
getMin() {
|
||||||
@@ -85,50 +292,103 @@ class MeasurementContainer {
|
|||||||
return measurement ? measurement.getAllValues() : null;
|
return measurement ? measurement.getAllValues() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Difference calculations between positions
|
getLaggedValue(lag = 1,requestedUnit = null ){
|
||||||
difference() {
|
const measurement = this.get();
|
||||||
if (!this._currentType || !this._currentVariant) {
|
if (!measurement) return null;
|
||||||
throw new Error('Type and variant must be specified for difference calculation');
|
|
||||||
|
let sample = measurement.getLaggedSample(lag);
|
||||||
|
if (sample === null) return null;
|
||||||
|
const value = sample.value;
|
||||||
|
|
||||||
|
// Return as-is if no unit conversion requested
|
||||||
|
if (!requestedUnit) {
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save position to restore chain state after operation
|
// Convert if needed
|
||||||
const savedPosition = this._currentPosition;
|
if (measurement.unit && requestedUnit !== measurement.unit) {
|
||||||
|
|
||||||
// Get upstream measurement
|
|
||||||
this._currentPosition = 'upstream';
|
|
||||||
const upstream = this.get();
|
|
||||||
|
|
||||||
// Get downstream measurement
|
|
||||||
this._currentPosition = 'downstream';
|
|
||||||
const downstream = this.get();
|
|
||||||
|
|
||||||
// Restore chain state
|
|
||||||
this._currentPosition = savedPosition;
|
|
||||||
|
|
||||||
if (!upstream || !downstream || upstream.values.length === 0 || downstream.values.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure units match
|
|
||||||
let downstreamForCalc = downstream;
|
|
||||||
if (upstream.unit && downstream.unit && upstream.unit !== downstream.unit) {
|
|
||||||
try {
|
try {
|
||||||
downstreamForCalc = downstream.convertTo(upstream.unit);
|
const convertedValue = convertModule(value).from(measurement.unit).to(requestedUnit);
|
||||||
|
//replace old value in sample and return obj
|
||||||
|
sample.value = convertedValue ;
|
||||||
|
sample.unit = requestedUnit;
|
||||||
|
return sample;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.logger) {
|
if (this.logger) {
|
||||||
this.logger.error(`Unit conversion failed: ${error.message}`);
|
this.logger.error(`Unit conversion failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
return null;
|
return sample; // Return original value if conversion fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return value;
|
||||||
value: downstreamForCalc.getCurrentValue() - upstream.getCurrentValue() ,
|
|
||||||
avgDiff: downstreamForCalc.getAverage() - upstream.getAverage() ,
|
|
||||||
unit: upstream.unit
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLaggedSample(lag = 1,requestedUnit = null ){
|
||||||
|
const measurement = this.get();
|
||||||
|
if (!measurement) return null;
|
||||||
|
|
||||||
|
let sample = measurement.getLaggedSample(lag);
|
||||||
|
if (sample === null) return null;
|
||||||
|
|
||||||
|
// Return as-is if no unit conversion requested
|
||||||
|
if (!requestedUnit) {
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert if needed
|
||||||
|
if (measurement.unit && requestedUnit !== measurement.unit) {
|
||||||
|
try {
|
||||||
|
const convertedValue = convertModule(value).from(measurement.unit).to(requestedUnit);
|
||||||
|
//replace old value in sample and return obj
|
||||||
|
sample.value = convertedValue ;
|
||||||
|
sample.unit = requestedUnit;
|
||||||
|
return sample;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error(`Unit conversion failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
return sample; // Return original value if conversion fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Difference calculations between positions
|
||||||
|
difference({ from = "downstream", to = "upstream", unit: requestedUnit } = {}) {
|
||||||
|
if (!this._currentType || !this._currentVariant) {
|
||||||
|
throw new Error("Type and variant must be specified for difference calculation");
|
||||||
|
}
|
||||||
|
|
||||||
|
const get = pos =>
|
||||||
|
this.measurements?.[this._currentType]?.[this._currentVariant]?.[pos] || null;
|
||||||
|
|
||||||
|
const a = get(from);
|
||||||
|
const b = get(to);
|
||||||
|
if (!a || !b || a.values.length === 0 || b.values.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUnit = requestedUnit || a.unit || b.unit;
|
||||||
|
const aVal = this._convertValueToUnit(a.getCurrentValue(), a.unit, targetUnit);
|
||||||
|
const bVal = this._convertValueToUnit(b.getCurrentValue(), b.unit, targetUnit);
|
||||||
|
|
||||||
|
const aAvg = this._convertValueToUnit(a.getAverage(), a.unit, targetUnit);
|
||||||
|
const bAvg = this._convertValueToUnit(b.getAverage(), b.unit, targetUnit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: aVal - bVal,
|
||||||
|
avgDiff: aAvg - bAvg,
|
||||||
|
unit: targetUnit,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
_ensureChainIsValid() {
|
_ensureChainIsValid() {
|
||||||
if (!this._currentType || !this._currentVariant || !this._currentPosition) {
|
if (!this._currentType || !this._currentVariant || !this._currentPosition) {
|
||||||
@@ -157,6 +417,7 @@ class MeasurementContainer {
|
|||||||
.setVariant(this._currentVariant)
|
.setVariant(this._currentVariant)
|
||||||
.setPosition(this._currentPosition)
|
.setPosition(this._currentPosition)
|
||||||
.setWindowSize(this.windowSize)
|
.setWindowSize(this.windowSize)
|
||||||
|
.setDistance(this._currentDistance)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +456,102 @@ class MeasurementContainer {
|
|||||||
this._currentVariant = null;
|
this._currentVariant = null;
|
||||||
this._currentPosition = null;
|
this._currentPosition = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper method for value conversion
|
||||||
|
_convertValueToUnit(value, fromUnit, toUnit) {
|
||||||
|
if (!value || !fromUnit || !toUnit || fromUnit === toUnit) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return convertModule(value).from(fromUnit).to(toUnit);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.warn(`Conversion failed from ${fromUnit} to ${toUnit}: ${error.message}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get available units for a measurement type
|
||||||
|
getAvailableUnits(measurementType = null) {
|
||||||
|
const type = measurementType || this._currentType;
|
||||||
|
if (!type) return [];
|
||||||
|
|
||||||
|
// Map measurement types to convert module measures
|
||||||
|
const measureMap = {
|
||||||
|
pressure: 'pressure',
|
||||||
|
flow: 'volumeFlowRate',
|
||||||
|
power: 'power',
|
||||||
|
temperature: 'temperature',
|
||||||
|
volume: 'volume',
|
||||||
|
length: 'length',
|
||||||
|
mass: 'mass',
|
||||||
|
energy: 'energy'
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertMeasure = measureMap[type];
|
||||||
|
if (!convertMeasure) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
return convertModule().possibilities(convertMeasure);
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get best unit for current value
|
||||||
|
getBestUnit(excludeUnits = []) {
|
||||||
|
const measurement = this.get();
|
||||||
|
if (!measurement || !measurement.unit) return null;
|
||||||
|
|
||||||
|
const currentValue = measurement.getCurrentValue();
|
||||||
|
if (currentValue === null) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const best = convertModule(currentValue)
|
||||||
|
.from(measurement.unit)
|
||||||
|
.toBest({ exclude: excludeUnits });
|
||||||
|
|
||||||
|
return best;
|
||||||
|
} catch (error) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error(`getBestUnit failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_convertPositionStr2Num(positionString) {
|
||||||
|
switch(positionString) {
|
||||||
|
case "atEquipment":
|
||||||
|
return 0;
|
||||||
|
case "upstream":
|
||||||
|
return Number.POSITIVE_INFINITY;
|
||||||
|
case "downstream":
|
||||||
|
return Number.NEGATIVE_INFINITY;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.error(`Invalid positionVsParent provided: ${positionString}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_convertPositionNum2Str(positionValue) {
|
||||||
|
switch (positionValue) {
|
||||||
|
case 0:
|
||||||
|
return "atEquipment";
|
||||||
|
case (positionValue < 0):
|
||||||
|
return "upstream";
|
||||||
|
case (positionValue > 0):
|
||||||
|
return "downstream";
|
||||||
|
default:
|
||||||
|
console.log(`Invalid position provided: ${positionValue}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = MeasurementContainer;
|
module.exports = MeasurementContainer;
|
||||||
|
|||||||
@@ -1,58 +1,380 @@
|
|||||||
const { MeasurementContainer } = require('./index');
|
const { MeasurementContainer } = require('./index');
|
||||||
|
|
||||||
// Create a container
|
console.log('=== MEASUREMENT CONTAINER EXAMPLES ===\n');
|
||||||
const container = new MeasurementContainer({ windowSize: 20 });
|
console.log('This guide shows how to use the MeasurementContainer for storing,');
|
||||||
|
console.log('retrieving, and converting measurement data with automatic unit handling.\n');
|
||||||
|
|
||||||
// Example 1: Setting values with chaining
|
// ====================================
|
||||||
console.log('--- Example 1: Setting values ---');
|
// BASIC SETUP EXAMPLES
|
||||||
container.type('pressure').variant('measured').position('upstream').value(100).unit('psi');
|
// ====================================
|
||||||
container.type('pressure').variant('measured').position('downstream').value(95).unit('psi');
|
console.log('--- Example 1: Basic Setup & Distance ---');
|
||||||
container.type('pressure').variant('measured').position('downstream').value(80);
|
|
||||||
|
|
||||||
// Example 2: Getting values with chaining
|
// Create a basic container
|
||||||
console.log('--- Example 2: Getting values ---');
|
const basicContainer = new MeasurementContainer({ windowSize: 20 });
|
||||||
const upstreamValue = container.type('pressure').variant('measured').position('upstream').getCurrentValue();
|
|
||||||
const upstreamUnit = container.type('pressure').variant('measured').position('upstream').get().unit;
|
|
||||||
console.log(`Upstream pressure: ${upstreamValue} ${upstreamUnit}`);
|
|
||||||
const downstreamValue = container.type('pressure').variant('measured').position('downstream').getCurrentValue();
|
|
||||||
const downstreamUnit = container.type('pressure').variant('measured').position('downstream').get().unit;
|
|
||||||
console.log(`Downstream pressure: ${downstreamValue} ${downstreamUnit}`);
|
|
||||||
|
|
||||||
// Example 3: Calculations using chained methods
|
// Subscribe to events to monitor changes
|
||||||
console.log('--- Example 3: Calculations ---');
|
basicContainer.emitter.on('flow.predicted.upstream', (data) => {
|
||||||
container.type('flow').variant('predicted').position('upstream').value(200).unit('gpm');
|
console.log(`📡 Event: Flow predicted upstream = ${data.value} ${data.unit || ''} (distance=${data.distance ?? 'n/a'}m)`);
|
||||||
container.type('flow').variant('predicted').position('downstream').value(195).unit('gpm');
|
});
|
||||||
|
|
||||||
const flowAvg = container.type('flow').variant('predicted').position('upstream').getAverage();
|
// Subscribe to all measured flow events using wildcard
|
||||||
console.log(`Average upstream flow: ${flowAvg} gpm`);
|
basicContainer.emitter.on('flow.measured.*', (data) => {
|
||||||
|
console.log(`📡 Event: Flow measured ${data.position} = ${data.value} ${data.unit || ''} (distance=${data.distance ?? 'n/a'}m)`);
|
||||||
|
});
|
||||||
|
|
||||||
// Example 4: Getting pressure difference
|
// Basic value setting with distance
|
||||||
console.log('--- Example 4: Difference calculations ---');
|
console.log('\nSetting pressure values with distances:');
|
||||||
const pressureDiff = container.type('pressure').variant('measured').difference();
|
basicContainer
|
||||||
console.log(`Pressure difference: ${pressureDiff.value} ${pressureDiff.unit}`);
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.position('upstream')
|
||||||
|
.distance(1.5)
|
||||||
|
.value(100)
|
||||||
|
.unit('psi');
|
||||||
|
|
||||||
// Example 5: Adding multiple values to track history
|
basicContainer
|
||||||
console.log('--- Example 5: Multiple values ---');
|
.type('pressure')
|
||||||
// Add several values to upstream flow
|
.variant('measured')
|
||||||
container.type('flow').variant('measured').position('upstream')
|
.position('downstream')
|
||||||
.value(210).value(215).value(205).unit('gpm');
|
.distance(5.2)
|
||||||
|
.value(95)
|
||||||
|
.unit('psi');
|
||||||
|
|
||||||
// Then get statistics
|
// Distance persists - no need to set it again for same position
|
||||||
console.log('Flow statistics:');
|
basicContainer
|
||||||
console.log(`- Current: ${container.type('flow').variant('measured').position('upstream').getCurrentValue()} gpm`);
|
.type('pressure')
|
||||||
console.log(`- Average: ${container.type('flow').variant('measured').position('upstream').getAverage()} gpm`);
|
.variant('measured')
|
||||||
console.log(`- Min: ${container.type('flow').variant('measured').position('upstream').getMin()} gpm`);
|
.position('downstream')
|
||||||
console.log(`- Max: ${container.type('flow').variant('measured').position('upstream').getMax()} gpm`);
|
.value(90); // distance 5.2 is automatically reused
|
||||||
console.log(`Show all values : ${JSON.stringify(container.type('flow').variant('measured').position('upstream').getAllValues())}`);
|
|
||||||
|
|
||||||
// Example 6: Listing available data
|
console.log('✅ Basic setup complete\n');
|
||||||
console.log('--- Example 6: Listing available data ---');
|
|
||||||
console.log('Types:', container.getTypes());
|
|
||||||
console.log('Pressure variants:', container.type('pressure').getVariants());
|
|
||||||
console.log('Measured pressure positions:', container.type('pressure').variant('measured').getPositions());
|
|
||||||
|
|
||||||
module.exports = {
|
// Retrieve and display the distance
|
||||||
runExamples: () => {
|
const upstreamPressure = basicContainer
|
||||||
console.log('Examples of the measurement chainable API');
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.position('upstream')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
console.log(`Retrieved upstream pressure: ${upstreamPressure.getCurrentValue()} ${upstreamPressure.unit}`);
|
||||||
|
console.log(`Distance from parent: ${upstreamPressure.distance ?? 'not set'} m\n`);
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// AUTO-CONVERSION SETUP
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 2: Auto-Conversion Setup ---');
|
||||||
|
|
||||||
|
const autoContainer = new MeasurementContainer({
|
||||||
|
autoConvert: true,
|
||||||
|
windowSize: 50,
|
||||||
|
defaultUnits: {
|
||||||
|
pressure: 'bar',
|
||||||
|
flow: 'l/min',
|
||||||
|
power: 'kW',
|
||||||
|
temperature: 'C'
|
||||||
|
},
|
||||||
|
preferredUnits: {
|
||||||
|
pressure: 'psi'
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
|
// Values automatically convert to preferred units
|
||||||
|
console.log('Adding pressure with auto-conversion:');
|
||||||
|
autoContainer
|
||||||
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.position('upstream')
|
||||||
|
.distance(0.5)
|
||||||
|
.value(1.5, Date.now(), 'bar'); // Input: 1.5 bar → Auto-stored as ~21.76 psi
|
||||||
|
|
||||||
|
|
||||||
|
const converted = autoContainer
|
||||||
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.position('upstream')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
console.log(`Stored as: ${converted.getCurrentValue()} ${converted.unit} (distance=${converted.distance}m)`);
|
||||||
|
console.log('✅ Auto-conversion complete\n');
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// UNIT CONVERSION ON RETRIEVAL
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 3: Unit Conversion on Retrieval ---');
|
||||||
|
|
||||||
|
autoContainer
|
||||||
|
.type('flow')
|
||||||
|
.variant('predicted')
|
||||||
|
.position('upstream')
|
||||||
|
.distance(2.4)
|
||||||
|
.value(100, Date.now(), 'l/min');
|
||||||
|
|
||||||
|
const flowMeasurement = autoContainer
|
||||||
|
.type('flow')
|
||||||
|
.variant('predicted')
|
||||||
|
.position('upstream')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
console.log(`Flow in l/min: ${flowMeasurement.getCurrentValue('l/min')}`);
|
||||||
|
console.log(`Flow in m³/h: ${flowMeasurement.getCurrentValue('m3/h').toFixed(2)}`);
|
||||||
|
console.log(`Flow in gal/min: ${flowMeasurement.getCurrentValue('gal/min').toFixed(2)}`);
|
||||||
|
console.log(`Distance: ${flowMeasurement.distance}m\n`);
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// SMART UNIT SELECTION
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 4: Smart Unit Selection ---');
|
||||||
|
|
||||||
|
autoContainer
|
||||||
|
.type('pressure')
|
||||||
|
.variant('test')
|
||||||
|
.position('sensor')
|
||||||
|
.distance(0.2)
|
||||||
|
.value(0.001, Date.now(), 'bar');
|
||||||
|
|
||||||
|
const bestUnit = autoContainer
|
||||||
|
.type('pressure')
|
||||||
|
.variant('test')
|
||||||
|
.position('sensor')
|
||||||
|
.getBestUnit();
|
||||||
|
|
||||||
|
if (bestUnit) {
|
||||||
|
console.log(`Best unit: ${bestUnit.val.toFixed(2)} ${bestUnit.unit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableUnits = autoContainer.getAvailableUnits('pressure');
|
||||||
|
console.log(`Available units: ${availableUnits.slice(0, 5).join(', ')}...\n`);
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// BASIC RETRIEVAL
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 5: Basic Value Retrieval ---');
|
||||||
|
|
||||||
|
const upstreamVal = basicContainer
|
||||||
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.position('upstream')
|
||||||
|
.getCurrentValue();
|
||||||
|
|
||||||
|
const upstreamData = basicContainer
|
||||||
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.position('upstream')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
console.log(`Upstream: ${upstreamVal} ${upstreamData.unit} at ${upstreamData.distance}m`);
|
||||||
|
|
||||||
|
const downstreamVal = basicContainer
|
||||||
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.position('downstream')
|
||||||
|
.getCurrentValue();
|
||||||
|
|
||||||
|
const downstreamData = basicContainer
|
||||||
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.position('downstream')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
//check wether a serie exists
|
||||||
|
const hasSeries = measurements
|
||||||
|
.type("flow")
|
||||||
|
.variant("measured")
|
||||||
|
.exists(); // true if any position exists
|
||||||
|
|
||||||
|
const hasUpstreamValues = measurements
|
||||||
|
.type("flow")
|
||||||
|
.variant("measured")
|
||||||
|
.exists({ position: "upstream", requireValues: true });
|
||||||
|
|
||||||
|
// Passing everything explicitly
|
||||||
|
const hasPercent = measurements.exists({
|
||||||
|
type: "volume",
|
||||||
|
variant: "percent",
|
||||||
|
position: "atEquipment",
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
console.log(`Downstream: ${downstreamVal} ${downstreamData.unit} at ${downstreamData.distance}m\n`);
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// CALCULATIONS & STATISTICS
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 6: Calculations & Statistics ---');
|
||||||
|
|
||||||
|
basicContainer
|
||||||
|
.type('flow')
|
||||||
|
.variant('predicted')
|
||||||
|
.position('upstream')
|
||||||
|
.distance(3.0)
|
||||||
|
.value(200)
|
||||||
|
.unit('gpm');
|
||||||
|
|
||||||
|
basicContainer
|
||||||
|
.type('flow')
|
||||||
|
.variant('predicted')
|
||||||
|
.position('downstream')
|
||||||
|
.distance(8.5)
|
||||||
|
.value(195)
|
||||||
|
.unit('gpm');
|
||||||
|
|
||||||
|
const flowAvg = basicContainer
|
||||||
|
.type('flow')
|
||||||
|
.variant('predicted')
|
||||||
|
.position('upstream')
|
||||||
|
.getAverage();
|
||||||
|
|
||||||
|
console.log(`Average upstream flow: ${flowAvg.toFixed(1)} gpm`);
|
||||||
|
|
||||||
|
const pressureDiff = basicContainer
|
||||||
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.difference();
|
||||||
|
|
||||||
|
console.log(`Pressure difference: ${pressureDiff.value} ${pressureDiff.unit}\n`);
|
||||||
|
|
||||||
|
//reversable difference
|
||||||
|
const deltaP = basicContainer.type("pressure").variant("measured").difference(); // defaults to downstream - upstream
|
||||||
|
const netFlow = basicContainer.type("flow").variant("measured").difference({ from: "upstream", to: "downstream" });
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// ADVANCED STATISTICS & HISTORY
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 7: Advanced Statistics & History ---');
|
||||||
|
|
||||||
|
basicContainer
|
||||||
|
.type('flow')
|
||||||
|
.variant('measured')
|
||||||
|
.position('upstream')
|
||||||
|
.distance(3.0)
|
||||||
|
.value(210)
|
||||||
|
.value(215)
|
||||||
|
.value(205)
|
||||||
|
.value(220)
|
||||||
|
.value(200)
|
||||||
|
.unit('m3/h');
|
||||||
|
|
||||||
|
const stats = basicContainer
|
||||||
|
.type('flow')
|
||||||
|
.variant('measured')
|
||||||
|
.position('upstream');
|
||||||
|
|
||||||
|
const statsData = stats.get();
|
||||||
|
|
||||||
|
console.log('Flow Statistics:');
|
||||||
|
console.log(` Current: ${stats.getCurrentValue()} ${statsData.unit}`);
|
||||||
|
console.log(` Average: ${stats.getAverage().toFixed(1)} ${statsData.unit}`);
|
||||||
|
console.log(` Min: ${stats.getMin()} ${statsData.unit}`);
|
||||||
|
console.log(` Max: ${stats.getMax()} ${statsData.unit}`);
|
||||||
|
console.log(` Distance: ${statsData.distance}m`);
|
||||||
|
|
||||||
|
const allValues = stats.getAllValues();
|
||||||
|
console.log(` Samples: ${allValues.values.length}`);
|
||||||
|
console.log(` History: [${allValues.values.join(', ')}]\n`);
|
||||||
|
|
||||||
|
console.log('--- Lagged sample comparison ---');
|
||||||
|
|
||||||
|
const latest = stats.getCurrentValue(); // existing helper
|
||||||
|
const prevSample = stats.getLaggedValue(1); // new helper
|
||||||
|
const prevPrevSample = stats.getLaggedValue(2); // optional
|
||||||
|
|
||||||
|
if (prevSample) {
|
||||||
|
const delta = latest - prevSample.value;
|
||||||
|
console.log(
|
||||||
|
`Current vs previous: ${latest} ${statsData.unit} (t=${stats.get().getLatestTimestamp()}) vs ` +
|
||||||
|
`${prevSample.value} ${prevSample.unit} (t=${prevSample.timestamp})`
|
||||||
|
);
|
||||||
|
console.log(`Δ = ${delta.toFixed(2)} ${statsData.unit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevPrevSample) {
|
||||||
|
console.log(
|
||||||
|
`Previous vs 2-steps-back timestamps: ${new Date(prevSample.timestamp).toISOString()} vs ` +
|
||||||
|
`${new Date(prevPrevSample.timestamp).toISOString()}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// DYNAMIC UNIT MANAGEMENT
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 8: Dynamic Unit Management ---');
|
||||||
|
|
||||||
|
autoContainer.setPreferredUnit('flow', 'm3/h');
|
||||||
|
console.log('Changed preferred flow unit to m³/h');
|
||||||
|
|
||||||
|
autoContainer
|
||||||
|
.type('flow')
|
||||||
|
.variant('realtime')
|
||||||
|
.position('inlet')
|
||||||
|
.distance(1.2)
|
||||||
|
.value(150, Date.now(), 'l/min');
|
||||||
|
|
||||||
|
const realtimeFlow = autoContainer
|
||||||
|
.type('flow')
|
||||||
|
.variant('realtime')
|
||||||
|
.position('inlet')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
console.log(`Stored as: ${realtimeFlow.getCurrentValue()} ${realtimeFlow.unit}`);
|
||||||
|
console.log(`Original: ${realtimeFlow.getCurrentValue('l/min').toFixed(1)} l/min`);
|
||||||
|
console.log(`Distance: ${realtimeFlow.distance}m\n`);
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// DATA EXPLORATION
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Example 9: Data Exploration ---');
|
||||||
|
|
||||||
|
console.log('Available types:', basicContainer.getTypes());
|
||||||
|
console.log('Pressure variants:', basicContainer.type('pressure').getVariants());
|
||||||
|
console.log('Measured pressure positions:', basicContainer.type('pressure').variant('measured').getPositions());
|
||||||
|
|
||||||
|
console.log('\nData Structure:');
|
||||||
|
basicContainer.getTypes().forEach(type => {
|
||||||
|
const variants = basicContainer.type(type).getVariants();
|
||||||
|
if (variants.length > 0) {
|
||||||
|
console.log(`${type.toUpperCase()}:`);
|
||||||
|
variants.forEach(variant => {
|
||||||
|
const positions = basicContainer.type(type).variant(variant).getPositions();
|
||||||
|
positions.forEach(position => {
|
||||||
|
const m = basicContainer.type(type).variant(variant).position(position).get();
|
||||||
|
if (m && m.values.length > 0) {
|
||||||
|
console.log(` └─ ${variant}.${position}: ${m.values.length} values, ${m.unit || 'no unit'}, dist=${m.distance ?? 'n/a'}m`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log('\n✅ All examples complete!\n');
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// BEST PRACTICES
|
||||||
|
// ====================================
|
||||||
|
console.log('--- Best Practices Summary ---\n');
|
||||||
|
|
||||||
|
console.log('SETUP:');
|
||||||
|
console.log(' • Enable autoConvert for consistent units');
|
||||||
|
console.log(' • Define defaultUnits for your measurement types');
|
||||||
|
console.log(' • Set windowSize based on your data retention needs\n');
|
||||||
|
|
||||||
|
console.log('STORING DATA:');
|
||||||
|
console.log(' • Chain methods: type().variant().position().distance().value()');
|
||||||
|
console.log(' • Set distance once - it persists for that position');
|
||||||
|
console.log(' • Specify source unit: .value(100, timestamp, "psi")');
|
||||||
|
console.log(' • Set unit immediately: .value(100).unit("psi")\n');
|
||||||
|
|
||||||
|
console.log('RETRIEVING DATA:');
|
||||||
|
console.log(' • Use .getCurrentValue("unit") for specific units');
|
||||||
|
console.log(' • Use .getBestUnit() for automatic selection');
|
||||||
|
console.log(' • Use .difference() for upstream/downstream deltas');
|
||||||
|
console.log(' • Access .get().distance for physical positioning\n');
|
||||||
|
|
||||||
|
console.log('MONITORING:');
|
||||||
|
console.log(' • Subscribe: .emitter.on("type.variant.position", callback)');
|
||||||
|
console.log(' • Event data includes: value, unit, timestamp, distance');
|
||||||
|
console.log(' • Explore data: .getTypes(), .getVariants(), .getPositions()\n');
|
||||||
|
|
||||||
|
module.exports = { basicContainer, autoContainer };
|
||||||
@@ -1,23 +1,26 @@
|
|||||||
const AssetMenu = require('./asset.js');
|
const AssetMenu = require('./asset.js');
|
||||||
|
const { TagcodeApp, DynamicAssetMenu } = require('./tagcodeApp.js');
|
||||||
const LoggerMenu = require('./logger.js');
|
const LoggerMenu = require('./logger.js');
|
||||||
const PhysicalPositionMenu = require('./physicalPosition.js');
|
const PhysicalPositionMenu = require('./physicalPosition.js');
|
||||||
|
|
||||||
class MenuManager {
|
class MenuManager {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.registeredMenus = new Map(); // Store menu type instances
|
this.registeredMenus = new Map();
|
||||||
this.registerMenu('asset', new AssetMenu()); // Register asset menu by default
|
// Register factory functions
|
||||||
this.registerMenu('logger', new LoggerMenu()); // Register logger menu by default
|
this.registerMenu('asset', () => new AssetMenu()); // static menu to be replaced by dynamic one but later
|
||||||
this.registerMenu('position', new PhysicalPositionMenu()); // Register position menu by default
|
//this.registerMenu('asset', (nodeName) => new DynamicAssetMenu(nodeName, new TagcodeApp()));
|
||||||
|
this.registerMenu('logger', () => new LoggerMenu());
|
||||||
|
this.registerMenu('position', () => new PhysicalPositionMenu());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a menu type with its handler instance
|
* Register a menu type with its handler factory function
|
||||||
* @param {string} menuType - The type of menu (e.g., 'asset', 'logging')
|
* @param {string} menuType - The type of menu (e.g., 'asset', 'logging')
|
||||||
* @param {object} menuHandler - The menu handler instance
|
* @param {function} menuFactory - The menu factory function
|
||||||
*/
|
*/
|
||||||
registerMenu(menuType, menuHandler) {
|
registerMenu(menuType, menuFactory) {
|
||||||
this.registeredMenus.set(menuType, menuHandler);
|
this.registeredMenus.set(menuType, menuFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,58 +30,145 @@ class MenuManager {
|
|||||||
* @returns {string} Complete JavaScript code to serve
|
* @returns {string} Complete JavaScript code to serve
|
||||||
*/
|
*/
|
||||||
createEndpoint(nodeName, menuTypes) {
|
createEndpoint(nodeName, menuTypes) {
|
||||||
// 1. Collect all menu data
|
try {
|
||||||
const menuData = {};
|
// ✅ Create instances using factory functions with proper error handling
|
||||||
|
const instantiatedMenus = new Map();
|
||||||
|
|
||||||
menuTypes.forEach(menuType => {
|
menuTypes.forEach(menuType => {
|
||||||
const handler = this.registeredMenus.get(menuType);
|
try {
|
||||||
if (handler && typeof handler.getAllMenuData === 'function') {
|
const factory = this.registeredMenus.get(menuType);
|
||||||
menuData[menuType] = handler.getAllMenuData();
|
if (typeof factory === 'function') {
|
||||||
|
const instance = factory(nodeName);
|
||||||
|
instantiatedMenus.set(menuType, instance);
|
||||||
|
} else {
|
||||||
|
console.warn(`No factory function found for menu type: ${menuType}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error creating instance for ${menuType}:`, error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate HTML injection code
|
// ✅ Collect all menu data with error handling
|
||||||
|
const menuData = {};
|
||||||
|
menuTypes.forEach(menuType => {
|
||||||
|
try {
|
||||||
|
const handler = instantiatedMenus.get(menuType);
|
||||||
|
if (handler && typeof handler.getAllMenuData === 'function') {
|
||||||
|
menuData[menuType] = handler.getAllMenuData();
|
||||||
|
} else {
|
||||||
|
// Provide default empty data if method doesn't exist
|
||||||
|
menuData[menuType] = {};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error getting menu data for ${menuType}:`, error);
|
||||||
|
menuData[menuType] = {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Generate HTML injection code with error handling
|
||||||
const htmlInjections = menuTypes.map(type => {
|
const htmlInjections = menuTypes.map(type => {
|
||||||
const menu = this.registeredMenus.get(type);
|
try {
|
||||||
if (menu && menu.getHtmlInjectionCode) {
|
const menu = instantiatedMenus.get(type);
|
||||||
|
if (menu && typeof menu.getHtmlInjectionCode === 'function') {
|
||||||
return menu.getHtmlInjectionCode(nodeName);
|
return menu.getHtmlInjectionCode(nodeName);
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error generating HTML injection for ${type}:`, error);
|
||||||
|
return `// Error generating HTML injection for ${type}: ${error.message}`;
|
||||||
|
}
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
// 2. Collect all client initialization code
|
// ✅ Collect all client initialization code with error handling
|
||||||
const initFunctions = [];
|
const initFunctions = [];
|
||||||
menuTypes.forEach(menuType => {
|
menuTypes.forEach(menuType => {
|
||||||
const handler = this.registeredMenus.get(menuType);
|
try {
|
||||||
|
const handler = instantiatedMenus.get(menuType);
|
||||||
if (handler && typeof handler.getClientInitCode === 'function') {
|
if (handler && typeof handler.getClientInitCode === 'function') {
|
||||||
initFunctions.push(handler.getClientInitCode(nodeName));
|
initFunctions.push(handler.getClientInitCode(nodeName));
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error generating init code for ${menuType}:`, error);
|
||||||
|
initFunctions.push(`// Error in ${menuType} initialization: ${error.message}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Convert menu data to JSON
|
// Convert menu data to JSON
|
||||||
const menuDataJSON = JSON.stringify(menuData, null, 2);
|
const menuDataJSON = JSON.stringify(menuData, null, 2);
|
||||||
|
|
||||||
// 4. Assemble the complete script
|
// ✅ Assemble the complete script with comprehensive error handling
|
||||||
return `
|
return `
|
||||||
// Create the namespace structure
|
try {
|
||||||
|
// Create the namespace structure with safety checks
|
||||||
window.EVOLV = window.EVOLV || {};
|
window.EVOLV = window.EVOLV || {};
|
||||||
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||||
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||||
|
|
||||||
|
// Initialize menu namespaces
|
||||||
|
${menuTypes.map(type => `window.EVOLV.nodes.${nodeName}.${type}Menu = window.EVOLV.nodes.${nodeName}.${type}Menu || {};`).join('\n ')}
|
||||||
|
|
||||||
// Inject the pre-loaded menu data directly into the namespace
|
// Inject the pre-loaded menu data directly into the namespace
|
||||||
window.EVOLV.nodes.${nodeName}.menuData = ${menuDataJSON};
|
window.EVOLV.nodes.${nodeName}.menuData = ${menuDataJSON};
|
||||||
|
|
||||||
${initFunctions.join('\n\n')}
|
// HTML injections with error handling
|
||||||
|
try {
|
||||||
|
${htmlInjections}
|
||||||
|
} catch (htmlError) {
|
||||||
|
console.error('Error in HTML injections for ${nodeName}:', htmlError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize functions with error handling
|
||||||
|
try {
|
||||||
|
${initFunctions.join('\n\n ')}
|
||||||
|
} catch (initError) {
|
||||||
|
console.error('Error in initialization functions for ${nodeName}:', initError);
|
||||||
|
}
|
||||||
|
|
||||||
// Main initialization function that calls all menu initializers
|
// Main initialization function that calls all menu initializers
|
||||||
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
|
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
|
||||||
|
try {
|
||||||
${menuTypes.map(type => `
|
${menuTypes.map(type => `
|
||||||
|
try {
|
||||||
if (window.EVOLV.nodes.${nodeName}.${type}Menu && window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor) {
|
if (window.EVOLV.nodes.${nodeName}.${type}Menu && window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor) {
|
||||||
window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor(node);
|
window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor(node);
|
||||||
|
}
|
||||||
|
} catch (${type}Error) {
|
||||||
|
console.error('Error initializing ${type} menu for ${nodeName}:', ${type}Error);
|
||||||
}`).join('')}
|
}`).join('')}
|
||||||
|
} catch (editorError) {
|
||||||
|
console.error('Error in main editor initialization for ${nodeName}:', editorError);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('${nodeName} menu data and initializers loaded for: ${menuTypes.join(', ')}');
|
console.log('${nodeName} menu data and initializers loaded for: ${menuTypes.join(', ')}');
|
||||||
|
|
||||||
|
} catch (globalError) {
|
||||||
|
console.error('Critical error in ${nodeName} menu initialization:', globalError);
|
||||||
|
|
||||||
|
// Fallback initialization
|
||||||
|
window.EVOLV = window.EVOLV || {};
|
||||||
|
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||||
|
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||||
|
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
|
||||||
|
console.warn('Using fallback editor initialization for ${nodeName}');
|
||||||
|
};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Critical error creating endpoint for ${nodeName}:`, error);
|
||||||
|
|
||||||
|
// Return minimal fallback script
|
||||||
|
return `
|
||||||
|
window.EVOLV = window.EVOLV || {};
|
||||||
|
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||||
|
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
|
||||||
|
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
|
||||||
|
console.error('Menu system failed to initialize for ${nodeName}');
|
||||||
|
};
|
||||||
|
console.error('Menu system failed for ${nodeName}:', '${error.message}');
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,30 @@ class PhysicalPositionMenu {
|
|||||||
return {
|
return {
|
||||||
positionGroups: [
|
positionGroups: [
|
||||||
{ group: 'Positional', options: [
|
{ group: 'Positional', options: [
|
||||||
{ value: 'upstream', label: '⬅ Upstream' },
|
{ value: 'upstream', label: '← Upstream', icon: '←'},
|
||||||
{ value: 'atEquipment', label: '⚙️ At Equipment' },
|
{ value: 'atEquipment', label: '⊥ in place' , icon: '⊥' },
|
||||||
{ value: 'downstream', label: '➡ Downstream' }
|
{ value: 'downstream', label: '→ Downstream' , icon: '→' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
// Distance contexts for each position
|
||||||
|
distanceContexts: {
|
||||||
|
upstream: {
|
||||||
|
description: 'Distance from parent inlet',
|
||||||
|
placeholder: 'e.g., 2.5 (meters before parent)',
|
||||||
|
helpText: 'How far upstream from the parent equipment'
|
||||||
|
},
|
||||||
|
downstream: {
|
||||||
|
description: 'Distance from parent outlet',
|
||||||
|
placeholder: 'e.g., 3.0 (meters after parent)',
|
||||||
|
helpText: 'How far downstream from the parent equipment'
|
||||||
|
},
|
||||||
|
atEquipment: {
|
||||||
|
description: 'Distance from parent start',
|
||||||
|
placeholder: 'e.g., 1.2 (meters from start)',
|
||||||
|
helpText: 'Position within the parent equipment boundaries'
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,6 +44,24 @@ class PhysicalPositionMenu {
|
|||||||
<!-- optgroups will be injected -->
|
<!-- optgroups will be injected -->
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Distance section -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label> </label>
|
||||||
|
<input type="checkbox" id="node-input-hasDistance" style="display:inline-block; width:auto; margin-right:5px;">
|
||||||
|
<label for="node-input-hasDistance" style="width:auto;">Specify 1D Distance</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="distance-section" class="form-row" style="display:none;">
|
||||||
|
<label for="node-input-distance"><i class="fa fa-ruler"></i>Distance</label>
|
||||||
|
<div style="display:flex; align-items:center; width:70%;">
|
||||||
|
<input type="number" id="node-input-distance" step="0.1" min="0" style="width:60%;" placeholder="0.0">
|
||||||
|
<span style="margin-left:5px; margin-right:5px;">meters</span>
|
||||||
|
</div>
|
||||||
|
<div id="distance-help" class="form-tips" style="margin-left:105px; font-size:11px; color:#666;">
|
||||||
|
Select a position to see distance context
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -52,7 +88,12 @@ class PhysicalPositionMenu {
|
|||||||
window.EVOLV.nodes.${nodeName}.positionMenu.loadData = function(node) {
|
window.EVOLV.nodes.${nodeName}.positionMenu.loadData = function(node) {
|
||||||
const data = window.EVOLV.nodes.${nodeName}.menuData.position;
|
const data = window.EVOLV.nodes.${nodeName}.menuData.position;
|
||||||
const sel = document.getElementById('node-input-positionVsParent');
|
const sel = document.getElementById('node-input-positionVsParent');
|
||||||
if (!sel) return;
|
const hasDistanceCheck = document.getElementById('node-input-hasDistance');
|
||||||
|
const distanceInput = document.getElementById('node-input-distance');
|
||||||
|
const distanceSection = document.getElementById('distance-section');
|
||||||
|
|
||||||
|
//Load position options
|
||||||
|
if (sel) {
|
||||||
sel.innerHTML = '';
|
sel.innerHTML = '';
|
||||||
(data.positionGroups||[]).forEach(grp => {
|
(data.positionGroups||[]).forEach(grp => {
|
||||||
const optg = document.createElement('optgroup');
|
const optg = document.createElement('optgroup');
|
||||||
@@ -61,12 +102,26 @@ class PhysicalPositionMenu {
|
|||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = o.value;
|
opt.value = o.value;
|
||||||
opt.textContent = o.label;
|
opt.textContent = o.label;
|
||||||
|
opt.setAttribute('data-icon', o.icon);
|
||||||
optg.appendChild(opt);
|
optg.appendChild(opt);
|
||||||
});
|
});
|
||||||
sel.appendChild(optg);
|
sel.appendChild(optg);
|
||||||
});
|
});
|
||||||
// default to “atEquipment” if not set
|
|
||||||
sel.value = node.positionVsParent || 'atEquipment';
|
sel.value = node.positionVsParent || 'atEquipment';
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load distance values
|
||||||
|
if (hasDistanceCheck) {
|
||||||
|
hasDistanceCheck.checked = node.hasDistance || false;
|
||||||
|
distanceSection.style.display = hasDistanceCheck.checked ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distanceInput) {
|
||||||
|
distanceInput.value = node.distance || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update distance context for current position
|
||||||
|
this.updateDistanceContext(node.positionVsParent || 'atEquipment', data.distanceContexts);
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -76,22 +131,119 @@ class PhysicalPositionMenu {
|
|||||||
return `
|
return `
|
||||||
// PhysicalPosition events for ${nodeName}
|
// PhysicalPosition events for ${nodeName}
|
||||||
window.EVOLV.nodes.${nodeName}.positionMenu.wireEvents = function(node) {
|
window.EVOLV.nodes.${nodeName}.positionMenu.wireEvents = function(node) {
|
||||||
// no dynamic behavior
|
const positionSel = document.getElementById('node-input-positionVsParent');
|
||||||
|
const hasDistanceCheck = document.getElementById('node-input-hasDistance');
|
||||||
|
const distanceSection = document.getElementById('distance-section');
|
||||||
|
const data = window.EVOLV.nodes.${nodeName}.menuData.position;
|
||||||
|
|
||||||
|
// Toggle distance section visibility
|
||||||
|
if (hasDistanceCheck && distanceSection) {
|
||||||
|
hasDistanceCheck.addEventListener('change', function() {
|
||||||
|
distanceSection.style.display = this.checked ? 'block' : 'none';
|
||||||
|
|
||||||
|
// Clear distance if unchecked
|
||||||
|
if (!this.checked) {
|
||||||
|
const distanceInput = document.getElementById('node-input-distance');
|
||||||
|
if (distanceInput) {
|
||||||
|
distanceInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update distance context when position changes
|
||||||
|
if (positionSel) {
|
||||||
|
positionSel.addEventListener('change', function() {
|
||||||
|
const position = this.value;
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu.updateDistanceContext(position, data.distanceContexts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to update distance context
|
||||||
|
window.EVOLV.nodes.${nodeName}.positionMenu.updateDistanceContext = function(position, contexts) {
|
||||||
|
const distanceInput = document.getElementById('node-input-distance');
|
||||||
|
const distanceHelp = document.getElementById('distance-help');
|
||||||
|
|
||||||
|
const context = contexts && contexts[position];
|
||||||
|
|
||||||
|
if (context && distanceInput && distanceHelp) {
|
||||||
|
distanceInput.placeholder = context.placeholder || '0.0';
|
||||||
|
distanceHelp.textContent = context.helpText || 'Enter distance in meters';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6) Save-logic injector
|
// 6) Save-logic injector
|
||||||
getSaveInjectionCode(nodeName) {
|
getSaveInjectionCode(nodeName) {
|
||||||
return `
|
return `
|
||||||
// PhysicalPosition Save injection for ${nodeName}
|
// PhysicalPosition Save injection for ${nodeName}
|
||||||
window.EVOLV.nodes.${nodeName}.positionMenu.saveEditor = function(node) {
|
window.EVOLV.nodes.${nodeName}.positionMenu.saveEditor = function(node) {
|
||||||
|
console.log("=== PhysicalPosition Save Debug ===");
|
||||||
|
|
||||||
const sel = document.getElementById('node-input-positionVsParent');
|
const sel = document.getElementById('node-input-positionVsParent');
|
||||||
node.positionVsParent = sel? sel.value : 'atEquipment';
|
const hasDistanceCheck = document.getElementById('node-input-hasDistance');
|
||||||
|
const distanceInput = document.getElementById('node-input-distance');
|
||||||
|
|
||||||
|
console.log("→ sel element found:", !!sel);
|
||||||
|
console.log("→ sel:", sel);
|
||||||
|
console.log("→ sel.value:", sel ? sel.value : "NO ELEMENT");
|
||||||
|
console.log("→ sel.selectedIndex:", sel ? sel.selectedIndex : "NO ELEMENT");
|
||||||
|
console.log("→ sel.options:", sel ? Array.from(sel.options).map(o => ({value: o.value, text: o.textContent})) : "NO OPTIONS");
|
||||||
|
|
||||||
|
if (!sel) {
|
||||||
|
console.error("→ positionMenu.saveEditor FAILED: select element not found!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save existing position data
|
||||||
|
const positionValue = sel.value;
|
||||||
|
const selectedOption = sel.options[sel.selectedIndex];
|
||||||
|
|
||||||
|
console.log("→ positionValue:", positionValue);
|
||||||
|
console.log("→ selectedOption:", selectedOption);
|
||||||
|
console.log("→ selectedOption.textContent:", selectedOption ? selectedOption.textContent : "NO OPTION");
|
||||||
|
console.log("→ selectedOption data-icon:", selectedOption ? selectedOption.getAttribute('data-icon') : "NO ICON");
|
||||||
|
|
||||||
|
node.positionVsParent = positionValue || 'atEquipment';
|
||||||
|
node.positionLabel = selectedOption ? selectedOption.textContent : 'At Equipment';
|
||||||
|
node.positionIcon = selectedOption ? selectedOption.getAttribute('data-icon') : 'fa fa-cog';
|
||||||
|
|
||||||
|
console.log("→ node.positionVsParent set to:", node.positionVsParent);
|
||||||
|
console.log("→ node.positionLabel set to:", node.positionLabel);
|
||||||
|
|
||||||
|
// Save distance data
|
||||||
|
console.log("→ hasDistanceCheck found:", !!hasDistanceCheck);
|
||||||
|
console.log("→ hasDistanceCheck.checked:", hasDistanceCheck ? hasDistanceCheck.checked : "NO ELEMENT");
|
||||||
|
|
||||||
|
node.hasDistance = hasDistanceCheck ? hasDistanceCheck.checked : false;
|
||||||
|
|
||||||
|
if (node.hasDistance && distanceInput && distanceInput.value) {
|
||||||
|
console.log("→ distanceInput.value:", distanceInput.value);
|
||||||
|
node.distance = parseFloat(distanceInput.value) || 0;
|
||||||
|
node.distanceUnit = 'm'; // Fixed to meters for now
|
||||||
|
|
||||||
|
// Generate distance description based on position
|
||||||
|
const contexts = window.EVOLV.nodes.${nodeName}.menuData.position.distanceContexts;
|
||||||
|
const context = contexts && contexts[node.positionVsParent];
|
||||||
|
node.distanceDescription = context ? context.description : 'Distance from parent';
|
||||||
|
|
||||||
|
console.log("→ distance set to:", node.distance);
|
||||||
|
} else {
|
||||||
|
console.log("→ clearing distance data");
|
||||||
|
delete node.distance;
|
||||||
|
delete node.distanceUnit;
|
||||||
|
delete node.distanceDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("→ positionMenu.saveEditor result: SUCCESS");
|
||||||
|
console.log("→ final node.positionVsParent:", node.positionVsParent);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7) Compose everything into one client bundle
|
// 7) Compose everything into one client bundle
|
||||||
getClientInitCode(nodeName) {
|
getClientInitCode(nodeName) {
|
||||||
|
|||||||
606
src/menu/tagcodeApp.js
Normal file
606
src/menu/tagcodeApp.js
Normal file
@@ -0,0 +1,606 @@
|
|||||||
|
/**
|
||||||
|
* taggcodeApp.js
|
||||||
|
* Dynamische AssetMenu implementatie met TagcodeApp API
|
||||||
|
* Vervangt de statische assetData met calls naar REST-endpoints.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class TagcodeApp {
|
||||||
|
constructor(baseURL = 'https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api') {
|
||||||
|
this.baseURL = baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchData(path, params = {}) {
|
||||||
|
const url = new URL(`${this.baseURL}/${path}`);
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
url.searchParams.append(key, value);
|
||||||
|
});
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
const json = await response.json();
|
||||||
|
if (!json.success) throw new Error(json.error || json.message);
|
||||||
|
return json.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asset endpoints
|
||||||
|
getAllAssets() {
|
||||||
|
return this.fetchData('asset/get_all_assets.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetDetail(tag_code) {
|
||||||
|
return this.fetchData('asset/get_detail_asset.php', { tag_code });
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetHistory(asset_tag_number) {
|
||||||
|
return this.fetchData('asset/get_history_asset.php', { asset_tag_number });
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetHierarchy(asset_tag_number) {
|
||||||
|
return this.fetchData('asset/get_asset_hierarchy.php', { asset_tag_number });
|
||||||
|
}
|
||||||
|
|
||||||
|
createOrUpdateAsset(params) {
|
||||||
|
return this.fetchData('asset/create_asset.php', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product & vendor endpoints
|
||||||
|
getVendors() {
|
||||||
|
return this.fetchData('vendor/get_vendors.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubtypes(vendor_name) {
|
||||||
|
return this.fetchData('product/get_subtypesFromVendor.php', { vendor_name });
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubtypesForCategory(vendor_name, category) {
|
||||||
|
return this.fetchData('product/get_subtypesFromVendorAndCategory.php', {
|
||||||
|
vendor_name,
|
||||||
|
category
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductModels(vendor_name, product_subtype_name) {
|
||||||
|
return this.fetchData('product/get_product_models.php', { vendor_name, product_subtype_name });
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocations() {
|
||||||
|
return this.fetchData('location/get_locations.php');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DynamicAssetMenu {
|
||||||
|
constructor(nodeName, api = new TagcodeApp()) {
|
||||||
|
|
||||||
|
this.nodeName = nodeName;
|
||||||
|
this.api = api;
|
||||||
|
|
||||||
|
//temp translation table for nodeName to API
|
||||||
|
// Mapping van nodeName naar softwareType
|
||||||
|
this.softwareTypeMapping = {
|
||||||
|
'measurement': 'Sensor',
|
||||||
|
'rotatingMachine': 'machine',
|
||||||
|
'valve': 'valve',
|
||||||
|
'pump': 'machine',
|
||||||
|
'heatExchanger': 'machine',
|
||||||
|
// Voeg meer mappings toe als nodig
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bepaal automatisch de softwareType
|
||||||
|
this.softwareType = this.softwareTypeMapping[nodeName] || nodeName;
|
||||||
|
|
||||||
|
|
||||||
|
this.data = {
|
||||||
|
vendors: [],
|
||||||
|
subtypes: {},
|
||||||
|
models: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//Added missing getAllMenuData method
|
||||||
|
|
||||||
|
getAllMenuData() {
|
||||||
|
return {
|
||||||
|
vendors: this.data.vendors || [],
|
||||||
|
locations: this.data.locations || [],
|
||||||
|
htmlTemplate: this.getHtmlTemplate()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialiseer: haal alleen de vendor-lijst en locaties op
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
this.data.suppliers = await this.api.getVendors();
|
||||||
|
this.data.locations = await this.api.getLocations();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize DynamicAssetMenu:', error);
|
||||||
|
this.data.suppliers = [];
|
||||||
|
this.data.locations = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Complete getClientInitCode method with full TagcodeApp definition
|
||||||
|
|
||||||
|
getClientInitCode(nodeName) {
|
||||||
|
return `
|
||||||
|
// --- DynamicAssetMenu voor ${nodeName} ---
|
||||||
|
|
||||||
|
// ✅ Define COMPLETE TagcodeApp class in browser context
|
||||||
|
window.TagcodeApp = window.TagcodeApp || class {
|
||||||
|
constructor(baseURL = 'https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api') {
|
||||||
|
this.baseURL = baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchData(path, params = {}) {
|
||||||
|
const url = new URL(this.baseURL + '/' + path);
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
url.searchParams.append(key, value);
|
||||||
|
});
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
||||||
|
const json = await response.json();
|
||||||
|
if (!json.success) throw new Error(json.error || json.message);
|
||||||
|
return json.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ALL API methods defined here
|
||||||
|
getAllAssets() {
|
||||||
|
return this.fetchData('asset/get_all_assets.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetDetail(tag_code) {
|
||||||
|
return this.fetchData('asset/get_detail_asset.php', { tag_code });
|
||||||
|
}
|
||||||
|
|
||||||
|
getVendors() {
|
||||||
|
return this.fetchData('vendor/get_vendors.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubtypes(vendor_name, category = null) {
|
||||||
|
const params = { vendor_name };
|
||||||
|
if (category) params.category = category;
|
||||||
|
return this.fetchData('product/get_subtypesFromVendor.php', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductModels(vendor_name, product_subtype_name) {
|
||||||
|
return this.fetchData('product/get_product_models.php', { vendor_name, product_subtype_name });
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocations() {
|
||||||
|
return this.fetchData('location/get_locations.php');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Initialize the API instance BEFORE it's needed
|
||||||
|
window.assetAPI = window.assetAPI || new window.TagcodeApp();
|
||||||
|
|
||||||
|
// Helper populate function
|
||||||
|
function populate(el, opts, sel) {
|
||||||
|
if (!el) return;
|
||||||
|
const old = el.value;
|
||||||
|
el.innerHTML = '<option value="">Select…</option>';
|
||||||
|
(opts||[]).forEach(o=>{
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = o;
|
||||||
|
opt.textContent = o;
|
||||||
|
el.appendChild(opt);
|
||||||
|
});
|
||||||
|
el.value = sel || '';
|
||||||
|
if (el.value !== old) el.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Ensure namespace exists and initialize properly
|
||||||
|
if (!window.EVOLV.nodes.${nodeName}.assetMenu) {
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Complete initEditor function
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu.initEditor = async function(node) {
|
||||||
|
try {
|
||||||
|
console.log('🚀 Starting asset menu initialization for ${nodeName}');
|
||||||
|
console.log('🎯 Automatic softwareType: ${this.softwareType}');
|
||||||
|
|
||||||
|
// ✅ Verify API is available
|
||||||
|
if (!window.assetAPI) {
|
||||||
|
console.error('❌ window.assetAPI not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Wait for DOM to be ready and inject HTML with retry
|
||||||
|
const waitForDialogAndInject = () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 20;
|
||||||
|
|
||||||
|
const tryInject = () => {
|
||||||
|
attempts++;
|
||||||
|
console.log('Injection attempt ' + attempts + '/' + maxAttempts);
|
||||||
|
|
||||||
|
const injectionSuccess = this.injectHtml ? this.injectHtml() : false;
|
||||||
|
|
||||||
|
if (injectionSuccess) {
|
||||||
|
console.log('✅ HTML injection successful on attempt:', attempts);
|
||||||
|
resolve(true);
|
||||||
|
} else if (attempts < maxAttempts) {
|
||||||
|
setTimeout(tryInject, 100);
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ HTML injection failed after ' + maxAttempts + ' attempts');
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(tryInject, 200);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait for HTML injection
|
||||||
|
const htmlReady = await waitForDialogAndInject();
|
||||||
|
|
||||||
|
if (!htmlReady) {
|
||||||
|
console.error('❌ Could not inject HTML, continuing without asset menu');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔧 Setting up asset menu functionality');
|
||||||
|
|
||||||
|
// ✅ Load vendor list with error handling
|
||||||
|
try {
|
||||||
|
console.log('📡 Loading vendors...');
|
||||||
|
const vendors = await window.assetAPI.getVendors();
|
||||||
|
console.log('✅ Vendors loaded:', vendors.length);
|
||||||
|
|
||||||
|
// ✅ Handle both string arrays and object arrays
|
||||||
|
const vendorNames = vendors.map(v => v.name || v);
|
||||||
|
populate(document.getElementById('node-input-supplier'), vendorNames, node.supplier);
|
||||||
|
} catch (vendorError) {
|
||||||
|
console.error('❌ Error loading vendors:', vendorError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Get form elements
|
||||||
|
const elems = {
|
||||||
|
supplier: document.getElementById('node-input-supplier'),
|
||||||
|
category: document.getElementById('node-input-category'),
|
||||||
|
type: document.getElementById('node-input-assetType'),
|
||||||
|
model: document.getElementById('node-input-model'),
|
||||||
|
unit: document.getElementById('node-input-unit')
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Set automatic category value
|
||||||
|
if (elems.category) {
|
||||||
|
elems.category.value = '${this.softwareType}';
|
||||||
|
console.log('✅ Automatic category set to:', elems.category.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Supplier change: load subtypes for automatic category
|
||||||
|
if (elems.supplier) {
|
||||||
|
elems.supplier.addEventListener('change', async () => {
|
||||||
|
const vendor = elems.supplier.value;
|
||||||
|
const category = '${this.softwareType}';
|
||||||
|
|
||||||
|
if (!vendor) {
|
||||||
|
populate(elems.type, [], '');
|
||||||
|
populate(elems.model, [], '');
|
||||||
|
populate(elems.unit, [], '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('📡 Loading subtypes for vendor:', vendor, 'category:', category);
|
||||||
|
const subtypes = await window.assetAPI.getSubtypes(vendor, category);
|
||||||
|
console.log('✅ Subtypes loaded:', subtypes.length);
|
||||||
|
|
||||||
|
const subtypeNames = subtypes.map(s => s.name || s.subtype_name || s);
|
||||||
|
populate(elems.type, subtypeNames, node.assetType);
|
||||||
|
|
||||||
|
populate(elems.model, [], '');
|
||||||
|
populate(elems.unit, [], '');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error loading subtypes:', error);
|
||||||
|
populate(elems.type, [], '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Type change: load models for vendor + selected subtype
|
||||||
|
if (elems.type) {
|
||||||
|
elems.type.addEventListener('change', async () => {
|
||||||
|
const vendor = elems.supplier.value;
|
||||||
|
const selectedSubtype = elems.type.value;
|
||||||
|
|
||||||
|
if (!vendor || !selectedSubtype) {
|
||||||
|
populate(elems.model, [], '');
|
||||||
|
populate(elems.unit, [], '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('📡 Loading models for vendor:', vendor, 'subtype:', selectedSubtype);
|
||||||
|
const models = await window.assetAPI.getProductModels(vendor, selectedSubtype);
|
||||||
|
console.log('✅ Models loaded:', models.length);
|
||||||
|
|
||||||
|
window._currentModels = models;
|
||||||
|
const modelNames = models.map(m => m.name || m.model_name || m);
|
||||||
|
populate(elems.model, modelNames, node.model);
|
||||||
|
|
||||||
|
populate(elems.unit, [], '');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error loading models:', error);
|
||||||
|
populate(elems.model, [], '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Model change: show units for selected model
|
||||||
|
if (elems.model) {
|
||||||
|
elems.model.addEventListener('change', () => {
|
||||||
|
const selectedModelName = elems.model.value;
|
||||||
|
const models = window._currentModels || [];
|
||||||
|
const selectedModel = models.find(m =>
|
||||||
|
(m.name || m.model_name) === selectedModelName
|
||||||
|
);
|
||||||
|
|
||||||
|
const units = selectedModel && selectedModel.product_model_meta ?
|
||||||
|
Object.keys(selectedModel.product_model_meta) : [];
|
||||||
|
populate(elems.unit, units, node.unit);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Trigger supplier change if there's a saved value
|
||||||
|
if (node.supplier && elems.supplier) {
|
||||||
|
setTimeout(() => {
|
||||||
|
elems.supplier.dispatchEvent(new Event('change'));
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Asset menu initialization complete for ${nodeName}');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error in asset menu initialization:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHtmlTemplate() {
|
||||||
|
return `
|
||||||
|
<!-- Asset Properties -->
|
||||||
|
<hr />
|
||||||
|
<h3>Asset selection (${this.softwareType})</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-supplier"><i class="fa fa-industry"></i> Supplier</label>
|
||||||
|
<select id="node-input-supplier" style="width:70%;"></select>
|
||||||
|
</div>
|
||||||
|
<!-- ✅ Toon softwareType als readonly info -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label><i class="fa fa-sitemap"></i> Category</label>
|
||||||
|
<input type="text" value="${this.softwareType}" readonly style="width:70%; background-color: #f5f5f5;" />
|
||||||
|
<input type="hidden" id="node-input-category" value="${this.softwareType}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-assetType"><i class="fa fa-puzzle-piece"></i> Type</label>
|
||||||
|
<select id="node-input-assetType" style="width:70%;"></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:70%;"></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:70%;"></select>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed getHtmlInjectionCode method
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Fixed getHtmlInjectionCode method with better element detection
|
||||||
|
*/
|
||||||
|
getHtmlInjectionCode(nodeName) {
|
||||||
|
const htmlTemplate = this.getHtmlTemplate().replace(/`/g, '\\`').replace(/\${/g, '\\${');
|
||||||
|
|
||||||
|
return `
|
||||||
|
// Enhanced HTML injection with multiple fallback strategies
|
||||||
|
window.EVOLV.nodes.${nodeName}.assetMenu.injectHtml = function() {
|
||||||
|
try {
|
||||||
|
// Strategy 1: Find the dialog form container
|
||||||
|
let targetContainer = document.querySelector('#red-ui-editor-dialog .red-ui-editDialog-content');
|
||||||
|
|
||||||
|
// Strategy 2: Fallback to the main dialog form
|
||||||
|
if (!targetContainer) {
|
||||||
|
targetContainer = document.querySelector('#dialog-form');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 3: Fallback to any form in the editor dialog
|
||||||
|
if (!targetContainer) {
|
||||||
|
targetContainer = document.querySelector('#red-ui-editor-dialog form');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 4: Find by Red UI classes
|
||||||
|
if (!targetContainer) {
|
||||||
|
targetContainer = document.querySelector('.red-ui-editor-dialog .editor-tray-content');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetContainer) {
|
||||||
|
// Remove any existing asset menu to prevent duplicates
|
||||||
|
const existingAssetMenu = targetContainer.querySelector('.asset-menu-section');
|
||||||
|
if (existingAssetMenu) {
|
||||||
|
existingAssetMenu.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create container div
|
||||||
|
const assetMenuDiv = document.createElement('div');
|
||||||
|
assetMenuDiv.className = 'asset-menu-section';
|
||||||
|
assetMenuDiv.innerHTML = \`${htmlTemplate}\`;
|
||||||
|
|
||||||
|
// Insert at the beginning of the form
|
||||||
|
targetContainer.insertBefore(assetMenuDiv, targetContainer.firstChild);
|
||||||
|
|
||||||
|
console.log(' Asset menu HTML injected successfully into:', targetContainer.className || targetContainer.tagName);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Could not find dialog form container. Available elements:');
|
||||||
|
console.log('Available dialogs:', document.querySelectorAll('[id*="dialog"], [class*="dialog"]'));
|
||||||
|
console.log('Available forms:', document.querySelectorAll('form'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error injecting HTML:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exporteer voor gebruik in Node-RED
|
||||||
|
module.exports = { TagcodeApp, DynamicAssetMenu };
|
||||||
|
|
||||||
|
/*
|
||||||
|
// --- Test CLI ---
|
||||||
|
// Voer deze test uit met `node tagcodeApp.js` om de API-client en menu-init logica te controleren
|
||||||
|
if (require.main === module) {
|
||||||
|
(async () => {
|
||||||
|
const api = new TagcodeApp();
|
||||||
|
console.log('=== Test: getVendors() ===');
|
||||||
|
let vendors;
|
||||||
|
try {
|
||||||
|
vendors = await api.getVendors();
|
||||||
|
console.log('Vendors:', vendors);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('getVendors() error:', e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== Test: getLocations() ===');
|
||||||
|
try {
|
||||||
|
const locations = await api.getLocations();
|
||||||
|
console.log('Locations:', locations);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('getLocations() error:', e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Test verschillende nodeNames met automatische softwareType mapping
|
||||||
|
const testNodes = [
|
||||||
|
{ nodeName: 'measurement', expectedSoftwareType: 'Sensor' },
|
||||||
|
{ nodeName: 'rotatingMachine', expectedSoftwareType: 'machine' },
|
||||||
|
{ nodeName: 'valve', expectedSoftwareType: 'valve' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testNode of testNodes) {
|
||||||
|
console.log(`\n=== Test: ${testNode.nodeName} → ${testNode.expectedSoftwareType} ===`);
|
||||||
|
|
||||||
|
// Initialize DynamicAssetMenu met automatische softwareType
|
||||||
|
const menu = new DynamicAssetMenu(testNode.nodeName, api);
|
||||||
|
console.log(`✅ Automatic softwareType for ${testNode.nodeName}:`, menu.softwareType);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await menu.init();
|
||||||
|
console.log('Preloaded suppliers:', menu.data.suppliers.map(v=>v.name || v));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`DynamicAssetMenu.init() error for ${testNode.nodeName}:`, e.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`=== Sequential dropdown simulation for ${testNode.nodeName} ===`);
|
||||||
|
|
||||||
|
// 1. Select supplier
|
||||||
|
const supplier = menu.data.suppliers[0];
|
||||||
|
const supplierName = supplier.name || supplier;
|
||||||
|
console.log('Selected supplier:', supplierName);
|
||||||
|
|
||||||
|
// 2. ✅ Gebruik automatische softwareType in plaats van dropdown
|
||||||
|
const automaticCategory = menu.softwareType;
|
||||||
|
console.log('Automatic category (softwareType):', automaticCategory);
|
||||||
|
|
||||||
|
// 3. ✅ Direct naar models met supplier + automatische category
|
||||||
|
let models;
|
||||||
|
try {
|
||||||
|
console.log(`📡 Loading models for supplier: "${supplierName}", category: "${automaticCategory}"`);
|
||||||
|
models = await api.getProductModels(supplierName, automaticCategory);
|
||||||
|
console.log('Fetched models:', models.map(m=>m.name || m));
|
||||||
|
|
||||||
|
if (models.length === 0) {
|
||||||
|
console.warn(`⚠️ No models found for ${supplierName} + ${automaticCategory}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`getProductModels error for ${supplierName} + ${automaticCategory}:`, e.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Extract unique types from models
|
||||||
|
const types = Array.from(new Set(models.map(m => m.product_model_type || m.type || 'Unknown')));
|
||||||
|
console.log('Available types:', types);
|
||||||
|
|
||||||
|
if (types.length === 0) {
|
||||||
|
console.warn('⚠️ No types found in models');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Choose first type
|
||||||
|
const selectedType = types[0];
|
||||||
|
console.log('Selected type:', selectedType);
|
||||||
|
|
||||||
|
// 6. Filter models by type
|
||||||
|
const filteredModels = models.filter(m =>
|
||||||
|
(m.product_model_type || m.type) === selectedType
|
||||||
|
);
|
||||||
|
console.log('Models for selected type:', filteredModels.map(m => m.name || m));
|
||||||
|
|
||||||
|
if (filteredModels.length === 0) {
|
||||||
|
console.warn('⚠️ No models found for selected type');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Choose first model and show units
|
||||||
|
const model = filteredModels[0];
|
||||||
|
console.log('Selected model:', model.name || model);
|
||||||
|
|
||||||
|
const units = model.product_model_meta ? Object.keys(model.product_model_meta) : [];
|
||||||
|
console.log('Available units:', units);
|
||||||
|
const unit = units[0] || 'N/A';
|
||||||
|
console.log('Selected unit:', unit);
|
||||||
|
|
||||||
|
console.log(`✅ Complete flow for ${testNode.nodeName}:`);
|
||||||
|
console.log(` Supplier: ${supplierName}`);
|
||||||
|
console.log(` Category: ${automaticCategory} (automatic)`);
|
||||||
|
console.log(` Type: ${selectedType}`);
|
||||||
|
console.log(` Model: ${model.name || model}`);
|
||||||
|
console.log(` Unit: ${unit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== Test verschillende softwareTypes ===');
|
||||||
|
|
||||||
|
// Test of de API verschillende categories ondersteunt
|
||||||
|
const testCategories = ['Sensor', 'machine', 'valve', 'pump'];
|
||||||
|
const testSupplier = 'Vega'; // Bijvoorbeeld
|
||||||
|
|
||||||
|
for (const category of testCategories) {
|
||||||
|
try {
|
||||||
|
console.log(`\n📡 Testing category: ${category} with supplier: ${testSupplier}`);
|
||||||
|
const models = await api.getProductModels(testSupplier, category);
|
||||||
|
console.log(`✅ Found ${models.length} models for ${testSupplier} + ${category}`);
|
||||||
|
|
||||||
|
if (models.length > 0) {
|
||||||
|
const sampleModel = models[0];
|
||||||
|
console.log(` Sample model:`, sampleModel.name || sampleModel);
|
||||||
|
|
||||||
|
const types = Array.from(new Set(models.map(m => m.product_model_type || m.type)));
|
||||||
|
console.log(` Available types:`, types);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`⚠️ No models found for ${testSupplier} + ${category}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== Klaar met alle tests ===');
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"general": {
|
"general": {
|
||||||
"name": {
|
"name": {
|
||||||
"default": "Interpolation Configuration",
|
"default": "interpolation configuration",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A human-readable name or label for this interpolation configuration."
|
"description": "A human-readable name or label for this interpolation configuration."
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"default": "Interpolator",
|
"default": "interpolator",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Indicates the role of this configuration (e.g., 'Interpolator', 'DataCurve', etc.)."
|
"description": "Indicates the role of this configuration (e.g., 'Interpolator', 'DataCurve', etc.)."
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ class Predict {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildAllFxyCurves(curve) {
|
buildAllFxyCurves(curve) {
|
||||||
|
|
||||||
let globalMinY = Infinity;
|
let globalMinY = Infinity;
|
||||||
let globalMaxY = -Infinity;
|
let globalMaxY = -Infinity;
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class movementManager {
|
|||||||
|
|
||||||
this.speed = speed;
|
this.speed = speed;
|
||||||
this.maxSpeed = maxSpeed;
|
this.maxSpeed = maxSpeed;
|
||||||
|
console.log(`MovementManager: Initial speed=${this.speed}, maxSpeed=${maxSpeed}`);
|
||||||
this.interval = interval;
|
this.interval = interval;
|
||||||
this.timeleft = 0; // timeleft of current movement
|
this.timeleft = 0; // timeleft of current movement
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class state{
|
|||||||
return this.stateManager.getRunTimeHours();
|
return this.stateManager.getRunTimeHours();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async moveTo(targetPosition) {
|
async moveTo(targetPosition) {
|
||||||
|
|
||||||
// Check for invalid conditions and throw errors
|
// Check for invalid conditions and throw errors
|
||||||
@@ -86,14 +87,33 @@ class state{
|
|||||||
|
|
||||||
// -------- State Transition Methods -------- //
|
// -------- State Transition Methods -------- //
|
||||||
|
|
||||||
|
abortCurrentMovement(reason = "group override") {
|
||||||
|
if (this.abortController && !this.abortController.signal.aborted) {
|
||||||
|
this.logger.warn(`Aborting movement: ${reason}`);
|
||||||
|
this.abortController.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async transitionToState(targetState, signal) {
|
async transitionToState(targetState, signal) {
|
||||||
|
|
||||||
const fromState = this.getCurrentState();
|
const fromState = this.getCurrentState();
|
||||||
const position = this.getCurrentPosition();
|
const position = this.getCurrentPosition();
|
||||||
|
|
||||||
|
// Define states that cannot be aborted for safety reasons
|
||||||
|
const protectedStates = ['warmingup', 'coolingdown'];
|
||||||
|
const isProtectedTransition = protectedStates.includes(fromState);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
this.logger.debug(`Starting transition from ${fromState} to ${targetState}.`);
|
this.logger.debug(`Starting transition from ${fromState} to ${targetState}.`);
|
||||||
|
if( isProtectedTransition){
|
||||||
|
//overrule signal to prevent abortion
|
||||||
|
signal = null; // Disable abortion for protected states
|
||||||
|
//spit warning
|
||||||
|
this.logger.warn(`Transition from ${fromState} to ${targetState} is protected and cannot be aborted.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Await the state transition and pass signal for abortion
|
||||||
const feedback = await this.stateManager.transitionTo(targetState,signal);
|
const feedback = await this.stateManager.transitionTo(targetState,signal);
|
||||||
this.logger.info(`Statemanager: ${feedback}`);
|
this.logger.info(`Statemanager: ${feedback}`);
|
||||||
|
|
||||||
@@ -108,7 +128,6 @@ class state{
|
|||||||
//trigger move
|
//trigger move
|
||||||
await this.moveTo(this.delayedMove,signal);
|
await this.moveTo(this.delayedMove,signal);
|
||||||
this.delayedMove = null;
|
this.delayedMove = null;
|
||||||
|
|
||||||
this.logger.info(`moveTo : ${feedback} `);
|
this.logger.info(`moveTo : ${feedback} `);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"general": {
|
"general": {
|
||||||
"name": {
|
"name": {
|
||||||
"default": "State Configuration",
|
"default": "state configuration",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A human-readable name for the state configuration."
|
"description": "A human-readable name for the state configuration."
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"default": "StateController",
|
"default": "statecontroller",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Functional role within the system."
|
"description": "Functional role within the system."
|
||||||
|
|||||||
Reference in New Issue
Block a user