@@ -1324,10 +1324,10 @@ class VwWeconnect extends utils.Adapter {
13241324 } ) ;
13251325 } ) ;
13261326 } else if ( this . config . type === "id" ) {
1327+ // Drop-locked timers handle the regular cadence; the manual refresh
1328+ // button just kicks every VIN's timer to fire immediately.
13271329 this . vinArray . forEach ( ( vin ) => {
1328- this . getEuDataActStatus ( vin ) . catch ( ( err ) => {
1329- this . log . error ( "EU Data Act status update failed: " + ( err && err . message ? err . message : err ) ) ;
1330- } ) ;
1330+ this . _scheduleEuDataActPoll ( vin , 0 ) ;
13311331 } ) ;
13321332 return ;
13331333 } else if ( this . config . type === "audietron" ) {
@@ -6727,17 +6727,44 @@ class VwWeconnect extends utils.Adapter {
67276727 return ;
67286728 }
67296729
6730- // initial fetch + arm periodic refresh on the user-configured interval
6730+ // initial fetch + arm adaptive (drop-locked) refresh per VIN. The portal
6731+ // emits a new dataset every ~15 min; we use the listing's createdOn
6732+ // timestamp to schedule the next poll precisely 15 min after the newest
6733+ // drop (plus a small provisioning buffer), instead of a blind setInterval
6734+ // that would drift up to 15 min off the real cadence.
6735+ this . euDataActTimers = this . euDataActTimers || { } ;
67316736 for ( const vin of this . vinArray ) {
6732- await this . getEuDataActStatus ( vin ) . catch ( ( err ) => {
6733- this . log . error ( `EU Data Act status for ${ vin } failed: ${ err . message || err } ` ) ;
6734- } ) ;
6737+ this . _scheduleEuDataActPoll ( vin , 0 ) ;
67356738 }
6739+ }
67366740
6737- this . updateInterval && clearInterval ( this . updateInterval ) ;
6738- this . updateInterval = setInterval ( ( ) => {
6739- this . updateStatus ( ) ;
6740- } , this . config . interval * 60 * 1000 ) ;
6741+ /**
6742+ * Adaptive poll for one VIN: fetch, then schedule the next call based on
6743+ * when the portal is expected to publish the next 15-min dataset.
6744+ */
6745+ _scheduleEuDataActPoll ( vin , delayMs ) {
6746+ if ( ! this . euDataActTimers ) this . euDataActTimers = { } ;
6747+ if ( this . euDataActTimers [ vin ] ) {
6748+ clearTimeout ( this . euDataActTimers [ vin ] ) ;
6749+ }
6750+ this . euDataActTimers [ vin ] = setTimeout ( async ( ) => {
6751+ const next = await this . getEuDataActStatus ( vin )
6752+ . catch ( ( err ) => {
6753+ this . log . error ( `EU Data Act status for ${ vin } failed: ${ err . message || err } ` ) ;
6754+ return null ;
6755+ } ) ;
6756+ // If the fetch didn't return a target time (NO_CONTENT, error,
6757+ // metadata still missing), fall back to a 1-min retry so we pick up
6758+ // the very next drop without waiting a full interval.
6759+ const fallbackMs = 60 * 1000 ;
6760+ const userMaxMs = Math . max ( 60 , ( this . config . interval || 15 ) * 60 ) * 1000 ;
6761+ let nextMs = typeof next === "number" && next > 0 ? next : fallbackMs ;
6762+ // Never poll faster than 30s and never slower than the user-configured
6763+ // ceiling — the latter caps schedule drift if createdOn is way off.
6764+ nextMs = Math . max ( 30 * 1000 , Math . min ( nextMs , userMaxMs ) ) ;
6765+ this . log . debug ( `EU Data Act: ${ vin } next poll in ${ Math . round ( nextMs / 1000 ) } s` ) ;
6766+ this . _scheduleEuDataActPoll ( vin , nextMs ) ;
6767+ } , delayMs ) ;
67416768 }
67426769
67436770 /**
@@ -6757,29 +6784,31 @@ class VwWeconnect extends utils.Adapter {
67576784 common : { name : v . nickname || vin } ,
67586785 native : { } ,
67596786 } ) ;
6760- await this . extendObjectAsync ( vin + ".general" , {
6761- type : "channel" ,
6762- common : { name : "General Information" } ,
6763- native : { } ,
6787+ await this . json2iob . parse ( vin + ".general" , { vin, nickname : v . nickname || "" } , {
6788+ forceIndex : true ,
6789+ channelName : "General Information" ,
67646790 } ) ;
6765- await this . setStateAsync ( vin + ".general.vin" , vin , true ) ;
6766- if ( v . nickname ) {
6767- await this . setStateAsync ( vin + ".general.nickname" , v . nickname , true ) ;
6768- }
67696791 try {
67706792 const meta = await this . euDataAct . getMetadata ( vin ) ;
67716793 if ( meta && meta . Identifier ) {
67726794 this . euDataActIdentifiers [ vin ] = meta . Identifier ;
67736795 this . log . debug (
6774- `EU Data Act: ${ vin } Identifier=${ meta . Identifier } Frequency=${ meta . Frequency || "?" } ` ,
6796+ `EU Data Act: ${ vin } data request - ` +
6797+ `Identifier=${ meta . Identifier } ` +
6798+ `Name=${ meta . Name || "?" } ` +
6799+ `Frequency=${ meta . Frequency || "?" } ` +
6800+ `StartDate=${ meta . StartDate || "?" } ` +
6801+ `EndDate=${ meta . EndDate || "?" } ` +
6802+ `EmailFrequency=${ meta . EmailFrequency || "?" } ` +
6803+ `LastNotificationDate=${ meta . LastNotificationDate || "?" } ` +
6804+ `DataClusters=[${ ( meta . DataClusters || [ ] ) . join ( ", " ) } ]` ,
67756805 ) ;
67766806 } else {
67776807 this . log . warn (
67786808 `EU Data Act: ${ vin } has no Identifier - enable a continuous data request on the portal first` ,
67796809 ) ;
6810+ this . log . debug ( `EU Data Act: ${ vin } metadata raw: ${ JSON . stringify ( meta || { } ) } ` ) ;
67806811 }
6781- // Persist metadata for transparency.
6782- await this . json2iob . parse ( vin + ".datarequest" , meta || { } , { forceIndex : true } ) ;
67836812 } catch ( err ) {
67846813 this . log . warn ( `EU Data Act: metadata fetch failed for ${ vin } : ${ err . message || err } ` ) ;
67856814 }
@@ -6792,7 +6821,7 @@ class VwWeconnect extends utils.Adapter {
67926821 * stored in `<vin>.statuseudata.rawJson` when `config.rawJson` is enabled.
67936822 */
67946823 async getEuDataActStatus ( vin ) {
6795- if ( ! this . euDataAct ) return ;
6824+ if ( ! this . euDataAct ) return null ;
67966825 const identifier = this . euDataActIdentifiers && this . euDataActIdentifiers [ vin ] ;
67976826 if ( ! identifier ) {
67986827 this . log . debug ( `EU Data Act: no Identifier for ${ vin } , retrying metadata` ) ;
@@ -6802,42 +6831,67 @@ class VwWeconnect extends utils.Adapter {
68026831 this . euDataActIdentifiers = this . euDataActIdentifiers || { } ;
68036832 this . euDataActIdentifiers [ vin ] = meta . Identifier ;
68046833 } else {
6805- return ;
6834+ return 60 * 1000 ;
68066835 }
68076836 } catch ( err ) {
68086837 this . log . debug ( `EU Data Act: metadata retry for ${ vin } failed: ${ err . message || err } ` ) ;
6809- return ;
6838+ return 60 * 1000 ;
68106839 }
68116840 }
68126841 try {
6842+ const startedAt = Date . now ( ) ;
68136843 const result = await this . euDataAct . getLatestStatus ( vin , this . euDataActIdentifiers [ vin ] ) ;
6844+ const elapsed = Date . now ( ) - startedAt ;
6845+ const dataPoints = ( result . raw . Data || [ ] ) . length ;
6846+ const normalizedKeys = Object . keys ( result . normalized || { } ) . length ;
68146847 this . log . debug (
6815- `EU Data Act: ${ vin } dataset=${ result . datasetName } (${ result . datasetCreatedOn } ) ` +
6816- `points=${ ( result . raw . Data || [ ] ) . length } ` ,
6848+ `EU Data Act: ${ vin } fetched dataset in ${ elapsed } ms - ` +
6849+ `name=${ result . datasetName } ` +
6850+ `createdOn=${ result . datasetCreatedOn || "?" } ` +
6851+ `rawPoints=${ dataPoints } ` +
6852+ `normalizedKeys=${ normalizedKeys } ` +
6853+ `listSeen=${ result . datasetCount || "?" } /${ result . contentCount || "?" } ` +
6854+ `bytes=${ result . byteSize || "?" } ` +
6855+ `inner=${ result . fileName } ` ,
68176856 ) ;
6818- await this . json2iob . parse ( vin + ".statuseudata" , result . normalized , { forceIndex : true } ) ;
6819- await this . setStateAsync ( vin + ".statuseudata._dataset_name" , result . datasetName , true ) ;
6857+ // json2iob creates the root channel + every leaf state itself; we just
6858+ // tag the dataset metadata into the same object so it gets the same
6859+ // treatment (no manual extendObject/setState dance needed).
6860+ const payload = {
6861+ ...result . normalized ,
6862+ _dataset_name : result . datasetName ,
6863+ } ;
68206864 if ( result . datasetCreatedOn ) {
6821- await this . setStateAsync ( vin + ".statuseudata. _dataset_created_on" , result . datasetCreatedOn , true ) ;
6865+ payload . _dataset_created_on = result . datasetCreatedOn ;
68226866 }
68236867 if ( this . config . rawJson ) {
6824- await this . extendObjectAsync ( vin + ".statuseudata.rawJson" , {
6825- type : "state" ,
6826- common : {
6827- name : "Raw EU Data Act dataset JSON" ,
6828- role : "json" ,
6829- type : "string" ,
6830- read : true ,
6831- write : false ,
6832- } ,
6833- native : { } ,
6834- } ) ;
6835- await this . setStateAsync ( vin + ".statuseudata.rawJson" , JSON . stringify ( result . raw ) , true ) ;
6868+ payload . _raw_json = JSON . stringify ( result . raw ) ;
68366869 }
6870+ await this . json2iob . parse ( vin + ".statuseudata" , payload , {
6871+ forceIndex : true ,
6872+ channelName : "EU Data Act 15-min dataset" ,
6873+ } ) ;
6874+ // Drop-locked rescheduling: the portal publishes one dataset per
6875+ // 15-min slot, timestamped in createdOn. Aim for createdOn + 15min +
6876+ // 45s buffer so we hit the new file as soon as it's provisioned. If
6877+ // that target is already in the past (the slot is overdue), retry in
6878+ // 60s until the new file shows up.
6879+ if ( result . datasetCreatedOn ) {
6880+ const created = Date . parse ( result . datasetCreatedOn ) ;
6881+ if ( ! isNaN ( created ) ) {
6882+ const target = created + 15 * 60 * 1000 + 45 * 1000 ;
6883+ const delta = target - Date . now ( ) ;
6884+ return delta > 30 * 1000 ? delta : 60 * 1000 ;
6885+ }
6886+ }
6887+ return 60 * 1000 ;
68376888 } catch ( err ) {
68386889 if ( err && err . code === "NO_CONTENT" ) {
68396890 this . log . debug ( `EU Data Act: ${ vin } no content datasets yet` ) ;
6840- return ;
6891+ // The portal has nothing yet (fresh setup or car asleep). 1-min
6892+ // retries until the first dataset shows up; once it does we lock
6893+ // onto the 15-min cadence.
6894+ return 60 * 1000 ;
68416895 }
68426896 // The lib already retries once on 401/403 internally; if it still fails
68436897 // here the session is genuinely dead and a manual re-login won't help.
@@ -6851,6 +6905,7 @@ class VwWeconnect extends utils.Adapter {
68516905 if ( this . euDataActIdentifiers ) delete this . euDataActIdentifiers [ vin ] ;
68526906 }
68536907 this . log . error ( `EU Data Act: status fetch failed for ${ vin } : ${ err . message || err } ` ) ;
6908+ return null ;
68546909 }
68556910 }
68566911
@@ -6865,6 +6920,10 @@ class VwWeconnect extends utils.Adapter {
68656920 clearTimeout ( this . refreshTokenTimeout ) ;
68666921 clearTimeout ( this . refreshTimeout ) ;
68676922 clearTimeout ( this . restartTimeout ) ;
6923+ if ( this . euDataActTimers ) {
6924+ for ( const t of Object . values ( this . euDataActTimers ) ) clearTimeout ( t ) ;
6925+ this . euDataActTimers = { } ;
6926+ }
68686927 this . mqttClient && this . mqttClient . end ( ) ;
68696928 callback ( ) ;
68706929 } catch ( e ) {
0 commit comments