heatingpro-efficiency-package 1.0.8
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/README.md +86 -0
- package/index.js +245 -0
- package/package.json +32 -0
- package/utils/index.js +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# MonitoringPro Utils
|
|
2
|
+
|
|
3
|
+
Utility functions for MonitoringPro efficiency calculations with flexible unit support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install heatingpro-efficiency
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const { computeEfficiency, areMetersBeingWrittenOff } = require('heatingpro-efficiency');
|
|
15
|
+
|
|
16
|
+
// Compute efficiency for data in GJ (default)
|
|
17
|
+
const result1 = computeEfficiency(row, prevRow, monthlyEffectivityConstant);
|
|
18
|
+
|
|
19
|
+
// Compute efficiency with field units mapping
|
|
20
|
+
const result2 = computeEfficiency(row, prevRow, monthlyEffectivityConstant, fieldUnits);
|
|
21
|
+
|
|
22
|
+
// Check if meters are being written off
|
|
23
|
+
const metersActive = areMetersBeingWrittenOff(valuesData, fieldUnits);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Functions
|
|
27
|
+
|
|
28
|
+
### `computeEfficiency(row, prevRow, monthlyEffectivityConstant, fieldUnits)`
|
|
29
|
+
|
|
30
|
+
Computes efficiency based on heat meter readings and gas consumption.
|
|
31
|
+
|
|
32
|
+
**Parameters:**
|
|
33
|
+
- `row` - Current row data object
|
|
34
|
+
- `prevRow` - Previous row data object
|
|
35
|
+
- `monthlyEffectivityConstant` - Monthly effectivity constant value
|
|
36
|
+
- `fieldUnits` (optional) - Object mapping field names to their units (e.g., `{ "VO1": "mwh" }`)
|
|
37
|
+
|
|
38
|
+
**Returns:** Object with `ucinnost` (efficiency) and `heatUnits` values
|
|
39
|
+
|
|
40
|
+
### `areMetersBeingWrittenOff(valuesData, fieldUnits)`
|
|
41
|
+
|
|
42
|
+
Checks if heat meters (VO1-VO8, MT TUV) exist and are being written off.
|
|
43
|
+
|
|
44
|
+
**Parameters:**
|
|
45
|
+
- `valuesData` - Array of row data objects
|
|
46
|
+
- `fieldUnits` (optional) - Object mapping field names to their units
|
|
47
|
+
|
|
48
|
+
**Returns:** `true` if meters exist and are being written off, `false` otherwise
|
|
49
|
+
|
|
50
|
+
## Supported Heat Units
|
|
51
|
+
|
|
52
|
+
- **GJ** (Gigajoules) - Default unit, converts using 1 GJ = 277.778 kWh
|
|
53
|
+
- **kWh** (Kilowatt-hours) - No conversion needed
|
|
54
|
+
- **MWh** (Megawatt-hours) - Converts using 1 MWh = 1000 kWh
|
|
55
|
+
|
|
56
|
+
## Version History
|
|
57
|
+
|
|
58
|
+
- **1.0.8** - Fixed efficiency calculation bug:
|
|
59
|
+
- Removed incorrect `* 10` multiplier that caused efficiency values to be 10x too high
|
|
60
|
+
- Added meter reset detection for gas and heat meters
|
|
61
|
+
- Improved validation for negative differences and edge cases
|
|
62
|
+
- Added comprehensive test suite
|
|
63
|
+
- **1.0.7** - Added `areMetersBeingWrittenOff` function to check if heat meters exist and are being written off
|
|
64
|
+
- **1.0.6** - Previous version
|
|
65
|
+
- **1.0.5** - Added flexible unit support for heat calculations
|
|
66
|
+
|
|
67
|
+
## Testing
|
|
68
|
+
|
|
69
|
+
Run tests with:
|
|
70
|
+
```bash
|
|
71
|
+
npm test
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Run tests in watch mode:
|
|
75
|
+
```bash
|
|
76
|
+
npm run test:watch
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Generate coverage report:
|
|
80
|
+
```bash
|
|
81
|
+
npm run test:coverage
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
ISC
|
package/index.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
const {
|
|
2
|
+
hasPropertiesAndNotDash,
|
|
3
|
+
toFloatWithDot,
|
|
4
|
+
convertHeatToKwh,
|
|
5
|
+
} = require("./utils/index.js");
|
|
6
|
+
|
|
7
|
+
const computeEfficiency = (
|
|
8
|
+
row,
|
|
9
|
+
prevRow,
|
|
10
|
+
monthlyEffectivityConstant,
|
|
11
|
+
fieldUnits = {}
|
|
12
|
+
) => {
|
|
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;
|
|
22
|
+
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
|
+
}
|
|
66
|
+
|
|
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;
|
|
70
|
+
|
|
71
|
+
const gasDiff = toFloatWithDot(row["Plyn"]) - toFloatWithDot(prevRow["Plyn"]);
|
|
72
|
+
|
|
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)
|
|
76
|
+
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)
|
|
82
|
+
|
|
83
|
+
let efficiency;
|
|
84
|
+
|
|
85
|
+
// Handle different scenarios for efficiency calculation
|
|
86
|
+
if (isGasMeterReset || isHeatMeterReset) {
|
|
87
|
+
// Meter reset detected - cannot calculate efficiency reliably
|
|
88
|
+
efficiency = "-";
|
|
89
|
+
} else if (gasDiff <= 0) {
|
|
90
|
+
// Negative or zero gas consumption - invalid
|
|
91
|
+
if (gasDiff < 0) {
|
|
92
|
+
// Negative gas difference indicates meter reset or data error
|
|
93
|
+
efficiency = "-";
|
|
94
|
+
} else if (sumOfDiffs === 0) {
|
|
95
|
+
// No consumption at all - no meaningful efficiency
|
|
96
|
+
efficiency = "-";
|
|
97
|
+
} else {
|
|
98
|
+
// Heat produced without gas consumption - invalid scenario
|
|
99
|
+
efficiency = "-";
|
|
100
|
+
}
|
|
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
|
|
105
|
+
efficiency = "-";
|
|
106
|
+
} else {
|
|
107
|
+
// Gas consumed but no heat produced - 0% efficiency
|
|
108
|
+
efficiency = "0.0000";
|
|
109
|
+
}
|
|
110
|
+
} else if (monthlyEffectivityConstant <= 0 || isNaN(monthlyEffectivityConstant)) {
|
|
111
|
+
// Invalid effectivity constant
|
|
112
|
+
efficiency = "-";
|
|
113
|
+
} 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) {
|
|
122
|
+
efficiency = "-";
|
|
123
|
+
} else {
|
|
124
|
+
efficiency = Number(calculatedEfficiency).toFixed(4);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
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;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
...row,
|
|
134
|
+
ucinnost: efficiency,
|
|
135
|
+
heatUnits: heatUnitsValue,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
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
|
+
const areMetersBeingWrittenOff = (valuesData, fieldUnits = {}) => {
|
|
146
|
+
if (!valuesData || valuesData.length < 2) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
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
|
+
let metersExist = false;
|
|
160
|
+
for (const row of valuesData) {
|
|
161
|
+
// Check VO1-VO8 meters
|
|
162
|
+
for (let j = 1; j <= 8; j++) {
|
|
163
|
+
const voKey = `VO${j}`;
|
|
164
|
+
if (
|
|
165
|
+
row.hasOwnProperty(voKey) &&
|
|
166
|
+
row[voKey] !== "-" &&
|
|
167
|
+
row[voKey] !== null &&
|
|
168
|
+
row[voKey] !== undefined &&
|
|
169
|
+
row[voKey] !== ""
|
|
170
|
+
) {
|
|
171
|
+
metersExist = true;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Check MT TUV meter
|
|
176
|
+
if (
|
|
177
|
+
row.hasOwnProperty("MT TUV") &&
|
|
178
|
+
row["MT TUV"] !== "-" &&
|
|
179
|
+
row["MT TUV"] !== null &&
|
|
180
|
+
row["MT TUV"] !== undefined &&
|
|
181
|
+
row["MT TUV"] !== ""
|
|
182
|
+
) {
|
|
183
|
+
metersExist = true;
|
|
184
|
+
}
|
|
185
|
+
if (metersExist) break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If meters don't exist, return false
|
|
189
|
+
if (!metersExist) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check if there are any valid meter readings with differences across rows
|
|
194
|
+
for (let i = 1; i < valuesData.length; i++) {
|
|
195
|
+
const row = valuesData[i];
|
|
196
|
+
const prevRow = valuesData[i - 1];
|
|
197
|
+
let sumOfDiffs = 0;
|
|
198
|
+
|
|
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
|
+
}
|
|
218
|
+
|
|
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
|
+
}
|
|
234
|
+
|
|
235
|
+
// If we found any non-zero difference, meters are being written off
|
|
236
|
+
if (sumOfDiffs > 0) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Meters exist but no differences found (not being written off yet)
|
|
242
|
+
return false;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
module.exports = { computeEfficiency, areMetersBeingWrittenOff };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "heatingpro-efficiency-package",
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "Utility functions for MonitoringPro efficiency calculations with flexible unit support (GJ, kWh, MWh)",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest",
|
|
8
|
+
"test:watch": "jest --watch",
|
|
9
|
+
"test:coverage": "jest --coverage"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"monitoring",
|
|
13
|
+
"efficiency",
|
|
14
|
+
"energy",
|
|
15
|
+
"utilities",
|
|
16
|
+
"gj",
|
|
17
|
+
"kwh",
|
|
18
|
+
"mwh",
|
|
19
|
+
"gas",
|
|
20
|
+
"heat"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"files": [
|
|
25
|
+
"index.js",
|
|
26
|
+
"utils/",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"jest": "^29.7.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/utils/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const toFloatWithDot = (value) => {
|
|
2
|
+
if (typeof value === "string") {
|
|
3
|
+
value = value.replace(",", ".");
|
|
4
|
+
}
|
|
5
|
+
return parseFloat(value);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const hasPropertiesAndNotDash = (a, b, key) =>
|
|
9
|
+
a.hasOwnProperty(key) &&
|
|
10
|
+
b.hasOwnProperty(key) &&
|
|
11
|
+
a[key] !== "-" &&
|
|
12
|
+
b[key] !== "-";
|
|
13
|
+
|
|
14
|
+
const convertHeatToKwh = (value, fromUnit) => {
|
|
15
|
+
const unit = fromUnit.toLowerCase();
|
|
16
|
+
|
|
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
|
+
);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = { toFloatWithDot, hasPropertiesAndNotDash, convertHeatToKwh };
|