From ebc32c9ac2eb1928316b4356791b0f9362b489f1 Mon Sep 17 00:00:00 2001 From: Greg Glockner Date: Wed, 10 Feb 2021 19:34:11 -0800 Subject: [PATCH 01/10] Initial requests Rough version with Requests --- CHANGES.md | 3 + LICENSE | 33 +++++----- README.md | 17 ++--- teslajson.py | 181 +++++++++++++++++++++++++-------------------------- 4 files changed, 119 insertions(+), 115 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fc6bca0..fc69272 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,8 @@ # Changelog +## Version 2.0 +- Rewritten with Requests library + ## Version 1.3 - Removed API and baseurl from constructor; added proxy support diff --git a/LICENSE b/LICENSE index 2d7419e..af08599 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,22 @@ MIT License -Copyright (c) 2016-2018 Greg Glockner +Copyright (c) 2016-2021 Greg Glockner -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index d2e13dc..47b90f6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Written by Greg Glockner ## Description This is a simple Python interface to the [Tesla JSON -API](http://docs.timdorr.apiary.io/). With this, you can query your +API](https://tesla-api.timdorr.com). With this, you can query your vehicle, control charge settings, turn on the air conditioning, and more. You can also embed this into other programs to automate these controls. @@ -56,12 +56,12 @@ All standard dictionary methods are supported. `Vehicle.data_request(name)`: Retrieve data values specified by _name_, such as _charge\_state_, _climate\_state_, _vehicle\_state_. Returns a dictionary (_dict_). For a full list of _name_ values, see the _GET_ -commands in the [Tesla JSON API](http://docs.timdorr.apiary.io/). +commands in the [Tesla JSON API](https://tesla-api.timdorr.com). `Vehicle.command(name)`: Execute the command specified by _name_, such as _charge\_port\_door\_open_, _charge\_max\_range_. Returns a -dictionary (_dict_). For a full list of _name_ values, see the _POST_ commands -in the [Tesla JSON API](http://docs.timdorr.apiary.io/). +dictionary (_dict_). For a full list of _name_ values, see the _POST_ commands +in the [Tesla JSON API](https://tesla-api.timdorr.com/). ## Example import teslajson @@ -76,8 +76,9 @@ Many thanks to [Tim Dorr](http://timdorr.com) for documenting the Tesla JSON API This would not be possible without his work. ## Disclaimer -This software is provided as-is. This software is not supported by or -endorsed by Tesla Motors. Tesla Motors does not publicly support the -underlying JSON API, so this software may stop working at any time. The -author makes no guarantee to release an updated version to fix any +This software is provided as-is. This software is not supported by or +endorsed by Tesla. It was developed via reverse-engineering of an +unpublished JSON API. Tesla does not publicly support the underlying +JSON API, so this software may stop working at any time. The author +makes no guarantee to release an updated version to fix any incompatibilities. diff --git a/teslajson.py b/teslajson.py index ec4284a..9979782 100644 --- a/teslajson.py +++ b/teslajson.py @@ -2,7 +2,7 @@ https://github.com/gglockner/teslajson The Tesla JSON API is described at: -http://docs.timdorr.apiary.io/ +https://tesla-api.timdorr.com Example: @@ -14,27 +14,20 @@ v.command('charge_start') """ -try: # Python 3 - from urllib.parse import urlencode - from urllib.request import Request, build_opener - from urllib.request import ProxyHandler, HTTPBasicAuthHandler, HTTPHandler -except: # Python 2 - from urllib import urlencode - from urllib2 import Request, build_opener - from urllib2 import ProxyHandler, HTTPBasicAuthHandler, HTTPHandler -import json -import datetime -import calendar +import requests_oauthlib +import string +import random +import base64 +import hashlib +import re +from oauthlib.oauth2 import BackendApplicationClient -class Connection(object): + +class Connection(requests_oauthlib.OAuth2Session): """Connection to Tesla Motors API""" def __init__(self, email='', - password='', - access_token='', - proxy_url = '', - proxy_user = '', - proxy_password = ''): + password=''): """Initialize connection object Sets the vehicles field, a list of Vehicle objects @@ -43,82 +36,88 @@ def __init__(self, Required parameters: email: your login for teslamotors.com password: your password for teslamotors.com - - Optional parameters: - access_token: API access token - proxy_url: URL for proxy server - proxy_user: username for proxy server - proxy_password: password for proxy server """ - self.proxy_url = proxy_url - self.proxy_user = proxy_user - self.proxy_password = proxy_password - tesla_client = self.__open("/raw/0a8e0xTJ", baseurl="http://pastebin.com") - current_client = tesla_client['v1'] - self.baseurl = current_client['baseurl'] - if not self.baseurl.startswith('https:') or not self.baseurl.endswith(('.teslamotors.com','.tesla.com')): - raise IOError("Unexpected URL (%s) from pastebin" % self.baseurl) - self.api = current_client['api'] - if access_token: - self.__sethead(access_token) - else: - self.oauth = { - "grant_type" : "password", - "client_id" : current_client['id'], - "client_secret" : current_client['secret'], - "email" : email, - "password" : password } - self.expiration = 0 # force refresh - self.vehicles = [Vehicle(v, self) for v in self.get('vehicles')['response']] - - def get(self, command): - """Utility command to get data from API""" - return self.post(command, None) + + self.auth_uri = "https://auth.tesla.com" + self.base_uri = "https://owner-api.teslamotors.com" + self.data_uri = self.base_uri + "/api/1/" + + self.get_sso_token(email, password) + self.fetch_token() + + # Get vehicles + self.vehicles = [Vehicle(v, self) for v in self.getdata('vehicles')['response']] + + + def get_sso_token(self, email, password): + redirect_uri = self.auth_uri + "/void/callback" + + # Step 1: Obtain the login page + auth_sess = requests_oauthlib.OAuth2Session( + redirect_uri = redirect_uri, + client_id='ownerapi') + self.__randchars = string.ascii_lowercase+string.digits + code_verifier = self.__randstr(86) + hexdigest = hashlib.sha256(code_verifier.encode('utf-8')).hexdigest() + code_challenge = base64.urlsafe_b64encode(hexdigest.encode('utf-8')).decode('utf-8') + login_uri = self.auth_uri+'/oauth2/v3/authorize' + auth_sess.params = { + 'client_id': 'ownerapi', + 'code_challenge': code_challenge, + 'code_challenge_method': 'S256', + 'redirect_uri': redirect_uri, + 'response_type': 'code', + 'scope': 'openid email offline_access', + 'state': self.__randstr(24) } + r = auth_sess.get(login_uri) + r.raise_for_status() + login_data = dict(re.findall( + ' self.expiration: - auth = self.__open("/oauth/token", data=self.oauth) - self.__sethead(auth['access_token'], - auth['created_at'] + auth['expires_in'] - 86400) - return self.__open("%s%s" % (self.api, command), headers=self.head, data=data) + # TODO: refresh_token - def __sethead(self, access_token, expiration=float('inf')): - """Set HTTP header""" - self.access_token = access_token - self.expiration = expiration - self.head = {"Authorization": "Bearer %s" % access_token} + def __randstr(self, n): + return ''.join(random.choice(self.__randchars) for i in range(n)) - def __open(self, url, headers={}, data=None, baseurl=""): - """Raw urlopen command""" - if not baseurl: - baseurl = self.baseurl - req = Request("%s%s" % (baseurl, url), headers=headers) - try: - req.data = urlencode(data).encode('utf-8') # Python 3 - except: - try: - req.add_data(urlencode(data)) # Python 2 - except: - pass + def getdata(self, command): + """Utility command to get data from API""" + r = self.get(self.data_uri + command) + r.raise_for_status() + return r.json() - # Proxy support - if self.proxy_url: - if self.proxy_user: - proxy = ProxyHandler({'https': 'https://%s:%s@%s' % (self.proxy_user, - self.proxy_password, - self.proxy_url)}) - auth = HTTPBasicAuthHandler() - opener = build_opener(proxy, auth, HTTPHandler) - else: - handler = ProxyHandler({'https': self.proxy_url}) - opener = build_opener(handler) - else: - opener = build_opener() - resp = opener.open(req) - charset = resp.info().get('charset', 'utf-8') - return json.loads(resp.read().decode(charset)) + def postdata(self, command, data={}): + """Utility command to post data to API""" + r = self.post(self.data_uri + command, data) + r.raise_for_status() + return r.json()['response'] class Vehicle(dict): @@ -151,8 +150,8 @@ def command(self, name, data={}): def get(self, command): """Utility command to get data from API""" - return self.connection.get('vehicles/%i/%s' % (self['id'], command)) + return self.connection.getdata('vehicles/%i/%s' % (self['id'], command)) def post(self, command, data={}): """Utility command to post data to API""" - return self.connection.post('vehicles/%i/%s' % (self['id'], command), data) + return self.connection.postdata('vehicles/%i/%s' % (self['id'], command), data) From 34b04da4d1487e48189d0e32ea65d1e4c9d52038 Mon Sep 17 00:00:00 2001 From: Greg Glockner Date: Wed, 10 Feb 2021 20:36:12 -0800 Subject: [PATCH 02/10] Update teslajson.py Refactor with objects for both kinds of sessions --- teslajson.py | 79 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/teslajson.py b/teslajson.py index 9979782..8768831 100644 --- a/teslajson.py +++ b/teslajson.py @@ -20,14 +20,15 @@ import base64 import hashlib import re -from oauthlib.oauth2 import BackendApplicationClient +import oauthlib.oauth2 -class Connection(requests_oauthlib.OAuth2Session): +class Connection(object): """Connection to Tesla Motors API""" def __init__(self, email='', - password=''): + password='', + **kwargs): """Initialize connection object Sets the vehicles field, a list of Vehicle objects @@ -38,30 +39,33 @@ def __init__(self, password: your password for teslamotors.com """ - self.auth_uri = "https://auth.tesla.com" - self.base_uri = "https://owner-api.teslamotors.com" - self.data_uri = self.base_uri + "/api/1/" + self.email = email + self.password = password + self.sso_uri = "https://auth.tesla.com" + self.api_uri = "https://owner-api.teslamotors.com" + self.data_uri = self.api_uri + "/api/1/" + self.api_client_id = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384" + self.api_client_secret = "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3" - self.get_sso_token(email, password) - self.fetch_token() + self.fetch_token(**kwargs) # Get vehicles self.vehicles = [Vehicle(v, self) for v in self.getdata('vehicles')['response']] - - def get_sso_token(self, email, password): - redirect_uri = self.auth_uri + "/void/callback" + def fetch_sso_token(self, **kwargs): + redirect_uri = self.sso_uri + "/void/callback" # Step 1: Obtain the login page - auth_sess = requests_oauthlib.OAuth2Session( + self.sso_session = requests_oauthlib.OAuth2Session( redirect_uri = redirect_uri, - client_id='ownerapi') + client_id='ownerapi', + **kwargs) self.__randchars = string.ascii_lowercase+string.digits code_verifier = self.__randstr(86) hexdigest = hashlib.sha256(code_verifier.encode('utf-8')).hexdigest() code_challenge = base64.urlsafe_b64encode(hexdigest.encode('utf-8')).decode('utf-8') - login_uri = self.auth_uri+'/oauth2/v3/authorize' - auth_sess.params = { + login_uri = self.sso_uri+'/oauth2/v3/authorize' + self.sso_session.params = { 'client_id': 'ownerapi', 'code_challenge': code_challenge, 'code_challenge_method': 'S256', @@ -69,53 +73,62 @@ def get_sso_token(self, email, password): 'response_type': 'code', 'scope': 'openid email offline_access', 'state': self.__randstr(24) } - r = auth_sess.get(login_uri) + r = self.sso_session.get(login_uri) r.raise_for_status() login_data = dict(re.findall( ' Date: Wed, 10 Feb 2021 20:38:25 -0800 Subject: [PATCH 03/10] Update README.md Fixed instructions --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 47b90f6..e654b33 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,7 @@ Required parameters: Optional parameters: -- _access\_token_: the session access token -- _proxy\_url_: URL for proxy server -- _proxy\_user_: username for proxy server -- _proxy\_password_: password for proxy server +- _\*\*kwargs_: arguments passed to the Requests objects `Connection.vehicles`: A list of Vehicle objects, corresponding to the From 5eb63ed3a5633ac7bbbbb5c2c9d1a2a232c58a79 Mon Sep 17 00:00:00 2001 From: Greg Glockner Date: Thu, 18 Feb 2021 20:01:52 -0800 Subject: [PATCH 04/10] Support for MFA --- teslajson.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/teslajson.py b/teslajson.py index 8768831..ccb8593 100644 --- a/teslajson.py +++ b/teslajson.py @@ -28,6 +28,8 @@ class Connection(object): def __init__(self, email='', password='', + mfa='', + mfa_id='', **kwargs): """Initialize connection object @@ -37,13 +39,20 @@ def __init__(self, Required parameters: email: your login for teslamotors.com password: your password for teslamotors.com + + Optional parameters: + mfa: multifactor passcode + mfa_id: multifactor id (if you have multiple MFA devices) """ self.email = email self.password = password + self.mfa = mfa + self.mfa_id = mfa_id self.sso_uri = "https://auth.tesla.com" self.api_uri = "https://owner-api.teslamotors.com" self.data_uri = self.api_uri + "/api/1/" + self.oauth_uri = self.sso_uri + "/oauth2/v3/" self.api_client_id = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384" self.api_client_secret = "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3" @@ -64,7 +73,7 @@ def fetch_sso_token(self, **kwargs): code_verifier = self.__randstr(86) hexdigest = hashlib.sha256(code_verifier.encode('utf-8')).hexdigest() code_challenge = base64.urlsafe_b64encode(hexdigest.encode('utf-8')).decode('utf-8') - login_uri = self.sso_uri+'/oauth2/v3/authorize' + login_uri = self.oauth_uri+"authorize" self.sso_session.params = { 'client_id': 'ownerapi', 'code_challenge': code_challenge, @@ -83,12 +92,35 @@ def fetch_sso_token(self, **kwargs): login_data['credential'] = self.password r = self.sso_session.post(login_uri, data=login_data, allow_redirects=False) r.raise_for_status() + + # Handle MFA + if (re.search('passcode', r.text)): + if not self.mfa: + raise RuntimeError('A MFA passcode is required') + mfa_data = {'transaction_id': login_data['transaction_id'], + 'passcode': str(self.mfa)} + if not self.mfa_id: + r = self.sso_session.get(self.oauth_uri+"authorize/mfa/factors", + params=mfa_data) + r.raise_for_status() + self.mfa_id = r.json()['data'][0]['id'] + mfa_data['factor_id'] = self.mfa_id + r = self.sso_session.post(self.oauth_uri+"authorize/mfa/verify", + json=mfa_data) + r.raise_for_status() + if not r.json()['data']['valid']: + raise RuntimeError('Invalid MFA passcode') + r = self.sso_session.post(login_uri, + data={'transaction_id': login_data['transaction_id']}, + allow_redirects=False) + r.raise_for_status() + m = re.search('code=([^&]*)',r.headers['location']) authorization_code = m.group(1) # Step 3: Exchange authorization code for bearer token self.sso_session.params = None - self.sso_session.token_url = self.sso_uri+'/oauth2/v3/token' + self.sso_session.token_url = self.oauth_uri+"token" self.sso_session.fetch_token( self.sso_session.token_url, code=authorization_code, code_verifier=code_verifier, From 42e8ddd9441c7874107e80663bf2eee82613526c Mon Sep 17 00:00:00 2001 From: Greg Glockner Date: Thu, 18 Feb 2021 20:07:26 -0800 Subject: [PATCH 05/10] Cleanup of MFA --- README.md | 3 ++- teslajson.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e654b33..672ae71 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ Required parameters: - _password_: your password for teslamotors.com Optional parameters: - +- _mfa_: your multi-factor authentication code, if enabled on your account +- _mfa\_id_: if you have multiple MFA devices, the UUID for the MFA device you want to use - _\*\*kwargs_: arguments passed to the Requests objects diff --git a/teslajson.py b/teslajson.py index ccb8593..53864a5 100644 --- a/teslajson.py +++ b/teslajson.py @@ -98,7 +98,7 @@ def fetch_sso_token(self, **kwargs): if not self.mfa: raise RuntimeError('A MFA passcode is required') mfa_data = {'transaction_id': login_data['transaction_id'], - 'passcode': str(self.mfa)} + 'passcode': self.mfa} if not self.mfa_id: r = self.sso_session.get(self.oauth_uri+"authorize/mfa/factors", params=mfa_data) From f8e4868efdc12dbb2d8775939da3e95a57cb7247 Mon Sep 17 00:00:00 2001 From: Greg Glockner Date: Thu, 18 Feb 2021 20:22:25 -0800 Subject: [PATCH 06/10] Update teslajson.py Improved security on MFA passcodes --- teslajson.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/teslajson.py b/teslajson.py index 53864a5..54ad38e 100644 --- a/teslajson.py +++ b/teslajson.py @@ -96,14 +96,14 @@ def fetch_sso_token(self, **kwargs): # Handle MFA if (re.search('passcode', r.text)): if not self.mfa: - raise RuntimeError('A MFA passcode is required') - mfa_data = {'transaction_id': login_data['transaction_id'], - 'passcode': self.mfa} + raise RuntimeError('MFA passcode is required') + mfa_data = {'transaction_id': login_data['transaction_id']} if not self.mfa_id: r = self.sso_session.get(self.oauth_uri+"authorize/mfa/factors", params=mfa_data) r.raise_for_status() self.mfa_id = r.json()['data'][0]['id'] + mfa_data['passcode'] = self.mfa mfa_data['factor_id'] = self.mfa_id r = self.sso_session.post(self.oauth_uri+"authorize/mfa/verify", json=mfa_data) From bc504a4b51af3cda9998f9561119baf521819789 Mon Sep 17 00:00:00 2001 From: Greg Glockner Date: Fri, 19 Feb 2021 12:04:17 -0800 Subject: [PATCH 07/10] Update teslajson.py Refactor to a single request method --- teslajson.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/teslajson.py b/teslajson.py index 54ad38e..4de730e 100644 --- a/teslajson.py +++ b/teslajson.py @@ -59,7 +59,7 @@ def __init__(self, self.fetch_token(**kwargs) # Get vehicles - self.vehicles = [Vehicle(v, self) for v in self.getdata('vehicles')['response']] + self.vehicles = [Vehicle(v, self) for v in self.request('GET', 'vehicles')] def fetch_sso_token(self, **kwargs): redirect_uri = self.sso_uri + "/void/callback" @@ -152,15 +152,13 @@ def refresh_token(self): def __randstr(self, n): return ''.join(random.choice(self.__randchars) for i in range(n)) - def getdata(self, command): - """Utility command to get data from API""" - r = self.api_session.get(self.data_uri + command) - r.raise_for_status() - return r.json() - - def postdata(self, command, data={}): - """Utility command to post data to API""" - r = self.api_session.post(self.data_uri + command, data) + def request(self, method, command, rdata=None): + try: + r = self.api_session.request(method, self.data_uri + command, data=rdata) + except oauthlib.oauth2.TokenExpiredError as e: + # refresh API token + self.api_session.refresh_token(self.api_session.token_url) + return self.requestdata(method, command, rdata) r.raise_for_status() return r.json()['response'] @@ -182,8 +180,7 @@ def __init__(self, data, connection): def data_request(self, name): """Get vehicle data""" - result = self.get('data_request/%s' % name) - return result['response'] + return self.get('data_request/%s' % name) def wake_up(self): """Wake the vehicle""" @@ -195,8 +192,8 @@ def command(self, name, data={}): def get(self, command): """Utility command to get data from API""" - return self.connection.getdata('vehicles/%i/%s' % (self['id'], command)) + return self.connection.request('GET', 'vehicles/%i/%s' % (self['id'], command)) def post(self, command, data={}): """Utility command to post data to API""" - return self.connection.postdata('vehicles/%i/%s' % (self['id'], command), data) + return self.connection.request('POST', 'vehicles/%i/%s' % (self['id'], command), data) From 8c914ff90a19c5cd8ad61585aebb9069869ef506 Mon Sep 17 00:00:00 2001 From: Greg Glockner Date: Fri, 19 Feb 2021 12:23:52 -0800 Subject: [PATCH 08/10] Update teslajson.py Refactor to cleanup --- teslajson.py | 86 ++++++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/teslajson.py b/teslajson.py index 4de730e..7149b45 100644 --- a/teslajson.py +++ b/teslajson.py @@ -47,14 +47,21 @@ def __init__(self, self.email = email self.password = password - self.mfa = mfa - self.mfa_id = mfa_id - self.sso_uri = "https://auth.tesla.com" - self.api_uri = "https://owner-api.teslamotors.com" - self.data_uri = self.api_uri + "/api/1/" - self.oauth_uri = self.sso_uri + "/oauth2/v3/" - self.api_client_id = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384" - self.api_client_secret = "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3" + self.mfa = { + 'passcode': mfa, + 'factor_id': mfa_id + } + self.uri = { + 'sso': "https://auth.tesla.com", + 'api': "https://owner-api.teslamotors.com" + } + self.uri['data'] = self.uri['api'] + "/api/1/" + self.uri['oauth'] = self.uri['sso'] + "/oauth2/v3/" + self.api = { + 'client_id': "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384", + 'client_secret': "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3" + } + self.session = {} self.fetch_token(**kwargs) @@ -62,10 +69,10 @@ def __init__(self, self.vehicles = [Vehicle(v, self) for v in self.request('GET', 'vehicles')] def fetch_sso_token(self, **kwargs): - redirect_uri = self.sso_uri + "/void/callback" + redirect_uri = self.uri['sso'] + "/void/callback" # Step 1: Obtain the login page - self.sso_session = requests_oauthlib.OAuth2Session( + self.session['sso'] = requests_oauthlib.OAuth2Session( redirect_uri = redirect_uri, client_id='ownerapi', **kwargs) @@ -73,8 +80,8 @@ def fetch_sso_token(self, **kwargs): code_verifier = self.__randstr(86) hexdigest = hashlib.sha256(code_verifier.encode('utf-8')).hexdigest() code_challenge = base64.urlsafe_b64encode(hexdigest.encode('utf-8')).decode('utf-8') - login_uri = self.oauth_uri+"authorize" - self.sso_session.params = { + login_uri = self.uri['oauth']+"authorize" + self.session['sso'].params = { 'client_id': 'ownerapi', 'code_challenge': code_challenge, 'code_challenge_method': 'S256', @@ -82,7 +89,7 @@ def fetch_sso_token(self, **kwargs): 'response_type': 'code', 'scope': 'openid email offline_access', 'state': self.__randstr(24) } - r = self.sso_session.get(login_uri) + r = self.session['sso'].get(login_uri) r.raise_for_status() login_data = dict(re.findall( ' Date: Sun, 21 Feb 2021 17:36:01 -0800 Subject: [PATCH 09/10] Packaging Final release packaging --- .gitignore | 2 ++ CHANGES.md | 3 ++- README.md | 12 +++++++----- setup.cfg | 18 ++++++++++++++++++ setup.py | 14 ++------------ teslajson/__init__.py | 1 + teslajson.py => teslajson/teslajson.py | 0 7 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 setup.cfg create mode 100644 teslajson/__init__.py rename teslajson.py => teslajson/teslajson.py (100%) diff --git a/.gitignore b/.gitignore index f74baa1..2f2f0fe 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ build/ teslajson.egg-info/ dist/ MANIFEST +new/* +old/* diff --git a/CHANGES.md b/CHANGES.md index fc69272..948c0c4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,8 @@ # Changelog ## Version 2.0 -- Rewritten with Requests library +- Rewritten with Requests library, in order to support the latest Tesla authentication protocol. +- Packaged via PyPI (pip) ## Version 1.3 - Removed API and baseurl from constructor; added proxy support diff --git a/README.md b/README.md index 672ae71..b9543b7 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,16 @@ methods on a _Vehicle_. There is a single get method that the class does not require changes when there are minor updates to the underlying JSON API. -This has been tested with Python 2.7 and Python 3.5. It has no dependencies -beyond the standard Python libraries. +This has been tested with Python 3.7 and requires the +[Requests-OAuthlib](https://requests-oauthlib.readthedocs.io) library. ## Installation -0. Download the repository zip file and uncompress it -0. Run the following command with your Python interpreter: `python setup.py install` -Alternately, add the teslajson.py code to your program. +Use any of these methods to download and install the teslajson module: + +1. Easiest method: use pip via the command :`pip install teslajson` +2. Download the source code from https://github.com/gglockner/teslajson, then run: `python setup.py install` +3. Download the source code, install requests-oauthlib, then add the file teslajson/teslajson.py to your Python project ## Public API `Connection(email, password, **kwargs)`: diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..7c8ec31 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,18 @@ +[metadata] +name = teslajson +version = 0.0.1 +author = Greg Glockner +author_email = greg.glockner@gmail.com +description = Simple Python class to access the Tesla JSON API +long_description = file: README.md, CHANGES.md +long_description_content_type = text/markdown +url = https://github.com/gglockner/teslajson +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + +[options] +packages = find: +install_requires = + requests_oauthlib diff --git a/setup.py b/setup.py index 347e5dd..b908cbe 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,3 @@ -from setuptools import setup, find_packages -# To use a consistent encoding -from codecs import open -from os import path +import setuptools -setup(name='teslajson', - version='1.3.1', - description='', - url='https://github.com/gglockner/teslajson', - py_modules=['teslajson'], - author='Greg Glockner', - license='MIT', - ) +setuptools.setup() diff --git a/teslajson/__init__.py b/teslajson/__init__.py new file mode 100644 index 0000000..9e39463 --- /dev/null +++ b/teslajson/__init__.py @@ -0,0 +1 @@ +from .teslajson import * diff --git a/teslajson.py b/teslajson/teslajson.py similarity index 100% rename from teslajson.py rename to teslajson/teslajson.py From d94d25aa67d9c140f4c70dbda20035b5332f11e2 Mon Sep 17 00:00:00 2001 From: Greg Glockner Date: Mon, 22 Feb 2021 21:33:39 -0800 Subject: [PATCH 10/10] Final 2.0 release --- CHANGES.md | 2 +- README.md | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 948c0c4..442b364 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ # Changelog ## Version 2.0 -- Rewritten with Requests library, in order to support the latest Tesla authentication protocol. +- Rewritten with Requests library, in order to support the latest Tesla authentication protocol - Packaged via PyPI (pip) ## Version 1.3 diff --git a/README.md b/README.md index b9543b7..ce8bc3b 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Use any of these methods to download and install the teslajson module: 1. Easiest method: use pip via the command :`pip install teslajson` 2. Download the source code from https://github.com/gglockner/teslajson, then run: `python setup.py install` -3. Download the source code, install requests-oauthlib, then add the file teslajson/teslajson.py to your Python project +3. Download the source code, install requests-oauthlib and its dependencies, then add the file teslajson/teslajson.py to your Python project ## Public API `Connection(email, password, **kwargs)`: diff --git a/setup.cfg b/setup.cfg index 7c8ec31..b7ed338 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = teslajson -version = 0.0.1 +version = 2.0.0 author = Greg Glockner author_email = greg.glockner@gmail.com description = Simple Python class to access the Tesla JSON API