@@ -498,11 +498,11 @@ def step4_register_key(token, energy_site_id, public_key_der, fleet_api_base):
498498
499499def owner_api_login (email = None , authpath = "" , force_reauth = False ):
500500 """
501- Authenticate with the Tesla Owner API using teslapy .
501+ Authenticate with the Tesla Owner API using tesla_auth .
502502
503- This is the same login flow as 'python -m pypowerwall setup' (Cloud Mode).
504- No developer app or hosted key is required — just your Tesla account
505- email and password .
503+ Uses the same native WebView PKCE flow as cloud mode setup
504+ ('python -m pypowerwall setup'). The tesla:// callback is intercepted
505+ by the WebView — no browser redirect issues .
506506
507507 Args:
508508 email: Tesla account email (prompted if not provided).
@@ -511,7 +511,7 @@ def owner_api_login(email=None, authpath="", force_reauth=False):
511511
512512 Returns the Bearer access token string on success.
513513 """
514- from pypowerwall .cloud . teslapy import Tesla
514+ from pypowerwall .tesla_auth import login as tesla_login , save_token , _refresh_access_token
515515
516516 authfile = os .path .join (authpath , OWNER_AUTHFILE ) if authpath else OWNER_AUTHFILE
517517
@@ -531,53 +531,84 @@ def owner_api_login(email=None, authpath="", force_reauth=False):
531531 print (" No developer app setup required." )
532532 print ()
533533
534- if not email :
535- while True :
536- email = input (" Tesla account email: " ).strip ()
537- if "@" in email :
538- break
539- print (" Invalid email address, please try again." )
540-
541- tesla = Tesla (email , cache_file = authfile )
542-
543- if tesla .authorized and not force_reauth :
544- print (f" Using cached credentials from { authfile } " )
545- else :
546- # PKCE OAuth flow — mirrors PyPowerwallCloud.setup()
547- state = tesla .new_state ()
548- code_verifier = tesla .new_code_verifier ()
549-
534+ # Check for existing cached credentials
535+ if os .path .exists (authfile ) and not force_reauth :
550536 try :
551- auth_url = tesla .authorization_url (state = state , code_verifier = code_verifier )
537+ with open (authfile ) as f :
538+ cache = json .load (f )
539+ # Select cached account matching the requested email, or
540+ # fall back to the first cached account when no email specified
541+ if email and email in cache :
542+ cached_email = email
543+ elif cache :
544+ cached_email = list (cache .keys ())[0 ]
545+ else :
546+ cached_email = None
547+ if cached_email :
548+ sso = cache [cached_email ].get ("sso" , {})
549+ access_token = sso .get ("access_token" )
550+ refresh_token = sso .get ("refresh_token" )
551+ expires_at = sso .get ("expires_at" , 0 )
552+
553+ # Try to use cached access token if not expired
554+ import time as _time
555+ if access_token and expires_at > _time .time () + 300 :
556+ print (f" Using cached credentials from { authfile } " )
557+ return access_token
558+
559+ # Try refresh token
560+ if refresh_token :
561+ print (f" Cached token expired, refreshing..." )
562+ try :
563+ new_data = _refresh_access_token (refresh_token )
564+ access_token = new_data .get ("access_token" , access_token )
565+ # Update cache
566+ sso .update (new_data )
567+ sso ["expires_at" ] = int (_time .time () + new_data .get ("expires_in" , 28800 ))
568+ cache [cached_email ]["sso" ] = sso
569+ with open (authfile , "w" ) as f :
570+ json .dump (cache , f , indent = 2 )
571+ os .chmod (authfile , 0o600 )
572+ print (f" Token refreshed successfully." )
573+ return access_token
574+ except Exception as e :
575+ print (f" Token refresh failed ({ e } ), requesting new login..." )
552576 except Exception as e :
553- print (f"\n ERROR: Could not generate login URL — { e } " )
554- sys .exit (1 )
577+ print (f" Could not read cached credentials: { e } " )
555578
556- print (" Open this URL in your browser to log in to your Tesla account:" )
557- print ()
558- print (f" { auth_url } " )
559- print ()
560- print (" After logging in, you will be redirected to a 'Page Not Found' page." )
561- print (" Copy the FULL URL from your browser's address bar and paste it below." )
562- print ()
579+ # Native WebView login — same as cloud mode setup
580+ refresh_token , detected_email , token_data = tesla_login (
581+ email = email ,
582+ headless = False ,
583+ debug = False ,
584+ )
563585
564- tesla .close ()
565- tesla = Tesla (email , state = state , code_verifier = code_verifier , cache_file = authfile )
586+ actual_email = detected_email or email
587+ if not actual_email :
588+ actual_email = input (" Tesla account email: " ).strip ()
566589
567- if not tesla .authorized :
568- try :
569- tesla .fetch_token (authorization_response = input (" Paste the redirect URL: " ).strip ())
570- except Exception as e :
571- print (f"\n ERROR: Login failed — { e } " )
572- sys .exit (1 )
590+ # Save to auth file in teslapy-compatible format
591+ if not token_data :
592+ token_data = {"refresh_token" : refresh_token , "token_type" : "Bearer" , "expires_in" : 28800 }
573593
574- print ( f" \n Login successful, credentials cached to { authfile } " )
594+ save_token ( token_data , path = authfile , email = actual_email )
575595
576- token = (tesla .token or {}).get ("access_token" )
577- if not token :
596+ # Read back the access token from the saved file
597+ try :
598+ with open (authfile ) as f :
599+ cache = json .load (f )
600+ access_token = cache [actual_email ]["sso" ]["access_token" ]
601+ except Exception :
602+ # Fallback: refresh the token we just got to get an access token
603+ new_data = _refresh_access_token (refresh_token )
604+ access_token = new_data .get ("access_token" )
605+
606+ if not access_token :
578607 print (" ERROR: Could not retrieve access token." )
579608 sys .exit (1 )
580- return token
609+
610+ print (f"\n Login successful, credentials cached to { authfile } " )
611+ return access_token
581612
582613
583614def main (authpath = "" ):
0 commit comments