signalk-usage 0.1.5 → 0.1.6
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/package.json +1 -1
- package/plugin/lib/tankageEngine.js +170 -25
package/package.json
CHANGED
|
@@ -2,20 +2,28 @@
|
|
|
2
2
|
* TankageEngine - Calculates tank usage (consumption and additions)
|
|
3
3
|
*
|
|
4
4
|
* Filtering approach:
|
|
5
|
-
* - Time filter: Only process changes >=
|
|
5
|
+
* - Time filter: Only process changes >= X minutes apart (filters rapid sensor noise)
|
|
6
6
|
* - Addition requirements:
|
|
7
|
-
* - Small tanks: >=
|
|
8
|
-
* - Large tanks: >=
|
|
7
|
+
* - Small tanks: >= X gallon increase over >= X minutes
|
|
8
|
+
* - Large tanks: >= X gallon increase over >= X minutes
|
|
9
9
|
* - Consumption: No quantity threshold, just time filter
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const MIN_TIME_BETWEEN_POINTS_MS = 2 * 60 * 1000;
|
|
13
|
-
const ADDITION_MIN_DURATION_MS = 5 * 60 * 1000;
|
|
14
|
-
const SMALL_TANK_ADDITION_GAL = 1.0;
|
|
15
|
-
const LARGE_TANK_ADDITION_GAL = 5.0;
|
|
12
|
+
const MIN_TIME_BETWEEN_POINTS_MS = 2 * 60 * 1000; // 2 minutes
|
|
16
13
|
const M3_TO_GAL = 264.172;
|
|
17
14
|
const GAL_TO_M3 = 0.00378541;
|
|
18
15
|
|
|
16
|
+
// Consumption calculation - different smoothing for short vs long periods
|
|
17
|
+
const SHORT_PERIOD_THRESHOLD_MS = 24 * 60 * 60 * 1000; // X hours
|
|
18
|
+
const SHORT_PERIOD_SMOOTHING_PERCENT = 0.25; // X% for periods ≤ X hours
|
|
19
|
+
const LONG_PERIOD_SMOOTHING_PERCENT = 0.06; // X% for periods > X hours
|
|
20
|
+
|
|
21
|
+
// Addition calculation - light smoothing to preserve refills
|
|
22
|
+
const ADDITION_SMOOTHING_MS = 1 * 60 * 60 * 1000; // X hour (fixed)
|
|
23
|
+
const SMALL_TANK_ADDITION_GAL = 1.0;
|
|
24
|
+
const LARGE_TANK_ADDITION_GAL = 5.0;
|
|
25
|
+
const ADDITION_MIN_DURATION_MS = 5 * 60 * 1000; // X minutes
|
|
26
|
+
|
|
19
27
|
function TankageEngine(app, influxClient, options) {
|
|
20
28
|
this.app = app;
|
|
21
29
|
this.influxClient = influxClient;
|
|
@@ -139,19 +147,90 @@ TankageEngine.prototype.calculateUsageForPeriod = async function(item, period) {
|
|
|
139
147
|
TankageEngine.prototype.processDataPoints = function(dataPoints, item) {
|
|
140
148
|
const { path } = item;
|
|
141
149
|
|
|
150
|
+
// Determine addition threshold based on tank size
|
|
142
151
|
const isLargeTank = item.largeTank || false;
|
|
143
152
|
const additionThresholdGal = isLargeTank ? LARGE_TANK_ADDITION_GAL : SMALL_TANK_ADDITION_GAL;
|
|
144
153
|
const additionThresholdM3 = additionThresholdGal * GAL_TO_M3;
|
|
145
154
|
|
|
146
|
-
|
|
155
|
+
// Calculate consumption smoothing window based on data range
|
|
156
|
+
const dataRangeMs = dataPoints[dataPoints.length - 1].timestamp - dataPoints[0].timestamp;
|
|
157
|
+
const smoothingPercent = dataRangeMs <= SHORT_PERIOD_THRESHOLD_MS
|
|
158
|
+
? SHORT_PERIOD_SMOOTHING_PERCENT
|
|
159
|
+
: LONG_PERIOD_SMOOTHING_PERCENT;
|
|
160
|
+
const consumptionSmoothingMs = dataRangeMs * smoothingPercent;
|
|
161
|
+
|
|
162
|
+
this.app.debug(`TankageEngine: ${path} - Processing ${dataPoints.length} points, Tank type: ${isLargeTank ? 'large' : 'small'}, Consumption smoothing: ${(consumptionSmoothingMs / 3600000).toFixed(1)} hours (${(smoothingPercent * 100).toFixed(0)}% of ${(dataRangeMs / 3600000).toFixed(1)} hour range)`);
|
|
163
|
+
|
|
164
|
+
// CONSUMPTION CALCULATION - Dual smoothing (X% for ≤Xh, X% for >Xh) to filter dips
|
|
165
|
+
const consumptionSmoothed = [];
|
|
166
|
+
for (let i = 0; i < dataPoints.length; i++) {
|
|
167
|
+
const currentPoint = dataPoints[i];
|
|
168
|
+
const windowStart = currentPoint.timestamp - consumptionSmoothingMs;
|
|
169
|
+
|
|
170
|
+
const windowPoints = [];
|
|
171
|
+
for (let j = 0; j <= i; j++) {
|
|
172
|
+
if (dataPoints[j].timestamp >= windowStart) {
|
|
173
|
+
windowPoints.push(dataPoints[j]);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const avgValue = windowPoints.reduce((sum, p) => sum + p.value, 0) / windowPoints.length;
|
|
178
|
+
consumptionSmoothed.push({
|
|
179
|
+
timestamp: currentPoint.timestamp,
|
|
180
|
+
value: avgValue
|
|
181
|
+
});
|
|
182
|
+
}
|
|
147
183
|
|
|
184
|
+
// Calculate consumption from heavily smoothed data
|
|
148
185
|
let totalConsumed = 0;
|
|
186
|
+
let lastConsumptionPoint = consumptionSmoothed[0];
|
|
187
|
+
|
|
188
|
+
for (let i = 1; i < consumptionSmoothed.length; i++) {
|
|
189
|
+
const prevPoint = lastConsumptionPoint;
|
|
190
|
+
const currPoint = consumptionSmoothed[i];
|
|
191
|
+
const timeDiffMs = currPoint.timestamp - prevPoint.timestamp;
|
|
192
|
+
|
|
193
|
+
if (timeDiffMs < MIN_TIME_BETWEEN_POINTS_MS) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const changeM3 = currPoint.value - prevPoint.value;
|
|
198
|
+
|
|
199
|
+
// Only count decreases
|
|
200
|
+
if (changeM3 < 0) {
|
|
201
|
+
totalConsumed += Math.abs(changeM3);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
lastConsumptionPoint = currPoint;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ADDITION CALCULATION - Light smoothing (X hour) to preserve refills
|
|
208
|
+
const additionSmoothed = [];
|
|
209
|
+
for (let i = 0; i < dataPoints.length; i++) {
|
|
210
|
+
const currentPoint = dataPoints[i];
|
|
211
|
+
const windowStart = currentPoint.timestamp - ADDITION_SMOOTHING_MS;
|
|
212
|
+
|
|
213
|
+
const windowPoints = [];
|
|
214
|
+
for (let j = 0; j <= i; j++) {
|
|
215
|
+
if (dataPoints[j].timestamp >= windowStart) {
|
|
216
|
+
windowPoints.push(dataPoints[j]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const avgValue = windowPoints.reduce((sum, p) => sum + p.value, 0) / windowPoints.length;
|
|
221
|
+
additionSmoothed.push({
|
|
222
|
+
timestamp: currentPoint.timestamp,
|
|
223
|
+
value: avgValue
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Calculate additions from lightly smoothed data
|
|
149
228
|
let totalAdded = 0;
|
|
150
|
-
let
|
|
229
|
+
let lastAdditionPoint = additionSmoothed[0];
|
|
151
230
|
|
|
152
|
-
for (let i = 1; i <
|
|
153
|
-
const prevPoint =
|
|
154
|
-
const currPoint =
|
|
231
|
+
for (let i = 1; i < additionSmoothed.length; i++) {
|
|
232
|
+
const prevPoint = lastAdditionPoint;
|
|
233
|
+
const currPoint = additionSmoothed[i];
|
|
155
234
|
const timeDiffMs = currPoint.timestamp - prevPoint.timestamp;
|
|
156
235
|
|
|
157
236
|
if (timeDiffMs < MIN_TIME_BETWEEN_POINTS_MS) {
|
|
@@ -160,6 +239,7 @@ TankageEngine.prototype.processDataPoints = function(dataPoints, item) {
|
|
|
160
239
|
|
|
161
240
|
const changeM3 = currPoint.value - prevPoint.value;
|
|
162
241
|
|
|
242
|
+
// Only count increases that meet thresholds
|
|
163
243
|
if (changeM3 > 0) {
|
|
164
244
|
const meetsQuantity = changeM3 >= additionThresholdM3;
|
|
165
245
|
const meetsDuration = timeDiffMs >= ADDITION_MIN_DURATION_MS;
|
|
@@ -167,15 +247,12 @@ TankageEngine.prototype.processDataPoints = function(dataPoints, item) {
|
|
|
167
247
|
if (meetsQuantity && meetsDuration) {
|
|
168
248
|
totalAdded += changeM3;
|
|
169
249
|
}
|
|
170
|
-
|
|
171
|
-
} else if (changeM3 < 0) {
|
|
172
|
-
totalConsumed += Math.abs(changeM3);
|
|
173
250
|
}
|
|
174
251
|
|
|
175
|
-
|
|
252
|
+
lastAdditionPoint = currPoint;
|
|
176
253
|
}
|
|
177
254
|
|
|
178
|
-
this.app.debug(`TankageEngine: ${path} - Consumed: ${(totalConsumed * M3_TO_GAL).toFixed(2)} gal, Added: ${(totalAdded * M3_TO_GAL).toFixed(2)} gal`);
|
|
255
|
+
this.app.debug(`TankageEngine: ${path} - Consumed: ${(totalConsumed * M3_TO_GAL).toFixed(2)} gal (Xhr smooth), Added: ${(totalAdded * M3_TO_GAL).toFixed(2)} gal (Xhr smooth)`);
|
|
179
256
|
|
|
180
257
|
return {
|
|
181
258
|
consumed: totalConsumed,
|
|
@@ -191,13 +268,80 @@ TankageEngine.prototype.calculateTankageFromData = function(dataPoints, isLargeT
|
|
|
191
268
|
const additionThresholdGal = isLargeTank ? LARGE_TANK_ADDITION_GAL : SMALL_TANK_ADDITION_GAL;
|
|
192
269
|
const additionThresholdM3 = additionThresholdGal * GAL_TO_M3;
|
|
193
270
|
|
|
271
|
+
// Calculate consumption smoothing window based on data range
|
|
272
|
+
const dataRangeMs = dataPoints[dataPoints.length - 1].timestamp - dataPoints[0].timestamp;
|
|
273
|
+
const smoothingPercent = dataRangeMs <= SHORT_PERIOD_THRESHOLD_MS
|
|
274
|
+
? SHORT_PERIOD_SMOOTHING_PERCENT
|
|
275
|
+
: LONG_PERIOD_SMOOTHING_PERCENT;
|
|
276
|
+
const consumptionSmoothingMs = dataRangeMs * smoothingPercent;
|
|
277
|
+
|
|
278
|
+
// CONSUMPTION - Dual smoothing (X% for ≤Xh, X% for >Xh)
|
|
279
|
+
const consumptionSmoothed = [];
|
|
280
|
+
for (let i = 0; i < dataPoints.length; i++) {
|
|
281
|
+
const currentPoint = dataPoints[i];
|
|
282
|
+
const windowStart = currentPoint.timestamp - consumptionSmoothingMs;
|
|
283
|
+
|
|
284
|
+
const windowPoints = [];
|
|
285
|
+
for (let j = 0; j <= i; j++) {
|
|
286
|
+
if (dataPoints[j].timestamp >= windowStart) {
|
|
287
|
+
windowPoints.push(dataPoints[j]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const avgValue = windowPoints.reduce((sum, p) => sum + p.value, 0) / windowPoints.length;
|
|
292
|
+
consumptionSmoothed.push({
|
|
293
|
+
timestamp: currentPoint.timestamp,
|
|
294
|
+
value: avgValue
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
194
298
|
let totalConsumed = 0;
|
|
299
|
+
let lastConsumptionPoint = consumptionSmoothed[0];
|
|
300
|
+
|
|
301
|
+
for (let i = 1; i < consumptionSmoothed.length; i++) {
|
|
302
|
+
const prevPoint = lastConsumptionPoint;
|
|
303
|
+
const currPoint = consumptionSmoothed[i];
|
|
304
|
+
const timeDiffMs = currPoint.timestamp - prevPoint.timestamp;
|
|
305
|
+
|
|
306
|
+
if (timeDiffMs < MIN_TIME_BETWEEN_POINTS_MS) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const changeM3 = currPoint.value - prevPoint.value;
|
|
311
|
+
|
|
312
|
+
if (changeM3 < 0) {
|
|
313
|
+
totalConsumed += Math.abs(changeM3);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
lastConsumptionPoint = currPoint;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ADDITION - Light smoothing (X hour)
|
|
320
|
+
const additionSmoothed = [];
|
|
321
|
+
for (let i = 0; i < dataPoints.length; i++) {
|
|
322
|
+
const currentPoint = dataPoints[i];
|
|
323
|
+
const windowStart = currentPoint.timestamp - ADDITION_SMOOTHING_MS;
|
|
324
|
+
|
|
325
|
+
const windowPoints = [];
|
|
326
|
+
for (let j = 0; j <= i; j++) {
|
|
327
|
+
if (dataPoints[j].timestamp >= windowStart) {
|
|
328
|
+
windowPoints.push(dataPoints[j]);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const avgValue = windowPoints.reduce((sum, p) => sum + p.value, 0) / windowPoints.length;
|
|
333
|
+
additionSmoothed.push({
|
|
334
|
+
timestamp: currentPoint.timestamp,
|
|
335
|
+
value: avgValue
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
195
339
|
let totalAdded = 0;
|
|
196
|
-
let
|
|
340
|
+
let lastAdditionPoint = additionSmoothed[0];
|
|
197
341
|
|
|
198
|
-
for (let i = 1; i <
|
|
199
|
-
const prevPoint =
|
|
200
|
-
const currPoint =
|
|
342
|
+
for (let i = 1; i < additionSmoothed.length; i++) {
|
|
343
|
+
const prevPoint = lastAdditionPoint;
|
|
344
|
+
const currPoint = additionSmoothed[i];
|
|
201
345
|
const timeDiffMs = currPoint.timestamp - prevPoint.timestamp;
|
|
202
346
|
|
|
203
347
|
if (timeDiffMs < MIN_TIME_BETWEEN_POINTS_MS) {
|
|
@@ -207,14 +351,15 @@ TankageEngine.prototype.calculateTankageFromData = function(dataPoints, isLargeT
|
|
|
207
351
|
const changeM3 = currPoint.value - prevPoint.value;
|
|
208
352
|
|
|
209
353
|
if (changeM3 > 0) {
|
|
210
|
-
|
|
354
|
+
const meetsQuantity = changeM3 >= additionThresholdM3;
|
|
355
|
+
const meetsDuration = timeDiffMs >= ADDITION_MIN_DURATION_MS;
|
|
356
|
+
|
|
357
|
+
if (meetsQuantity && meetsDuration) {
|
|
211
358
|
totalAdded += changeM3;
|
|
212
359
|
}
|
|
213
|
-
} else if (changeM3 < 0) {
|
|
214
|
-
totalConsumed += Math.abs(changeM3);
|
|
215
360
|
}
|
|
216
361
|
|
|
217
|
-
|
|
362
|
+
lastAdditionPoint = currPoint;
|
|
218
363
|
}
|
|
219
364
|
|
|
220
365
|
return {
|