mlowijs / tesla_api

Lightweight Python API client for the Tesla API.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Any suggestions?

jackkorber opened this issue · comments

I am getting the error below. Any suggestions?

Traceback (most recent call last):
File "/Users/jackkorber/Desktop/Python/GetPowerwallCapacity/powerwallCapacity.py", line 31, in
asyncio.run(main())
File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/runners.py", line 43, in run
return loop.run_until_complete(main)
File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/base_events.py", line 608, in run_until_complete
return future.result()
File "/Users/jackkorber/Desktop/Python/GetPowerwallCapacity/powerwallCapacity.py", line 12, in main
energy_sites = await client.list_energy_sites()
File "/Users/jackkorber/Desktop/Python/GetPowerwallCapacity/tesla_api/init.py", line 123, in list_energy_sites
for product in await self.get("products") if "energy_site_id" in product]
File "/Users/jackkorber/Desktop/Python/GetPowerwallCapacity/tesla_api/init.py", line 91, in get
await self.authenticate()
File "/Users/jackkorber/Desktop/Python/GetPowerwallCapacity/tesla_api/init.py", line 81, in authenticate
expiry_time = timedelta(seconds=self._token["expires_in"])
KeyError: 'expires_in'
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7fa22a881ac0>
Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x7fa22a8a5520>, 0.69530855)]']
connector: <aiohttp.connector.TCPConnector object at 0x7fa22a881af0>

<ClientResponse(https://owner-api.teslamotors.com/oauth/token) [400 Bad Request]>

In my experience, errors like this come and go. They seem to play with the API from time to time.

I think there is a new problem now though with Captcha being added as discussed here: timdorr/tesla-api#390

I managed to work around the captcha by grabbing a token in Postman using the built in authentication stuff, then popping it in a json file and removing direct user and pass. Works for MFA too as you're just logging in a browser as you would in the app - and the tokens refresh anyway, so should never have to do it again hypothetically.

Settings for Postman that worked:

Client secret:c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3

t1

t2

This is the rest.

The START_TOKEN variable is a json token from .env to get things started (the one manually generated in postman)

It looks like this:

{"resfresh_token": "", "access_token": "", "expires_in": 28800, "created_at": 1622326394.014721}
async def save_token(token):
    open("token_file.json", "w").write(token)

async def load_token():
    try:
        with open('token_file.json', "r") as f:
            return f.read()        
    except IOError:
        await save_token(START_TOKEN)
        return await load_token()    
    


async def getreserve():
    token = await load_token()
    client = TeslaApiClient(token=token, on_new_token=save_token) 

Just create a new request and switch to the authorization tab and you should see it. BTW you don't need an account in Postman!

Capture

Just create a new request and switch to the authorization tab and you should see it. BTW you don't need an account in Postman!

I deleted my previous response out of embarrassment but thank very much for holding my hand. I don't know why I couldn't fumble my way through the UI to find this!

I find it to be unintuitive... could be a better UI .

There is a problem I've come across now that the code does not refresh the tokens properly, gives an invalid_grant error with a grant type of refresh_token. Have not had time to really pull it apart yet. Will create a new issue.

the code does not refresh the tokens properly

well that's unfortunate. I think I maybe saw this when I first wrote my script, but probably just attributed to my lack of understanding/knowledge, and just pass the credentials each time (the script only has 1 user - me!)

The auth token works fine, just when it expires the refresh step fails.

Thanks @jakkaj, your token suggestion is working for me and but I'm also seeing the refresh step failing.
The error I get is:

...
  File ".../.venv/lib/python3.8/site-packages/tesla_api/__init__.py", line 61, in _get_token
    raise AuthenticationError(response_json)
tesla_api.exceptions.AuthenticationError: Authentication to the Tesla API failed: {'error': 'invalid_grant', 'error_description': 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.'}

The "issued to another client" makes me wonder if the OAUTH_CLIENT_ID is the problem. I note that tesla_api uses 81527cff…, but the Postman process uses ownerapi. I tried getting a token using the 81527cff… ID, but that resulted in an error in the authorization popup. I tried changing OAUTH_CLIENT_ID to ownerapi for just the _get_token method, but resulted in the same error response.

I was able to refresh the token!

I used ownerapi for the client id, https://auth.tesla.com/oauth2/v3/token for the URL, and added "scope": "openid email offline_access" to the refresh data. I'm not sure if scope is actually required here. I saw it used in a refresh on another project and haven't tried it without. I then also added created_at to the response_json to support the refresh check.

I can make a proper PR, but for now my changes are below:

diff --git a/tesla_api/__init__.py b/tesla_api/__init__.py
index 6d5ab02..775a18d 100644
--- a/tesla_api/__init__.py
+++ b/tesla_api/__init__.py
@@ -9,9 +9,11 @@ from .exceptions import ApiError, AuthenticationError, VehicleUnavailableError
 from .vehicle import Vehicle
 
 TESLA_API_BASE_URL = "https://owner-api.teslamotors.com/"
-TOKEN_URL = TESLA_API_BASE_URL + "oauth/token"
+TESLA_TOKEN_BASE_URL = "https://auth.tesla.com/"
+TOKEN_URL = TESLA_TOKEN_BASE_URL + "oauth2/v3/token"
 API_URL = TESLA_API_BASE_URL + "api/1"
 
+TOKEN_CLIENT_ID = "ownerapi"
 OAUTH_CLIENT_ID = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384"
 OAUTH_CLIENT_SECRET = "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3"
 
@@ -50,7 +52,7 @@ class TeslaApiClient:
 
     async def _get_token(self, data):
         request_data = {
-            "client_id": OAUTH_CLIENT_ID,
+            "client_id": TOKEN_CLIENT_ID,
             "client_secret": OAUTH_CLIENT_SECRET,
         }
         request_data.update(data)
@@ -60,6 +62,7 @@ class TeslaApiClient:
             if resp.status == 401:
                 raise AuthenticationError(response_json)
 
+        response_json['created_at'] = datetime.now().timestamp()
         # Send token to application via callback.
         if self._new_token_callback:
             asyncio.create_task(self._new_token_callback(json.dumps(response_json)))
@@ -71,7 +74,7 @@ class TeslaApiClient:
         return await self._get_token(data)
 
     async def _refresh_token(self, refresh_token):
-        data = {"grant_type": "refresh_token", "refresh_token": refresh_token}
+        data = {"grant_type": "refresh_token", "refresh_token": refresh_token, "scope": "openid email offline_access"}
         return await self._get_token(data)
 
     async def authenticate(self):

Looks like some of my changes are covered by the v3 PRs that already exist.
#38
#39

This project seems to handle the captcha pretty well, might be useful to lift some of their work:
tdorssers/TeslaPy@22540ab

@jcam Seems like a lot of extra work around to handle Capcha. This really looks like a lot of work when perhaps a one time interactive login is needed + decent token refreshing.

Yeah it is a lot. Probably way more than needed. The built in interactive login (kicks out to system web browser) and token refresh handling seems pretty solid though. Not really suggesting to duplicate all the work, but since it all works it might be a useful resource for getting the flows right.