fleetmap-reports 2.0.378 → 2.0.380

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": "fleetmap-reports",
3
- "version": "2.0.378",
3
+ "version": "2.0.380",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -219,10 +219,11 @@ async function processDevices (from, to, devices, data, userData, traccarInstanc
219
219
  summaryCurrentDay.distance = summaryByDevice.distance
220
220
  summaryCurrentDay.averageSpeed = distance > 0 ? ((tripsByDay.reduce((a, b) => a + (b.averageSpeed * b.distance), 0)) / distance) : 0
221
221
  summaryCurrentDay.maxSpeed = tripsByDay.reduce((a, b) => { return a > b.maxSpeed ? a : b.maxSpeed }, 0)
222
- // Read the day's fuel from the CAN "fuelUsed" counter over the full route between
223
- // fromDay/toDay (falls back to routeByDay for tank-level-only vehicles), so the
224
- // daily figure and its total match the Manager's "Fuel Used".
225
- summaryCurrentDay.convertedSpentFuel = calculateConsumption(d, { route: deviceRoute.length ? deviceRoute : routeByDay, from: fromDay, to: toDay })
222
+ // Prefer Traccar's own per-trip spentFuel (tripsByDay) - it's already correctly
223
+ // scaled server-side, unlike a raw fuelUsed-counter subtraction which some
224
+ // devices/protocols report non-monotonically or in different units. route/fromDay/
225
+ // toDay remain the fallback for days with no trips.
226
+ summaryCurrentDay.convertedSpentFuel = calculateConsumption(d, { trips: tripsByDay, stops: [], route: deviceRoute.length ? deviceRoute : routeByDay, from: fromDay, to: toDay })
226
227
  summaryCurrentDay.startTime = tripsByDay.length && tripsByDay[0].startTime
227
228
  summaryCurrentDay.startAddress = tripsByDay.length && tripsByDay[0].startAddress
228
229
  summaryCurrentDay.endTime = tripsByDay.length && tripsByDay[tripsByDay.length - 1].endTime
@@ -259,7 +260,9 @@ async function processDevices (from, to, devices, data, userData, traccarInstanc
259
260
  if (summary.length) {
260
261
  summary.forEach(s => {
261
262
  s.engineHours = deviceTrips.reduce((a, b) => a + b.duration, 0)
262
- s.convertedSpentFuel = calculateConsumption(d, { route: deviceRoute, summary: s })
263
+ // Prefer Traccar's own per-trip spentFuel over the period summary/raw route
264
+ // counter - see the per-day branch above for why.
265
+ s.convertedSpentFuel = calculateConsumption(d, { trips: deviceTrips, stops: [], route: deviceRoute, summary: s })
263
266
  s.startTime = deviceTrips.length && deviceTrips[0].startTime
264
267
  s.startAddress = deviceTrips.length && deviceTrips[0].startAddress
265
268
  s.endTime = deviceTrips.length && deviceTrips[deviceTrips.length - 1].endTime
@@ -8,7 +8,7 @@ const { getStyle } = require('./reportStyle')
8
8
  const { getUserPartner } = require('fleetmap-partners')
9
9
  const { addTable, headerFromUser } = require('./util/pdfDocument')
10
10
  const jsPDF = require('jspdf')
11
- const { calculateConsumption, calculateTotalFuelFromSensor } = require('./util/fuel')
11
+ const { calculateConsumption, calculateTotalFuelFromSensor, shouldUseFuelSensor } = require('./util/fuel')
12
12
  const { getSummary } = require('./util/traccar')
13
13
  const { calculatePartialTrip } = require('./util/trips')
14
14
 
@@ -171,23 +171,30 @@ async function createFuelConsumptionReport (from, to, userData, traccar) {
171
171
 
172
172
  if (trips.length > 0) {
173
173
  const refuelingPositions = await refuelingReport.calculateRefuelingPositions(userData, d, { route, trips, fuelServices: deviceFuelServices })
174
+ // The CAN "fuelUsed" counter - when the device actually reports it - is what the
175
+ // Manager shows as "Fuel Used", so shouldUseFuelSensor() makes it win over the analog
176
+ // tank-level sensor below regardless of whether fuel_low_threshold/fuel_high_threshold
177
+ // happen to be configured on the device (that config alone doesn't mean the analog
178
+ // input is wired or reporting; some CAN vehicles carry stale/unrelated thresholds).
179
+ const useFuelSensor = shouldUseFuelSensor(d, route)
174
180
  const days = daysByDevice.get(d.id) || []
175
181
  for (const date of dates) {
176
182
  const [dayStart, dayEnd] = getDayWindow(date, timezone)
177
- const { tripsByDay, stopsByDay, routeByDay, refuelingPositionsByDay } = await getReportDataByDay(d, dayStart, dayEnd, { trips, stops, route, refuelingPositions })
183
+ const { tripsByDay, stopsByDay, refuelingPositionsByDay } = await getReportDataByDay(d, dayStart, dayEnd, { trips, stops, route, refuelingPositions })
178
184
  const summaryByDay = summary.find(a => a.date === date).summary
179
185
  const summaryByDevice = summaryByDay.find(a => a.deviceId === d.id)
180
186
 
181
187
  const firstStopIndex = !stopsByDay.length || !tripsByDay.length ? 0 : (new Date(tripsByDay[0].startTime) < new Date(stopsByDay[0].startTime) ? 0 : 1)
182
188
  const distance = summaryByDevice.distance
183
189
 
184
- const hasFuelSensor = 'fuel_low_threshold' in d.attributes &&
185
- 'fuel_high_threshold' in d.attributes &&
186
- d.attributes.fuel_tank_capacity
187
-
188
- const spentFuel = hasFuelSensor
190
+ // For CAN vehicles calculateConsumption reads the "fuelUsed" counter directly from
191
+ // the full chunk route between dayStart/dayEnd, so the day value matches the
192
+ // Manager's "Fuel Used" even when routeByDay itself has too few (or no) fuelUsed
193
+ // readings - e.g. on a day with a refuel or sparse CAN reporting. The analog
194
+ // tank-level sensor is only used as a fallback for vehicles with no CAN fuel data.
195
+ const spentFuel = useFuelSensor
189
196
  ? tripsByDay.length > 0 ? calculateTotalFuelFromSensor(tripsByDay, stopsByDay, route, d, firstStopIndex) : 0
190
- : calculateConsumption(d, { trips: tripsByDay, stops: stopsByDay, route: routeByDay, summary: summaryByDevice })
197
+ : calculateConsumption(d, { trips: tripsByDay, stops: stopsByDay, route, summary: summaryByDevice, from: dayStart, to: dayEnd })
191
198
 
192
199
  const dataRow = {
193
200
  date,
@@ -0,0 +1,1034 @@
1
+ {
2
+ "trips": [
3
+ {
4
+ "startTime": "2026-06-25T04:34:28.000+00:00",
5
+ "endTime": "2026-06-25T04:55:18.000+00:00",
6
+ "spentFuel": 3.3,
7
+ "distance": 13895
8
+ },
9
+ {
10
+ "startTime": "2026-06-25T07:11:25.000+00:00",
11
+ "endTime": "2026-06-25T07:12:29.000+00:00",
12
+ "spentFuel": 0,
13
+ "distance": 0
14
+ },
15
+ {
16
+ "startTime": "2026-06-25T07:46:06.000+00:00",
17
+ "endTime": "2026-06-25T08:30:19.000+00:00",
18
+ "spentFuel": 8.3,
19
+ "distance": 26675
20
+ },
21
+ {
22
+ "startTime": "2026-06-25T08:49:34.000+00:00",
23
+ "endTime": "2026-06-25T08:51:14.000+00:00",
24
+ "spentFuel": 0,
25
+ "distance": 5
26
+ },
27
+ {
28
+ "startTime": "2026-06-25T09:24:38.000+00:00",
29
+ "endTime": "2026-06-25T09:25:40.000+00:00",
30
+ "spentFuel": 0,
31
+ "distance": 0
32
+ },
33
+ {
34
+ "startTime": "2026-06-25T10:51:08.000+00:00",
35
+ "endTime": "2026-06-25T10:52:17.000+00:00",
36
+ "spentFuel": 0,
37
+ "distance": 0
38
+ },
39
+ {
40
+ "startTime": "2026-06-25T10:59:45.000+00:00",
41
+ "endTime": "2026-06-25T11:01:13.000+00:00",
42
+ "spentFuel": 0.1,
43
+ "distance": 50
44
+ },
45
+ {
46
+ "startTime": "2026-06-25T11:45:44.000+00:00",
47
+ "endTime": "2026-06-25T14:26:14.000+00:00",
48
+ "spentFuel": 56.1,
49
+ "distance": 201545
50
+ },
51
+ {
52
+ "startTime": "2026-06-25T15:09:00.000+00:00",
53
+ "endTime": "2026-06-25T15:13:11.000+00:00",
54
+ "spentFuel": 0.1,
55
+ "distance": 15
56
+ },
57
+ {
58
+ "startTime": "2026-06-25T16:54:01.000+00:00",
59
+ "endTime": "2026-06-25T19:37:39.000+00:00",
60
+ "spentFuel": 41.9,
61
+ "distance": 196270
62
+ },
63
+ {
64
+ "startTime": "2026-06-25T19:44:38.000+00:00",
65
+ "endTime": "2026-06-25T19:55:15.000+00:00",
66
+ "spentFuel": 0.8,
67
+ "distance": 2765
68
+ }
69
+ ],
70
+ "stops": [
71
+ {
72
+ "startTime": "2026-06-25T04:32:53.000+00:00",
73
+ "endTime": "2026-06-25T04:34:28.000+00:00",
74
+ "spentFuel": -18045.3
75
+ },
76
+ {
77
+ "startTime": "2026-06-25T04:55:18.000+00:00",
78
+ "endTime": "2026-06-25T07:11:25.000+00:00",
79
+ "spentFuel": 0
80
+ },
81
+ {
82
+ "startTime": "2026-06-25T07:12:29.000+00:00",
83
+ "endTime": "2026-06-25T07:46:06.000+00:00",
84
+ "spentFuel": 0
85
+ },
86
+ {
87
+ "startTime": "2026-06-25T08:30:19.000+00:00",
88
+ "endTime": "2026-06-25T08:49:34.000+00:00",
89
+ "spentFuel": 0.7
90
+ },
91
+ {
92
+ "startTime": "2026-06-25T08:51:14.000+00:00",
93
+ "endTime": "2026-06-25T09:24:38.000+00:00",
94
+ "spentFuel": 0
95
+ },
96
+ {
97
+ "startTime": "2026-06-25T09:25:40.000+00:00",
98
+ "endTime": "2026-06-25T10:51:08.000+00:00",
99
+ "spentFuel": 0
100
+ },
101
+ {
102
+ "startTime": "2026-06-25T10:52:17.000+00:00",
103
+ "endTime": "2026-06-25T10:59:45.000+00:00",
104
+ "spentFuel": 0
105
+ },
106
+ {
107
+ "startTime": "2026-06-25T11:01:13.000+00:00",
108
+ "endTime": "2026-06-25T11:45:44.000+00:00",
109
+ "spentFuel": 0.1
110
+ },
111
+ {
112
+ "startTime": "2026-06-25T14:26:14.000+00:00",
113
+ "endTime": "2026-06-25T15:09:00.000+00:00",
114
+ "spentFuel": 0
115
+ },
116
+ {
117
+ "startTime": "2026-06-25T15:13:11.000+00:00",
118
+ "endTime": "2026-06-25T16:54:01.000+00:00",
119
+ "spentFuel": 0
120
+ },
121
+ {
122
+ "startTime": "2026-06-25T19:37:39.000+00:00",
123
+ "endTime": "2026-06-25T19:44:38.000+00:00",
124
+ "spentFuel": 0
125
+ },
126
+ {
127
+ "startTime": "2026-06-25T19:55:15.000+00:00",
128
+ "endTime": "2026-06-25T20:41:17.000+00:00",
129
+ "spentFuel": 0
130
+ }
131
+ ],
132
+ "route": [
133
+ {
134
+ "fixTime": "2026-06-25T04:32:53.000+00:00",
135
+ "attributes": {
136
+ "fuelUsed": 33439.5
137
+ }
138
+ },
139
+ {
140
+ "fixTime": "2026-06-25T04:33:04.000+00:00",
141
+ "attributes": {
142
+ "fuelUsed": 15394.2
143
+ }
144
+ },
145
+ {
146
+ "fixTime": "2026-06-25T04:34:10.000+00:00",
147
+ "attributes": {
148
+ "fuelUsed": 15394.2
149
+ }
150
+ },
151
+ {
152
+ "fixTime": "2026-06-25T04:34:28.000+00:00",
153
+ "attributes": {
154
+ "fuelUsed": 15394.2
155
+ }
156
+ },
157
+ {
158
+ "fixTime": "2026-06-25T04:34:32.000+00:00",
159
+ "attributes": {
160
+ "fuelUsed": 15394.2
161
+ }
162
+ },
163
+ {
164
+ "fixTime": "2026-06-25T04:34:33.000+00:00",
165
+ "attributes": {
166
+ "fuelUsed": 15394.2
167
+ }
168
+ },
169
+ {
170
+ "fixTime": "2026-06-25T04:34:43.000+00:00",
171
+ "attributes": {
172
+ "fuelUsed": 15394.2
173
+ }
174
+ },
175
+ {
176
+ "fixTime": "2026-06-25T04:34:44.000+00:00",
177
+ "attributes": {
178
+ "fuelUsed": 15394.2
179
+ }
180
+ },
181
+ {
182
+ "fixTime": "2026-06-25T04:34:45.000+00:00",
183
+ "attributes": {
184
+ "fuelUsed": 15394.2
185
+ }
186
+ },
187
+ {
188
+ "fixTime": "2026-06-25T04:34:47.000+00:00",
189
+ "attributes": {
190
+ "fuelUsed": 15394.2
191
+ }
192
+ },
193
+ {
194
+ "fixTime": "2026-06-25T04:34:48.000+00:00",
195
+ "attributes": {
196
+ "fuelUsed": 15394.2
197
+ }
198
+ },
199
+ {
200
+ "fixTime": "2026-06-25T04:37:33.000+00:00",
201
+ "attributes": {
202
+ "fuelUsed": 15394.6
203
+ }
204
+ },
205
+ {
206
+ "fixTime": "2026-06-25T04:41:08.000+00:00",
207
+ "attributes": {
208
+ "fuelUsed": 15395.2
209
+ }
210
+ },
211
+ {
212
+ "fixTime": "2026-06-25T04:43:06.000+00:00",
213
+ "attributes": {
214
+ "fuelUsed": 15395.6
215
+ }
216
+ },
217
+ {
218
+ "fixTime": "2026-06-25T04:45:54.000+00:00",
219
+ "attributes": {
220
+ "fuelUsed": 15396.1
221
+ }
222
+ },
223
+ {
224
+ "fixTime": "2026-06-25T04:48:26.000+00:00",
225
+ "attributes": {
226
+ "fuelUsed": 15396.5
227
+ }
228
+ },
229
+ {
230
+ "fixTime": "2026-06-25T04:51:22.000+00:00",
231
+ "attributes": {
232
+ "fuelUsed": 15397.4
233
+ }
234
+ },
235
+ {
236
+ "fixTime": "2026-06-25T05:08:18.000+00:00",
237
+ "attributes": {
238
+ "fuelUsed": 15397.5
239
+ }
240
+ },
241
+ {
242
+ "fixTime": "2026-06-25T05:48:18.000+00:00",
243
+ "attributes": {
244
+ "fuelUsed": 15397.5
245
+ }
246
+ },
247
+ {
248
+ "fixTime": "2026-06-25T06:28:18.000+00:00",
249
+ "attributes": {
250
+ "fuelUsed": 15397.5
251
+ }
252
+ },
253
+ {
254
+ "fixTime": "2026-06-25T07:06:52.000+00:00",
255
+ "attributes": {
256
+ "fuelUsed": 15397.5
257
+ }
258
+ },
259
+ {
260
+ "fixTime": "2026-06-25T07:40:29.000+00:00",
261
+ "attributes": {
262
+ "fuelUsed": 15397.5
263
+ }
264
+ },
265
+ {
266
+ "fixTime": "2026-06-25T07:53:15.000+00:00",
267
+ "attributes": {
268
+ "fuelUsed": 15398.5
269
+ }
270
+ },
271
+ {
272
+ "fixTime": "2026-06-25T07:56:50.000+00:00",
273
+ "attributes": {
274
+ "fuelUsed": 15399.4
275
+ }
276
+ },
277
+ {
278
+ "fixTime": "2026-06-25T07:59:29.000+00:00",
279
+ "attributes": {
280
+ "fuelUsed": 15399.9
281
+ }
282
+ },
283
+ {
284
+ "fixTime": "2026-06-25T08:03:03.000+00:00",
285
+ "attributes": {
286
+ "fuelUsed": 15400.7
287
+ }
288
+ },
289
+ {
290
+ "fixTime": "2026-06-25T08:07:34.000+00:00",
291
+ "attributes": {
292
+ "fuelUsed": 15401.6
293
+ }
294
+ },
295
+ {
296
+ "fixTime": "2026-06-25T08:11:33.000+00:00",
297
+ "attributes": {
298
+ "fuelUsed": 15402.8
299
+ }
300
+ },
301
+ {
302
+ "fixTime": "2026-06-25T08:15:30.000+00:00",
303
+ "attributes": {
304
+ "fuelUsed": 15403.7
305
+ }
306
+ },
307
+ {
308
+ "fixTime": "2026-06-25T08:19:19.000+00:00",
309
+ "attributes": {
310
+ "fuelUsed": 15404.4
311
+ }
312
+ },
313
+ {
314
+ "fixTime": "2026-06-25T08:22:32.000+00:00",
315
+ "attributes": {
316
+ "fuelUsed": 15405.4
317
+ }
318
+ },
319
+ {
320
+ "fixTime": "2026-06-25T08:32:19.000+00:00",
321
+ "attributes": {
322
+ "fuelUsed": 15405.9
323
+ }
324
+ },
325
+ {
326
+ "fixTime": "2026-06-25T09:06:25.000+00:00",
327
+ "attributes": {
328
+ "fuelUsed": 15406.5
329
+ }
330
+ },
331
+ {
332
+ "fixTime": "2026-06-25T09:42:40.000+00:00",
333
+ "attributes": {
334
+ "fuelUsed": 15406.5
335
+ }
336
+ },
337
+ {
338
+ "fixTime": "2026-06-25T10:21:04.000+00:00",
339
+ "attributes": {
340
+ "fuelUsed": 15406.5
341
+ }
342
+ },
343
+ {
344
+ "fixTime": "2026-06-25T10:57:17.000+00:00",
345
+ "attributes": {
346
+ "fuelUsed": 15406.5
347
+ }
348
+ },
349
+ {
350
+ "fixTime": "2026-06-25T11:32:56.000+00:00",
351
+ "attributes": {
352
+ "fuelUsed": 15406.6
353
+ }
354
+ },
355
+ {
356
+ "fixTime": "2026-06-25T11:47:11.000+00:00",
357
+ "attributes": {
358
+ "fuelUsed": 15406.9
359
+ }
360
+ },
361
+ {
362
+ "fixTime": "2026-06-25T11:50:29.000+00:00",
363
+ "attributes": {
364
+ "fuelUsed": 15408
365
+ }
366
+ },
367
+ {
368
+ "fixTime": "2026-06-25T11:53:46.000+00:00",
369
+ "attributes": {
370
+ "fuelUsed": 15408.9
371
+ }
372
+ },
373
+ {
374
+ "fixTime": "2026-06-25T11:57:12.000+00:00",
375
+ "attributes": {
376
+ "fuelUsed": 15410.2
377
+ }
378
+ },
379
+ {
380
+ "fixTime": "2026-06-25T12:00:19.000+00:00",
381
+ "attributes": {
382
+ "fuelUsed": 15411.1
383
+ }
384
+ },
385
+ {
386
+ "fixTime": "2026-06-25T12:02:55.000+00:00",
387
+ "attributes": {
388
+ "fuelUsed": 15412.2
389
+ }
390
+ },
391
+ {
392
+ "fixTime": "2026-06-25T12:06:19.000+00:00",
393
+ "attributes": {
394
+ "fuelUsed": 15413.4
395
+ }
396
+ },
397
+ {
398
+ "fixTime": "2026-06-25T12:09:16.000+00:00",
399
+ "attributes": {
400
+ "fuelUsed": 15414.7
401
+ }
402
+ },
403
+ {
404
+ "fixTime": "2026-06-25T12:11:52.000+00:00",
405
+ "attributes": {
406
+ "fuelUsed": 15415.8
407
+ }
408
+ },
409
+ {
410
+ "fixTime": "2026-06-25T12:14:44.000+00:00",
411
+ "attributes": {
412
+ "fuelUsed": 15417.1
413
+ }
414
+ },
415
+ {
416
+ "fixTime": "2026-06-25T12:17:34.000+00:00",
417
+ "attributes": {
418
+ "fuelUsed": 15418.3
419
+ }
420
+ },
421
+ {
422
+ "fixTime": "2026-06-25T12:20:58.000+00:00",
423
+ "attributes": {
424
+ "fuelUsed": 15419.4
425
+ }
426
+ },
427
+ {
428
+ "fixTime": "2026-06-25T12:24:10.000+00:00",
429
+ "attributes": {
430
+ "fuelUsed": 15420.4
431
+ }
432
+ },
433
+ {
434
+ "fixTime": "2026-06-25T12:26:59.000+00:00",
435
+ "attributes": {
436
+ "fuelUsed": 15421.6
437
+ }
438
+ },
439
+ {
440
+ "fixTime": "2026-06-25T12:29:57.000+00:00",
441
+ "attributes": {
442
+ "fuelUsed": 15422.8
443
+ }
444
+ },
445
+ {
446
+ "fixTime": "2026-06-25T12:32:38.000+00:00",
447
+ "attributes": {
448
+ "fuelUsed": 15423.8
449
+ }
450
+ },
451
+ {
452
+ "fixTime": "2026-06-25T12:35:29.000+00:00",
453
+ "attributes": {
454
+ "fuelUsed": 15425
455
+ }
456
+ },
457
+ {
458
+ "fixTime": "2026-06-25T12:38:15.000+00:00",
459
+ "attributes": {
460
+ "fuelUsed": 15426.2
461
+ }
462
+ },
463
+ {
464
+ "fixTime": "2026-06-25T12:40:50.000+00:00",
465
+ "attributes": {
466
+ "fuelUsed": 15427.3
467
+ }
468
+ },
469
+ {
470
+ "fixTime": "2026-06-25T12:43:24.000+00:00",
471
+ "attributes": {
472
+ "fuelUsed": 15428
473
+ }
474
+ },
475
+ {
476
+ "fixTime": "2026-06-25T12:46:03.000+00:00",
477
+ "attributes": {
478
+ "fuelUsed": 15429.1
479
+ }
480
+ },
481
+ {
482
+ "fixTime": "2026-06-25T12:48:57.000+00:00",
483
+ "attributes": {
484
+ "fuelUsed": 15430
485
+ }
486
+ },
487
+ {
488
+ "fixTime": "2026-06-25T12:51:45.000+00:00",
489
+ "attributes": {
490
+ "fuelUsed": 15431.2
491
+ }
492
+ },
493
+ {
494
+ "fixTime": "2026-06-25T12:54:15.000+00:00",
495
+ "attributes": {
496
+ "fuelUsed": 15432
497
+ }
498
+ },
499
+ {
500
+ "fixTime": "2026-06-25T12:56:54.000+00:00",
501
+ "attributes": {
502
+ "fuelUsed": 15433.4
503
+ }
504
+ },
505
+ {
506
+ "fixTime": "2026-06-25T12:59:27.000+00:00",
507
+ "attributes": {
508
+ "fuelUsed": 15434.8
509
+ }
510
+ },
511
+ {
512
+ "fixTime": "2026-06-25T13:02:20.000+00:00",
513
+ "attributes": {
514
+ "fuelUsed": 15435.5
515
+ }
516
+ },
517
+ {
518
+ "fixTime": "2026-06-25T13:04:44.000+00:00",
519
+ "attributes": {
520
+ "fuelUsed": 15436.1
521
+ }
522
+ },
523
+ {
524
+ "fixTime": "2026-06-25T13:07:26.000+00:00",
525
+ "attributes": {
526
+ "fuelUsed": 15437.3
527
+ }
528
+ },
529
+ {
530
+ "fixTime": "2026-06-25T13:10:02.000+00:00",
531
+ "attributes": {
532
+ "fuelUsed": 15438.6
533
+ }
534
+ },
535
+ {
536
+ "fixTime": "2026-06-25T13:13:06.000+00:00",
537
+ "attributes": {
538
+ "fuelUsed": 15439.4
539
+ }
540
+ },
541
+ {
542
+ "fixTime": "2026-06-25T13:16:00.000+00:00",
543
+ "attributes": {
544
+ "fuelUsed": 15440.2
545
+ }
546
+ },
547
+ {
548
+ "fixTime": "2026-06-25T13:19:00.000+00:00",
549
+ "attributes": {
550
+ "fuelUsed": 15441.7
551
+ }
552
+ },
553
+ {
554
+ "fixTime": "2026-06-25T13:21:43.000+00:00",
555
+ "attributes": {
556
+ "fuelUsed": 15442.2
557
+ }
558
+ },
559
+ {
560
+ "fixTime": "2026-06-25T13:24:27.000+00:00",
561
+ "attributes": {
562
+ "fuelUsed": 15443.8
563
+ }
564
+ },
565
+ {
566
+ "fixTime": "2026-06-25T13:27:30.000+00:00",
567
+ "attributes": {
568
+ "fuelUsed": 15446
569
+ }
570
+ },
571
+ {
572
+ "fixTime": "2026-06-25T13:30:27.000+00:00",
573
+ "attributes": {
574
+ "fuelUsed": 15447.6
575
+ }
576
+ },
577
+ {
578
+ "fixTime": "2026-06-25T13:33:10.000+00:00",
579
+ "attributes": {
580
+ "fuelUsed": 15448.2
581
+ }
582
+ },
583
+ {
584
+ "fixTime": "2026-06-25T13:36:05.000+00:00",
585
+ "attributes": {
586
+ "fuelUsed": 15448.9
587
+ }
588
+ },
589
+ {
590
+ "fixTime": "2026-06-25T13:38:50.000+00:00",
591
+ "attributes": {
592
+ "fuelUsed": 15449.8
593
+ }
594
+ },
595
+ {
596
+ "fixTime": "2026-06-25T13:41:40.000+00:00",
597
+ "attributes": {
598
+ "fuelUsed": 15450.3
599
+ }
600
+ },
601
+ {
602
+ "fixTime": "2026-06-25T13:44:52.000+00:00",
603
+ "attributes": {
604
+ "fuelUsed": 15451.5
605
+ }
606
+ },
607
+ {
608
+ "fixTime": "2026-06-25T13:48:25.000+00:00",
609
+ "attributes": {
610
+ "fuelUsed": 15452.3
611
+ }
612
+ },
613
+ {
614
+ "fixTime": "2026-06-25T13:51:32.000+00:00",
615
+ "attributes": {
616
+ "fuelUsed": 15453.8
617
+ }
618
+ },
619
+ {
620
+ "fixTime": "2026-06-25T13:54:23.000+00:00",
621
+ "attributes": {
622
+ "fuelUsed": 15454.6
623
+ }
624
+ },
625
+ {
626
+ "fixTime": "2026-06-25T13:57:24.000+00:00",
627
+ "attributes": {
628
+ "fuelUsed": 15455.9
629
+ }
630
+ },
631
+ {
632
+ "fixTime": "2026-06-25T14:00:15.000+00:00",
633
+ "attributes": {
634
+ "fuelUsed": 15457.1
635
+ }
636
+ },
637
+ {
638
+ "fixTime": "2026-06-25T14:03:24.000+00:00",
639
+ "attributes": {
640
+ "fuelUsed": 15458.1
641
+ }
642
+ },
643
+ {
644
+ "fixTime": "2026-06-25T14:06:39.000+00:00",
645
+ "attributes": {
646
+ "fuelUsed": 15458.9
647
+ }
648
+ },
649
+ {
650
+ "fixTime": "2026-06-25T14:10:00.000+00:00",
651
+ "attributes": {
652
+ "fuelUsed": 15459.8
653
+ }
654
+ },
655
+ {
656
+ "fixTime": "2026-06-25T14:14:03.000+00:00",
657
+ "attributes": {
658
+ "fuelUsed": 15460.8
659
+ }
660
+ },
661
+ {
662
+ "fixTime": "2026-06-25T14:18:24.000+00:00",
663
+ "attributes": {
664
+ "fuelUsed": 15461.7
665
+ }
666
+ },
667
+ {
668
+ "fixTime": "2026-06-25T14:22:44.000+00:00",
669
+ "attributes": {
670
+ "fuelUsed": 15462.6
671
+ }
672
+ },
673
+ {
674
+ "fixTime": "2026-06-25T14:24:15.000+00:00",
675
+ "attributes": {
676
+ "fuelUsed": 15462.7
677
+ }
678
+ },
679
+ {
680
+ "fixTime": "2026-06-25T15:02:55.000+00:00",
681
+ "attributes": {
682
+ "fuelUsed": 15462.8
683
+ }
684
+ },
685
+ {
686
+ "fixTime": "2026-06-25T15:35:11.000+00:00",
687
+ "attributes": {
688
+ "fuelUsed": 15462.9
689
+ }
690
+ },
691
+ {
692
+ "fixTime": "2026-06-25T16:15:11.000+00:00",
693
+ "attributes": {
694
+ "fuelUsed": 15462.9
695
+ }
696
+ },
697
+ {
698
+ "fixTime": "2026-06-25T16:54:01.000+00:00",
699
+ "attributes": {
700
+ "fuelUsed": 15462.9
701
+ }
702
+ },
703
+ {
704
+ "fixTime": "2026-06-25T16:58:54.000+00:00",
705
+ "attributes": {
706
+ "fuelUsed": 15463.4
707
+ }
708
+ },
709
+ {
710
+ "fixTime": "2026-06-25T17:03:04.000+00:00",
711
+ "attributes": {
712
+ "fuelUsed": 15463.7
713
+ }
714
+ },
715
+ {
716
+ "fixTime": "2026-06-25T17:07:00.000+00:00",
717
+ "attributes": {
718
+ "fuelUsed": 15464.2
719
+ }
720
+ },
721
+ {
722
+ "fixTime": "2026-06-25T17:10:26.000+00:00",
723
+ "attributes": {
724
+ "fuelUsed": 15464.8
725
+ }
726
+ },
727
+ {
728
+ "fixTime": "2026-06-25T17:14:13.000+00:00",
729
+ "attributes": {
730
+ "fuelUsed": 15465.4
731
+ }
732
+ },
733
+ {
734
+ "fixTime": "2026-06-25T17:17:11.000+00:00",
735
+ "attributes": {
736
+ "fuelUsed": 15466.4
737
+ }
738
+ },
739
+ {
740
+ "fixTime": "2026-06-25T17:20:32.000+00:00",
741
+ "attributes": {
742
+ "fuelUsed": 15467.4
743
+ }
744
+ },
745
+ {
746
+ "fixTime": "2026-06-25T17:23:26.000+00:00",
747
+ "attributes": {
748
+ "fuelUsed": 15468.5
749
+ }
750
+ },
751
+ {
752
+ "fixTime": "2026-06-25T17:26:16.000+00:00",
753
+ "attributes": {
754
+ "fuelUsed": 15469.2
755
+ }
756
+ },
757
+ {
758
+ "fixTime": "2026-06-25T17:29:13.000+00:00",
759
+ "attributes": {
760
+ "fuelUsed": 15470.4
761
+ }
762
+ },
763
+ {
764
+ "fixTime": "2026-06-25T17:32:13.000+00:00",
765
+ "attributes": {
766
+ "fuelUsed": 15471.5
767
+ }
768
+ },
769
+ {
770
+ "fixTime": "2026-06-25T17:36:09.000+00:00",
771
+ "attributes": {
772
+ "fuelUsed": 15472.3
773
+ }
774
+ },
775
+ {
776
+ "fixTime": "2026-06-25T17:39:07.000+00:00",
777
+ "attributes": {
778
+ "fuelUsed": 15473.2
779
+ }
780
+ },
781
+ {
782
+ "fixTime": "2026-06-25T17:42:03.000+00:00",
783
+ "attributes": {
784
+ "fuelUsed": 15474.4
785
+ }
786
+ },
787
+ {
788
+ "fixTime": "2026-06-25T17:45:57.000+00:00",
789
+ "attributes": {
790
+ "fuelUsed": 15475.1
791
+ }
792
+ },
793
+ {
794
+ "fixTime": "2026-06-25T17:49:06.000+00:00",
795
+ "attributes": {
796
+ "fuelUsed": 15476.5
797
+ }
798
+ },
799
+ {
800
+ "fixTime": "2026-06-25T17:51:56.000+00:00",
801
+ "attributes": {
802
+ "fuelUsed": 15477.6
803
+ }
804
+ },
805
+ {
806
+ "fixTime": "2026-06-25T17:54:52.000+00:00",
807
+ "attributes": {
808
+ "fuelUsed": 15478.3
809
+ }
810
+ },
811
+ {
812
+ "fixTime": "2026-06-25T17:57:44.000+00:00",
813
+ "attributes": {
814
+ "fuelUsed": 15478.8
815
+ }
816
+ },
817
+ {
818
+ "fixTime": "2026-06-25T18:00:38.000+00:00",
819
+ "attributes": {
820
+ "fuelUsed": 15479.4
821
+ }
822
+ },
823
+ {
824
+ "fixTime": "2026-06-25T18:03:35.000+00:00",
825
+ "attributes": {
826
+ "fuelUsed": 15480.5
827
+ }
828
+ },
829
+ {
830
+ "fixTime": "2026-06-25T18:06:23.000+00:00",
831
+ "attributes": {
832
+ "fuelUsed": 15481.3
833
+ }
834
+ },
835
+ {
836
+ "fixTime": "2026-06-25T18:09:17.000+00:00",
837
+ "attributes": {
838
+ "fuelUsed": 15482.4
839
+ }
840
+ },
841
+ {
842
+ "fixTime": "2026-06-25T18:12:02.000+00:00",
843
+ "attributes": {
844
+ "fuelUsed": 15483.5
845
+ }
846
+ },
847
+ {
848
+ "fixTime": "2026-06-25T18:14:56.000+00:00",
849
+ "attributes": {
850
+ "fuelUsed": 15484.2
851
+ }
852
+ },
853
+ {
854
+ "fixTime": "2026-06-25T18:17:51.000+00:00",
855
+ "attributes": {
856
+ "fuelUsed": 15485.1
857
+ }
858
+ },
859
+ {
860
+ "fixTime": "2026-06-25T18:20:46.000+00:00",
861
+ "attributes": {
862
+ "fuelUsed": 15486.2
863
+ }
864
+ },
865
+ {
866
+ "fixTime": "2026-06-25T18:23:43.000+00:00",
867
+ "attributes": {
868
+ "fuelUsed": 15486.8
869
+ }
870
+ },
871
+ {
872
+ "fixTime": "2026-06-25T18:26:37.000+00:00",
873
+ "attributes": {
874
+ "fuelUsed": 15487.6
875
+ }
876
+ },
877
+ {
878
+ "fixTime": "2026-06-25T18:29:40.000+00:00",
879
+ "attributes": {
880
+ "fuelUsed": 15488.6
881
+ }
882
+ },
883
+ {
884
+ "fixTime": "2026-06-25T18:32:34.000+00:00",
885
+ "attributes": {
886
+ "fuelUsed": 15489.6
887
+ }
888
+ },
889
+ {
890
+ "fixTime": "2026-06-25T18:35:27.000+00:00",
891
+ "attributes": {
892
+ "fuelUsed": 15490.5
893
+ }
894
+ },
895
+ {
896
+ "fixTime": "2026-06-25T18:38:22.000+00:00",
897
+ "attributes": {
898
+ "fuelUsed": 15491.5
899
+ }
900
+ },
901
+ {
902
+ "fixTime": "2026-06-25T18:41:11.000+00:00",
903
+ "attributes": {
904
+ "fuelUsed": 15492.4
905
+ }
906
+ },
907
+ {
908
+ "fixTime": "2026-06-25T18:44:03.000+00:00",
909
+ "attributes": {
910
+ "fuelUsed": 15493.3
911
+ }
912
+ },
913
+ {
914
+ "fixTime": "2026-06-25T18:46:56.000+00:00",
915
+ "attributes": {
916
+ "fuelUsed": 15494.2
917
+ }
918
+ },
919
+ {
920
+ "fixTime": "2026-06-25T18:49:50.000+00:00",
921
+ "attributes": {
922
+ "fuelUsed": 15495.1
923
+ }
924
+ },
925
+ {
926
+ "fixTime": "2026-06-25T18:52:48.000+00:00",
927
+ "attributes": {
928
+ "fuelUsed": 15496
929
+ }
930
+ },
931
+ {
932
+ "fixTime": "2026-06-25T18:55:40.000+00:00",
933
+ "attributes": {
934
+ "fuelUsed": 15496.9
935
+ }
936
+ },
937
+ {
938
+ "fixTime": "2026-06-25T18:58:52.000+00:00",
939
+ "attributes": {
940
+ "fuelUsed": 15497.7
941
+ }
942
+ },
943
+ {
944
+ "fixTime": "2026-06-25T19:01:44.000+00:00",
945
+ "attributes": {
946
+ "fuelUsed": 15498.6
947
+ }
948
+ },
949
+ {
950
+ "fixTime": "2026-06-25T19:04:38.000+00:00",
951
+ "attributes": {
952
+ "fuelUsed": 15499.5
953
+ }
954
+ },
955
+ {
956
+ "fixTime": "2026-06-25T19:07:36.000+00:00",
957
+ "attributes": {
958
+ "fuelUsed": 15500.3
959
+ }
960
+ },
961
+ {
962
+ "fixTime": "2026-06-25T19:10:34.000+00:00",
963
+ "attributes": {
964
+ "fuelUsed": 15501.2
965
+ }
966
+ },
967
+ {
968
+ "fixTime": "2026-06-25T19:13:33.000+00:00",
969
+ "attributes": {
970
+ "fuelUsed": 15502.1
971
+ }
972
+ },
973
+ {
974
+ "fixTime": "2026-06-25T19:16:47.000+00:00",
975
+ "attributes": {
976
+ "fuelUsed": 15502.5
977
+ }
978
+ },
979
+ {
980
+ "fixTime": "2026-06-25T19:20:05.000+00:00",
981
+ "attributes": {
982
+ "fuelUsed": 15503.2
983
+ }
984
+ },
985
+ {
986
+ "fixTime": "2026-06-25T19:23:50.000+00:00",
987
+ "attributes": {
988
+ "fuelUsed": 15503.7
989
+ }
990
+ },
991
+ {
992
+ "fixTime": "2026-06-25T19:27:08.000+00:00",
993
+ "attributes": {
994
+ "fuelUsed": 15504
995
+ }
996
+ },
997
+ {
998
+ "fixTime": "2026-06-25T19:30:05.000+00:00",
999
+ "attributes": {
1000
+ "fuelUsed": 15504.3
1001
+ }
1002
+ },
1003
+ {
1004
+ "fixTime": "2026-06-25T19:34:16.000+00:00",
1005
+ "attributes": {
1006
+ "fuelUsed": 15504.5
1007
+ }
1008
+ },
1009
+ {
1010
+ "fixTime": "2026-06-25T19:43:39.000+00:00",
1011
+ "attributes": {
1012
+ "fuelUsed": 15504.8
1013
+ }
1014
+ },
1015
+ {
1016
+ "fixTime": "2026-06-25T19:48:22.000+00:00",
1017
+ "attributes": {
1018
+ "fuelUsed": 15505
1019
+ }
1020
+ },
1021
+ {
1022
+ "fixTime": "2026-06-25T19:52:30.000+00:00",
1023
+ "attributes": {
1024
+ "fuelUsed": 15505.6
1025
+ }
1026
+ },
1027
+ {
1028
+ "fixTime": "2026-06-25T20:24:17.000+00:00",
1029
+ "attributes": {
1030
+ "fuelUsed": 15505.6
1031
+ }
1032
+ }
1033
+ ]
1034
+ }
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable no-undef */
2
2
  const assert = require('assert')
3
- const { getFuelUsedBetween, calculateConsumption } = require('../util/fuel')
3
+ const { getFuelUsedBetween, calculateConsumption, shouldUseFuelSensor } = require('../util/fuel')
4
4
 
5
5
  function pos (iso, fuelUsed, extra = {}) {
6
6
  return { fixTime: new Date(iso).toISOString(), attributes: { fuelUsed, ...extra } }
@@ -59,3 +59,49 @@ describe('fuelUsed counter based consumption', function () {
59
59
  assert.strictEqual(calculateConsumption(device, { route: noCan, summary: { spentFuel: 12 } }), 12)
60
60
  })
61
61
  })
62
+
63
+ describe('shouldUseFuelSensor', function () {
64
+ const sensorDevice = { attributes: { fuel_low_threshold: 10, fuel_high_threshold: 90, fuel_tank_capacity: 500 } }
65
+
66
+ // Reproduces the Fuel Consumption Report bug still seen after the fuelUsed fix: a
67
+ // vehicle configured with analog sensor thresholds (fuel_low/high_threshold, tank
68
+ // capacity) that ALSO reports the CAN fuelUsed counter. The report must still prefer
69
+ // fuelUsed (== Manager "Fuel Used"), not fall back to the analog tank-level segmentation,
70
+ // which produced 0 on refuel days (e.g. FORD CARGO / 28669-B-59, 25/06/2026).
71
+ it('prefers the CAN counter over the analog sensor when both are configured', () => {
72
+ assert.strictEqual(shouldUseFuelSensor(sensorDevice, route), false)
73
+ })
74
+
75
+ it('uses the analog sensor when the device has no CAN fuelUsed data', () => {
76
+ const noCan = [{ fixTime: new Date('2026-06-01T06:00:00Z').toISOString(), attributes: { fuel: 50 } }]
77
+ assert.strictEqual(shouldUseFuelSensor(sensorDevice, noCan), true)
78
+ })
79
+
80
+ it('does not use the sensor when thresholds are not configured', () => {
81
+ assert.strictEqual(shouldUseFuelSensor({ attributes: {} }, []), false)
82
+ })
83
+ })
84
+
85
+ describe('calculateConsumption against real production data (FORD CARGO / 28669-B-59)', function () {
86
+ // Trimmed allinone response for 25/06/2026 (route thinned to every ~40th position plus
87
+ // its fuelUsed min/max, trips/stops kept in full). This device's route reports fuelUsed
88
+ // NON-monotonically that day (33439.5 -> 15505.6, i.e. a raw delta of about -17934) -
89
+ // subtracting it directly is wrong, and is exactly why the report still read 0.0 L after
90
+ // the fuelUsed-counter fix (PRs #258/#259) shipped. Traccar's own per-trip/per-stop
91
+ // spentFuel (11 trips summing to 110.6, one stray stop at -18045.3 that gets floored to 0,
92
+ // two small positive stops totalling 0.8) is correct and must be preferred: 110.6 + 0.8 =
93
+ // 111.4, matching the ~111 L the user confirmed via the Manager's "Fuel Used".
94
+ const fixture = require('./fixtures/ford-cargo-28669-2026-06-25.json')
95
+ const device = { attributes: {} }
96
+ const from = new Date('2026-06-24T23:00:00.000Z')
97
+ const to = new Date('2026-06-25T22:59:59.000Z')
98
+
99
+ it('sums Traccar trip/stop spentFuel instead of the non-monotonic raw fuelUsed counter', () => {
100
+ const result = calculateConsumption(device, { trips: fixture.trips, stops: fixture.stops, route: fixture.route, summary: null, from, to })
101
+ assert.ok(Math.abs(result - 111.4) < 0.01, result)
102
+ })
103
+
104
+ it('sanity check: a naive route fuelUsed(last)-fuelUsed(first) would be deeply negative here', () => {
105
+ assert.ok(getFuelUsedBetween(fixture.route, from, to) === 0) // clamped from a large negative
106
+ })
107
+ })
package/src/util/fuel.js CHANGED
@@ -9,6 +9,23 @@ function getRefuelingLiters (d, position, diff) {
9
9
  return Math.round(diff * d.attributes.fuel_tank_capacity / 100)
10
10
  }
11
11
 
12
+ function hasCanFuelUsed (route) {
13
+ return Array.isArray(route) && route.some(p => p && p.attributes && p.attributes.fuelUsed)
14
+ }
15
+
16
+ // Whether a report should fall back to the analog tank-level sensor (calculateTotalFuelFromSensor)
17
+ // for this device/route. The CAN "fuelUsed" counter - when the device actually reports it - is
18
+ // the exact value the Manager shows as "Fuel Used", so it must always win over the analog sensor,
19
+ // even if fuel_low_threshold/fuel_high_threshold/fuel_tank_capacity happen to be configured on the
20
+ // device: that config alone doesn't mean the analog input is wired or currently reporting, and a
21
+ // device can carry stale/unrelated sensor thresholds while actually being CAN-fuelled.
22
+ function shouldUseFuelSensor (device, route) {
23
+ return !hasCanFuelUsed(route) &&
24
+ 'fuel_low_threshold' in device.attributes &&
25
+ 'fuel_high_threshold' in device.attributes &&
26
+ !!device.attributes.fuel_tank_capacity
27
+ }
28
+
12
29
  function calculateTotalFuelFromSensor(trips, stops, positions, device, firstStopIndex) {
13
30
  const lastStopIndex = firstStopIndex + trips.length - 1
14
31
  const lastStop = stops.length > lastStopIndex ? stops[lastStopIndex] : null
@@ -120,13 +137,24 @@ function getFuelUsedBetween (route, from, to) {
120
137
  function calculateConsumption (d, data) {
121
138
  let fuelConsumption = 0
122
139
  const withFuelUsed = data.route && data.route.find(p => p.attributes.fuelUsed)
123
- if (withFuelUsed && data.route.length > 1) {
124
- // CAN "fuelUsed" counter - keep every report aligned with the Manager's "Fuel Used".
140
+ if (data.trips && data.stops) {
141
+ // Trust Traccar's own already-computed per-trip/per-stop spentFuel over any
142
+ // period-level aggregate. Two other sources have each turned out unreliable for
143
+ // real devices: the daily summary's spentFuel (tank-level based - goes <= 0 on a
144
+ // refuel day) and a naive fuelUsed(last)-fuelUsed(first) over the raw route (some
145
+ // devices/protocols report that counter non-monotonically or in units other than
146
+ // litres, e.g. FORD CARGO / 28669-B-59 - the raw route delta comes out at -18000+
147
+ // while Traccar's own per-trip spentFuel sums to the correct ~111L for that day).
148
+ // Each individual trip/stop value is still floored at 0 below so one bad segment
149
+ // (e.g. a counter glitch) can't drag the whole sum negative.
150
+ fuelConsumption = data.trips.reduce((a, b) => a + calculateSpentFuel(b.spentFuel, d, withFuelUsed), 0) + data.stops.reduce((a, b) => a + calculateSpentFuel(b.spentFuel, d, withFuelUsed), 0)
151
+ } else if (withFuelUsed && data.route.length > 1) {
152
+ // No trips/stops to sum (e.g. a period-only aggregate) - fall back to the raw
153
+ // fuelUsed counter delta, which matches the Manager's "Fuel Used" for devices
154
+ // that do report it monotonically in litres.
125
155
  fuelConsumption = getFuelUsedBetween(data.route, data.from, data.to)
126
156
  } else if (data.summary && withFuelUsed) {
127
157
  fuelConsumption = calculateSpentFuel(data.summary.spentFuel, d, withFuelUsed)
128
- } else if (data.trips && data.stops) {
129
- fuelConsumption = data.trips.reduce((a, b) => a + calculateSpentFuel(b.spentFuel, d, withFuelUsed), 0) + data.stops.reduce((a, b) => a + calculateSpentFuel(b.spentFuel, d, withFuelUsed), 0)
130
158
  } else if (data.summary) {
131
159
  fuelConsumption = data.summary.spentFuel
132
160
  }
@@ -162,3 +190,5 @@ exports.calculateConsumption = calculateConsumption
162
190
  exports.getFuelUsedBetween = getFuelUsedBetween
163
191
  exports.useOdooFuelData = useOdooFuelData
164
192
  exports.calculateTotalFuelFromSensor = calculateTotalFuelFromSensor
193
+ exports.hasCanFuelUsed = hasCanFuelUsed
194
+ exports.shouldUseFuelSensor = shouldUseFuelSensor