fleetmap-reports 2.0.377 → 2.0.378
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/src/activity-report.js +5 -2
- package/src/tests/activity.test.js +13 -2
- package/src/tests/fuelUsed.test.js +61 -0
- package/src/trip-report.js +1 -1
- package/src/util/fuel.js +66 -4
- package/src/util/trips.js +1 -1
package/package.json
CHANGED
package/src/activity-report.js
CHANGED
|
@@ -187,7 +187,7 @@ async function processDevices (from, to, devices, data, userData, traccarInstanc
|
|
|
187
187
|
}
|
|
188
188
|
const dates = getDates(from, to, userData.user.attributes.timezone || getUserPartner(userData.user).timezone)
|
|
189
189
|
for (const date of dates) {
|
|
190
|
-
const { tripsByDay, routeByDay } = await getDataByDay(d, date, { trips, route: deviceRoute }, userData, traccarInstance)
|
|
190
|
+
const { tripsByDay, routeByDay, fromDay, toDay } = await getDataByDay(d, date, { trips, route: deviceRoute }, userData, traccarInstance)
|
|
191
191
|
const distance = getKms(tripsByDay)
|
|
192
192
|
if (!userData.allWeek && userData.weekDays && userData.dayHours) {
|
|
193
193
|
if (weekDaySelected(date, userData.weekDays)) {
|
|
@@ -219,7 +219,10 @@ 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
|
-
|
|
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 })
|
|
223
226
|
summaryCurrentDay.startTime = tripsByDay.length && tripsByDay[0].startTime
|
|
224
227
|
summaryCurrentDay.startAddress = tripsByDay.length && tripsByDay[0].startAddress
|
|
225
228
|
summaryCurrentDay.endTime = tripsByDay.length && tripsByDay[tripsByDay.length - 1].endTime
|
|
@@ -56,7 +56,7 @@ describe('activity report', function () {
|
|
|
56
56
|
assert.equal(data.length, 1)
|
|
57
57
|
const device = data[0].devices.find(d => d.device.id === 69114)
|
|
58
58
|
console.log(device)
|
|
59
|
-
assert.
|
|
59
|
+
assert.ok(Math.abs(device.summary[0].convertedSpentFuel - 168.7) <= 1.5, device.summary[0].convertedSpentFuel)
|
|
60
60
|
}, 800000)
|
|
61
61
|
// eslint-disable-next-line no-undef
|
|
62
62
|
it('Activity check fuel consumption group by day', async () => {
|
|
@@ -70,7 +70,18 @@ describe('activity report', function () {
|
|
|
70
70
|
assert.equal(data.length, 1)
|
|
71
71
|
const device = data[0].devices.find(d => d.device.id === 69114)
|
|
72
72
|
console.log(device)
|
|
73
|
-
|
|
73
|
+
// group-by-day must add up to the same "Fuel Used" total as the single-period value
|
|
74
|
+
const groupedTotal = device.summary.reduce((a, b) => a + b.convertedSpentFuel, 0)
|
|
75
|
+
|
|
76
|
+
const ungrouped = await getReports().then(async (r) => {
|
|
77
|
+
const ud = await r.getUserData()
|
|
78
|
+
ud.devices = ud.devices.filter(d => d.id === 69114)
|
|
79
|
+
const dd = await r.activityReport(new Date(2023, 5, 11, 0, 0, 0, 0), new Date(2023, 6, 11, 23, 59, 59, 0), ud)
|
|
80
|
+
return dd[0].devices.find(d => d.device.id === 69114).summary[0].convertedSpentFuel
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
assert.ok(Math.abs(groupedTotal - ungrouped) <= 1, `grouped ${groupedTotal} vs ungrouped ${ungrouped}`)
|
|
84
|
+
assert.ok(Math.abs(groupedTotal - 168.7) <= 1.5, groupedTotal)
|
|
74
85
|
}, 800000)
|
|
75
86
|
// eslint-disable-next-line no-undef
|
|
76
87
|
it('works with timetable', async () => {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
const assert = require('assert')
|
|
3
|
+
const { getFuelUsedBetween, calculateConsumption } = require('../util/fuel')
|
|
4
|
+
|
|
5
|
+
function pos (iso, fuelUsed, extra = {}) {
|
|
6
|
+
return { fixTime: new Date(iso).toISOString(), attributes: { fuelUsed, ...extra } }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Reproduces ticket "fuel consumption of a full day not taken into account":
|
|
10
|
+
// a CAN vehicle that refuels mid-period. Traccar's daily summary spentFuel goes
|
|
11
|
+
// negative on the refuel day, but the "fuelUsed" cumulative counter (shown as
|
|
12
|
+
// "Fuel Used" in the Manager) keeps rising and is the source of truth.
|
|
13
|
+
const route = [
|
|
14
|
+
pos('2026-06-01T06:00:00Z', 1000),
|
|
15
|
+
pos('2026-06-01T20:00:00Z', 1116), // +116 day 1
|
|
16
|
+
pos('2026-06-02T07:00:00Z', 1130, { fuel: 40 }),
|
|
17
|
+
pos('2026-06-02T10:00:00Z', 1150, { fuel: 250 }), // refuelled
|
|
18
|
+
pos('2026-06-02T19:00:00Z', 1227, { fuel: 210 }), // +111 day 2
|
|
19
|
+
{ fixTime: new Date('2026-06-03T05:30:00Z').toISOString(), attributes: {} }, // no counter on first fix
|
|
20
|
+
pos('2026-06-03T18:00:00Z', 1269) // +42 day 3
|
|
21
|
+
]
|
|
22
|
+
const device = { attributes: {} }
|
|
23
|
+
const day = (from, to) => [new Date(from).toISOString(), new Date(to).toISOString()]
|
|
24
|
+
|
|
25
|
+
describe('fuelUsed counter based consumption', function () {
|
|
26
|
+
it('uses the fuelUsed counter for each day and ignores a negative summary', () => {
|
|
27
|
+
const [f1, t1] = day('2026-06-01T00:00:00Z', '2026-06-01T23:59:59Z')
|
|
28
|
+
const [f2, t2] = day('2026-06-02T00:00:00Z', '2026-06-02T23:59:59Z')
|
|
29
|
+
const [f3, t3] = day('2026-06-03T00:00:00Z', '2026-06-03T23:59:59Z')
|
|
30
|
+
|
|
31
|
+
const d1 = calculateConsumption(device, { route, summary: { spentFuel: -170 }, from: f1, to: t1 })
|
|
32
|
+
const d2 = calculateConsumption(device, { route, summary: { spentFuel: -170 }, from: f2, to: t2 })
|
|
33
|
+
const d3 = calculateConsumption(device, { route, summary: { spentFuel: 0 }, from: f3, to: t3 })
|
|
34
|
+
|
|
35
|
+
assert.strictEqual(d1, 116)
|
|
36
|
+
assert.strictEqual(d2, 111) // refuel day is no longer dropped
|
|
37
|
+
assert.strictEqual(d3, 42) // missing attribute on first fix is no longer dropped
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('daily values sum to the period total shown as "Fuel Used"', () => {
|
|
41
|
+
const [f1, t1] = day('2026-06-01T00:00:00Z', '2026-06-01T23:59:59Z')
|
|
42
|
+
const [f2, t2] = day('2026-06-02T00:00:00Z', '2026-06-02T23:59:59Z')
|
|
43
|
+
const [f3, t3] = day('2026-06-03T00:00:00Z', '2026-06-03T23:59:59Z')
|
|
44
|
+
const sum = calculateConsumption(device, { route, from: f1, to: t1 }) +
|
|
45
|
+
calculateConsumption(device, { route, from: f2, to: t2 }) +
|
|
46
|
+
calculateConsumption(device, { route, from: f3, to: t3 })
|
|
47
|
+
const period = calculateConsumption(device, { route, summary: { spentFuel: -170 } })
|
|
48
|
+
|
|
49
|
+
assert.strictEqual(period, 269) // 1269 - 1000
|
|
50
|
+
assert.strictEqual(Number(sum.toFixed(3)), period)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('clamps a counter reset to 0', () => {
|
|
54
|
+
assert.strictEqual(getFuelUsedBetween([pos('2026-06-01T06:00:00Z', 5000), pos('2026-06-01T12:00:00Z', 30)]), 0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('falls back to summary spentFuel when the route has no fuelUsed', () => {
|
|
58
|
+
const noCan = [{ fixTime: new Date('2026-06-01T06:00:00Z').toISOString(), attributes: {} }]
|
|
59
|
+
assert.strictEqual(calculateConsumption(device, { route: noCan, summary: { spentFuel: 12 } }), 12)
|
|
60
|
+
})
|
|
61
|
+
})
|
package/src/trip-report.js
CHANGED
|
@@ -216,7 +216,7 @@ async function processDevices (from, to, devices, data, userData, traccar) {
|
|
|
216
216
|
|
|
217
217
|
if (deviceRoute.some(r => r.attributes.fuel) ||
|
|
218
218
|
automaticReports.deviceWithFuelInfo(d)) {
|
|
219
|
-
trip.fuelConsumption = calculateConsumption(d, { trips: [trip], stops: (stop ? [stop] : []), route: deviceRoute })
|
|
219
|
+
trip.fuelConsumption = calculateConsumption(d, { trips: [trip], stops: (stop ? [stop] : []), route: deviceRoute, from: trip.startTime, to: trip.endTime })
|
|
220
220
|
trip.avgFuelConsumption = trip.totalKms > 0 ? trip.fuelConsumption * 100 / trip.totalKms : 0
|
|
221
221
|
}
|
|
222
222
|
|
package/src/util/fuel.js
CHANGED
|
@@ -57,15 +57,76 @@ function calculateTotalFuelFromSensor(trips, stops, positions, device, firstStop
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
function getPositionFuelUsed (position) {
|
|
61
|
+
const raw = position && position.attributes ? position.attributes.fuelUsed : undefined
|
|
62
|
+
const value = typeof raw === 'string' ? Number.parseFloat(raw) : raw
|
|
63
|
+
return typeof value === 'number' && !Number.isNaN(value) ? value : null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Fuel consumed over a time window, read straight from the CAN "fuelUsed" cumulative
|
|
68
|
+
* counter - the exact value the Manager shows as "Fuel Used".
|
|
69
|
+
*
|
|
70
|
+
* This is the single source of truth for every report whenever the route carries
|
|
71
|
+
* "fuelUsed". It must NOT be derived from the daily summary "spentFuel" (which Traccar
|
|
72
|
+
* computes from the tank fuel-level and returns as <= 0 on any day the vehicle was
|
|
73
|
+
* refuelled, making a whole day silently drop out of the report).
|
|
74
|
+
*
|
|
75
|
+
* Robust against:
|
|
76
|
+
* - positions without the fuelUsed attribute (e.g. the first/last fix of a day)
|
|
77
|
+
* - refuelling (a cumulative counter never goes down, unlike a tank-level reading)
|
|
78
|
+
* - the window opening between two fixes (uses the last known counter before it, so
|
|
79
|
+
* consecutive days partition the period with no gap and sum to the period total)
|
|
80
|
+
* - counter resets (negative overall difference is clamped to 0, as before)
|
|
81
|
+
*
|
|
82
|
+
* @param {Array} route positions for a single device (any order)
|
|
83
|
+
* @param {Date|string|number} [from] window start; omit for "from the first fix"
|
|
84
|
+
* @param {Date|string|number} [to] window end; omit for "until the last fix"
|
|
85
|
+
*/
|
|
86
|
+
function getFuelUsedBetween (route, from, to) {
|
|
87
|
+
if (!Array.isArray(route)) { return 0 }
|
|
88
|
+
|
|
89
|
+
const fromTime = from != null ? new Date(from).getTime() : null
|
|
90
|
+
const toTime = to != null ? new Date(to).getTime() : null
|
|
91
|
+
|
|
92
|
+
const samples = []
|
|
93
|
+
for (const p of route) {
|
|
94
|
+
const value = getPositionFuelUsed(p)
|
|
95
|
+
if (value == null) { continue }
|
|
96
|
+
samples.push({ t: new Date(p.fixTime).getTime(), value })
|
|
97
|
+
}
|
|
98
|
+
if (samples.length < 2) { return 0 }
|
|
99
|
+
samples.sort((a, b) => a.t - b.t)
|
|
100
|
+
|
|
101
|
+
// end = last counter reading at or before the end of the window
|
|
102
|
+
let end = null
|
|
103
|
+
for (const s of samples) {
|
|
104
|
+
if (toTime == null || s.t <= toTime) { end = s.value } else { break }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// start = last counter reading at or before the start of the window,
|
|
108
|
+
// or the first reading inside it when the window opens before any fix
|
|
109
|
+
let start = null
|
|
110
|
+
for (const s of samples) {
|
|
111
|
+
if (fromTime == null) { start = s.value; break }
|
|
112
|
+
if (s.t <= fromTime) { start = s.value } else { if (start == null) { start = s.value } break }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (start == null || end == null) { return 0 }
|
|
116
|
+
const diff = end - start
|
|
117
|
+
return diff > 0 ? diff : 0
|
|
118
|
+
}
|
|
119
|
+
|
|
60
120
|
function calculateConsumption (d, data) {
|
|
61
121
|
let fuelConsumption = 0
|
|
62
122
|
const withFuelUsed = data.route && data.route.find(p => p.attributes.fuelUsed)
|
|
63
|
-
if (data.
|
|
123
|
+
if (withFuelUsed && data.route.length > 1) {
|
|
124
|
+
// CAN "fuelUsed" counter - keep every report aligned with the Manager's "Fuel Used".
|
|
125
|
+
fuelConsumption = getFuelUsedBetween(data.route, data.from, data.to)
|
|
126
|
+
} else if (data.summary && withFuelUsed) {
|
|
64
127
|
fuelConsumption = calculateSpentFuel(data.summary.spentFuel, d, withFuelUsed)
|
|
65
128
|
} else if (data.trips && data.stops) {
|
|
66
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)
|
|
67
|
-
} else if (data.route && data.route.length > 1 && data.route[0].attributes.fuelUsed) {
|
|
68
|
-
fuelConsumption = data.route[data.route.length - 1].attributes.fuelUsed - data.route[0].attributes.fuelUsed
|
|
69
130
|
} else if (data.summary) {
|
|
70
131
|
fuelConsumption = data.summary.spentFuel
|
|
71
132
|
}
|
|
@@ -98,5 +159,6 @@ function useOdooFuelData (d, route) {
|
|
|
98
159
|
exports.calculateFuelDiff = calculateFuelDiff
|
|
99
160
|
exports.getRefuelingLiters = getRefuelingLiters
|
|
100
161
|
exports.calculateConsumption = calculateConsumption
|
|
162
|
+
exports.getFuelUsedBetween = getFuelUsedBetween
|
|
101
163
|
exports.useOdooFuelData = useOdooFuelData
|
|
102
|
-
exports.calculateTotalFuelFromSensor = calculateTotalFuelFromSensor
|
|
164
|
+
exports.calculateTotalFuelFromSensor = calculateTotalFuelFromSensor
|
package/src/util/trips.js
CHANGED
|
@@ -112,7 +112,7 @@ async function getDataByDay (device, date, data, userData, traccarInstance) {
|
|
|
112
112
|
(!z.outTime || (new Date(z.outTime.fixTime) > startDate)))
|
|
113
113
|
: []
|
|
114
114
|
|
|
115
|
-
return { tripsByDay, stopsByDay, routeByDay, refuelingPositionsByDay, zoneInOutDayData }
|
|
115
|
+
return { tripsByDay, stopsByDay, routeByDay, refuelingPositionsByDay, zoneInOutDayData, fromDay: startDate, toDay: endDate }
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
function isValidAvgSpeed (kms, startTime, endTime) {
|