11import { 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