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.
Files changed (3) hide show
  1. package/index.js +75 -145
  2. package/package.json +1 -1
  3. package/utils/index.js +41 -14
package/index.js CHANGED
@@ -1,133 +1,109 @@
1
1
  const {
2
2
  hasPropertiesAndNotDash,
3
3
  toFloatWithDot,
4
- convertHeatToKwh,
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
- // Helper function to convert MWh to GJ if needed
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 totalPreviousHeat = 0;
24
-
25
- for (let i = 1; i <= 8; i++) {
26
- const voKey = `VO${i}`;
27
-
28
- if (hasPropertiesAndNotDash(row, prevRow, voKey)) {
29
- const currentVal = toFloatWithDot(row[voKey]);
30
- const prevVal = toFloatWithDot(prevRow[voKey]);
31
-
32
- // Convert values if they are in MWh
33
- const convertedCurrentVal = convertValue(currentVal, voKey);
34
- const convertedPrevVal = convertValue(prevVal, voKey);
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
- // Detect heat meter reset: negative difference that's significant (> 50% of previous total)
68
- // This indicates the meter was reset (current reading < previous reading by a large margin)
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
- const gasDiff = toFloatWithDot(row["Plyn"]) - toFloatWithDot(prevRow["Plyn"]);
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
- // Handle gas meter reset: if current reading is significantly less than previous,
74
- // it's likely a meter reset. A reset is detected when current < previous AND
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 = gasDiff < 0 && prevGasValue > 0 && Math.abs(gasDiff) > prevGasValue * 0.5;
78
-
79
- // Convert heat to kWh based on input units (always GJ now since we converted above)
80
- const voDiffInKwh = convertHeatToKwh(sumOfDiffs, "GJ");
81
- const gasDiffInKwh = gasDiff * 9.855; // 1 m³ gas = 9.855 kWh (Method 2)
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 (sumOfDiffs === 0) {
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 (sumOfDiffs <= 0) {
102
- // Negative or zero heat production
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 (monthlyEffectivityConstant <= 0 || isNaN(monthlyEffectivityConstant)) {
111
- // Invalid effectivity constant
88
+ } else if (
89
+ monthlyEffectivityConstant <= 0 ||
90
+ isNaN(monthlyEffectivityConstant)
91
+ ) {
112
92
  efficiency = "-";
113
93
  } else {
114
- // Normal calculation: efficiency = (heat output / (gas input * constant))
115
- // Returns decimal value (e.g., 0.88 for 88%) - will be multiplied by 100 in display code
116
- const calculatedEfficiency = voDiffInKwh / (gasDiffInKwh * monthlyEffectivityConstant);
117
-
118
- // Validate result: efficiency should be reasonable
119
- // Check for NaN, Infinity, or negative values
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
- // Return heat units value (always in GJ for display)
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
- // Check VO1-VO8 meters
200
- for (let j = 1; j <= 8; j++) {
201
- const voKey = `VO${j}`;
202
-
203
- if (hasPropertiesAndNotDash(row, prevRow, voKey)) {
204
- const currentVal = toFloatWithDot(row[voKey]);
205
- const prevVal = toFloatWithDot(prevRow[voKey]);
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
- // Check MT TUV meter
220
- if (hasPropertiesAndNotDash(row, prevRow, "MT TUV")) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heatingpro-efficiency-package",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Utility functions for MonitoringPro efficiency calculations with flexible unit support (GJ, kWh, MWh)",
5
5
  "main": "index.js",
6
6
  "scripts": {
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
- const convertHeatToKwh = (value, fromUnit) => {
15
- const unit = fromUnit.toLowerCase();
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
- switch (unit) {
18
- case "gj":
19
- return value * 277.778; // 1 GJ = 277.778 kWh
20
- case "kwh":
21
- return value; // Already in kWh
22
- case "mwh":
23
- return value * 1000; // 1 MWh = 1000 kWh
24
- default:
25
- throw new Error(
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 = { toFloatWithDot, hasPropertiesAndNotDash, convertHeatToKwh };
52
+ module.exports = {
53
+ toFloatWithDot,
54
+ hasPropertiesAndNotDash,
55
+ convertHeatToGj,
56
+ convertHeatToKwh,
57
+ HEAT_TO_GJ,
58
+ };