Skip to content

Commit ae21cd5

Browse files
authored
Merge pull request #301 from jasonacox-sam/fix/v1r-owner-api-auth
fix: v1r Owner API login — use tesla_auth WebView instead of teslapy browser redirect
2 parents 51b5032 + 47c1035 commit ae21cd5

3 files changed

Lines changed: 81 additions & 44 deletions

File tree

RELEASE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# RELEASE NOTES
22

3+
## v0.15.7 - v1r Owner API Login Fix
4+
5+
* Fix: v1r Owner API registration (`python -m pypowerwall setup -v1r` → option 1) now uses the native `tesla_auth` WebView PKCE flow instead of the broken `teslapy` browser redirect. The `tesla://` custom URL scheme callback is intercepted by the WebView, eliminating the "missing_code" login failure (#300, reported in discussion #299)
6+
* Fix: Cached token lookup in `owner_api_login()` now selects the account matching the requested `email` argument instead of always using the first entry in `.pypowerwall.auth`
7+
* Bump library version to `0.15.7`
8+
39
## v0.15.6 - Reserve Percent Scaling Fix + CLI Redesign
410

511
* Fix: `set_operation()` reserve percent scaling — reverse Tesla App scaling (0–100%) to raw API scale (5–100%) only in TEDAPI v1r mode, avoiding incorrect round-trip values in cloud and FleetAPI modes

pypowerwall/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
from json import JSONDecodeError
9090
from typing import Optional, Union
9191

92-
version_tuple = (0, 15, 6)
92+
version_tuple = (0, 15, 7)
9393
version = __version__ = '%d.%d.%d' % version_tuple
9494
__author__ = 'jasonacox'
9595

pypowerwall/v1r_register.py

Lines changed: 74 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -498,11 +498,11 @@ def step4_register_key(token, energy_site_id, public_key_der, fleet_api_base):
498498

499499
def 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

583614
def main(authpath=""):

0 commit comments

Comments
 (0)