Compare commits
5 Commits
d52a1827e3
...
dev-Pieter
| Author | SHA1 | Date | |
|---|---|---|---|
| d5d078413c | |||
| 17662ef7cb | |||
| f653a1e98c | |||
| 3886277616 | |||
| 83018fabe0 |
@@ -66,6 +66,15 @@
|
||||
"units": ["g/m³", "mol/m³"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Quantity (TSS)",
|
||||
"models": [
|
||||
{
|
||||
"name": "VegaSolidsProbe",
|
||||
"units": ["g/m³"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -83,13 +92,7 @@
|
||||
{
|
||||
"id": "hidrostal-pump-001",
|
||||
"name": "hidrostal-H05K-S03R",
|
||||
|
||||
"units": ["l/s"]
|
||||
},
|
||||
{
|
||||
"id": "hidrostal-pump-002",
|
||||
"name": "hidrostal-C5-D03R-SHN1",
|
||||
"units": ["l/s"]
|
||||
"units": ["m³/h", "gpm", "l/min"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,838 +0,0 @@
|
||||
{
|
||||
"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,89 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class AssetCategoryManager {
|
||||
constructor(relPath = '.') {
|
||||
this.assetDir = path.resolve(__dirname, relPath);
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
getCategory(softwareType) {
|
||||
if (!softwareType) {
|
||||
throw new Error('softwareType is required');
|
||||
}
|
||||
|
||||
if (this.cache.has(softwareType)) {
|
||||
return this.cache.get(softwareType);
|
||||
}
|
||||
|
||||
const filePath = path.resolve(this.assetDir, `${softwareType}.json`);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Asset data '${softwareType}' not found in ${this.assetDir}`);
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
this.cache.set(softwareType, parsed);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
hasCategory(softwareType) {
|
||||
const filePath = path.resolve(this.assetDir, `${softwareType}.json`);
|
||||
return fs.existsSync(filePath);
|
||||
}
|
||||
|
||||
listCategories({ withMeta = false } = {}) {
|
||||
const files = fs.readdirSync(this.assetDir, { withFileTypes: true });
|
||||
|
||||
return files
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.isFile() &&
|
||||
entry.name.endsWith('.json') &&
|
||||
entry.name !== 'index.json' &&
|
||||
entry.name !== 'assetData.json'
|
||||
)
|
||||
.map((entry) => path.basename(entry.name, '.json'))
|
||||
.map((name) => {
|
||||
if (!withMeta) {
|
||||
return name;
|
||||
}
|
||||
|
||||
const data = this.getCategory(name);
|
||||
return {
|
||||
softwareType: data.softwareType || name,
|
||||
label: data.label || name,
|
||||
file: `${name}.json`
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
searchCategories(query) {
|
||||
const term = (query || '').trim().toLowerCase();
|
||||
if (!term) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.listCategories({ withMeta: true }).filter(
|
||||
({ softwareType, label }) =>
|
||||
softwareType.toLowerCase().includes(term) ||
|
||||
label.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const assetCategoryManager = new AssetCategoryManager();
|
||||
|
||||
module.exports = {
|
||||
AssetCategoryManager,
|
||||
assetCategoryManager,
|
||||
getCategory: (softwareType) => assetCategoryManager.getCategory(softwareType),
|
||||
listCategories: (options) => assetCategoryManager.listCategories(options),
|
||||
searchCategories: (query) => assetCategoryManager.searchCategories(query),
|
||||
hasCategory: (softwareType) => assetCategoryManager.hasCategory(softwareType),
|
||||
clearCache: () => assetCategoryManager.clearCache()
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"id": "machine",
|
||||
"label": "machine",
|
||||
"softwareType": "machine",
|
||||
"suppliers": [
|
||||
{
|
||||
"id": "hidrostal",
|
||||
"name": "Hidrostal",
|
||||
"types": [
|
||||
{
|
||||
"id": "pump-centrifugal",
|
||||
"name": "Centrifugal",
|
||||
"models": [
|
||||
{ "id": "hidrostal-H05K-S03R", "name": "hidrostal-H05K-S03R", "units": ["l/s","m3/h"] },
|
||||
{ "id": "hidrostal-C5-D03R-SHN1", "name": "hidrostal-C5-D03R-SHN1", "units": ["l/s"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"id": "sensor",
|
||||
"label": "Sensor",
|
||||
"softwareType": "measurement",
|
||||
"suppliers": [
|
||||
{
|
||||
"id": "vega",
|
||||
"name": "Vega",
|
||||
"types": [
|
||||
{
|
||||
"id": "temperature",
|
||||
"name": "Temperature",
|
||||
"models": [
|
||||
{ "id": "vega-temp-10", "name": "VegaTemp 10", "units": ["degC", "degF"] },
|
||||
{ "id": "vega-temp-20", "name": "VegaTemp 20", "units": ["degC", "degF"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pressure",
|
||||
"name": "Pressure",
|
||||
"models": [
|
||||
{ "id": "vega-pressure-10", "name": "VegaPressure 10", "units": ["bar", "mbar", "psi"] },
|
||||
{ "id": "vega-pressure-20", "name": "VegaPressure 20", "units": ["bar", "mbar", "psi"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "flow",
|
||||
"name": "Flow",
|
||||
"models": [
|
||||
{ "id": "vega-flow-10", "name": "VegaFlow 10", "units": ["m3/h", "gpm", "l/min"] },
|
||||
{ "id": "vega-flow-20", "name": "VegaFlow 20", "units": ["m3/h", "gpm", "l/min"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "level",
|
||||
"name": "Level",
|
||||
"models": [
|
||||
{ "id": "vega-level-10", "name": "VegaLevel 10", "units": ["m", "ft", "mm"] },
|
||||
{ "id": "vega-level-20", "name": "VegaLevel 20", "units": ["m", "ft", "mm"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "oxygen",
|
||||
"name": "Quantity (oxygen)",
|
||||
"models": [
|
||||
{ "id": "vega-oxy-10", "name": "VegaOxySense 10", "units": ["g/m3", "mol/m3"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"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]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,838 +0,0 @@
|
||||
{
|
||||
"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
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,124 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class AssetLoader {
|
||||
constructor() {
|
||||
this.relPath = './'
|
||||
this.baseDir = path.resolve(__dirname, this.relPath);
|
||||
this.cache = new Map(); // Cache loaded JSON files for better performance
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a specific curve by type
|
||||
* @param {string} curveType - The curve identifier (e.g., 'hidrostal-H05K-S03R')
|
||||
* @returns {Object|null} The curve data object or null if not found
|
||||
*/
|
||||
loadModel(modelType) {
|
||||
return this.loadAsset('models', modelType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load any asset from a specific dataset folder
|
||||
* @param {string} datasetType - The dataset folder name (e.g., 'curves', 'assetData')
|
||||
* @param {string} assetId - The specific asset identifier
|
||||
* @returns {Object|null} The asset data object or null if not found
|
||||
*/
|
||||
loadAsset(datasetType, assetId) {
|
||||
//const cacheKey = `${datasetType}/${assetId}`;
|
||||
const cacheKey = `${assetId}`;
|
||||
|
||||
|
||||
// Check cache first
|
||||
if (this.cache.has(cacheKey)) {
|
||||
return this.cache.get(cacheKey);
|
||||
}
|
||||
|
||||
try {
|
||||
const filePath = path.join(this.baseDir, `${assetId}.json`);
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.warn(`Asset not found: ${filePath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load and parse JSON
|
||||
const rawData = fs.readFileSync(filePath, 'utf8');
|
||||
const assetData = JSON.parse(rawData);
|
||||
|
||||
// Cache the result
|
||||
this.cache.set(cacheKey, assetData);
|
||||
|
||||
return assetData;
|
||||
} catch (error) {
|
||||
console.error(`Error loading asset ${cacheKey}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available assets in a dataset
|
||||
* @param {string} datasetType - The dataset folder name
|
||||
* @returns {string[]} Array of available asset IDs
|
||||
*/
|
||||
getAvailableAssets(datasetType) {
|
||||
try {
|
||||
const datasetPath = path.join(this.baseDir, datasetType);
|
||||
|
||||
if (!fs.existsSync(datasetPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs.readdirSync(datasetPath)
|
||||
.filter(file => file.endsWith('.json'))
|
||||
.map(file => file.replace('.json', ''));
|
||||
} catch (error) {
|
||||
console.error(`Error reading dataset ${datasetType}:`, error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache (useful for development/testing)
|
||||
*/
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export a singleton instance
|
||||
const assetLoader = new AssetLoader();
|
||||
|
||||
module.exports = {
|
||||
AssetLoader,
|
||||
assetLoader,
|
||||
// Convenience methods for backward compatibility
|
||||
loadModel: (modelType) => assetLoader.loadModel(modelType),
|
||||
loadAsset: (datasetType, assetId) => assetLoader.loadAsset(datasetType, assetId),
|
||||
getAvailableAssets: (datasetType) => assetLoader.getAvailableAssets(datasetType)
|
||||
};
|
||||
|
||||
/*
|
||||
// Example usage in your scripts
|
||||
const loader = new AssetLoader();
|
||||
|
||||
// Load a specific curve
|
||||
const curve = loader.loadModel('hidrostal-H05K-S03R');
|
||||
if (curve) {
|
||||
console.log('Model loaded:', curve);
|
||||
} else {
|
||||
console.log('Model not found');
|
||||
}
|
||||
/*
|
||||
// Load any asset from any dataset
|
||||
const someAsset = loadAsset('assetData', 'some-asset-id');
|
||||
|
||||
// Get list of available models
|
||||
const availableCurves = getAvailableAssets('curves');
|
||||
console.log('Available curves:', availableCurves);
|
||||
|
||||
// Using the class directly for more control
|
||||
const { AssetLoader } = require('./index.js');
|
||||
const customLoader = new AssetLoader();
|
||||
const data = customLoader.loadCurve('hidrostal-H05K-S03R');
|
||||
*/
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"id": "valve",
|
||||
"label": "valve",
|
||||
"softwareType": "valve",
|
||||
"suppliers": [
|
||||
{
|
||||
"id": "binder",
|
||||
"name": "Binder Engineering",
|
||||
"types": [
|
||||
{
|
||||
"id": "valve-gate",
|
||||
"name": "Gate",
|
||||
"models": [
|
||||
{ "id": "binder-valve-001", "name": "ECDV", "units": ["m3/h", "gpm", "l/min"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "valve-jet",
|
||||
"name": "Jet",
|
||||
"models": [
|
||||
{ "id": "binder-valve-002", "name": "JCV", "units": ["m3/h", "gpm", "l/min"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
10
index.js
10
index.js
@@ -13,8 +13,6 @@ const logger = require('./src/helper/logger.js');
|
||||
const validation = require('./src/helper/validationUtils.js');
|
||||
const configUtils = require('./src/helper/configUtils.js');
|
||||
const assertions = require('./src/helper/assertionUtils.js')
|
||||
const coolprop = require('./src/coolprop-node/src/index.js');
|
||||
const gravity = require('./src/helper/gravity.js')
|
||||
|
||||
// Domain-specific modules
|
||||
const { MeasurementContainer } = require('./src/measurements/index.js');
|
||||
@@ -26,8 +24,7 @@ const MenuManager = require('./src/menu/index.js');
|
||||
const predict = require('./src/predict/predict_class.js');
|
||||
const interpolation = require('./src/predict/interpolation.js');
|
||||
const childRegistrationUtils = require('./src/helper/childRegistrationUtils.js');
|
||||
const { loadCurve } = require('./datasets/assetData/curves/index.js'); //deprecated replace with load model data
|
||||
const { loadModel } = require('./datasets/assetData/modelData/index.js');
|
||||
const { loadCurve } = require('./datasets/assetData/curves/index.js');
|
||||
|
||||
// Export everything
|
||||
module.exports = {
|
||||
@@ -42,11 +39,8 @@ module.exports = {
|
||||
MeasurementContainer,
|
||||
nrmse,
|
||||
state,
|
||||
coolprop,
|
||||
convert,
|
||||
MenuManager,
|
||||
childRegistrationUtils,
|
||||
loadCurve, //deprecated replace with loadModel
|
||||
loadModel,
|
||||
gravity
|
||||
loadCurve
|
||||
};
|
||||
|
||||
@@ -1,791 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"minHeightBasedOn": {
|
||||
"default": "outlet",
|
||||
"rules": {
|
||||
"type": "enum",
|
||||
"values": [
|
||||
{
|
||||
"value": "inlet",
|
||||
"description": "Minimum height is based on inlet elevation."
|
||||
},
|
||||
{
|
||||
"value": "outlet",
|
||||
"description": "Minimum height is based on outlet elevation."
|
||||
}
|
||||
],
|
||||
"description": "Basis for minimum height check: inlet or outlet."
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"mode": {
|
||||
"default": "levelbased",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"values": [
|
||||
{
|
||||
"value": "levelbased",
|
||||
"description": "Lead and lag pumps are controlled by basin level thresholds."
|
||||
},
|
||||
{
|
||||
"value": "pressureBased",
|
||||
"description": "Pumps target a discharge pressure setpoint."
|
||||
},
|
||||
{
|
||||
"value": "flowBased",
|
||||
"description": "Pumps modulate to match measured inflow or downstream demand."
|
||||
},
|
||||
{
|
||||
"value": "percentageBased",
|
||||
"description": "Pumps operate to maintain basin volume at a target percentage."
|
||||
},
|
||||
{
|
||||
"value":"powerBased",
|
||||
"description": "Pumps are controlled based on power consumption.For example, to limit peak power usage or operate within netcongestion limits."
|
||||
},
|
||||
{
|
||||
"value": "hybrid",
|
||||
"description": "Combines multiple control strategies for optimized operation."
|
||||
},
|
||||
{
|
||||
"value": "manual",
|
||||
"description": "Pumps are operated manually or by an external controller."
|
||||
}
|
||||
],
|
||||
"description": "Primary control philosophy for pump actuation."
|
||||
}
|
||||
},
|
||||
"allowedModes": {
|
||||
"default": [
|
||||
"levelbased",
|
||||
"pressurebased",
|
||||
"flowbased",
|
||||
"percentagebased",
|
||||
"powerbased",
|
||||
"manual"
|
||||
],
|
||||
"rules": {
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "List of control modes that the station is permitted to operate in."
|
||||
}
|
||||
},
|
||||
"levelbased": {
|
||||
"thresholds": {
|
||||
"default": [30,40,50,60,70,80,90],
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"description": "Each time a threshold is overwritten a new pump can start or kick into higher gear. Volume thresholds (%) in ascending order used for level-based control."
|
||||
}
|
||||
},
|
||||
"timeThresholdSeconds": {
|
||||
"default": 5,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"description": "Duration the volume condition must persist before triggering pump actions (seconds)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"pressureBased": {
|
||||
"pressureSetpoint": {
|
||||
"default": 1000,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"max": 5000,
|
||||
"description": "Target discharge pressure when operating in pressure control (kPa)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"flowBased": {
|
||||
"equalizationTargetPercent": {
|
||||
"default": 60,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"description": "Target fill percentage of the basin when operating in equalization mode."
|
||||
}
|
||||
},
|
||||
"flowBalanceTolerance": {
|
||||
"default": 5,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"description": "Allowable error between inflow and outflow before adjustments are triggered (m3/h)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"percentageBased": {
|
||||
"targetVolumePercent": {
|
||||
"default": 50,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"description": "Target basin volume percentage to maintain during percentage-based control."
|
||||
}
|
||||
},
|
||||
"tolerancePercent": {
|
||||
"default": 5,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"description": "Acceptable deviation from the target volume percentage before corrective action is taken."
|
||||
}
|
||||
}
|
||||
},
|
||||
"powerBased": {
|
||||
"maxPowerKW": {
|
||||
"default": 50,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"description": "Maximum allowable power consumption for the pumping station (kW)."
|
||||
}
|
||||
},
|
||||
"powerControlMode": {
|
||||
"default": "limit",
|
||||
"rules": {
|
||||
"type": "enum",
|
||||
"values": [
|
||||
{
|
||||
"value": "limit",
|
||||
"description": "Limit pump operation to stay below the max power threshold."
|
||||
},
|
||||
{
|
||||
"value": "optimize",
|
||||
"description": "Optimize pump scheduling to minimize power usage while meeting flow demands."
|
||||
}
|
||||
],
|
||||
"description": "Defines how power constraints are managed during operation."
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualOverrideTimeoutMinutes": {
|
||||
"default": 30,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"description": "Duration after which a manual override expires automatically (minutes)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"safety": {
|
||||
"enableDryRunProtection": {
|
||||
"default": true,
|
||||
"rules": {
|
||||
"type": "boolean",
|
||||
"description": "If true, pumps will be prevented from running if basin volume is too low."
|
||||
}
|
||||
},
|
||||
"dryRunThresholdPercent": {
|
||||
"default": 2,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"description": "Volume percentage below which dry run protection activates."
|
||||
}
|
||||
},
|
||||
"dryRunDebounceSeconds": {
|
||||
"default": 30,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"description": "Time the low-volume condition must persist before dry-run protection engages (seconds)."
|
||||
}
|
||||
},
|
||||
"enableOverfillProtection": {
|
||||
"default": true,
|
||||
"rules": {
|
||||
"type": "boolean",
|
||||
"description": "If true, high level alarms and shutdowns will be enforced to prevent overfilling."
|
||||
}
|
||||
},
|
||||
"overfillThresholdPercent": {
|
||||
"default": 98,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"description": "Volume percentage above which overfill protection activates."
|
||||
}
|
||||
},
|
||||
"overfillDebounceSeconds": {
|
||||
"default": 30,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"description": "Time the high-volume condition must persist before overfill protection engages (seconds)."
|
||||
}
|
||||
},
|
||||
"timeleftToFullOrEmptyThresholdSeconds": {
|
||||
"default": 120,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"min": 0,
|
||||
"description": "Time threshold (seconds) used to predict imminent full or empty conditions."
|
||||
}
|
||||
}
|
||||
},
|
||||
"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)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,6 +245,10 @@
|
||||
{
|
||||
"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."
|
||||
@@ -256,14 +260,7 @@
|
||||
"type": "object",
|
||||
"schema":{
|
||||
"auto": {
|
||||
"default": [
|
||||
"statuscheck",
|
||||
"execmovement",
|
||||
"execsequence",
|
||||
"flowmovement",
|
||||
"emergencystop",
|
||||
"entermaintenance"
|
||||
],
|
||||
"default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"],
|
||||
"rules": {
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
@@ -271,14 +268,7 @@
|
||||
}
|
||||
},
|
||||
"virtualControl": {
|
||||
"default": [
|
||||
"statuscheck",
|
||||
"execmovement",
|
||||
"flowmovement",
|
||||
"execsequence",
|
||||
"emergencystop",
|
||||
"exitmaintenance"
|
||||
],
|
||||
"default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"],
|
||||
"rules": {
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
@@ -286,21 +276,24 @@
|
||||
}
|
||||
},
|
||||
"fysicalControl": {
|
||||
"default": [
|
||||
"statuscheck",
|
||||
"emergencystop",
|
||||
"entermaintenance",
|
||||
"exitmaintenance"
|
||||
],
|
||||
"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": {},
|
||||
@@ -393,22 +386,6 @@
|
||||
"itemType": "string",
|
||||
"description": "Sequence of states for booting up the machine."
|
||||
}
|
||||
},
|
||||
"entermaintenance":{
|
||||
"default": ["stopping","coolingdown","idle","maintenance"],
|
||||
"rules": {
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Sequence of states if the machine is running to put it in maintenance state"
|
||||
}
|
||||
},
|
||||
"exitmaintenance":{
|
||||
"default": ["off","idle"],
|
||||
"rules": {
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Sequence of states if the machine is running to put it in maintenance state"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -435,6 +412,14 @@
|
||||
],
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
src/coolprop-node/.gitattributes
vendored
2
src/coolprop-node/.gitattributes
vendored
@@ -1,2 +0,0 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
@@ -1,253 +0,0 @@
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
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);
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
@@ -1,487 +0,0 @@
|
||||
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;
|
||||
this.PropsSI = this._propsSI.bind(this);
|
||||
|
||||
|
||||
// 🔹 Wastewater correction options (defaults)
|
||||
this._ww = {
|
||||
enabled: true,
|
||||
tss_g_per_L: 3.5, // default MLSS / TSS
|
||||
density_k: 2e-4, // +0.02% per g/L
|
||||
viscosity_k: 0.07, // +7% per g/L (clamped)
|
||||
viscosity_max_gpl: 4 // cap effect at 4 g/L
|
||||
};
|
||||
|
||||
this._initPromise = null;
|
||||
this._autoInit({ refrigerant: 'Water' });
|
||||
|
||||
}
|
||||
|
||||
_isWastewaterFluid(fluidRaw) {
|
||||
if (!fluidRaw) return false;
|
||||
const token = String(fluidRaw).trim().toLowerCase();
|
||||
return token === 'wastewater' || token.startsWith('wastewater:');
|
||||
}
|
||||
|
||||
_parseWastewaterFluid(fluidRaw) {
|
||||
if (!this._isWastewaterFluid(fluidRaw)) return null;
|
||||
const ww = { ...this._ww };
|
||||
const [, tail] = String(fluidRaw).split(':');
|
||||
if (tail) {
|
||||
tail.split(',').forEach(pair => {
|
||||
const [key, value] = pair.split('=').map(s => s.trim().toLowerCase());
|
||||
if (key === 'tss' && !Number.isNaN(Number(value))) {
|
||||
ww.tss_g_per_L = Number(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
return ww;
|
||||
}
|
||||
|
||||
_applyWastewaterCorrection(outputKey, baseValue, ww) {
|
||||
if (!Number.isFinite(baseValue) || !ww || !ww.enabled) return baseValue;
|
||||
switch (outputKey.toUpperCase()) {
|
||||
case 'D': // density
|
||||
return baseValue * (1 + ww.density_k * ww.tss_g_per_L);
|
||||
case 'V': // viscosity
|
||||
const effTss = Math.min(ww.tss_g_per_L, ww.viscosity_max_gpl);
|
||||
return baseValue * (1 + ww.viscosity_k * effTss);
|
||||
default:
|
||||
return baseValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 };
|
||||
}
|
||||
}
|
||||
|
||||
_autoInit(defaults) {
|
||||
if (!this._initPromise) {
|
||||
this._initPromise = this.init(defaults);
|
||||
}
|
||||
return this._initPromise;
|
||||
}
|
||||
|
||||
_propsSI(outputKey, inKey1, inVal1, inKey2, inVal2, fluidRaw) {
|
||||
if (!this.initialized) {
|
||||
// Start init if no one else asked yet
|
||||
this._autoInit({ refrigerant: this.defaultRefrigerant || 'Water' });
|
||||
throw new Error('CoolProp is still warming up, retry PropsSI in a moment');
|
||||
}
|
||||
const ww = this._parseWastewaterFluid(fluidRaw);
|
||||
const fluid = ww ? 'Water' : (this.customRefString || fluidRaw);
|
||||
const baseValue = coolprop.PropsSI(outputKey, inKey1, inVal1, inKey2, inVal2, fluid);
|
||||
return ww ? this._applyWastewaterCorrection(outputKey, baseValue, ww) : baseValue;
|
||||
}
|
||||
|
||||
//Access to coolprop
|
||||
async getPropsSI() {
|
||||
await this._ensureInit({ refrigerant: this.defaultRefrigerant || 'Water' });
|
||||
return this.PropsSI;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = new CoolPropWrapper();
|
||||
@@ -1,308 +0,0 @@
|
||||
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;
|
||||
@@ -1,94 +0,0 @@
|
||||
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
|
||||
});
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
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
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
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
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
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
|
||||
});
|
||||
});
|
||||
@@ -1,296 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,128 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,14 +5,18 @@ class ChildRegistrationUtils {
|
||||
this.registeredChildren = new Map();
|
||||
}
|
||||
|
||||
async registerChild(child, positionVsParent, distance) {
|
||||
async registerChild(child, positionVsParent) {
|
||||
const { softwareType } = child.config.functionality;
|
||||
const { name, id } = child.config.general;
|
||||
|
||||
this.logger.debug(`Registering child: ${name} (${id}) as ${softwareType} at ${positionVsParent}`);
|
||||
|
||||
// Enhanced child setup
|
||||
child.parent = this.mainClass;
|
||||
// Enhanced child setup - multiple parents
|
||||
if (Array.isArray(child.parent)) {
|
||||
child.parent.push(this.mainClass);
|
||||
} else {
|
||||
child.parent = [this.mainClass];
|
||||
}
|
||||
child.positionVsParent = positionVsParent;
|
||||
|
||||
// Enhanced measurement container with rich context
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* Gravity calculations based on WGS-84 ellipsoid model.
|
||||
* Author: Rene de Ren (Waterschap Brabantse Delta)
|
||||
* License: EUPL-1.2
|
||||
*/
|
||||
|
||||
class Gravity {
|
||||
constructor() {
|
||||
// Standard (conventional) gravity at 45° latitude, sea level
|
||||
this.g0 = 9.80665; // m/s²
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns standard gravity (constant)
|
||||
* @returns {number} gravity in m/s²
|
||||
*/
|
||||
getStandardGravity() {
|
||||
return this.g0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes local gravity based on latitude and elevation.
|
||||
* Formula: WGS-84 normal gravity (Somigliana)
|
||||
* @param {number} latitudeDeg Latitude in degrees (−90 → +90)
|
||||
* @param {number} elevationM Elevation above sea level [m]
|
||||
* @returns {number} gravity in m/s²
|
||||
*/
|
||||
getLocalGravity(latitudeDeg, elevationM = 0) {
|
||||
const phi = (latitudeDeg * Math.PI) / 180;
|
||||
const sinPhi = Math.sin(phi);
|
||||
const sin2 = sinPhi * sinPhi;
|
||||
const sin2_2phi = Math.sin(2 * phi) ** 2;
|
||||
|
||||
// WGS-84 normal gravity on the ellipsoid
|
||||
const gSurface =
|
||||
9.780327 * (1 + 0.0053024 * sin2 - 0.0000058 * sin2_2phi);
|
||||
|
||||
// Free-air correction for elevation (~ −3.086×10⁻⁶ m/s² per m)
|
||||
const gLocal = gSurface - 3.086e-6 * elevationM;
|
||||
return gLocal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates hydrostatic pressure difference (ΔP = ρ g h)
|
||||
* @param {number} density Fluid density [kg/m³]
|
||||
* @param {number} heightM Height difference [m]
|
||||
* @param {number} latitudeDeg Latitude (for local g)
|
||||
* @param {number} elevationM Elevation (for local g)
|
||||
* @returns {number} Pressure difference [Pa]
|
||||
*/
|
||||
pressureHead(density, heightM, latitudeDeg = 45, elevationM = 0) {
|
||||
const g = this.getLocalGravity(latitudeDeg, elevationM);
|
||||
return density * g * heightM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates weight force (F = m g)
|
||||
* @param {number} massKg Mass [kg]
|
||||
* @param {number} latitudeDeg Latitude (for local g)
|
||||
* @param {number} elevationM Elevation (for local g)
|
||||
* @returns {number} Force [N]
|
||||
*/
|
||||
weightForce(massKg, latitudeDeg = 45, elevationM = 0) {
|
||||
const g = this.getLocalGravity(latitudeDeg, elevationM);
|
||||
return massKg * g;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Gravity();
|
||||
|
||||
|
||||
/*
|
||||
const gravity = gravity;
|
||||
|
||||
// Standard gravity
|
||||
console.log('g₀ =', gravity.getStandardGravity(), 'm/s²');
|
||||
|
||||
// Local gravity (Breda ≈ 51.6° N, 3 m elevation)
|
||||
console.log('g @ Breda =', gravity.getLocalGravity(51.6, 3).toFixed(6), 'm/s²');
|
||||
|
||||
// Head pressure for 5 m water column at Breda
|
||||
console.log(
|
||||
'ΔP =',
|
||||
gravity.pressureHead(1000, 5, 51.6, 3).toFixed(1),
|
||||
'Pa'
|
||||
);
|
||||
|
||||
// Weight of 1 kg mass at Breda
|
||||
console.log('Weight =', gravity.weightForce(1, 51.6, 3).toFixed(6), 'N');
|
||||
*/
|
||||
@@ -180,6 +180,7 @@ async apiCall(node) {
|
||||
// Only add tagCode to URL if it exists
|
||||
if (tagCode) {
|
||||
apiUrl += `&asset_tag_number=${tagCode}`;
|
||||
console.log('hello there');
|
||||
}
|
||||
|
||||
assetregisterAPI += apiUrl;
|
||||
@@ -460,6 +461,10 @@ populateModels(
|
||||
// Store only the metadata for the selected model
|
||||
node["modelMetadata"] = modelData.find((model) => model.name === selectedModel);
|
||||
});
|
||||
/*
|
||||
console.log('hello here I am:');
|
||||
console.log(node["modelMetadata"]);
|
||||
*/
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
@@ -180,6 +180,7 @@ async apiCall(node) {
|
||||
// Only add tagCode to URL if it exists
|
||||
if (tagCode) {
|
||||
apiUrl += `&asset_tag_number=${tagCode}`;
|
||||
console.log('hello there');
|
||||
}
|
||||
|
||||
assetregisterAPI += apiUrl;
|
||||
@@ -460,7 +461,10 @@ populateModels(
|
||||
// Store only the metadata for the selected model
|
||||
node["modelMetadata"] = modelData.find((model) => model.name === selectedModel);
|
||||
});
|
||||
|
||||
/*
|
||||
console.log('hello here I am:');
|
||||
console.log(node["modelMetadata"]);
|
||||
*/
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
@@ -64,7 +64,7 @@ class OutputUtils {
|
||||
|
||||
influxDBFormat(changedFields, config , flatTags) {
|
||||
// Create the measurement and topic using softwareType and name config.functionality.softwareType + .
|
||||
const measurement = `${config.functionality?.softwareType}_${config.general?.id}`;
|
||||
const measurement = config.general.name;
|
||||
const payload = {
|
||||
measurement: measurement,
|
||||
fields: changedFields,
|
||||
@@ -104,23 +104,24 @@ class OutputUtils {
|
||||
return {
|
||||
// general properties
|
||||
id: config.general?.id,
|
||||
name: config.general?.name,
|
||||
unit: config.general?.unit,
|
||||
// functionality properties
|
||||
softwareType: config.functionality?.softwareType,
|
||||
role: config.functionality?.role,
|
||||
// asset properties (exclude machineCurve)
|
||||
uuid: config.asset?.uuid,
|
||||
tagcode: config.asset?.tagcode,
|
||||
geoLocation: config.asset?.geoLocation,
|
||||
category: config.asset?.category,
|
||||
supplier: config.asset?.supplier,
|
||||
type: config.asset?.type,
|
||||
subType: config.asset?.subType,
|
||||
model: config.asset?.model,
|
||||
unit: config.general?.unit,
|
||||
};
|
||||
}
|
||||
|
||||
processFormat(changedFields,config) {
|
||||
// Create the measurement and topic using softwareType and name config.functionality.softwareType + .
|
||||
const measurement = `${config.functionality?.softwareType}_${config.general?.id}`;
|
||||
const measurement = config.general.name;
|
||||
const payload = changedFields;
|
||||
const topic = measurement;
|
||||
const msg = { topic: topic, payload: payload };
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
const convertModule = require('../convert/index');
|
||||
|
||||
class Measurement {
|
||||
constructor(type, variant, position, windowSize, distance = null) {
|
||||
constructor(type, variant, position, windowSize) {
|
||||
this.type = type; // e.g. 'pressure', 'flow', etc.
|
||||
this.variant = variant; // e.g. 'predicted' or 'measured', etc..
|
||||
this.position = position; // Downstream or upstream of parent object
|
||||
this.windowSize = windowSize; // Rolling window size
|
||||
this.distance = distance; // Distance from parent, if applicable
|
||||
|
||||
// Place all data inside an array
|
||||
this.values = []; // Array to store all values
|
||||
@@ -37,12 +36,13 @@ class Measurement {
|
||||
return this;
|
||||
}
|
||||
|
||||
setDistance(distance) {
|
||||
this.distance = distance;
|
||||
return this;
|
||||
}
|
||||
|
||||
setValue(value, timestamp = Date.now()) {
|
||||
/*
|
||||
if (value === undefined || value === null) {
|
||||
value = null ;
|
||||
//throw new Error('Value cannot be null or undefined');
|
||||
}
|
||||
*/
|
||||
|
||||
//shift the oldest value
|
||||
if(this.values.length >= this.windowSize){
|
||||
@@ -68,23 +68,6 @@ class Measurement {
|
||||
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() {
|
||||
if (this.values.length === 0) return null;
|
||||
const sum = this.values.reduce((acc, val) => acc + val, 0);
|
||||
@@ -113,7 +96,7 @@ class Measurement {
|
||||
|
||||
// Create a new measurement that is the difference between two positions
|
||||
static createDifference(upstreamMeasurement, downstreamMeasurement) {
|
||||
|
||||
console.log('hello:');
|
||||
if (upstreamMeasurement.type !== downstreamMeasurement.type ||
|
||||
upstreamMeasurement.variant !== downstreamMeasurement.variant) {
|
||||
throw new Error('Cannot calculate difference between different measurement types or variants');
|
||||
@@ -185,8 +168,7 @@ class Measurement {
|
||||
this.type,
|
||||
this.variant,
|
||||
this.position,
|
||||
this.windowSize,
|
||||
this.distance
|
||||
this.windowSize
|
||||
);
|
||||
|
||||
// Copy values and timestamps
|
||||
|
||||
@@ -5,7 +5,6 @@ class MeasurementBuilder {
|
||||
this.type = null;
|
||||
this.variant = null;
|
||||
this.position = null;
|
||||
this.distance = null;
|
||||
this.windowSize = 10; // Default window size
|
||||
}
|
||||
|
||||
@@ -33,11 +32,6 @@ class MeasurementBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
setDistance(distance) {
|
||||
this.distance = distance;
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
// Validate required fields
|
||||
if (!this.type) {
|
||||
@@ -49,14 +43,12 @@ class MeasurementBuilder {
|
||||
if (!this.position) {
|
||||
throw new Error('Measurement position is required');
|
||||
}
|
||||
// distance is not a requirement as it can be derived from position
|
||||
|
||||
return new Measurement(
|
||||
this.type,
|
||||
this.variant,
|
||||
this.position,
|
||||
this.windowSize,
|
||||
this.distance
|
||||
this.windowSize
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ const EventEmitter = require('events');
|
||||
const convertModule = require('../convert/index');
|
||||
|
||||
class MeasurementContainer {
|
||||
constructor(options = {},logger) {
|
||||
constructor(options = {}) {
|
||||
this.emitter = new EventEmitter();
|
||||
this.measurements = {};
|
||||
this.windowSize = options.windowSize || 10; // Default window size
|
||||
@@ -12,7 +12,6 @@ class MeasurementContainer {
|
||||
this._currentType = null;
|
||||
this._currentVariant = null;
|
||||
this._currentPosition = null;
|
||||
this._currentDistance = null;
|
||||
this._unit = null;
|
||||
|
||||
// Default units for each measurement type
|
||||
@@ -94,19 +93,12 @@ class MeasurementContainer {
|
||||
throw new Error('Variant must be specified before position');
|
||||
}
|
||||
|
||||
|
||||
this._currentPosition = positionValue.toString().toLowerCase();;
|
||||
|
||||
return this;
|
||||
// Turn string positions into numeric values
|
||||
if (typeof positionValue == "string") {
|
||||
positionValue = this._convertPositionStr2Num(positionValue);
|
||||
}
|
||||
|
||||
distance(distance) {
|
||||
// If distance is not provided, derive from positionVsParent
|
||||
if(distance === null) {
|
||||
distance = this._convertPositionStr2Num(this._currentPosition);
|
||||
}
|
||||
|
||||
this._currentDistance = distance;
|
||||
this._currentPosition = positionValue;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -153,13 +145,12 @@ class MeasurementContainer {
|
||||
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,
|
||||
parentRef: this.parentRef
|
||||
};
|
||||
|
||||
// Emit the exact event your parent expects
|
||||
@@ -169,50 +160,6 @@ class MeasurementContainer {
|
||||
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) {
|
||||
if (!this._ensureChainIsValid()) return this;
|
||||
@@ -292,102 +239,47 @@ class MeasurementContainer {
|
||||
return measurement ? measurement.getAllValues() : null;
|
||||
}
|
||||
|
||||
getLaggedValue(lag = 1,requestedUnit = null ){
|
||||
const measurement = this.get();
|
||||
if (!measurement) return null;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 value;
|
||||
}
|
||||
|
||||
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 } = {}) {
|
||||
difference(requestedUnit = null) {
|
||||
if (!this._currentType || !this._currentVariant) {
|
||||
throw new Error("Type and variant must be specified for difference calculation");
|
||||
throw new Error('Type and variant must be specified for difference calculation');
|
||||
}
|
||||
|
||||
const get = pos =>
|
||||
this.measurements?.[this._currentType]?.[this._currentVariant]?.[pos] || null;
|
||||
const savedPosition = this._currentPosition;
|
||||
|
||||
const a = get(from);
|
||||
const b = get(to);
|
||||
if (!a || !b || a.values.length === 0 || b.values.length === 0) {
|
||||
// Get upstream and downstream measurements
|
||||
const positions = this.getPositions();
|
||||
|
||||
this._currentPosition = Math.min(...positions);
|
||||
const upstream = this.get();
|
||||
|
||||
this._currentPosition = Math.max(...positions);
|
||||
const downstream = this.get();
|
||||
|
||||
this._currentPosition = savedPosition;
|
||||
|
||||
if (!upstream || !downstream || upstream.values.length === 0 || downstream.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);
|
||||
// Get target unit for conversion
|
||||
const targetUnit = requestedUnit || upstream.unit || downstream.unit;
|
||||
|
||||
const aAvg = this._convertValueToUnit(a.getAverage(), a.unit, targetUnit);
|
||||
const bAvg = this._convertValueToUnit(b.getAverage(), b.unit, targetUnit);
|
||||
// Get values in the same unit
|
||||
const upstreamValue = this._convertValueToUnit(upstream.getCurrentValue(), upstream.unit, targetUnit);
|
||||
const downstreamValue = this._convertValueToUnit(downstream.getCurrentValue(), downstream.unit, targetUnit);
|
||||
|
||||
const upstreamAvg = this._convertValueToUnit(upstream.getAverage(), upstream.unit, targetUnit);
|
||||
const downstreamAvg = this._convertValueToUnit(downstream.getAverage(), downstream.unit, targetUnit);
|
||||
|
||||
return {
|
||||
value: aVal - bVal,
|
||||
avgDiff: aAvg - bAvg,
|
||||
unit: targetUnit,
|
||||
from,
|
||||
to,
|
||||
value: downstreamValue - upstreamValue,
|
||||
avgDiff: downstreamAvg - upstreamAvg,
|
||||
unit: targetUnit
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
_ensureChainIsValid() {
|
||||
@@ -417,7 +309,6 @@ difference({ from = "downstream", to = "upstream", unit: requestedUnit } = {}) {
|
||||
.setVariant(this._currentVariant)
|
||||
.setPosition(this._currentPosition)
|
||||
.setWindowSize(this.windowSize)
|
||||
.setDistance(this._currentDistance)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -437,7 +328,7 @@ difference({ from = "downstream", to = "upstream", unit: requestedUnit } = {}) {
|
||||
Object.keys(this.measurements[this._currentType]) : [];
|
||||
}
|
||||
|
||||
getPositions() {
|
||||
getPositions(asNumber = false) {
|
||||
if (!this._currentType || !this._currentVariant) {
|
||||
throw new Error('Type and variant must be specified before listing positions');
|
||||
}
|
||||
@@ -447,9 +338,13 @@ difference({ from = "downstream", to = "upstream", unit: requestedUnit } = {}) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (asNumber) {
|
||||
return Object.keys(this.measurements[this._currentType][this._currentVariant]);
|
||||
}
|
||||
|
||||
return Object.keys(this.measurements[this._currentType][this._currentVariant]).map(this._convertPositionNum2Str);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.measurements = {};
|
||||
this._currentType = null;
|
||||
@@ -540,15 +435,18 @@ difference({ from = "downstream", to = "upstream", unit: requestedUnit } = {}) {
|
||||
}
|
||||
|
||||
_convertPositionNum2Str(positionValue) {
|
||||
switch (positionValue) {
|
||||
case 0:
|
||||
if (positionValue === 0) {
|
||||
return "atEquipment";
|
||||
case (positionValue < 0):
|
||||
}
|
||||
if (positionValue < 0) {
|
||||
return "upstream";
|
||||
case (positionValue > 0):
|
||||
}
|
||||
if (positionValue > 0) {
|
||||
return "downstream";
|
||||
default:
|
||||
console.log(`Invalid position provided: ${positionValue}`);
|
||||
}
|
||||
|
||||
if (this.logger) {
|
||||
this.logger.error(`Invalid position provided: ${positionValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,374 +7,249 @@ console.log('retrieving, and converting measurement data with automatic unit han
|
||||
// ====================================
|
||||
// BASIC SETUP EXAMPLES
|
||||
// ====================================
|
||||
console.log('--- Example 1: Basic Setup & Distance ---');
|
||||
console.log('--- Example 1: Basic Setup & Event Subscription ---');
|
||||
|
||||
// Create a basic container
|
||||
const basicContainer = new MeasurementContainer({ windowSize: 20 });
|
||||
|
||||
// Subscribe to events to monitor changes
|
||||
// Subscribe to flow events to monitor changes
|
||||
basicContainer.emitter.on('flow.predicted.upstream', (data) => {
|
||||
console.log(`📡 Event: Flow predicted upstream = ${data.value} ${data.unit || ''} (distance=${data.distance ?? 'n/a'}m)`);
|
||||
console.log(`📡 Event: Flow predicted upstream update: ${data.value} at ${new Date(data.timestamp).toLocaleTimeString()}`);
|
||||
});
|
||||
|
||||
// Subscribe to all measured flow events using wildcard
|
||||
//show all flow values from variant measured
|
||||
basicContainer.emitter.on('flow.measured.*', (data) => {
|
||||
console.log(`📡 Event: Flow measured ${data.position} = ${data.value} ${data.unit || ''} (distance=${data.distance ?? 'n/a'}m)`);
|
||||
console.log(`📡 Event---------- I DID IT: Flow measured ${data.position} update: ${data.value}`)
|
||||
});
|
||||
|
||||
// Basic value setting with distance
|
||||
console.log('\nSetting pressure values with distances:');
|
||||
basicContainer
|
||||
.type('pressure')
|
||||
.variant('measured')
|
||||
.position('upstream')
|
||||
.distance(1.5)
|
||||
.value(100)
|
||||
.unit('psi');
|
||||
|
||||
basicContainer
|
||||
.type('pressure')
|
||||
.variant('measured')
|
||||
.position('downstream')
|
||||
.distance(5.2)
|
||||
.value(95)
|
||||
.unit('psi');
|
||||
|
||||
// Distance persists - no need to set it again for same position
|
||||
basicContainer
|
||||
.type('pressure')
|
||||
.variant('measured')
|
||||
.position('downstream')
|
||||
.value(90); // distance 5.2 is automatically reused
|
||||
// Basic value setting with chaining
|
||||
console.log('Setting basic pressure values...');
|
||||
basicContainer.type('pressure').variant('measured').position('upstream').value(100).unit('psi');
|
||||
basicContainer.type('pressure').variant('measured').position('downstream').value(95).unit('psi');
|
||||
basicContainer.type('pressure').variant('measured').position('downstream').value(80); // Additional value
|
||||
|
||||
console.log('✅ Basic setup complete\n');
|
||||
|
||||
// Retrieve and display the distance
|
||||
const upstreamPressure = basicContainer
|
||||
.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
|
||||
// AUTO-CONVERSION SETUP EXAMPLES
|
||||
// ====================================
|
||||
console.log('--- Example 2: Auto-Conversion Setup ---');
|
||||
console.log('Setting up a container with automatic unit conversion...\n');
|
||||
|
||||
// Create container with auto-conversion enabled
|
||||
const autoContainer = new MeasurementContainer({
|
||||
autoConvert: true,
|
||||
windowSize: 50,
|
||||
defaultUnits: {
|
||||
pressure: 'bar',
|
||||
flow: 'l/min',
|
||||
power: 'kW',
|
||||
temperature: 'C'
|
||||
pressure: 'bar', // Default pressure unit
|
||||
flow: 'l/min', // Default flow unit
|
||||
power: 'kW', // Default power unit
|
||||
temperature: 'C' // Default temperature unit
|
||||
},
|
||||
preferredUnits: {
|
||||
pressure: 'psi'
|
||||
pressure: 'psi' // Override: store pressure in PSI instead of bar
|
||||
}
|
||||
});
|
||||
|
||||
// Values automatically convert to preferred units
|
||||
console.log('Adding pressure with auto-conversion:');
|
||||
autoContainer
|
||||
.type('pressure')
|
||||
.variant('measured')
|
||||
.position('upstream')
|
||||
.distance(0.5)
|
||||
// Values are automatically converted to preferred units
|
||||
console.log('Adding pressure data with auto-conversion:');
|
||||
autoContainer.type('pressure').variant('measured').position('upstream')
|
||||
.value(1.5, Date.now(), 'bar'); // Input: 1.5 bar → Auto-stored as ~21.76 psi
|
||||
|
||||
autoContainer.type('pressure').variant('measured').position('downstream')
|
||||
.value(20, Date.now(), 'psi'); // Input: 20 psi → Stored as 20 psi (already in preferred unit)
|
||||
|
||||
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');
|
||||
// Check what was actually stored
|
||||
const storedPressure = autoContainer.type('pressure').variant('measured').position('upstream').get();
|
||||
console.log(` Stored upstream pressure: ${storedPressure.getCurrentValue()} ${storedPressure.unit}`);
|
||||
console.log(' Auto-conversion setup complete\n');
|
||||
|
||||
// ====================================
|
||||
// UNIT CONVERSION ON RETRIEVAL
|
||||
// UNIT CONVERSION EXAMPLES
|
||||
// ====================================
|
||||
console.log('--- Example 3: Unit Conversion on Retrieval ---');
|
||||
console.log('Getting values in different units without changing stored data...\n');
|
||||
|
||||
autoContainer
|
||||
.type('flow')
|
||||
.variant('predicted')
|
||||
.position('upstream')
|
||||
.distance(2.4)
|
||||
.value(100, Date.now(), 'l/min');
|
||||
// Add flow data in different units
|
||||
autoContainer.type('flow').variant('predicted').position('upstream')
|
||||
.value(100, Date.now(), 'l/min'); // Stored in l/min (default)
|
||||
|
||||
const flowMeasurement = autoContainer
|
||||
.type('flow')
|
||||
.variant('predicted')
|
||||
.position('upstream')
|
||||
.get();
|
||||
autoContainer.type('flow').variant('predicted').position('downstream')
|
||||
.value(6, Date.now(), 'm3/h'); // Auto-converted from m3/h to l/min
|
||||
|
||||
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`);
|
||||
// Retrieve the same data in different units
|
||||
const flowLPM = autoContainer.type('flow').variant('predicted').position('upstream').getCurrentValue('l/min');
|
||||
const flowM3H = autoContainer.type('flow').variant('predicted').position('upstream').getCurrentValue('m3/h');
|
||||
const flowGPM = autoContainer.type('flow').variant('predicted').position('upstream').getCurrentValue('gal/min');
|
||||
|
||||
console.log(`Flow in l/min: ${flowLPM}`);
|
||||
console.log(`Flow in m³/h: ${flowM3H.toFixed(2)}`);
|
||||
console.log(`Flow in gal/min: ${flowGPM.toFixed(2)}`);
|
||||
console.log('Unit conversion examples complete\n');
|
||||
|
||||
// ====================================
|
||||
// SMART UNIT SELECTION
|
||||
// ====================================
|
||||
console.log('--- Example 4: Smart Unit Selection ---');
|
||||
console.log('Automatically finding the best unit for readability...\n');
|
||||
|
||||
autoContainer
|
||||
.type('pressure')
|
||||
.variant('test')
|
||||
.position('sensor')
|
||||
.distance(0.2)
|
||||
// Add a very small pressure value
|
||||
autoContainer.type('pressure').variant('test').position('sensor')
|
||||
.value(0.001, Date.now(), 'bar');
|
||||
|
||||
const bestUnit = autoContainer
|
||||
.type('pressure')
|
||||
.variant('test')
|
||||
.position('sensor')
|
||||
.getBestUnit();
|
||||
|
||||
// Get the best unit for this small value
|
||||
const bestUnit = autoContainer.type('pressure').variant('test').position('sensor').getBestUnit();
|
||||
if (bestUnit) {
|
||||
console.log(`Best unit: ${bestUnit.val.toFixed(2)} ${bestUnit.unit}`);
|
||||
console.log(`Best unit representation: ${bestUnit.val} ${bestUnit.unit}`);
|
||||
}
|
||||
|
||||
// Get all available units for pressure
|
||||
const availableUnits = autoContainer.getAvailableUnits('pressure');
|
||||
console.log(`Available units: ${availableUnits.slice(0, 5).join(', ')}...\n`);
|
||||
console.log(`Available pressure units: ${availableUnits.slice(0, 8).join(', ')}... (${availableUnits.length} total)`);
|
||||
console.log('Smart unit selection complete\n');
|
||||
|
||||
// ====================================
|
||||
// BASIC RETRIEVAL
|
||||
// BASIC RETRIEVAL AND CALCULATIONS
|
||||
// ====================================
|
||||
console.log('--- Example 5: Basic Value Retrieval ---');
|
||||
console.log('Getting individual values and their units...\n');
|
||||
|
||||
const upstreamVal = basicContainer
|
||||
.type('pressure')
|
||||
.variant('measured')
|
||||
.position('upstream')
|
||||
.getCurrentValue();
|
||||
// Using basic container for clear examples
|
||||
const upstreamValue = basicContainer.type('pressure').variant('measured').position('upstream').getCurrentValue();
|
||||
const upstreamUnit = basicContainer.type('pressure').variant('measured').position('upstream').get().unit;
|
||||
console.log(`Upstream pressure: ${upstreamValue} ${upstreamUnit}`);
|
||||
|
||||
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`);
|
||||
const downstreamValue = basicContainer.type('pressure').variant('measured').position('downstream').getCurrentValue();
|
||||
const downstreamUnit = basicContainer.type('pressure').variant('measured').position('downstream').get().unit;
|
||||
console.log(`Downstream pressure: ${downstreamValue} ${downstreamUnit}`);
|
||||
console.log('Basic retrieval complete\n');
|
||||
|
||||
// ====================================
|
||||
// CALCULATIONS & STATISTICS
|
||||
// CALCULATIONS AND STATISTICS
|
||||
// ====================================
|
||||
console.log('--- Example 6: Calculations & Statistics ---');
|
||||
console.log('Using built-in calculation methods...\n');
|
||||
|
||||
basicContainer
|
||||
.type('flow')
|
||||
.variant('predicted')
|
||||
.position('upstream')
|
||||
.distance(3.0)
|
||||
.value(200)
|
||||
.unit('gpm');
|
||||
// Add flow data for calculations
|
||||
basicContainer.type('flow').variant('predicted').position('upstream').value(200).unit('gpm');
|
||||
basicContainer.type('flow').variant('predicted').position('downstream').value(195).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} 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" });
|
||||
// Calculate pressure difference between upstream and downstream
|
||||
const pressureDiff = basicContainer.type('pressure').variant('measured').difference();
|
||||
console.log(`Pressure difference: ${pressureDiff.value} ${pressureDiff.unit}`);
|
||||
console.log('Calculations complete\n');
|
||||
|
||||
// ====================================
|
||||
// ADVANCED STATISTICS & HISTORY
|
||||
// ADVANCED STATISTICS
|
||||
// ====================================
|
||||
console.log('--- Example 7: Advanced Statistics & History ---');
|
||||
console.log('Adding multiple values and getting comprehensive statistics...\n');
|
||||
|
||||
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();
|
||||
// Add several flow measurements to build history
|
||||
basicContainer.type('flow').variant('measured').position('upstream')
|
||||
.value(210).value(215).value(205).value(220).value(200).unit('m3/h');
|
||||
basicContainer.type('flow').variant('measured').position('downstream')
|
||||
.value(190).value(195).value(185).value(200).value(180).unit('m3/h');
|
||||
|
||||
// Get comprehensive statistics
|
||||
const measurement = basicContainer.type('flow').variant('measured').position('upstream');
|
||||
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`);
|
||||
console.log(`- Current value: ${measurement.getCurrentValue()} ${measurement.get().unit}`);
|
||||
console.log(`- Average: ${measurement.getAverage().toFixed(1)} ${measurement.get().unit}`);
|
||||
console.log(`- Minimum: ${measurement.getMin()} ${measurement.get().unit}`);
|
||||
console.log(`- Maximum: ${measurement.getMax()} ${measurement.get().unit}`);
|
||||
|
||||
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()}`
|
||||
);
|
||||
}
|
||||
// Show all values with timestamps
|
||||
const allValues = measurement.getAllValues();
|
||||
console.log(`- Total samples: ${allValues.values.length}`);
|
||||
console.log(`- Value history: [${allValues.values.join(', ')}]`);
|
||||
console.log('Advanced statistics complete\n');
|
||||
|
||||
// ====================================
|
||||
// DYNAMIC UNIT MANAGEMENT
|
||||
// ====================================
|
||||
console.log('--- Example 8: Dynamic Unit Management ---');
|
||||
console.log('Changing preferred units at runtime...\n');
|
||||
|
||||
// Change preferred unit for flow measurements
|
||||
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');
|
||||
// Add new flow data - will auto-convert to new preferred unit
|
||||
autoContainer.type('flow').variant('realtime').position('inlet')
|
||||
.value(150, Date.now(), 'l/min'); // Input in l/min, stored as m³/h
|
||||
|
||||
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`);
|
||||
const realtimeFlow = autoContainer.type('flow').variant('realtime').position('inlet');
|
||||
console.log(`Stored as: ${realtimeFlow.getCurrentValue()} ${realtimeFlow.get().unit}`);
|
||||
console.log(`Original unit: ${realtimeFlow.getCurrentValue('l/min')} l/min`);
|
||||
console.log('Dynamic unit management complete\n');
|
||||
|
||||
// ====================================
|
||||
// DATA EXPLORATION
|
||||
// ====================================
|
||||
console.log('--- Example 9: Data Exploration ---');
|
||||
console.log('Discovering what data is available in the container...\n');
|
||||
|
||||
console.log('Available types:', basicContainer.getTypes());
|
||||
console.log('Available measurement 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:');
|
||||
// Show data structure overview
|
||||
console.log('\nData Structure Overview:');
|
||||
basicContainer.getTypes().forEach(type => {
|
||||
const variants = basicContainer.type(type).getVariants();
|
||||
if (variants.length > 0) {
|
||||
console.log(`${type.toUpperCase()}:`);
|
||||
const variants = basicContainer.type(type).getVariants();
|
||||
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`);
|
||||
const measurement = basicContainer.type(type).variant(variant).position(position).get();
|
||||
if (measurement && measurement.values.length > 0) {
|
||||
console.log(` └── ${variant}.${position}: ${measurement.values.length} values (${measurement.unit || 'no unit'})`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
console.log('\n✅ All examples complete!\n');
|
||||
|
||||
console.log('Data exploration complete\n');
|
||||
|
||||
// ====================================
|
||||
// BEST PRACTICES
|
||||
// BEST PRACTICES SUMMARY
|
||||
// ====================================
|
||||
console.log('--- Best Practices Summary ---\n');
|
||||
console.log('--- Best Practices Summary ---');
|
||||
console.log('BEST PRACTICES FOR NEW USERS:\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('1. SETUP:');
|
||||
console.log(' • Enable auto-conversion for consistent units');
|
||||
console.log(' • Define default units for your measurement types');
|
||||
console.log(' • Set appropriate window size for your data 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('2. STORING DATA:');
|
||||
console.log(' • Always use the full chain: type().variant().position().value()');
|
||||
console.log(' • Specify source unit when adding values: .value(100, timestamp, "psi")');
|
||||
console.log(' • Set units immediately after first value: .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('3. RETRIEVING DATA:');
|
||||
console.log(' • Use .getCurrentValue("unit") to get values in specific units');
|
||||
console.log(' • Use .getBestUnit() for automatic unit selection');
|
||||
console.log(' • Use .difference() for automatic upstream/downstream calculations\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');
|
||||
console.log('4. MONITORING:');
|
||||
console.log(' • Subscribe to events for real-time updates');
|
||||
console.log(' • Use .emitter.on("type.variant.position", callback)');
|
||||
console.log(' • Explore available data with .getTypes(), .getVariants(), .getPositions()\n');
|
||||
|
||||
module.exports = { basicContainer, autoContainer };
|
||||
console.log('All examples complete! Ready to use MeasurementContainer');
|
||||
|
||||
// Export for programmatic use
|
||||
module.exports = {
|
||||
runExamples: () => {
|
||||
console.log('Measurement Container Examples - Complete Guide for New Users');
|
||||
console.log('This file demonstrates all features with practical examples.');
|
||||
},
|
||||
|
||||
// Export containers for testing
|
||||
basicContainer,
|
||||
autoContainer
|
||||
};
|
||||
@@ -1,82 +1,61 @@
|
||||
const { assetCategoryManager } = require('../../datasets/assetData');
|
||||
// asset.js
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class AssetMenu {
|
||||
constructor({ manager = assetCategoryManager, softwareType = null } = {}) {
|
||||
this.manager = manager;
|
||||
this.softwareType = softwareType;
|
||||
this.categories = this.manager
|
||||
.listCategories({ withMeta: true })
|
||||
.reduce((map, meta) => {
|
||||
map[meta.softwareType] = this.manager.getCategory(meta.softwareType);
|
||||
return map;
|
||||
}, {});
|
||||
/** Define path where to find data of assets in constructor for now */
|
||||
constructor(relPath = '../../datasets/assetData') {
|
||||
this.baseDir = path.resolve(__dirname, relPath);
|
||||
this.assetData = this._loadJSON('assetData');
|
||||
}
|
||||
|
||||
normalizeCategory(key) {
|
||||
const category = this.categories[key];
|
||||
if (!category) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...category,
|
||||
label: category.label || category.softwareType || key,
|
||||
suppliers: (category.suppliers || []).map((supplier) => ({
|
||||
...supplier,
|
||||
id: supplier.id || supplier.name,
|
||||
types: (supplier.types || []).map((type) => ({
|
||||
...type,
|
||||
id: type.id || type.name,
|
||||
models: (type.models || []).map((model) => ({
|
||||
...model,
|
||||
id: model.id || model.name,
|
||||
units: model.units || []
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
resolveCategoryForNode(nodeName) {
|
||||
const keys = Object.keys(this.categories);
|
||||
if (keys.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.softwareType && this.categories[this.softwareType]) {
|
||||
return this.softwareType;
|
||||
}
|
||||
|
||||
if (nodeName) {
|
||||
const normalized = typeof nodeName === 'string' ? nodeName.toLowerCase() : nodeName;
|
||||
if (normalized && this.categories[normalized]) {
|
||||
return normalized;
|
||||
_loadJSON(...segments) {
|
||||
const filePath = path.resolve(this.baseDir, ...segments) + '.json';
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to load ${filePath}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return keys[0];
|
||||
/**
|
||||
* ADD THIS METHOD
|
||||
* Compiles all menu data from the file system into a single nested object.
|
||||
* This is run once on the server to pre-load everything.
|
||||
* @returns {object} A comprehensive object with all menu options.
|
||||
*/
|
||||
getAllMenuData() {
|
||||
// load the raw JSON once
|
||||
const data = this._loadJSON('assetData');
|
||||
const allData = {};
|
||||
|
||||
data.suppliers.forEach(sup => {
|
||||
allData[sup.name] = {};
|
||||
sup.categories.forEach(cat => {
|
||||
allData[sup.name][cat.name] = {};
|
||||
cat.types.forEach(type => {
|
||||
// here: store the full array of model objects, not just names
|
||||
allData[sup.name][cat.name][type.name] = type.models;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return allData;
|
||||
}
|
||||
|
||||
getAllMenuData(nodeName) {
|
||||
const categoryKey = this.resolveCategoryForNode(nodeName);
|
||||
const selectedCategories = {};
|
||||
|
||||
if (categoryKey && this.categories[categoryKey]) {
|
||||
selectedCategories[categoryKey] = this.normalizeCategory(categoryKey);
|
||||
}
|
||||
|
||||
return {
|
||||
categories: selectedCategories,
|
||||
defaultCategory: categoryKey
|
||||
};
|
||||
}
|
||||
|
||||
getClientInitCode(nodeName) {
|
||||
/**
|
||||
* Convert the static initEditor function to a string that can be served to the client
|
||||
* @param {string} nodeName - The name of the node type
|
||||
* @returns {string} JavaScript code as a string
|
||||
*/
|
||||
getClientInitCode(nodeName) {
|
||||
// step 1: get the two helper strings
|
||||
const htmlCode = this.getHtmlInjectionCode(nodeName);
|
||||
const dataCode = this.getDataInjectionCode(nodeName);
|
||||
const eventsCode = this.getEventInjectionCode(nodeName);
|
||||
const saveCode = this.getSaveInjectionCode(nodeName);
|
||||
|
||||
|
||||
return `
|
||||
// --- AssetMenu for ${nodeName} ---
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu =
|
||||
@@ -87,276 +66,103 @@ class AssetMenu {
|
||||
${eventsCode}
|
||||
${saveCode}
|
||||
|
||||
// wire it all up when the editor loads
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.initEditor = function(node) {
|
||||
console.log('Initializing asset properties for ${nodeName}');
|
||||
// ------------------ BELOW sequence is important! -------------------------------
|
||||
console.log('Initializing asset properties for ${nodeName}…');
|
||||
this.injectHtml();
|
||||
// load the data and wire up events
|
||||
// this will populate the fields and set up the event listeners
|
||||
this.wireEvents(node);
|
||||
// this will load the initial data into the fields
|
||||
// this is important to ensure the fields are populated correctly
|
||||
this.loadData(node);
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
getDataInjectionCode(nodeName) {
|
||||
}
|
||||
|
||||
getDataInjectionCode(nodeName) {
|
||||
return `
|
||||
// Asset data loader for ${nodeName}
|
||||
// Asset Data loader for ${nodeName}
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.loadData = function(node) {
|
||||
const menuAsset = window.EVOLV.nodes.${nodeName}.menuData.asset || {};
|
||||
const categories = menuAsset.categories || {};
|
||||
const defaultCategory = menuAsset.defaultCategory || Object.keys(categories)[0] || null;
|
||||
const data = window.EVOLV.nodes.${nodeName}.menuData.asset;
|
||||
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')
|
||||
};
|
||||
|
||||
function populate(selectEl, items = [], selectedValue, mapFn, placeholderText = 'Select...') {
|
||||
const previous = selectEl.value;
|
||||
const mapper = typeof mapFn === 'function'
|
||||
? mapFn
|
||||
: (value) => ({ value, label: value });
|
||||
|
||||
selectEl.innerHTML = '';
|
||||
|
||||
const placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = placeholderText;
|
||||
placeholder.disabled = true;
|
||||
placeholder.selected = true;
|
||||
selectEl.appendChild(placeholder);
|
||||
|
||||
items.forEach((item) => {
|
||||
const option = mapper(item);
|
||||
if (!option || typeof option.value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
function populate(el, opts, sel) {
|
||||
const old = el.value;
|
||||
el.innerHTML = '<option value="">Select…</option>';
|
||||
(opts||[]).forEach(o=>{
|
||||
const opt = document.createElement('option');
|
||||
opt.value = option.value;
|
||||
opt.textContent = option.label;
|
||||
selectEl.appendChild(opt);
|
||||
opt.value = o; opt.textContent = o;
|
||||
el.appendChild(opt);
|
||||
});
|
||||
|
||||
if (selectedValue) {
|
||||
selectEl.value = selectedValue;
|
||||
if (!selectEl.value) {
|
||||
selectEl.value = '';
|
||||
el.value = sel||"";
|
||||
if(el.value!==old) el.dispatchEvent(new Event('change'));
|
||||
}
|
||||
} else {
|
||||
selectEl.value = '';
|
||||
}
|
||||
if (selectEl.value !== previous) {
|
||||
selectEl.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
const resolveCategoryKey = () => {
|
||||
if (node.softwareType && categories[node.softwareType]) {
|
||||
return node.softwareType;
|
||||
}
|
||||
if (node.category && categories[node.category]) {
|
||||
return node.category;
|
||||
}
|
||||
return defaultCategory;
|
||||
// initial population
|
||||
populate(elems.supplier, Object.keys(data), node.supplier);
|
||||
};
|
||||
|
||||
const categoryKey = resolveCategoryKey();
|
||||
node.category = categoryKey;
|
||||
const activeCategory = categoryKey ? categories[categoryKey] : null;
|
||||
|
||||
const suppliers = activeCategory ? activeCategory.suppliers : [];
|
||||
populate(
|
||||
elems.supplier,
|
||||
suppliers,
|
||||
node.supplier,
|
||||
(supplier) => ({ value: supplier.id || supplier.name, label: supplier.name }),
|
||||
suppliers.length ? 'Select...' : 'No suppliers available'
|
||||
);
|
||||
|
||||
const activeSupplier = suppliers.find(
|
||||
(supplier) => (supplier.id || supplier.name) === node.supplier
|
||||
);
|
||||
const types = activeSupplier ? activeSupplier.types : [];
|
||||
populate(
|
||||
elems.type,
|
||||
types,
|
||||
node.assetType,
|
||||
(type) => ({ value: type.id || type.name, label: type.name }),
|
||||
activeSupplier ? 'Select...' : 'Awaiting Supplier Selection'
|
||||
);
|
||||
|
||||
const activeType = types.find(
|
||||
(type) => (type.id || type.name) === node.assetType
|
||||
);
|
||||
const models = activeType ? activeType.models : [];
|
||||
populate(
|
||||
elems.model,
|
||||
models,
|
||||
node.model,
|
||||
(model) => ({ value: model.id || model.name, label: model.name }),
|
||||
activeType ? 'Select...' : 'Awaiting Type Selection'
|
||||
);
|
||||
|
||||
const activeModel = models.find(
|
||||
(model) => (model.id || model.name) === node.model
|
||||
);
|
||||
populate(
|
||||
elems.unit,
|
||||
activeModel ? activeModel.units || [] : [],
|
||||
node.unit,
|
||||
(unit) => ({ value: unit, label: unit }),
|
||||
activeModel ? 'Select...' : activeType ? 'Awaiting Model Selection' : 'Awaiting Type Selection'
|
||||
);
|
||||
};
|
||||
`;
|
||||
`
|
||||
}
|
||||
|
||||
getEventInjectionCode(nodeName) {
|
||||
getEventInjectionCode(nodeName) {
|
||||
return `
|
||||
// Asset event wiring for ${nodeName}
|
||||
// Asset Event wiring for ${nodeName}
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.wireEvents = function(node) {
|
||||
const menuAsset = window.EVOLV.nodes.${nodeName}.menuData.asset || {};
|
||||
const categories = menuAsset.categories || {};
|
||||
const defaultCategory = menuAsset.defaultCategory || Object.keys(categories)[0] || null;
|
||||
const data = window.EVOLV.nodes.${nodeName}.menuData.asset;
|
||||
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')
|
||||
};
|
||||
|
||||
function populate(selectEl, items = [], selectedValue, mapFn, placeholderText = 'Select...') {
|
||||
const previous = selectEl.value;
|
||||
const mapper = typeof mapFn === 'function'
|
||||
? mapFn
|
||||
: (value) => ({ value, label: value });
|
||||
|
||||
selectEl.innerHTML = '';
|
||||
|
||||
const placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = placeholderText;
|
||||
placeholder.disabled = true;
|
||||
placeholder.selected = true;
|
||||
selectEl.appendChild(placeholder);
|
||||
|
||||
items.forEach((item) => {
|
||||
const option = mapper(item);
|
||||
if (!option || typeof option.value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
function populate(el, opts, sel) {
|
||||
const old = el.value;
|
||||
el.innerHTML = '<option value="">Select…</option>';
|
||||
(opts||[]).forEach(o=>{
|
||||
const opt = document.createElement('option');
|
||||
opt.value = option.value;
|
||||
opt.textContent = option.label;
|
||||
selectEl.appendChild(opt);
|
||||
opt.value = o; opt.textContent = o;
|
||||
el.appendChild(opt);
|
||||
});
|
||||
|
||||
if (selectedValue) {
|
||||
selectEl.value = selectedValue;
|
||||
if (!selectEl.value) {
|
||||
selectEl.value = '';
|
||||
el.value = sel||"";
|
||||
if(el.value!==old) el.dispatchEvent(new Event('change'));
|
||||
}
|
||||
} else {
|
||||
selectEl.value = '';
|
||||
}
|
||||
if (selectEl.value !== previous) {
|
||||
selectEl.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
const resolveCategoryKey = () => {
|
||||
if (node.softwareType && categories[node.softwareType]) {
|
||||
return node.softwareType;
|
||||
}
|
||||
if (node.category && categories[node.category]) {
|
||||
return node.category;
|
||||
}
|
||||
return defaultCategory;
|
||||
};
|
||||
|
||||
const getActiveCategory = () => {
|
||||
const key = resolveCategoryKey();
|
||||
return key ? categories[key] : null;
|
||||
};
|
||||
|
||||
node.category = resolveCategoryKey();
|
||||
|
||||
elems.supplier.addEventListener('change', () => {
|
||||
const category = getActiveCategory();
|
||||
const supplier = category
|
||||
? category.suppliers.find(
|
||||
(item) => (item.id || item.name) === elems.supplier.value
|
||||
)
|
||||
: null;
|
||||
const types = supplier ? supplier.types : [];
|
||||
populate(
|
||||
elems.type,
|
||||
types,
|
||||
node.assetType,
|
||||
(type) => ({ value: type.id || type.name, label: type.name }),
|
||||
supplier ? 'Select...' : 'Awaiting Supplier Selection'
|
||||
);
|
||||
populate(elems.model, [], '', undefined, 'Awaiting Type Selection');
|
||||
populate(elems.unit, [], '', undefined, 'Awaiting Type Selection');
|
||||
elems.supplier.addEventListener('change', ()=>{
|
||||
populate(elems.category,
|
||||
elems.supplier.value? Object.keys(data[elems.supplier.value]||{}) : [],
|
||||
node.category);
|
||||
});
|
||||
|
||||
elems.type.addEventListener('change', () => {
|
||||
const category = getActiveCategory();
|
||||
const supplier = category
|
||||
? category.suppliers.find(
|
||||
(item) => (item.id || item.name) === elems.supplier.value
|
||||
)
|
||||
: null;
|
||||
const type = supplier
|
||||
? supplier.types.find(
|
||||
(item) => (item.id || item.name) === elems.type.value
|
||||
)
|
||||
: null;
|
||||
const models = type ? type.models : [];
|
||||
populate(
|
||||
elems.model,
|
||||
models,
|
||||
node.model,
|
||||
(model) => ({ value: model.id || model.name, label: model.name }),
|
||||
type ? 'Select...' : 'Awaiting Type Selection'
|
||||
);
|
||||
populate(
|
||||
elems.unit,
|
||||
[],
|
||||
'',
|
||||
undefined,
|
||||
type ? 'Awaiting Model Selection' : 'Awaiting Type Selection'
|
||||
);
|
||||
elems.category.addEventListener('change', ()=>{
|
||||
const s=elems.supplier.value, c=elems.category.value;
|
||||
populate(elems.type,
|
||||
(s&&c)? Object.keys(data[s][c]||{}) : [],
|
||||
node.assetType);
|
||||
});
|
||||
|
||||
elems.model.addEventListener('change', () => {
|
||||
const category = getActiveCategory();
|
||||
const supplier = category
|
||||
? category.suppliers.find(
|
||||
(item) => (item.id || item.name) === elems.supplier.value
|
||||
)
|
||||
: null;
|
||||
const type = supplier
|
||||
? supplier.types.find(
|
||||
(item) => (item.id || item.name) === elems.type.value
|
||||
)
|
||||
: null;
|
||||
const model = type
|
||||
? type.models.find(
|
||||
(item) => (item.id || item.name) === elems.model.value
|
||||
)
|
||||
: null;
|
||||
populate(
|
||||
elems.unit,
|
||||
model ? model.units || [] : [],
|
||||
node.unit,
|
||||
(unit) => ({ value: unit, label: unit }),
|
||||
model ? 'Select...' : type ? 'Awaiting Model Selection' : 'Awaiting Type Selection'
|
||||
);
|
||||
elems.type.addEventListener('change', ()=>{
|
||||
const s=elems.supplier.value, c=elems.category.value, t=elems.type.value;
|
||||
const md = (s&&c&&t)? data[s][c][t]||[] : [];
|
||||
populate(elems.model, md.map(m=>m.name), node.model);
|
||||
});
|
||||
elems.model.addEventListener('change', ()=>{
|
||||
const s=elems.supplier.value, c=elems.category.value, t=elems.type.value, m=elems.model.value;
|
||||
const md = (s&&c&&t)? data[s][c][t]||[] : [];
|
||||
const entry = md.find(x=>x.name===m);
|
||||
populate(elems.unit, entry? entry.units : [], node.unit);
|
||||
});
|
||||
};
|
||||
`;
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML template for asset fields
|
||||
*/
|
||||
getHtmlTemplate() {
|
||||
return `
|
||||
<!-- Asset Properties -->
|
||||
@@ -366,6 +172,10 @@ class AssetMenu {
|
||||
<label for="node-input-supplier"><i class="fa fa-industry"></i> Supplier</label>
|
||||
<select id="node-input-supplier" style="width:70%;"></select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-category"><i class="fa fa-sitemap"></i> Category</label>
|
||||
<select id="node-input-category" style="width:70%;"></select>
|
||||
</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>
|
||||
@@ -382,10 +192,11 @@ class AssetMenu {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client-side HTML injection code
|
||||
*/
|
||||
getHtmlInjectionCode(nodeName) {
|
||||
const htmlTemplate = this.getHtmlTemplate()
|
||||
.replace(/`/g, '\\`')
|
||||
.replace(/\$/g, '\\$');
|
||||
const htmlTemplate = this.getHtmlTemplate().replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
||||
|
||||
return `
|
||||
// Asset HTML injection for ${nodeName}
|
||||
@@ -399,53 +210,33 @@ class AssetMenu {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JS that injects the saveEditor function
|
||||
*/
|
||||
getSaveInjectionCode(nodeName) {
|
||||
return `
|
||||
// Asset save handler for ${nodeName}
|
||||
// Asset Save injection for ${nodeName}
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.saveEditor = function(node) {
|
||||
console.log('Saving asset properties for ${nodeName}');
|
||||
const menuAsset = window.EVOLV.nodes.${nodeName}.menuData.asset || {};
|
||||
const categories = menuAsset.categories || {};
|
||||
const defaultCategory = menuAsset.defaultCategory || Object.keys(categories)[0] || null;
|
||||
const resolveCategoryKey = () => {
|
||||
if (node.softwareType && categories[node.softwareType]) {
|
||||
return node.softwareType;
|
||||
}
|
||||
if (node.category && categories[node.category]) {
|
||||
return node.category;
|
||||
}
|
||||
return defaultCategory || '';
|
||||
};
|
||||
|
||||
node.category = resolveCategoryKey();
|
||||
|
||||
const fields = ['supplier', 'assetType', 'model', 'unit'];
|
||||
console.log('Saving asset properties for ${nodeName}…');
|
||||
const fields = ['supplier','category','assetType','model','unit'];
|
||||
const errors = [];
|
||||
|
||||
fields.forEach((field) => {
|
||||
const el = document.getElementById(\`node-input-\${field}\`);
|
||||
node[field] = el ? el.value : '';
|
||||
fields.forEach(f => {
|
||||
const el = document.getElementById(\`node-input-\${f}\`);
|
||||
node[f] = el ? el.value : '';
|
||||
});
|
||||
if (node.assetType && !node.unit) errors.push('Unit must be set when type is specified.');
|
||||
if (!node.unit) errors.push('Unit is required.');
|
||||
errors.forEach(e=>RED.notify(e,'error'));
|
||||
|
||||
if (node.assetType && !node.unit) {
|
||||
errors.push('Unit must be set when a type is specified.');
|
||||
}
|
||||
if (!node.unit) {
|
||||
errors.push('Unit is required.');
|
||||
}
|
||||
// --- DEBUG: show exactly what was saved ---
|
||||
const saved = fields.reduce((o,f) => { o[f] = node[f]; return o; }, {});
|
||||
console.log('→ assetMenu.saveEditor result:', saved);
|
||||
|
||||
errors.forEach((msg) => RED.notify(msg, 'error'));
|
||||
|
||||
const saved = fields.reduce((acc, field) => {
|
||||
acc[field] = node[field];
|
||||
return acc;
|
||||
}, {});
|
||||
console.log('[AssetMenu] save result:', saved);
|
||||
|
||||
return errors.length === 0;
|
||||
return errors.length===0;
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = AssetMenu;
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
// asset.js
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
|
||||
class AssetMenu {
|
||||
/** Define path where to find data of assets in constructor for now */
|
||||
constructor(relPath = '../../datasets/assetData') {
|
||||
this.baseDir = path.resolve(__dirname, relPath);
|
||||
this.assetData = this._loadJSON('assetData');
|
||||
}
|
||||
|
||||
_loadJSON(...segments) {
|
||||
const filePath = path.resolve(this.baseDir, ...segments) + '.json';
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to load ${filePath}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ADD THIS METHOD
|
||||
* Compiles all menu data from the file system into a single nested object.
|
||||
* This is run once on the server to pre-load everything.
|
||||
* @returns {object} A comprehensive object with all menu options.
|
||||
*/
|
||||
getAllMenuData() {
|
||||
// load the raw JSON once
|
||||
const data = this._loadJSON('assetData');
|
||||
const allData = {};
|
||||
|
||||
data.suppliers.forEach(sup => {
|
||||
allData[sup.name] = {};
|
||||
sup.categories.forEach(cat => {
|
||||
allData[sup.name][cat.name] = {};
|
||||
cat.types.forEach(type => {
|
||||
// here: store the full array of model objects, not just names
|
||||
allData[sup.name][cat.name][type.name] = type.models;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return allData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the static initEditor function to a string that can be served to the client
|
||||
* @param {string} nodeName - The name of the node type
|
||||
* @returns {string} JavaScript code as a string
|
||||
*/
|
||||
getClientInitCode(nodeName) {
|
||||
// step 1: get the two helper strings
|
||||
const htmlCode = this.getHtmlInjectionCode(nodeName);
|
||||
const dataCode = this.getDataInjectionCode(nodeName);
|
||||
const eventsCode = this.getEventInjectionCode(nodeName);
|
||||
const saveCode = this.getSaveInjectionCode(nodeName);
|
||||
|
||||
|
||||
return `
|
||||
// --- AssetMenu for ${nodeName} ---
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu =
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu || {};
|
||||
|
||||
${htmlCode}
|
||||
${dataCode}
|
||||
${eventsCode}
|
||||
${saveCode}
|
||||
|
||||
// wire it all up when the editor loads
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.initEditor = function(node) {
|
||||
// ------------------ BELOW sequence is important! -------------------------------
|
||||
console.log('Initializing asset properties for ${nodeName}…');
|
||||
this.injectHtml();
|
||||
// load the data and wire up events
|
||||
// this will populate the fields and set up the event listeners
|
||||
this.wireEvents(node);
|
||||
// this will load the initial data into the fields
|
||||
// this is important to ensure the fields are populated correctly
|
||||
this.loadData(node);
|
||||
};
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
getDataInjectionCode(nodeName) {
|
||||
return `
|
||||
// Asset Data loader for ${nodeName}
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.loadData = function(node) {
|
||||
const data = window.EVOLV.nodes.${nodeName}.menuData.asset;
|
||||
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')
|
||||
};
|
||||
function populate(el, opts, sel) {
|
||||
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'));
|
||||
}
|
||||
// initial population
|
||||
populate(elems.supplier, Object.keys(data), node.supplier);
|
||||
};
|
||||
`
|
||||
}
|
||||
|
||||
getEventInjectionCode(nodeName) {
|
||||
return `
|
||||
// Asset Event wiring for ${nodeName}
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.wireEvents = function(node) {
|
||||
const data = window.EVOLV.nodes.${nodeName}.menuData.asset;
|
||||
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')
|
||||
};
|
||||
function populate(el, opts, sel) {
|
||||
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'));
|
||||
}
|
||||
elems.supplier.addEventListener('change', ()=>{
|
||||
populate(elems.category,
|
||||
elems.supplier.value? Object.keys(data[elems.supplier.value]||{}) : [],
|
||||
node.category);
|
||||
});
|
||||
elems.category.addEventListener('change', ()=>{
|
||||
const s=elems.supplier.value, c=elems.category.value;
|
||||
populate(elems.type,
|
||||
(s&&c)? Object.keys(data[s][c]||{}) : [],
|
||||
node.assetType);
|
||||
});
|
||||
elems.type.addEventListener('change', ()=>{
|
||||
const s=elems.supplier.value, c=elems.category.value, t=elems.type.value;
|
||||
const md = (s&&c&&t)? data[s][c][t]||[] : [];
|
||||
populate(elems.model, md.map(m=>m.name), node.model);
|
||||
});
|
||||
elems.model.addEventListener('change', ()=>{
|
||||
const s=elems.supplier.value, c=elems.category.value, t=elems.type.value, m=elems.model.value;
|
||||
const md = (s&&c&&t)? data[s][c][t]||[] : [];
|
||||
const entry = md.find(x=>x.name===m);
|
||||
populate(elems.unit, entry? entry.units : [], node.unit);
|
||||
});
|
||||
};
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML template for asset fields
|
||||
*/
|
||||
getHtmlTemplate() {
|
||||
return `
|
||||
<!-- Asset Properties -->
|
||||
<hr />
|
||||
<h3>Asset selection</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>
|
||||
<div class="form-row">
|
||||
<label for="node-input-category"><i class="fa fa-sitemap"></i> Category</label>
|
||||
<select id="node-input-category" style="width:70%;"></select>
|
||||
</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 />
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client-side HTML injection code
|
||||
*/
|
||||
getHtmlInjectionCode(nodeName) {
|
||||
const htmlTemplate = this.getHtmlTemplate().replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
||||
|
||||
return `
|
||||
// Asset HTML injection for ${nodeName}
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.injectHtml = function() {
|
||||
const placeholder = document.getElementById('asset-fields-placeholder');
|
||||
if (placeholder && !placeholder.hasChildNodes()) {
|
||||
placeholder.innerHTML = \`${htmlTemplate}\`;
|
||||
console.log('Asset HTML injected successfully');
|
||||
}
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JS that injects the saveEditor function
|
||||
*/
|
||||
getSaveInjectionCode(nodeName) {
|
||||
return `
|
||||
// Asset Save injection for ${nodeName}
|
||||
window.EVOLV.nodes.${nodeName}.assetMenu.saveEditor = function(node) {
|
||||
console.log('Saving asset properties for ${nodeName}…');
|
||||
const fields = ['supplier','category','assetType','model','unit'];
|
||||
const errors = [];
|
||||
fields.forEach(f => {
|
||||
const el = document.getElementById(\`node-input-\${f}\`);
|
||||
node[f] = el ? el.value : '';
|
||||
});
|
||||
if (node.assetType && !node.unit) errors.push('Unit must be set when type is specified.');
|
||||
if (!node.unit) errors.push('Unit is required.');
|
||||
errors.forEach(e=>RED.notify(e,'error'));
|
||||
|
||||
// --- DEBUG: show exactly what was saved ---
|
||||
const saved = fields.reduce((o,f) => { o[f] = node[f]; return o; }, {});
|
||||
console.log('→ assetMenu.saveEditor result:', saved);
|
||||
|
||||
return errors.length===0;
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = AssetMenu;
|
||||
@@ -2,17 +2,13 @@ const AssetMenu = require('./asset.js');
|
||||
const { TagcodeApp, DynamicAssetMenu } = require('./tagcodeApp.js');
|
||||
const LoggerMenu = require('./logger.js');
|
||||
const PhysicalPositionMenu = require('./physicalPosition.js');
|
||||
const ConfigManager = require('../configs');
|
||||
|
||||
class MenuManager {
|
||||
|
||||
constructor() {
|
||||
this.registeredMenus = new Map();
|
||||
this.configManager = new ConfigManager('../configs');
|
||||
// Register factory functions
|
||||
this.registerMenu('asset', (nodeName) => new AssetMenu({
|
||||
softwareType: this._getSoftwareType(nodeName)
|
||||
})); // static menu to be replaced by dynamic one but later
|
||||
this.registerMenu('asset', () => new AssetMenu()); // static menu to be replaced by dynamic one but later
|
||||
//this.registerMenu('asset', (nodeName) => new DynamicAssetMenu(nodeName, new TagcodeApp()));
|
||||
this.registerMenu('logger', () => new LoggerMenu());
|
||||
this.registerMenu('position', () => new PhysicalPositionMenu());
|
||||
@@ -27,20 +23,6 @@ class MenuManager {
|
||||
this.registeredMenus.set(menuType, menuFactory);
|
||||
}
|
||||
|
||||
_getSoftwareType(nodeName) {
|
||||
if (!nodeName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const config = this.configManager.getConfig(nodeName);
|
||||
return config?.functionality?.softwareType || nodeName;
|
||||
} catch (error) {
|
||||
console.warn(`Unable to determine softwareType for ${nodeName}: ${error.message}`);
|
||||
return nodeName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a complete endpoint script with data and initialization functions
|
||||
* @param {string} nodeName - The name of the node type
|
||||
@@ -72,7 +54,7 @@ class MenuManager {
|
||||
try {
|
||||
const handler = instantiatedMenus.get(menuType);
|
||||
if (handler && typeof handler.getAllMenuData === 'function') {
|
||||
menuData[menuType] = handler.getAllMenuData(nodeName);
|
||||
menuData[menuType] = handler.getAllMenuData();
|
||||
} else {
|
||||
// Provide default empty data if method doesn't exist
|
||||
menuData[menuType] = {};
|
||||
|
||||
@@ -6,9 +6,9 @@ class PhysicalPositionMenu {
|
||||
return {
|
||||
positionGroups: [
|
||||
{ group: 'Positional', options: [
|
||||
{ value: 'upstream', label: '→ Upstream', icon: '→'}, //flow is then typically left to right
|
||||
{ value: 'upstream', label: '← Upstream', icon: '←'},
|
||||
{ value: 'atEquipment', label: '⊥ in place' , icon: '⊥' },
|
||||
{ value: 'downstream', label: '← Downstream' , icon: '←' }
|
||||
{ value: 'downstream', label: '→ Downstream' , icon: '→' }
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -180,47 +180,19 @@ getSaveInjectionCode(nodeName) {
|
||||
return `
|
||||
// PhysicalPosition Save injection for ${nodeName}
|
||||
window.EVOLV.nodes.${nodeName}.positionMenu.saveEditor = function(node) {
|
||||
console.log("=== PhysicalPosition Save Debug ===");
|
||||
|
||||
const sel = document.getElementById('node-input-positionVsParent');
|
||||
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.positionVsParent = sel ? sel.value : 'atEquipment';
|
||||
node.positionLabel = sel ? sel.options[sel.selectedIndex].textContent : 'At Equipment';
|
||||
node.positionIcon = sel ? sel.options[sel.selectedIndex].getAttribute('data-icon') : 'fa fa-cog';
|
||||
|
||||
// Save distance data (NEW)
|
||||
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
|
||||
|
||||
@@ -228,18 +200,13 @@ getSaveInjectionCode(nodeName) {
|
||||
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");
|
||||
// Clear distance data if not specified
|
||||
delete node.distance;
|
||||
delete node.distanceUnit;
|
||||
delete node.distanceDescription;
|
||||
}
|
||||
|
||||
console.log("→ positionMenu.saveEditor result: SUCCESS");
|
||||
console.log("→ final node.positionVsParent:", node.positionVsParent);
|
||||
|
||||
return true;
|
||||
};
|
||||
`;
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Discrete PID controller with optional derivative filtering and integral limits.
|
||||
* Sample times are expressed in milliseconds to align with Node.js timestamps.
|
||||
*/
|
||||
class PIDController {
|
||||
constructor(options = {}) {
|
||||
const {
|
||||
kp = 1,
|
||||
ki = 0,
|
||||
kd = 0,
|
||||
sampleTime = 1000,
|
||||
derivativeFilter = 0.15,
|
||||
outputMin = Number.NEGATIVE_INFINITY,
|
||||
outputMax = Number.POSITIVE_INFINITY,
|
||||
integralMin = null,
|
||||
integralMax = null,
|
||||
derivativeOnMeasurement = true,
|
||||
autoMode = true
|
||||
} = options;
|
||||
|
||||
this.kp = 0;
|
||||
this.ki = 0;
|
||||
this.kd = 0;
|
||||
|
||||
this.setTunings({ kp, ki, kd });
|
||||
this.setSampleTime(sampleTime);
|
||||
this.setOutputLimits(outputMin, outputMax);
|
||||
this.setIntegralLimits(integralMin, integralMax);
|
||||
this.setDerivativeFilter(derivativeFilter);
|
||||
|
||||
this.derivativeOnMeasurement = Boolean(derivativeOnMeasurement);
|
||||
this.autoMode = Boolean(autoMode);
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update controller gains at runtime.
|
||||
* Accepts partial objects, e.g. setTunings({ kp: 2.0 }).
|
||||
*/
|
||||
setTunings({ kp = this.kp, ki = this.ki, kd = this.kd } = {}) {
|
||||
[kp, ki, kd].forEach((gain, index) => {
|
||||
if (!Number.isFinite(gain)) {
|
||||
const label = ['kp', 'ki', 'kd'][index];
|
||||
throw new TypeError(`${label} must be a finite number`);
|
||||
}
|
||||
});
|
||||
|
||||
this.kp = kp;
|
||||
this.ki = ki;
|
||||
this.kd = kd;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the controller execution interval in milliseconds.
|
||||
*/
|
||||
setSampleTime(sampleTimeMs = this.sampleTime) {
|
||||
if (!Number.isFinite(sampleTimeMs) || sampleTimeMs <= 0) {
|
||||
throw new RangeError('sampleTime must be a positive number of milliseconds');
|
||||
}
|
||||
|
||||
this.sampleTime = sampleTimeMs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrain controller output.
|
||||
*/
|
||||
setOutputLimits(min = this.outputMin, max = this.outputMax) {
|
||||
if (!Number.isFinite(min) && min !== Number.NEGATIVE_INFINITY) {
|
||||
throw new TypeError('outputMin must be finite or -Infinity');
|
||||
}
|
||||
if (!Number.isFinite(max) && max !== Number.POSITIVE_INFINITY) {
|
||||
throw new TypeError('outputMax must be finite or Infinity');
|
||||
}
|
||||
if (min >= max) {
|
||||
throw new RangeError('outputMin must be smaller than outputMax');
|
||||
}
|
||||
|
||||
this.outputMin = min;
|
||||
this.outputMax = max;
|
||||
this.lastOutput = this._clamp(this.lastOutput ?? 0, this.outputMin, this.outputMax);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrain the accumulated integral term.
|
||||
*/
|
||||
setIntegralLimits(min = this.integralMin ?? null, max = this.integralMax ?? null) {
|
||||
if (min !== null && !Number.isFinite(min)) {
|
||||
throw new TypeError('integralMin must be null or a finite number');
|
||||
}
|
||||
if (max !== null && !Number.isFinite(max)) {
|
||||
throw new TypeError('integralMax must be null or a finite number');
|
||||
}
|
||||
if (min !== null && max !== null && min > max) {
|
||||
throw new RangeError('integralMin must be smaller than integralMax');
|
||||
}
|
||||
|
||||
this.integralMin = min;
|
||||
this.integralMax = max;
|
||||
this.integral = this._applyIntegralLimits(this.integral ?? 0);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure exponential filter applied to the derivative term.
|
||||
* Value 0 disables filtering, 1 keeps the previous derivative entirely.
|
||||
*/
|
||||
setDerivativeFilter(value = this.derivativeFilter ?? 0) {
|
||||
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
||||
throw new RangeError('derivativeFilter must be between 0 and 1');
|
||||
}
|
||||
|
||||
this.derivativeFilter = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch between automatic (closed-loop) and manual mode.
|
||||
*/
|
||||
setMode(mode) {
|
||||
if (mode !== 'automatic' && mode !== 'manual') {
|
||||
throw new Error('mode must be either "automatic" or "manual"');
|
||||
}
|
||||
|
||||
this.autoMode = mode === 'automatic';
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a manual output (typically when in manual mode).
|
||||
*/
|
||||
setManualOutput(value) {
|
||||
this._assertNumeric('manual output', value);
|
||||
this.lastOutput = this._clamp(value, this.outputMin, this.outputMax);
|
||||
return this.lastOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset dynamic state (integral, derivative memory, timestamps).
|
||||
*/
|
||||
reset(state = {}) {
|
||||
const {
|
||||
integral = 0,
|
||||
lastOutput = 0,
|
||||
timestamp = null
|
||||
} = state;
|
||||
|
||||
this.integral = this._applyIntegralLimits(Number.isFinite(integral) ? integral : 0);
|
||||
this.prevError = null;
|
||||
this.prevMeasurement = null;
|
||||
this.lastOutput = this._clamp(
|
||||
Number.isFinite(lastOutput) ? lastOutput : 0,
|
||||
this.outputMin ?? Number.NEGATIVE_INFINITY,
|
||||
this.outputMax ?? Number.POSITIVE_INFINITY
|
||||
);
|
||||
this.lastTimestamp = Number.isFinite(timestamp) ? timestamp : null;
|
||||
this.derivativeState = 0;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute one control loop iteration.
|
||||
*/
|
||||
update(setpoint, measurement, timestamp = Date.now()) {
|
||||
this._assertNumeric('setpoint', setpoint);
|
||||
this._assertNumeric('measurement', measurement);
|
||||
this._assertNumeric('timestamp', timestamp);
|
||||
|
||||
if (!this.autoMode) {
|
||||
this.prevError = setpoint - measurement;
|
||||
this.prevMeasurement = measurement;
|
||||
this.lastTimestamp = timestamp;
|
||||
return this.lastOutput;
|
||||
}
|
||||
|
||||
if (this.lastTimestamp !== null && (timestamp - this.lastTimestamp) < this.sampleTime) {
|
||||
return this.lastOutput;
|
||||
}
|
||||
|
||||
const elapsedMs = this.lastTimestamp === null ? this.sampleTime : (timestamp - this.lastTimestamp);
|
||||
const dtSeconds = Math.max(elapsedMs / 1000, Number.EPSILON);
|
||||
|
||||
const error = setpoint - measurement;
|
||||
this.integral = this._applyIntegralLimits(this.integral + error * dtSeconds);
|
||||
|
||||
const derivative = this._computeDerivative({ error, measurement, dtSeconds });
|
||||
this.derivativeState = this.derivativeFilter === 0
|
||||
? derivative
|
||||
: this.derivativeState + (derivative - this.derivativeState) * (1 - this.derivativeFilter);
|
||||
|
||||
const output = (this.kp * error) + (this.ki * this.integral) + (this.kd * this.derivativeState);
|
||||
this.lastOutput = this._clamp(output, this.outputMin, this.outputMax);
|
||||
|
||||
this.prevError = error;
|
||||
this.prevMeasurement = measurement;
|
||||
this.lastTimestamp = timestamp;
|
||||
|
||||
return this.lastOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect controller state for diagnostics or persistence.
|
||||
*/
|
||||
getState() {
|
||||
return {
|
||||
kp: this.kp,
|
||||
ki: this.ki,
|
||||
kd: this.kd,
|
||||
sampleTime: this.sampleTime,
|
||||
outputLimits: { min: this.outputMin, max: this.outputMax },
|
||||
integralLimits: { min: this.integralMin, max: this.integralMax },
|
||||
derivativeFilter: this.derivativeFilter,
|
||||
derivativeOnMeasurement: this.derivativeOnMeasurement,
|
||||
autoMode: this.autoMode,
|
||||
integral: this.integral,
|
||||
lastOutput: this.lastOutput,
|
||||
lastTimestamp: this.lastTimestamp
|
||||
};
|
||||
}
|
||||
|
||||
getLastOutput() {
|
||||
return this.lastOutput;
|
||||
}
|
||||
|
||||
_computeDerivative({ error, measurement, dtSeconds }) {
|
||||
if (!(dtSeconds > 0) || !Number.isFinite(dtSeconds)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.derivativeOnMeasurement && this.prevMeasurement !== null) {
|
||||
return -(measurement - this.prevMeasurement) / dtSeconds;
|
||||
}
|
||||
|
||||
if (this.prevError === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (error - this.prevError) / dtSeconds;
|
||||
}
|
||||
|
||||
_applyIntegralLimits(value) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let result = value;
|
||||
if (this.integralMin !== null && result < this.integralMin) {
|
||||
result = this.integralMin;
|
||||
}
|
||||
if (this.integralMax !== null && result > this.integralMax) {
|
||||
result = this.integralMax;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_assertNumeric(label, value) {
|
||||
if (!Number.isFinite(value)) {
|
||||
throw new TypeError(`${label} must be a finite number`);
|
||||
}
|
||||
}
|
||||
|
||||
_clamp(value, min, max) {
|
||||
if (value < min) {
|
||||
return min;
|
||||
}
|
||||
if (value > max) {
|
||||
return max;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PIDController;
|
||||
@@ -1,87 +0,0 @@
|
||||
const { PIDController } = require('./index');
|
||||
|
||||
console.log('=== PID CONTROLLER EXAMPLES ===\n');
|
||||
console.log('This guide shows how to instantiate, tune, and operate the PID helper.\n');
|
||||
|
||||
// ====================================
|
||||
// EXAMPLE 1: FLOW CONTROL LOOP
|
||||
// ====================================
|
||||
console.log('--- Example 1: Pump speed control ---');
|
||||
|
||||
const pumpController = new PIDController({
|
||||
kp: 1.1,
|
||||
ki: 0.35,
|
||||
kd: 0.08,
|
||||
sampleTime: 250, // ms
|
||||
outputMin: 0,
|
||||
outputMax: 100,
|
||||
derivativeFilter: 0.2
|
||||
});
|
||||
|
||||
const pumpSetpoint = 75; // desired flow percentage
|
||||
let pumpFlow = 20;
|
||||
const pumpStart = Date.now();
|
||||
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
const timestamp = pumpStart + (i + 1) * pumpController.sampleTime;
|
||||
const controlSignal = pumpController.update(pumpSetpoint, pumpFlow, timestamp);
|
||||
|
||||
// Simple first-order plant approximation
|
||||
pumpFlow += (controlSignal - pumpFlow) * 0.12;
|
||||
pumpFlow -= (pumpFlow - pumpSetpoint) * 0.05; // disturbance rejection
|
||||
|
||||
console.log(
|
||||
`Cycle ${i + 1}: output=${controlSignal.toFixed(2)}% | flow=${pumpFlow.toFixed(2)}%`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Pump loop state:', pumpController.getState(), '\n');
|
||||
|
||||
// ====================================
|
||||
// EXAMPLE 2: TANK LEVEL WITH MANUAL/AUTO
|
||||
// ====================================
|
||||
console.log('--- Example 2: Tank level handover ---');
|
||||
|
||||
const tankController = new PIDController({
|
||||
kp: 2.0,
|
||||
ki: 0.5,
|
||||
kd: 0.25,
|
||||
sampleTime: 400,
|
||||
derivativeFilter: 0.25,
|
||||
outputMin: 0,
|
||||
outputMax: 1
|
||||
}).setIntegralLimits(-0.3, 0.3);
|
||||
|
||||
tankController.setMode('manual');
|
||||
tankController.setManualOutput(0.4);
|
||||
console.log(`Manual output locked at ${tankController.getLastOutput().toFixed(2)}\n`);
|
||||
|
||||
tankController.setMode('automatic');
|
||||
|
||||
let level = 0.2;
|
||||
const levelSetpoint = 0.8;
|
||||
const tankStart = Date.now();
|
||||
|
||||
for (let step = 0; step < 8; step += 1) {
|
||||
const timestamp = tankStart + (step + 1) * tankController.sampleTime;
|
||||
const output = tankController.update(levelSetpoint, level, timestamp);
|
||||
|
||||
// Integrating process with slight disturbance
|
||||
level += (output - 0.5) * 0.18;
|
||||
level += 0.02; // inflow bump
|
||||
level = Math.max(0, Math.min(1, level));
|
||||
|
||||
console.log(
|
||||
`Cycle ${step + 1}: output=${output.toFixed(3)} | level=${level.toFixed(3)}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('\nBest practice tips:');
|
||||
console.log(' - Call update() on a fixed interval (sampleTime).');
|
||||
console.log(' - Clamp output and integral to avoid windup.');
|
||||
console.log(' - Use setMode("manual") during maintenance or bump-less transfer.');
|
||||
|
||||
module.exports = {
|
||||
pumpController,
|
||||
tankController
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
const PIDController = require('./PIDController');
|
||||
|
||||
/**
|
||||
* Convenience factory for one-line instantiation.
|
||||
*/
|
||||
const createPidController = (options) => new PIDController(options);
|
||||
|
||||
module.exports = {
|
||||
PIDController,
|
||||
createPidController
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"general": {
|
||||
"name": {
|
||||
"default": "interpolation configuration",
|
||||
"default": "Interpolation Configuration",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "A human-readable name or label for this interpolation configuration."
|
||||
@@ -70,7 +70,7 @@
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"default": "interpolator",
|
||||
"default": "Interpolator",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "Indicates the role of this configuration (e.g., 'Interpolator', 'DataCurve', etc.)."
|
||||
|
||||
@@ -350,7 +350,6 @@ class Predict {
|
||||
}
|
||||
|
||||
buildAllFxyCurves(curve) {
|
||||
|
||||
let globalMinY = Infinity;
|
||||
let globalMaxY = -Infinity;
|
||||
|
||||
|
||||
@@ -81,8 +81,11 @@ class movementManager {
|
||||
const direction = targetPosition > this.currentPosition ? 1 : -1;
|
||||
const distance = Math.abs(targetPosition - this.currentPosition);
|
||||
|
||||
const velocity = this.getVelocity(); // units per second
|
||||
if (velocity <= 0) {
|
||||
// Speed is a fraction [0,1] of full-range per second
|
||||
this.speed = Math.min(Math.max(this.speed, 0), 1);
|
||||
const fullRange = this.maxPosition - this.minPosition;
|
||||
const velocity = this.speed * fullRange; // units per second
|
||||
if (velocity === 0) {
|
||||
return reject(new Error("Movement aborted: zero speed"));
|
||||
}
|
||||
|
||||
@@ -151,11 +154,11 @@ class movementManager {
|
||||
const direction = targetPosition > this.currentPosition ? 1 : -1;
|
||||
const distance = Math.abs(targetPosition - this.currentPosition);
|
||||
|
||||
const velocity = this.getVelocity();
|
||||
if (velocity <= 0) {
|
||||
return reject(new Error("Movement aborted: zero speed"));
|
||||
}
|
||||
const duration = distance / velocity;
|
||||
// Ensure speed is a percentage [0, 1]
|
||||
this.speed = Math.min(Math.max(this.speed, 0), 1);
|
||||
|
||||
// Calculate duration based on percentage of distance per second
|
||||
const duration = 1 / this.speed; // 1 second for 100% of the distance
|
||||
|
||||
this.timeleft = duration; //set this so other classes can use it
|
||||
this.logger.debug(
|
||||
@@ -214,16 +217,13 @@ class movementManager {
|
||||
const direction = targetPosition > this.currentPosition ? 1 : -1;
|
||||
const totalDistance = Math.abs(targetPosition - this.currentPosition);
|
||||
const startPosition = this.currentPosition;
|
||||
const velocity = this.getVelocity();
|
||||
if (velocity <= 0) {
|
||||
return reject(new Error("Movement aborted: zero speed"));
|
||||
}
|
||||
this.speed = Math.min(Math.max(this.speed, 0), 1);
|
||||
|
||||
const easeFunction = (t) =>
|
||||
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
|
||||
let elapsedTime = 0;
|
||||
const duration = totalDistance / velocity;
|
||||
const duration = totalDistance / this.speed;
|
||||
this.timeleft = duration;
|
||||
const interval = this.interval;
|
||||
|
||||
@@ -273,20 +273,6 @@ class movementManager {
|
||||
constrain(value) {
|
||||
return Math.min(Math.max(value, this.minPosition), this.maxPosition);
|
||||
}
|
||||
|
||||
getNormalizedSpeed() {
|
||||
const rawSpeed = Number.isFinite(this.speed) ? this.speed : 0;
|
||||
const clampedSpeed = Math.max(0, rawSpeed);
|
||||
const hasMax = Number.isFinite(this.maxSpeed) && this.maxSpeed > 0;
|
||||
const effectiveSpeed = hasMax ? Math.min(clampedSpeed, this.maxSpeed) : clampedSpeed;
|
||||
return effectiveSpeed / 100; // convert %/s -> fraction of range per second
|
||||
}
|
||||
|
||||
getVelocity() {
|
||||
const normalizedSpeed = this.getNormalizedSpeed();
|
||||
const fullRange = this.maxPosition - this.minPosition;
|
||||
return normalizedSpeed * fullRange;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = movementManager;
|
||||
|
||||
@@ -52,11 +52,6 @@ class state{
|
||||
return this.stateManager.getRunTimeHours();
|
||||
}
|
||||
|
||||
getMaintenanceTimeHours(){
|
||||
return this.stateManager.getMaintenanceTimeHours();
|
||||
}
|
||||
|
||||
|
||||
async moveTo(targetPosition) {
|
||||
|
||||
// Check for invalid conditions and throw errors
|
||||
@@ -91,33 +86,14 @@ class state{
|
||||
|
||||
// -------- 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) {
|
||||
|
||||
const fromState = this.getCurrentState();
|
||||
const position = this.getCurrentPosition();
|
||||
|
||||
// Define states that cannot be aborted for safety reasons
|
||||
const protectedStates = ['warmingup', 'coolingdown'];
|
||||
const isProtectedTransition = protectedStates.includes(fromState);
|
||||
|
||||
try {
|
||||
|
||||
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);
|
||||
this.logger.info(`Statemanager: ${feedback}`);
|
||||
|
||||
@@ -132,6 +108,7 @@ class state{
|
||||
//trigger move
|
||||
await this.moveTo(this.delayedMove,signal);
|
||||
this.delayedMove = null;
|
||||
|
||||
this.logger.info(`moveTo : ${feedback} `);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"general": {
|
||||
"name": {
|
||||
"default": "state configuration",
|
||||
"default": "State Configuration",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "A human-readable name for the state configuration."
|
||||
@@ -65,7 +65,7 @@
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"default": "statecontroller",
|
||||
"default": "StateController",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "Functional role within the system."
|
||||
@@ -127,7 +127,7 @@
|
||||
}
|
||||
},
|
||||
"maxSpeed": {
|
||||
"default": 1000,
|
||||
"default": 10,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "Maximum speed setting."
|
||||
@@ -205,10 +205,6 @@
|
||||
{
|
||||
"value": "off",
|
||||
"description": "Machine is off."
|
||||
},
|
||||
{
|
||||
"value": "maintenance",
|
||||
"description": "Machine locked for inspection or repair; automatic control disabled."
|
||||
}
|
||||
],
|
||||
"description": "Current state of the machine."
|
||||
@@ -220,7 +216,7 @@
|
||||
"type": "object",
|
||||
"schema": {
|
||||
"idle": {
|
||||
"default": ["starting", "off","emergencystop","maintenance"],
|
||||
"default": ["starting", "off","emergencystop"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
@@ -284,7 +280,7 @@
|
||||
}
|
||||
},
|
||||
"off": {
|
||||
"default": ["idle","emergencystop","maintenance"],
|
||||
"default": ["idle","emergencystop"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
@@ -292,20 +288,12 @@
|
||||
}
|
||||
},
|
||||
"emergencystop": {
|
||||
"default": ["idle","off","maintenance"],
|
||||
"default": ["idle","off"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Allowed transitions from emergency stop state."
|
||||
}
|
||||
},
|
||||
"maintenance": {
|
||||
"default": ["maintenance","idle","off"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Allowed transitions for maintenance mode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Allowed transitions between states."
|
||||
|
||||
@@ -48,14 +48,10 @@ class stateManager {
|
||||
// Define valid transitions (can be extended dynamically if needed)
|
||||
this.validTransitions = config.state.allowedTransitions;
|
||||
|
||||
//runtime tracking
|
||||
// NEW: Initialize runtime tracking
|
||||
this.runTimeHours = 0; // cumulative runtime in hours
|
||||
this.runTimeStart = null; // timestamp when active state began
|
||||
|
||||
//maintenance tracking
|
||||
this.maintenanceTimeStart = null; //timestamp when active state began
|
||||
this.maintenanceTimeHours = 0; //cumulative
|
||||
|
||||
// Define active states (runtime counts only in these states)
|
||||
this.activeStates = config.state.activeStates;
|
||||
}
|
||||
@@ -77,9 +73,8 @@ class stateManager {
|
||||
); //go back early and reject promise
|
||||
}
|
||||
|
||||
//Time tracking based on active states
|
||||
// NEW: Handle runtime tracking based on active states
|
||||
this.handleRuntimeTracking(newState);
|
||||
this.handleMaintenancetimeTracking(newState);
|
||||
|
||||
const transitionDuration = this.transitionTimes[this.currentState] || 0; // Default to 0 if no transition time
|
||||
this.logger.debug(
|
||||
@@ -105,7 +100,7 @@ class stateManager {
|
||||
}
|
||||
|
||||
handleRuntimeTracking(newState) {
|
||||
//Handle runtime tracking based on active states
|
||||
// NEW: Handle runtime tracking based on active states
|
||||
const wasActive = this.activeStates.has(this.currentState);
|
||||
const willBeActive = this.activeStates.has(newState);
|
||||
if (wasActive && !willBeActive && this.runTimeStart) {
|
||||
@@ -125,28 +120,6 @@ class stateManager {
|
||||
}
|
||||
}
|
||||
|
||||
handleMaintenancetimeTracking(newState) {
|
||||
//is this maintenance time ?
|
||||
const wasActive = (this.currentState == "maintenance"? true:false);
|
||||
const willBeActive = ( newState == "maintenance" ? true:false );
|
||||
|
||||
if (wasActive && this.maintenanceTimeStart) {
|
||||
// stop runtime timer and accumulate elapsed time
|
||||
const elapsed = (Date.now() - this.maintenanceTimeStart) / 3600000; // hours
|
||||
this.maintenanceTimeHours += elapsed;
|
||||
this.maintenanceTimeStart = null;
|
||||
this.logger.debug(
|
||||
`Maintenance timer stopped; elapsed=${elapsed.toFixed(
|
||||
3
|
||||
)}h, total=${this.maintenanceTimeHours.toFixed(3)}h.`
|
||||
);
|
||||
} else if (willBeActive && !this.runTimeStart) {
|
||||
// starting new runtime
|
||||
this.maintenanceTimeStart = Date.now();
|
||||
this.logger.debug("Runtime timer started.");
|
||||
}
|
||||
}
|
||||
|
||||
isValidTransition(newState) {
|
||||
this.logger.debug(
|
||||
`Check 1 Transition valid ? From ${
|
||||
@@ -177,6 +150,7 @@ class stateManager {
|
||||
return this.descriptions[state] || "No description available.";
|
||||
}
|
||||
|
||||
// NEW: Getter to retrieve current cumulative runtime (active time) in hours.
|
||||
getRunTimeHours() {
|
||||
// If currently active add the ongoing duration.
|
||||
let currentElapsed = 0;
|
||||
@@ -185,15 +159,6 @@ class stateManager {
|
||||
}
|
||||
return this.runTimeHours + currentElapsed;
|
||||
}
|
||||
|
||||
getMaintenanceTimeHours() {
|
||||
// If currently active add the ongoing duration.
|
||||
let currentElapsed = 0;
|
||||
if (this.maintenanceTimeStart) {
|
||||
currentElapsed = (Date.now() - this.maintenanceTimeStart) / 3600000;
|
||||
}
|
||||
return this.maintenanceTimeHours + currentElapsed;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = stateManager;
|
||||
|
||||
Reference in New Issue
Block a user