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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-usage",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Track electrical and tank usage",
5
5
  "main": "plugin/index.js",
6
6
  "keywords": [
@@ -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 >= 2 minutes apart (filters rapid sensor noise)
5
+ * - Time filter: Only process changes >= X minutes apart (filters rapid sensor noise)
6
6
  * - Addition requirements:
7
- * - Small tanks: >= 1 gallon increase over >= 5 minutes
8
- * - Large tanks: >= 5 gallon increase over >= 5 minutes
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
- this.app.debug(`TankageEngine: ${path} - Processing ${dataPoints.length} points, Tank type: ${isLargeTank ? 'large' : 'small'} (${additionThresholdGal} gal threshold)`);
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 lastTrackedPoint = dataPoints[0];
229
+ let lastAdditionPoint = additionSmoothed[0];
151
230
 
152
- for (let i = 1; i < dataPoints.length; i++) {
153
- const prevPoint = lastTrackedPoint;
154
- const currPoint = dataPoints[i];
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
- lastTrackedPoint = currPoint;
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 lastTrackedPoint = dataPoints[0];
340
+ let lastAdditionPoint = additionSmoothed[0];
197
341
 
198
- for (let i = 1; i < dataPoints.length; i++) {
199
- const prevPoint = lastTrackedPoint;
200
- const currPoint = dataPoints[i];
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
- if (changeM3 >= additionThresholdM3 && timeDiffMs >= ADDITION_MIN_DURATION_MS) {
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
- lastTrackedPoint = currPoint;
362
+ lastAdditionPoint = currPoint;
218
363
  }
219
364
 
220
365
  return {