Skip to content

Commit e3dee7e

Browse files
committed
fix: show proven trip odometer gaps and semantic times
1 parent d3de0de commit e3dee7e

1 file changed

Lines changed: 49 additions & 28 deletions

File tree

custom_components/sv_dashboard/static/trip-history-card.js

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { LitElement, html, css, nothing } from "./vendor-lit.js?v=0.6.0-beta.7";
2-
import { localeFor, textFor } from "./i18n.js?v=0.6.0-beta.10";
2+
import { localeFor, textFor } from "./i18n.js?v=issue52.1";
3+
import { insertOdometerGapRows, semanticTripTime, tripFilterTime } from "./trip-gap-core.js?v=issue52.1";
34

45
/**
56
* Standalone Lovelace card for the historic Stellantis "last trip" sensor.
@@ -41,6 +42,8 @@ class CodexStellantisTripHistoryCardV4 extends LitElement {
4142
.trip-table td { border-top: 1px solid var(--divider-color); padding: 9px 8px 9px 0; vertical-align: top; white-space: nowrap; }
4243
.trip-table td:first-child { white-space: normal; }
4344
.trip-row { cursor: pointer; }
45+
.trip-row.reconstructed td { font-style: italic; color: var(--secondary-text-color); }
46+
.trip-row.reconstructed td:nth-child(3) { color: var(--primary-text-color); font-weight: 600; }
4447
.trip-row:hover td, .trip-row:focus td { background: color-mix(in srgb, var(--primary-color) 8%, transparent); }
4548
.trip-row:focus { outline: 2px solid var(--primary-color); outline-offset: -2px; }
4649
.trip-details td { padding: 0 0 10px 0; border-top: 0; white-space: normal; }
@@ -139,24 +142,29 @@ class CodexStellantisTripHistoryCardV4 extends LitElement {
139142
}))
140143
: (serverState.attributes.zero_distance_events ?? []);
141144
const allServerTrips = [
142-
...serverTrips,
145+
...insertOdometerGapRows(serverTrips),
143146
...(this._showZeroEvents ? zeroEvents : []),
144147
];
145148
const normalizedTrips = this._filterServerTrips(allServerTrips).map((trip) => ({
146149
state: trip.distance_km,
147-
last_updated: trip.end_time || trip.start_time,
150+
last_updated: trip.reconstructed_gap ? undefined : (trip.end_time || trip.start_time),
151+
_sortTime: tripFilterTime(trip),
148152
_sourceEntityId: this._config.server_entity,
149-
_rowId: `stellantis|${trip.server_id}`,
153+
_rowId: `${trip.reconstructed_gap ? "odometer-gap" : "stellantis"}|${trip.server_id}`,
150154
attributes: {
151155
...trip,
152156
id: trip.server_id,
153157
start_time: trip.start_time,
154158
end_time: trip.end_time,
155159
start_mileage: trip.start_mileage,
160+
end_mileage: trip.end_mileage,
156161
duration_seconds: trip.duration_seconds,
157162
energy_kwh: trip.energy_kwh,
158163
energy_per_100_km: trip.energy_per_100_km,
159164
avg_speed: trip.average_speed,
165+
reconstructed_gap: Boolean(trip.reconstructed_gap),
166+
gap_after_time: trip.gap_after_time,
167+
gap_before_time: trip.gap_before_time,
160168
valid_for_statistics: trip.valid_for_statistics,
161169
quality_flags: trip.quality_flags,
162170
},
@@ -191,7 +199,7 @@ class CodexStellantisTripHistoryCardV4 extends LitElement {
191199
let carriedAttributes = {};
192200
return rawStates
193201
.map((raw) => this._normalizeState(raw))
194-
.sort((a, b) => new Date(a.last_updated).getTime() - new Date(b.last_updated).getTime())
202+
.sort((a, b) => new Date(semanticTripTime(a) || 0).getTime() - new Date(semanticTripTime(b) || 0).getTime())
195203
.map((state) => {
196204
carriedAttributes = {
197205
...carriedAttributes,
@@ -203,7 +211,7 @@ class CodexStellantisTripHistoryCardV4 extends LitElement {
203211
const enrichedStates = tripEntityIds
204212
.flatMap((entityId) => enrichAttributes(statesFor(entityId))
205213
.map((state) => ({ ...state, _sourceEntityId: entityId })))
206-
.sort((a, b) => new Date(a.last_updated).getTime() - new Date(b.last_updated).getTime());
214+
.sort((a, b) => new Date(semanticTripTime(a) || 0).getTime() - new Date(semanticTripTime(b) || 0).getTime());
207215
const parseNumber = (value) => Number.parseFloat(String(value ?? "").replace(",", "."));
208216
const durationSeconds = (value) => {
209217
const text = String(value ?? "");
@@ -222,7 +230,7 @@ class CodexStellantisTripHistoryCardV4 extends LitElement {
222230
...state,
223231
_rowId: [
224232
state._sourceEntityId,
225-
state.last_updated ?? state.last_changed ?? "",
233+
semanticTripTime(state) ?? "",
226234
state.state,
227235
state.attributes?.start_time ?? "",
228236
state.attributes?.end_time ?? "",
@@ -244,8 +252,8 @@ class CodexStellantisTripHistoryCardV4 extends LitElement {
244252
const rightDuration = durationSeconds(rightAttributes.duration);
245253
if (!Number.isFinite(leftDuration) || !Number.isFinite(rightDuration) || leftDuration !== rightDuration) return false;
246254
if (!isLocal) return true;
247-
const leftTime = Date.parse(left.last_updated || "");
248-
const rightTime = Date.parse(right.last_updated || "");
255+
const leftTime = Date.parse(semanticTripTime(left) || "");
256+
const rightTime = Date.parse(semanticTripTime(right) || "");
249257
return Number.isFinite(leftTime) && Number.isFinite(rightTime) && Math.abs(leftTime - rightTime) <= 90 * 1000;
250258
};
251259
const uniquePerSource = (trips) => trips.reduce((result, trip) => {
@@ -274,10 +282,10 @@ class CodexStellantisTripHistoryCardV4 extends LitElement {
274282
Math.abs(nativeDuration - localDuration) <= 120;
275283
});
276284
const uniqueTrips = [...localTrips, ...nativeTrips.filter((trip) => !isNativeDuplicateOfLocal(trip))]
277-
.sort((a, b) => new Date(a.last_updated).getTime() - new Date(b.last_updated).getTime());
285+
.sort((a, b) => new Date(semanticTripTime(a) || 0).getTime() - new Date(semanticTripTime(b) || 0).getTime());
278286
const normalizedTrips = uniqueTrips
279287
.map((trip) => {
280-
const tripTime = new Date(trip.last_updated).getTime();
288+
const tripTime = new Date(semanticTripTime(trip) || 0).getTime();
281289
const tripDistance = Number.parseFloat(trip.state);
282290
const result = energyResults
283291
.map((energy) => ({
@@ -323,15 +331,18 @@ class CodexStellantisTripHistoryCardV4 extends LitElement {
323331
}
324332

325333
_formatDate(value) {
326-
return new Date(value).toLocaleString(this._locale(), { dateStyle: "short", timeStyle: "short" });
334+
const timestamp = Date.parse(value || "");
335+
return Number.isFinite(timestamp)
336+
? new Date(timestamp).toLocaleString(this._locale(), { dateStyle: "short", timeStyle: "short" })
337+
: "—";
327338
}
328339

329340
_filterServerTrips(trips) {
330341
const days = Number(this._filterDays);
331342
const cutoff = Number.isFinite(days) && days > 0 ? Date.now() - days * 86400000 : null;
332343
return trips.filter((trip) => {
333344
const distance = Number(trip.distance_km);
334-
const timestamp = Date.parse(trip.end_time || trip.start_time || "");
345+
const timestamp = Date.parse(tripFilterTime(trip) || "");
335346
if (cutoff && (!Number.isFinite(timestamp) || timestamp < cutoff)) return false;
336347
if (!this._showZeroEvents && distance === 0) return false;
337348
if (this._hideShortTrips && distance > 0 && distance <= 1) return false;
@@ -371,15 +382,19 @@ class CodexStellantisTripHistoryCardV4 extends LitElement {
371382
}
372383

373384
_tripKey(trip, index) {
374-
return trip._rowId || `${trip._sourceEntityId ?? "trip"}|${trip.last_updated ?? trip.last_changed}|${trip.state}|${index}`;
385+
return trip._rowId || `${trip._sourceEntityId ?? "trip"}|${semanticTripTime(trip) ?? trip._sortTime ?? ""}|${trip.state}|${index}`;
375386
}
376387

377388
_toggleTrip(key) {
378389
this._expandedTripKey = this._expandedTripKey === key ? undefined : key;
379390
}
380391

392+
_isReconstructedGap(trip) {
393+
return Boolean(trip.attributes?.reconstructed_gap);
394+
}
395+
381396
_isInvalidTrip(trip) {
382-
return trip.attributes?.valid_for_statistics === false;
397+
return !this._isReconstructedGap(trip) && trip.attributes?.valid_for_statistics === false;
383398
}
384399

385400
_formatMileage(value) {
@@ -390,7 +405,11 @@ class CodexStellantisTripHistoryCardV4 extends LitElement {
390405
}
391406

392407
_formatDuration(trip) {
393-
const seconds = Number(trip.attributes?.duration_seconds);
408+
if (this._isReconstructedGap(trip)) return "—";
409+
const rawSeconds = trip.attributes?.duration_seconds;
410+
const seconds = rawSeconds === null || rawSeconds === undefined || rawSeconds === ""
411+
? Number.NaN
412+
: Number(rawSeconds);
394413
const raw = String(trip.attributes?.duration ?? "");
395414
const clock = raw.match(/^(\d+):(\d{2})(?::(\d{2}))?/);
396415
const fromRaw = clock
@@ -412,7 +431,7 @@ class CodexStellantisTripHistoryCardV4 extends LitElement {
412431
}
413432

414433
_formatSpeed(trip) {
415-
if (this._isInvalidTrip(trip)) return "—";
434+
if (this._isReconstructedGap(trip) || this._isInvalidTrip(trip)) return "—";
416435
const numeric = Number.parseFloat(String(trip.attributes?.average_speed ?? trip.attributes?.avg_speed ?? "").replace(",", "."));
417436
return Number.isFinite(numeric)
418437
? `${numeric.toLocaleString(this._locale(), { minimumFractionDigits: 1, maximumFractionDigits: 1 })} km/h`
@@ -471,34 +490,36 @@ class CodexStellantisTripHistoryCardV4 extends LitElement {
471490
<tbody>${trips.map((trip, index) => {
472491
const key = this._tripKey(trip, index);
473492
const expanded = this._expandedTripKey === key;
493+
const reconstructed = this._isReconstructedGap(trip);
474494
const invalid = this._isInvalidTrip(trip);
475-
return html`<tr class="trip-row" tabindex="0" role="button" aria-expanded=${expanded} @click=${() => this._toggleTrip(key)} @keydown=${(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); this._toggleTrip(key); } }}>
476-
<td>${this._formatDate(trip.last_updated ?? trip.last_changed)}</td>
495+
return html`<tr class=${reconstructed ? "trip-row reconstructed" : "trip-row"} tabindex="0" role="button" aria-expanded=${expanded} @click=${() => this._toggleTrip(key)} @keydown=${(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); this._toggleTrip(key); } }}>
496+
<td>${reconstructed ? text.reconstructedGap : this._formatDate(semanticTripTime(trip))}</td>
477497
<td>${this._formatDuration(trip)}</td>
478498
<td>${this._formatDistance(trip)}</td>
479499
<td>${this._formatSpeed(trip)}</td>
480500
${hybridLayout ? html`
481-
<td>${invalid ? "—" : this._value(trip.attributes?.energy_per_100_km)}</td>
482-
<td>${invalid ? "—" : this._value(trip.attributes?.fuel_consumption_l_100km)}</td>
483-
<td><span class="trip-type">${({ ev: "EV", hybrid: "Hybrid", ice: "ICE" })[trip.attributes?.trip_type] || "—"}</span></td>
501+
<td>${invalid || reconstructed ? "—" : this._value(trip.attributes?.energy_per_100_km)}</td>
502+
<td>${invalid || reconstructed ? "—" : this._value(trip.attributes?.fuel_consumption_l_100km)}</td>
503+
<td><span class="trip-type">${reconstructed ? "—" : (({ ev: "EV", hybrid: "Hybrid", ice: "ICE" })[trip.attributes?.trip_type] || "—")}</span></td>
484504
` : html`
485-
${hasEnergy ? html`<td>${this._value(trip.attributes?.energy_kwh)}</td><td>${invalid ? "—" : this._value(trip.attributes?.energy_per_100_km)}</td>` : nothing}
486-
${hasFuel ? html`<td>${invalid ? "—" : this._value(trip.attributes?.fuel_consumption_l_100km)}</td>` : nothing}
487-
${hasMaxSpeed ? html`<td>${invalid ? "—" : this._value(trip.attributes?.max_speed)}</td>` : nothing}
505+
${hasEnergy ? html`<td>${reconstructed ? "—" : this._value(trip.attributes?.energy_kwh)}</td><td>${invalid || reconstructed ? "—" : this._value(trip.attributes?.energy_per_100_km)}</td>` : nothing}
506+
${hasFuel ? html`<td>${invalid || reconstructed ? "—" : this._value(trip.attributes?.fuel_consumption_l_100km)}</td>` : nothing}
507+
${hasMaxSpeed ? html`<td>${invalid || reconstructed ? "—" : this._value(trip.attributes?.max_speed)}</td>` : nothing}
488508
`}
489509
</tr>${expanded ? html`<tr class="trip-details">
490510
<td colspan=${columnCount}>
491511
<div class="trip-details-content">
512+
${reconstructed ? html`<span class="quality-warning">${text.reconstructedGap}</span>` : nothing}
492513
${invalid ? html`<span class="quality-warning">${text.invalidServerTrip}</span>` : nothing}
493-
${showTripType ? html`<span><strong>${dashboardText.powertrain}:</strong> <span class="trip-type">${({ ev: "EV", hybrid: "Hybrid", ice: "ICE" })[trip.attributes?.trip_type] || "—"}</span></span>` : nothing}
514+
${showTripType && !reconstructed ? html`<span><strong>${dashboardText.powertrain}:</strong> <span class="trip-type">${({ ev: "EV", hybrid: "Hybrid", ice: "ICE" })[trip.attributes?.trip_type] || "—"}</span></span>` : nothing}
494515
<span><strong>${text.startMileage}:</strong> ${this._formatMileage(trip.attributes?.start_mileage)}</span>
495516
<span><strong>${text.endMileage}:</strong> ${this._formatMileage(this._endMileage(trip))}</span>
496517
${(trip.attributes?.soc_start !== null && trip.attributes?.soc_start !== undefined) || (trip.attributes?.soc_end !== null && trip.attributes?.soc_end !== undefined) ? html`<span><strong>${text.socStart} / ${text.socEnd}:</strong> ${this._value(trip.attributes?.soc_start)} % → ${this._value(trip.attributes?.soc_end)} %</span>` : nothing}
497518
${trip.attributes?.energy_kwh !== null && trip.attributes?.energy_kwh !== undefined ? html`<span><strong>${text.energy}:</strong> ${this._value(trip.attributes?.energy_kwh)} kWh · ${this._value(trip.attributes?.energy_per_100_km)} kWh/100 km</span>` : nothing}
498519
${(trip.attributes?.fuel_level_start !== null && trip.attributes?.fuel_level_start !== undefined) || (trip.attributes?.fuel_level_end !== null && trip.attributes?.fuel_level_end !== undefined) ? html`<span><strong>${dashboardText.fuel}:</strong> ${this._value(trip.attributes?.fuel_level_start)} % → ${this._value(trip.attributes?.fuel_level_end)} %</span>` : nothing}
499520
${(trip.attributes?.fuel_range_start_km !== null && trip.attributes?.fuel_range_start_km !== undefined) || (trip.attributes?.fuel_range_end_km !== null && trip.attributes?.fuel_range_end_km !== undefined) ? html`<span><strong>${dashboardText.fuelRange}:</strong> ${this._value(trip.attributes?.fuel_range_start_km)} km → ${this._value(trip.attributes?.fuel_range_end_km)} km</span>` : nothing}
500521
${trip.attributes?.fuel_consumption_l !== null && trip.attributes?.fuel_consumption_l !== undefined ? html`<span><strong>${dashboardText.fuelConsumption}:</strong> ${this._value(trip.attributes?.fuel_consumption_l)} l · ${this._value(trip.attributes?.fuel_consumption_l_100km)} l/100 km</span>` : nothing}
501-
${hasMaxSpeed ? html`<span><strong>${text.maximum}:</strong> ${invalid ? "—" : this._value(trip.attributes?.max_speed)} km/h</span>` : nothing}
522+
${hasMaxSpeed ? html`<span><strong>${text.maximum}:</strong> ${invalid || reconstructed ? "—" : this._value(trip.attributes?.max_speed)} km/h</span>` : nothing}
502523
</div>
503524
</td>
504525
</tr>` : nothing}`;
@@ -517,4 +538,4 @@ window.customCards.push({
517538
type: "sv-dashboard-trip-history-card",
518539
name: "SV Dashboard Trip History",
519540
preview: true,
520-
});
541+
});

0 commit comments

Comments
 (0)