heatingpro-efficiency-package 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +75 -145
- package/package.json +1 -1
- package/utils/index.js +41 -14
package/index.js
CHANGED
|
@@ -1,133 +1,109 @@
|
|
|
1
1
|
const {
|
|
2
2
|
hasPropertiesAndNotDash,
|
|
3
3
|
toFloatWithDot,
|
|
4
|
-
|
|
4
|
+
convertHeatToGj,
|
|
5
5
|
} = require("./utils/index.js");
|
|
6
6
|
|
|
7
|
+
// Convert a heat-meter reading (in its column unit) to GJ.
|
|
8
|
+
// Unknown / missing unit falls back to GJ to preserve legacy behaviour
|
|
9
|
+
// for boilers configured before per-column units existed.
|
|
10
|
+
const heatValueToGj = (value, fieldName, fieldUnits) => {
|
|
11
|
+
const unit = fieldUnits[fieldName];
|
|
12
|
+
if (!unit) return value; // legacy: assume already GJ
|
|
13
|
+
try {
|
|
14
|
+
return convertHeatToGj(value, unit);
|
|
15
|
+
} catch (_) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
7
20
|
const computeEfficiency = (
|
|
8
21
|
row,
|
|
9
22
|
prevRow,
|
|
10
23
|
monthlyEffectivityConstant,
|
|
11
24
|
fieldUnits = {}
|
|
12
25
|
) => {
|
|
13
|
-
|
|
14
|
-
const convertValue = (value, fieldName) => {
|
|
15
|
-
if (fieldUnits[fieldName] === "mwh") {
|
|
16
|
-
return value * 3.6; // Convert MWh to GJ
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
let sumOfDiffs = 0;
|
|
26
|
+
let sumOfDiffsGj = 0;
|
|
22
27
|
let hasNegativeDiff = false;
|
|
23
|
-
let
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const diff = convertedCurrentVal - convertedPrevVal;
|
|
37
|
-
|
|
38
|
-
if (!isNaN(diff)) {
|
|
39
|
-
if (diff < 0) {
|
|
40
|
-
hasNegativeDiff = true;
|
|
41
|
-
}
|
|
42
|
-
totalPreviousHeat += convertedPrevVal;
|
|
43
|
-
sumOfDiffs += diff;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (hasPropertiesAndNotDash(row, prevRow, "MT TUV")) {
|
|
49
|
-
const currentVal = toFloatWithDot(row["MT TUV"]);
|
|
50
|
-
const prevVal = toFloatWithDot(prevRow["MT TUV"]);
|
|
51
|
-
|
|
52
|
-
// Convert values if they are in MWh
|
|
53
|
-
const convertedCurrentVal = convertValue(currentVal, "MT TUV");
|
|
54
|
-
const convertedPrevVal = convertValue(prevVal, "MT TUV");
|
|
55
|
-
|
|
56
|
-
const diff = convertedCurrentVal - convertedPrevVal;
|
|
57
|
-
|
|
58
|
-
if (!isNaN(diff)) {
|
|
59
|
-
if (diff < 0) {
|
|
60
|
-
hasNegativeDiff = true;
|
|
61
|
-
}
|
|
62
|
-
totalPreviousHeat += convertedPrevVal;
|
|
63
|
-
sumOfDiffs += diff;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
28
|
+
let totalPreviousHeatGj = 0;
|
|
29
|
+
|
|
30
|
+
const accumulateHeat = (key) => {
|
|
31
|
+
if (!hasPropertiesAndNotDash(row, prevRow, key)) return;
|
|
32
|
+
const currentGj = heatValueToGj(toFloatWithDot(row[key]), key, fieldUnits);
|
|
33
|
+
const prevGj = heatValueToGj(toFloatWithDot(prevRow[key]), key, fieldUnits);
|
|
34
|
+
const diff = currentGj - prevGj;
|
|
35
|
+
if (isNaN(diff)) return;
|
|
36
|
+
if (diff < 0) hasNegativeDiff = true;
|
|
37
|
+
totalPreviousHeatGj += prevGj;
|
|
38
|
+
sumOfDiffsGj += diff;
|
|
39
|
+
};
|
|
66
40
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const isHeatMeterReset = hasNegativeDiff && totalPreviousHeat > 0 && sumOfDiffs < 0 && Math.abs(sumOfDiffs) > totalPreviousHeat * 0.5;
|
|
41
|
+
for (let i = 1; i <= 8; i++) accumulateHeat(`VO${i}`);
|
|
42
|
+
accumulateHeat("MT TUV");
|
|
70
43
|
|
|
71
|
-
|
|
44
|
+
// Heat-meter reset: large aggregate negative diff (>50% of prev total).
|
|
45
|
+
const isHeatMeterReset =
|
|
46
|
+
hasNegativeDiff &&
|
|
47
|
+
totalPreviousHeatGj > 0 &&
|
|
48
|
+
sumOfDiffsGj < 0 &&
|
|
49
|
+
Math.abs(sumOfDiffsGj) > totalPreviousHeatGj * 0.5;
|
|
72
50
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// the difference is large (> 50% of previous value)
|
|
51
|
+
const gasDiff =
|
|
52
|
+
toFloatWithDot(row["Plyn"]) - toFloatWithDot(prevRow["Plyn"]);
|
|
76
53
|
const prevGasValue = toFloatWithDot(prevRow["Plyn"]);
|
|
77
|
-
const isGasMeterReset =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
54
|
+
const isGasMeterReset =
|
|
55
|
+
gasDiff < 0 &&
|
|
56
|
+
prevGasValue > 0 &&
|
|
57
|
+
Math.abs(gasDiff) > prevGasValue * 0.5;
|
|
58
|
+
|
|
59
|
+
// Heat in kWh (1 GJ = 1000/3.6 kWh).
|
|
60
|
+
const heatDiffInKwh = sumOfDiffsGj * (1000 / 3.6);
|
|
61
|
+
|
|
62
|
+
// Gas in kWh. monthlyEffectivityConstant is the natural-gas calorific value
|
|
63
|
+
// for that month in kWh/m³ (typical ~10.8–11.0). If the gas meter is already
|
|
64
|
+
// configured in kWh, use the raw kWh diff and ignore the constant for unit
|
|
65
|
+
// purposes — but the constant must still be a valid positive number so we
|
|
66
|
+
// refuse to compute on missing data, matching prior behaviour.
|
|
67
|
+
const gasUnit = String(fieldUnits["Plyn"] || "m3").toLowerCase();
|
|
68
|
+
const gasDiffInKwh = gasUnit === "kwh" ? gasDiff : gasDiff * monthlyEffectivityConstant;
|
|
82
69
|
|
|
83
70
|
let efficiency;
|
|
84
71
|
|
|
85
|
-
// Handle different scenarios for efficiency calculation
|
|
86
72
|
if (isGasMeterReset || isHeatMeterReset) {
|
|
87
|
-
// Meter reset detected - cannot calculate efficiency reliably
|
|
88
73
|
efficiency = "-";
|
|
89
74
|
} else if (gasDiff <= 0) {
|
|
90
|
-
// Negative or zero gas consumption - invalid
|
|
91
75
|
if (gasDiff < 0) {
|
|
92
|
-
// Negative gas difference indicates meter reset or data error
|
|
93
76
|
efficiency = "-";
|
|
94
|
-
} else if (
|
|
95
|
-
// No consumption at all - no meaningful efficiency
|
|
77
|
+
} else if (sumOfDiffsGj === 0) {
|
|
96
78
|
efficiency = "-";
|
|
97
79
|
} else {
|
|
98
|
-
// Heat produced without gas consumption - invalid scenario
|
|
99
80
|
efficiency = "-";
|
|
100
81
|
}
|
|
101
|
-
} else if (
|
|
102
|
-
|
|
103
|
-
if (sumOfDiffs < 0) {
|
|
104
|
-
// Negative heat difference indicates meter reset or data error
|
|
82
|
+
} else if (sumOfDiffsGj <= 0) {
|
|
83
|
+
if (sumOfDiffsGj < 0) {
|
|
105
84
|
efficiency = "-";
|
|
106
85
|
} else {
|
|
107
|
-
// Gas consumed but no heat produced - 0% efficiency
|
|
108
86
|
efficiency = "0.0000";
|
|
109
87
|
}
|
|
110
|
-
} else if (
|
|
111
|
-
|
|
88
|
+
} else if (
|
|
89
|
+
monthlyEffectivityConstant <= 0 ||
|
|
90
|
+
isNaN(monthlyEffectivityConstant)
|
|
91
|
+
) {
|
|
112
92
|
efficiency = "-";
|
|
113
93
|
} else {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Note: Upper bound removed as efficiency can vary based on effectivityConstant value
|
|
121
|
-
if (isNaN(calculatedEfficiency) || !isFinite(calculatedEfficiency) || calculatedEfficiency < 0) {
|
|
94
|
+
const calculatedEfficiency = heatDiffInKwh / gasDiffInKwh;
|
|
95
|
+
if (
|
|
96
|
+
isNaN(calculatedEfficiency) ||
|
|
97
|
+
!isFinite(calculatedEfficiency) ||
|
|
98
|
+
calculatedEfficiency < 0
|
|
99
|
+
) {
|
|
122
100
|
efficiency = "-";
|
|
123
101
|
} else {
|
|
124
102
|
efficiency = Number(calculatedEfficiency).toFixed(4);
|
|
125
103
|
}
|
|
126
104
|
}
|
|
127
105
|
|
|
128
|
-
|
|
129
|
-
// Make sure we don't return NaN
|
|
130
|
-
const heatUnitsValue = isNaN(sumOfDiffs) ? 0 : sumOfDiffs;
|
|
106
|
+
const heatUnitsValue = isNaN(sumOfDiffsGj) ? 0 : sumOfDiffsGj;
|
|
131
107
|
|
|
132
108
|
return {
|
|
133
109
|
...row,
|
|
@@ -136,29 +112,13 @@ const computeEfficiency = (
|
|
|
136
112
|
};
|
|
137
113
|
};
|
|
138
114
|
|
|
139
|
-
/**
|
|
140
|
-
* Checks if heat meters are being written off (have valid readings and differences)
|
|
141
|
-
* @param {Array} valuesData - Array of row data objects
|
|
142
|
-
* @param {Object} fieldUnits - Mapping of field names to their units
|
|
143
|
-
* @returns {boolean} - True if meters exist and are being written off, false otherwise
|
|
144
|
-
*/
|
|
145
115
|
const areMetersBeingWrittenOff = (valuesData, fieldUnits = {}) => {
|
|
146
116
|
if (!valuesData || valuesData.length < 2) {
|
|
147
117
|
return false;
|
|
148
118
|
}
|
|
149
119
|
|
|
150
|
-
// Helper function to convert MWh to GJ if needed
|
|
151
|
-
const convertValue = (value, fieldName) => {
|
|
152
|
-
if (fieldUnits[fieldName] === "mwh") {
|
|
153
|
-
return value * 3.6; // Convert MWh to GJ
|
|
154
|
-
}
|
|
155
|
-
return value;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// First, check if meters exist (have non-dash values in any row)
|
|
159
120
|
let metersExist = false;
|
|
160
121
|
for (const row of valuesData) {
|
|
161
|
-
// Check VO1-VO8 meters
|
|
162
122
|
for (let j = 1; j <= 8; j++) {
|
|
163
123
|
const voKey = `VO${j}`;
|
|
164
124
|
if (
|
|
@@ -172,7 +132,6 @@ const areMetersBeingWrittenOff = (valuesData, fieldUnits = {}) => {
|
|
|
172
132
|
break;
|
|
173
133
|
}
|
|
174
134
|
}
|
|
175
|
-
// Check MT TUV meter
|
|
176
135
|
if (
|
|
177
136
|
row.hasOwnProperty("MT TUV") &&
|
|
178
137
|
row["MT TUV"] !== "-" &&
|
|
@@ -185,60 +144,31 @@ const areMetersBeingWrittenOff = (valuesData, fieldUnits = {}) => {
|
|
|
185
144
|
if (metersExist) break;
|
|
186
145
|
}
|
|
187
146
|
|
|
188
|
-
// If meters don't exist, return false
|
|
189
147
|
if (!metersExist) {
|
|
190
148
|
return false;
|
|
191
149
|
}
|
|
192
150
|
|
|
193
|
-
// Check if there are any valid meter readings with differences across rows
|
|
194
151
|
for (let i = 1; i < valuesData.length; i++) {
|
|
195
152
|
const row = valuesData[i];
|
|
196
153
|
const prevRow = valuesData[i - 1];
|
|
197
154
|
let sumOfDiffs = 0;
|
|
198
155
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// Convert values if they are in MWh
|
|
208
|
-
const convertedCurrentVal = convertValue(currentVal, voKey);
|
|
209
|
-
const convertedPrevVal = convertValue(prevVal, voKey);
|
|
210
|
-
|
|
211
|
-
const diff = convertedCurrentVal - convertedPrevVal;
|
|
212
|
-
|
|
213
|
-
if (!isNaN(diff) && diff !== 0) {
|
|
214
|
-
sumOfDiffs += Math.abs(diff);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
156
|
+
const tally = (key) => {
|
|
157
|
+
if (!hasPropertiesAndNotDash(row, prevRow, key)) return;
|
|
158
|
+
const cur = heatValueToGj(toFloatWithDot(row[key]), key, fieldUnits);
|
|
159
|
+
const prev = heatValueToGj(toFloatWithDot(prevRow[key]), key, fieldUnits);
|
|
160
|
+
const diff = cur - prev;
|
|
161
|
+
if (!isNaN(diff) && diff !== 0) sumOfDiffs += Math.abs(diff);
|
|
162
|
+
};
|
|
218
163
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const currentVal = toFloatWithDot(row["MT TUV"]);
|
|
222
|
-
const prevVal = toFloatWithDot(prevRow["MT TUV"]);
|
|
223
|
-
|
|
224
|
-
// Convert values if they are in MWh
|
|
225
|
-
const convertedCurrentVal = convertValue(currentVal, "MT TUV");
|
|
226
|
-
const convertedPrevVal = convertValue(prevVal, "MT TUV");
|
|
227
|
-
|
|
228
|
-
const diff = convertedCurrentVal - convertedPrevVal;
|
|
229
|
-
|
|
230
|
-
if (!isNaN(diff) && diff !== 0) {
|
|
231
|
-
sumOfDiffs += Math.abs(diff);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
164
|
+
for (let j = 1; j <= 8; j++) tally(`VO${j}`);
|
|
165
|
+
tally("MT TUV");
|
|
234
166
|
|
|
235
|
-
// If we found any non-zero difference, meters are being written off
|
|
236
167
|
if (sumOfDiffs > 0) {
|
|
237
168
|
return true;
|
|
238
169
|
}
|
|
239
170
|
}
|
|
240
171
|
|
|
241
|
-
// Meters exist but no differences found (not being written off yet)
|
|
242
172
|
return false;
|
|
243
173
|
};
|
|
244
174
|
|
package/package.json
CHANGED
package/utils/index.js
CHANGED
|
@@ -11,21 +11,48 @@ const hasPropertiesAndNotDash = (a, b, key) =>
|
|
|
11
11
|
a[key] !== "-" &&
|
|
12
12
|
b[key] !== "-";
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Heat-energy conversion factors → GJ.
|
|
15
|
+
// Source values are exact where physically exact (e.g. 1 MWh = 3.6 GJ);
|
|
16
|
+
// kcal uses 1 cal = 4.184 J (thermochemical) for consistency with EU practice.
|
|
17
|
+
const HEAT_TO_GJ = {
|
|
18
|
+
kj: 1e-6,
|
|
19
|
+
mj: 1e-3,
|
|
20
|
+
gj: 1,
|
|
21
|
+
kwh: 3.6e-3,
|
|
22
|
+
mwh: 3.6,
|
|
23
|
+
gwh: 3600,
|
|
24
|
+
kcal: 4.184e-6,
|
|
25
|
+
mcal: 4.184e-3,
|
|
26
|
+
gcal: 4.184,
|
|
27
|
+
};
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`Unsupported heat unit: ${fromUnit}. Supported units: GJ, kWh, MWh`
|
|
27
|
-
);
|
|
29
|
+
const convertHeatToGj = (value, fromUnit) => {
|
|
30
|
+
const unit = String(fromUnit || "gj").toLowerCase();
|
|
31
|
+
const factor = HEAT_TO_GJ[unit];
|
|
32
|
+
if (factor === undefined) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Unsupported heat unit: ${fromUnit}. Supported: ${Object.keys(HEAT_TO_GJ)
|
|
35
|
+
.map((u) => u.toUpperCase())
|
|
36
|
+
.join(", ")}`
|
|
37
|
+
);
|
|
28
38
|
}
|
|
39
|
+
return value * factor;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const convertHeatToKwh = (value, fromUnit) => {
|
|
43
|
+
const unit = String(fromUnit || "").toLowerCase();
|
|
44
|
+
// Short-circuit exact-unit cases to avoid float round-trip.
|
|
45
|
+
if (unit === "kwh") return value;
|
|
46
|
+
if (unit === "mwh") return value * 1000;
|
|
47
|
+
if (unit === "gwh") return value * 1_000_000;
|
|
48
|
+
// 1 GJ ≡ 1000 / 3.6 kWh ≈ 277.7778. Route through GJ for everything else.
|
|
49
|
+
return convertHeatToGj(value, fromUnit) * (1000 / 3.6);
|
|
29
50
|
};
|
|
30
51
|
|
|
31
|
-
module.exports = {
|
|
52
|
+
module.exports = {
|
|
53
|
+
toFloatWithDot,
|
|
54
|
+
hasPropertiesAndNotDash,
|
|
55
|
+
convertHeatToGj,
|
|
56
|
+
convertHeatToKwh,
|
|
57
|
+
HEAT_TO_GJ,
|
|
58
|
+
};
|