@@ -114,9 +114,13 @@ class Senec extends utils.Adapter {
114114 maxConcurrency : 6 ,
115115 } ) ;
116116
117- this . refreshPromise = null ;
118117 this . currentToken = null ;
119- this . authRetryCount = 0 ;
118+ this . refreshToken = null ;
119+ this . tokenExpiresAt = 0 ;
120+
121+ this . timerTokenRefresh = null ;
122+ this . tokenFailureCount = 0 ;
123+ this . refreshPromise = null ;
120124
121125 this . apiPollRunning = false ;
122126 this . lastHeavyUpdate = 0 ;
@@ -137,6 +141,7 @@ class Senec extends utils.Adapter {
137141
138142 try {
139143 await this . checkConfig ( ) ;
144+
140145 if ( this . config . lala_use ) {
141146 this . log . info ( "Usage of lala.cgi (local) configured." ) ;
142147 await this . initPollSettings ( ) ;
@@ -148,22 +153,25 @@ class Senec extends utils.Adapter {
148153 } else {
149154 this . log . warn ( "Usage of lala.cgi (local) not configured. Only polling SENEC App API if configured." ) ;
150155 }
156+
151157 if ( this . config . api_use ) {
152158 this . log . info ( "Usage of SENEC App API configured." ) ;
153- apiConnected = await this . senecLogin ( ) ;
159+ apiConnected = await this . startTokenManager ( ) ;
154160 if ( apiConnected != null ) {
155- await this . pollSenecApi ( 0 ) ;
161+ await this . pollSenecApi ( ) ;
156162 }
157163 } else {
158164 this . log . warn (
159165 "Usage of SENEC App API not configured. Only polling appliance via local network if configured." ,
160166 ) ;
161167 }
168+
162169 if ( lalaConnected || apiConnected ) {
163170 this . setState ( "info.connection" , true , true ) ;
164171 } else {
165172 this . log . error ( "Neither local connection nor API connection configured. Please check config!" ) ;
166173 }
174+
167175 if ( this . config . control_active ) {
168176 this . log . info ( "Active appliance control (local) activated!" ) ;
169177 await this . subscribeStatesAsync ( "control.*" ) ; // subscribe on all state changes in control.
@@ -262,6 +270,9 @@ class Senec extends utils.Adapter {
262270 if ( this . timerAPI ) {
263271 clearTimeout ( this . timerAPI ) ;
264272 }
273+ if ( this . timerTokenRefresh ) {
274+ clearTimeout ( this . timerTokenRefresh ) ;
275+ }
265276 this . log . info ( "cleaned everything up..." ) ;
266277 this . setState ( "info.connection" , false , true ) ;
267278 callback ( ) ;
@@ -489,6 +500,26 @@ class Senec extends utils.Adapter {
489500 }
490501 }
491502
503+ async startTokenManager ( ) {
504+ try {
505+ // No refresh token at all → full login
506+ if ( ! this . refreshToken ) {
507+ this . log . info ( "🔐 No refresh token present. Performing full login..." ) ;
508+ const token = await this . senecLogin ( ) ;
509+ return ! ! token ;
510+ }
511+
512+ // We have a refresh token → try refresh
513+ this . log . info ( "🔐 Trying initial token refresh..." ) ;
514+ await this . refreshTokenSingleFlight ( ) ;
515+ return true ;
516+ } catch ( error ) {
517+ this . log . warn ( `⚠️ Initial refresh failed. Falling back to full login... ${ error . message } ` ) ;
518+ const token = await this . senecLogin ( ) ;
519+ return ! ! token ;
520+ }
521+ }
522+
492523 async senecLogin ( ) {
493524 this . log . info ( "🔄 Start Senec API Login Flow..." ) ;
494525 jar = new CookieJar ( ) ;
@@ -508,7 +539,7 @@ class Senec extends utils.Adapter {
508539 const pageRes = await api_client . get ( `${ CONFIG . authUrl } ?${ authParams } ` , { jar } ) ;
509540 let actionUrl = extractFormAction ( pageRes . data ) ;
510541 if ( ! actionUrl ) {
511- throw new Error ( "Login-Formular URL nicht gefunden ." ) ;
542+ throw new Error ( "Login-Form URL not found ." ) ;
512543 }
513544
514545 const postForm = ( url , data ) =>
@@ -553,8 +584,8 @@ class Senec extends utils.Adapter {
553584 if ( ! redirectLocation ) {
554585 throw new Error (
555586 loginRes . status === 200
556- ? "Login fehlgeschlagen (Kein Redirect) ."
557- : `Login unerwarteter Status : ${ loginRes . status } ` ,
587+ ? "Login failed: no redirect ."
588+ : `Login unexpected State : ${ loginRes . status } ` ,
558589 ) ;
559590 }
560591
@@ -578,41 +609,113 @@ class Senec extends utils.Adapter {
578609 ) ;
579610
580611 this . currentToken = tokenRes . data . access_token ;
581- this . log . info ( "✅ API Login erfolgreich." ) ;
612+ this . refreshToken = tokenRes . data . refresh_token ;
613+ const expiresIn = tokenRes . data . expires_in || 600 ; // fallback 10 min
614+ this . tokenExpiresAt = Date . now ( ) + expiresIn * 1000 ;
615+
616+ this . log . info ( "✅ API Login successful." ) ;
617+ this . scheduleTokenRefresh ( ) ;
582618 return this . currentToken ;
583619 } catch ( e ) {
584620 this . log . error ( `❌ Login Error: ${ e . message } ` ) ;
585621 return null ;
586622 }
587623 }
588624
625+ scheduleTokenRefresh ( ) {
626+ if ( ! this . tokenExpiresAt || unloaded ) {
627+ return ;
628+ }
629+
630+ const safetyMargin = 60 * 1000 ; // refresh 60s before expiry
631+ const now = Date . now ( ) ;
632+
633+ let delay = this . tokenExpiresAt - now - safetyMargin ;
634+ if ( delay < 5000 ) {
635+ delay = 5000 ; // minimum 5s
636+ }
637+
638+ if ( this . timerTokenRefresh ) {
639+ clearTimeout ( this . timerTokenRefresh ) ;
640+ }
641+
642+ if ( ! unloaded ) {
643+ this . log . debug ( `🔐 Scheduling token refresh in ${ ( delay / 1000 ) . toFixed ( 0 ) } s` ) ;
644+ this . timerTokenRefresh = setTimeout ( ( ) => {
645+ this . refreshTokenSingleFlight ( ) . catch ( ( err ) => {
646+ this . log . error ( `Token background refresh failed: ${ err . message } ` ) ;
647+ } ) ;
648+ } , delay ) ;
649+ }
650+ }
651+
589652 async refreshTokenSingleFlight ( ) {
653+ if ( ! this . refreshToken ) {
654+ this . log . debug ( "No refresh token available — skipping refresh." ) ;
655+ return this . senecLogin ( ) ;
656+ }
657+
590658 if ( this . refreshPromise ) {
591- this . log . debug ( "🔐 Waiting for ongoing token refresh..." ) ;
592659 return this . refreshPromise ;
593660 }
594661
595662 this . refreshPromise = ( async ( ) => {
596663 try {
597- this . log . info ( "🔐 Refreshing token..." ) ;
664+ this . log . debug ( "🔐 Refreshing API token..." ) ;
665+
666+ const response = await api_client . post (
667+ CONFIG . tokenUrl ,
668+ new URLSearchParams ( {
669+ grant_type : "refresh_token" ,
670+ client_id : CONFIG . clientId ,
671+ refresh_token : this . refreshToken ,
672+ } ) ,
673+ { headers : { "Content-Type" : "application/x-www-form-urlencoded" } } ,
674+ ) ;
598675
599- const newToken = await this . senecLogin ( ) ;
600- if ( ! newToken ) {
601- throw new Error ( "Token refresh failed" ) ;
602- }
676+ const data = response . data ;
677+
678+ this . currentToken = data . access_token ;
679+ this . refreshToken = data . refresh_token || this . refreshToken ;
680+
681+ const expiresIn = data . expires_in || 600 ; // fallback 10min
682+ this . tokenExpiresAt = Date . now ( ) + expiresIn * 1000 ;
603683
604- this . currentToken = newToken ;
605- this . authRetryCount = 0 ;
684+ this . tokenFailureCount = 0 ;
606685
607- return newToken ;
686+ this . log . info ( `✅ Token refreshed. Expires in ${ expiresIn } s` ) ;
687+
688+ this . scheduleTokenRefresh ( ) ;
608689 } catch ( err ) {
609- this . authRetryCount ++ ;
690+ const status = err . response ?. status ;
691+ const errorCode = err . response ?. data ?. error ;
692+
693+ this . log . error ( `❌ Token refresh failed: ${ err . message } ` ) ;
694+
695+ // If refresh token invalid → fallback to full login
696+ if ( errorCode === "invalid_grant" || status === 400 ) {
697+ this . log . warn ( "⚠️ Refresh token invalid. Performing full login." ) ;
698+ await this . senecLogin ( ) ;
699+ return ;
700+ }
701+
702+ this . tokenFailureCount ++ ;
610703
611- const delay = Math . min ( 60000 , 2000 * Math . pow ( 2 , this . authRetryCount ) ) ;
704+ const baseDelay = 5000 ;
705+ let retryDelay = computeBackoffDelay ( baseDelay , this . tokenFailureCount ) ;
706+ retryDelay = Math . min ( retryDelay , 60000 ) ;
612707
613- this . log . error ( `🔐 Token refresh failed. Backing off ${ delay / 1000 } s` ) ;
708+ if ( this . timerTokenRefresh ) {
709+ clearTimeout ( this . timerTokenRefresh ) ;
710+ }
711+
712+ if ( ! unloaded ) {
713+ this . log . warn ( `🔁 Retrying token refresh in ${ ( retryDelay / 1000 ) . toFixed ( 0 ) } s` ) ;
714+ this . timerTokenRefresh = setTimeout ( ( ) => {
715+ this . refreshTokenSingleFlight ( ) . catch ( ( ) => { } ) ;
716+ } , retryDelay ) ;
717+ }
614718
615- await new Promise ( ( res ) => setTimeout ( res , delay ) ) ;
616719 throw err ;
617720 } finally {
618721 this . refreshPromise = null ;
@@ -755,6 +858,12 @@ class Senec extends utils.Adapter {
755858 */
756859 async apiGet ( url , config = { } ) {
757860 return this . apiQueue . add ( async ( ) => {
861+ // Proactive expiry check
862+ if ( Date . now ( ) >= this . tokenExpiresAt - 30000 ) {
863+ this . log . debug ( "🔐 Token close to expiry. Refreshing before request..." ) ;
864+ await this . refreshTokenSingleFlight ( ) ;
865+ }
866+
758867 const maxAttempts = 3 ;
759868 for ( let attempt = 0 ; attempt < maxAttempts ; attempt ++ ) {
760869 try {
@@ -1091,10 +1200,12 @@ class Senec extends utils.Adapter {
10911200 await this . evalPoll ( obj , "" , "" ) ;
10921201
10931202 retry = 0 ;
1094- if ( unloaded ) {
1095- return ;
1203+ if ( ! unloaded ) {
1204+ this . timer = setTimeout ( ( ) => this . pollSenecLocal ( isHighPrio , retry ) , interval ) ;
1205+ this . log . debug (
1206+ `⏱ Next local poll (highPrio=${ isHighPrio } ) scheduled in ${ ( interval / 1000 ) . toFixed ( 0 ) } s` ,
1207+ ) ;
10961208 }
1097- this . timer = setTimeout ( ( ) => this . pollSenecLocal ( isHighPrio , retry ) , interval ) ;
10981209 } catch ( error ) {
10991210 if ( retry == this . config . retries && this . config . retries < 999 ) {
11001211 this . log . error (
@@ -1112,10 +1223,13 @@ class Senec extends utils.Adapter {
11121223 error
11131224 } )`,
11141225 ) ;
1115- this . timer = setTimeout (
1116- ( ) => this . pollSenecLocal ( isHighPrio , retry ) ,
1117- interval * this . config . retrymultiplier * retry ,
1118- ) ;
1226+ if ( ! unloaded ) {
1227+ const delay = interval * this . config . retrymultiplier * retry ;
1228+ this . timer = setTimeout ( ( ) => this . pollSenecLocal ( isHighPrio , retry ) , delay ) ;
1229+ this . log . debug (
1230+ `⏱ Next local poll (highPrio=${ isHighPrio } ) scheduled in ${ ( delay / 1000 ) . toFixed ( 0 ) } s` ,
1231+ ) ;
1232+ }
11191233 }
11201234 }
11211235 }
0 commit comments