From b7ea00d7dc4b947387763472967f270bc7623cdb Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 3 Nov 2020 13:34:44 +1000 Subject: [PATCH 1/5] Save token and car polling --- fordpass/demo.py | 23 ++++++++ fordpass/fordpass.py | 127 ++++++++++++++++++++++++++++++++++--------- setup.py | 6 +- 3 files changed, 128 insertions(+), 28 deletions(-) create mode 100644 fordpass/demo.py diff --git a/fordpass/demo.py b/fordpass/demo.py new file mode 100644 index 0000000..c34f53b --- /dev/null +++ b/fordpass/demo.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +""" +Simple script to demo the API +""" + +import sys, os, logging, time +from fordpass import Vehicle + +if __name__ == "__main__": + + if len(sys.argv) < 4: + raise Exception('Must specify Username, Password and VIN as arguments, e.g. demo.py test@test.com password123 WX231231232') + else: + r = Vehicle(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) # Username, Password, VIN + r.requestUpdate() # Poll the car for an update + print(r.status()) # Print the status of the car + + # r.unlock() # Unlock the doors + + # time.sleep(10) # Wait 10 seconds + + # r.lock() # Lock the doors diff --git a/fordpass/fordpass.py b/fordpass/fordpass.py index 02a67e4..0d4c951 100644 --- a/fordpass/fordpass.py +++ b/fordpass/fordpass.py @@ -1,33 +1,37 @@ import requests import logging import time +import json +import os.path defaultHeaders = { 'Accept': '*/*', 'Accept-Language': 'en-us', - 'User-Agent': 'fordpass-na/353 CFNetwork/1121.2.2 Darwin/19.3.0', + 'User-Agent': 'fordpass-ap/93 CFNetwork/1197 Darwin/20.0.0', 'Accept-Encoding': 'gzip, deflate, br', } apiHeaders = { **defaultHeaders, - 'Application-Id': '71A3AD0A-CF46-4CCF-B473-FC7FE5BC4592', + 'Application-Id': '5C80A6BB-CF0D-4A30-BDBF-FC804B5C1A98', 'Content-Type': 'application/json', } baseUrl = 'https://usapi.cv.ford.com/api' class Vehicle(object): - '''Represents a Ford vehicle, with methods for status and issuing commands''' + #Represents a Ford vehicle, with methods for status and issuing commands - def __init__(self, username, password, vin): + def __init__(self, username, password, vin, saveToken=False): self.username = username self.password = password + self.saveToken = saveToken self.vin = vin self.token = None self.expires = None - - def auth(self): + self.expiresAt = None + self.refresh_token = None + def auth(self): '''Authenticate and store the token''' data = { @@ -41,32 +45,97 @@ def auth(self): **defaultHeaders, 'Content-Type': 'application/x-www-form-urlencoded' } + # Fetch OAUTH token stage 1 + r = requests.post('https://sso.ci.ford.com/oidc/endpoint/default/token', data=data, headers=headers) - r = requests.post('https://fcis.ice.ibmcloud.com/v1.0/endpoint/default/token', data=data, headers=headers) + if r.status_code == 200: + logging.info('Succesfully fetched token Stage1') + result = r.json() + data = { + "code": result["access_token"] + } + headers = { + **apiHeaders + } + #Fetch OAUTH token stage 2 and refresh token + r = requests.put('https://api.mps.ford.com/api/oauth2/v1/token', data=json.dumps(data), headers=headers) + if r.status_code == 200: + result = r.json() + self.token = result['access_token'] + self.refresh_token = result["refresh_token"] + self.expiresAt = time.time() + result['expires_in'] + if self.saveToken: + result["expiry_date"] = time.time() + result['expires_in'] + self.writeToken(result) + return True + else: + r.raise_for_status() + + def refreshToken(self, token): + #Token is invalid so let's try refreshing it + data = { + "refresh_token": token["refresh_token"] + } + headers = { + **apiHeaders + } + + r = requests.put('https://api.mps.ford.com/api/oauth2/v1/refresh', data=json.dumps(data), headers=headers) if r.status_code == 200: - logging.info('Succesfully fetched token') result = r.json() + if self.saveToken: + result["expiry_date"] = time.time() + result['expires_in'] + self.writeToken(result) self.token = result['access_token'] + self.refreshToken = result["refresh_token"] self.expiresAt = time.time() + result['expires_in'] - return True - else: - r.raise_for_status() - - def __acquireToken(self): - '''Fetch and refresh token as needed''' - if (self.token == None) or (time.time() >= self.expiresAt): - logging.info('No token, or has expired, requesting new token') + def __acquireToken(self): + #Fetch and refresh token as needed + #If file exists read in token file and check it's valid + if self.saveToken: + if os.path.isfile('/tmp/token.txt'): + data = self.readToken() + else: + data = dict() + data["access_token"] = self.token + data["refresh_token"] = self.refresh_token + data["expiry_date"] = self.expiresAt + else: + data = dict() + data["access_token"] = self.token + data["refresh_token"] = self.refresh_token + data["expiry_date"] = self.expiresAt + self.token=data["access_token"] + self.expiresAt = data["expiry_date"] + if self.expiresAt: + if time.time() >= self.expiresAt: + logging.info('No token, or has expired, requesting new token') + self.refreshToken(data) + #self.auth() + if self.token == None: + #No existing token exists so refreshing library self.auth() else: logging.info('Token is valid, continuing') pass - + + def writeToken(self, token): + #Save token to file to be reused + with open('/tmp/token.txt', 'w') as outfile: + token["expiry_date"] = time.time() + token['expires_in'] + json.dump(token, outfile) + + def readToken(self): + #Get saved token from file + with open('/tmp/token.txt') as token_file: + return json.load(token_file) + def status(self): - '''Get the status of the vehicle''' + #Get the status of the vehicle - self.__acquireToken() + self.__acquireToken() params = { 'lrdt': '01-01-1970 00:00:00' @@ -78,13 +147,14 @@ def status(self): } r = requests.get(f'{baseUrl}/vehicles/v4/{self.vin}/status', params=params, headers=headers) - if r.status_code == 200: result = r.json() + if result["status"] == 402: + r.raise_for_status() return result['vehiclestatus'] else: r.raise_for_status() - + def start(self): ''' Issue a start command to the engine @@ -109,7 +179,14 @@ def unlock(self): ''' Issue an unlock command to the doors ''' - return self.__requestAndPoll('DELETE', f'{baseUrl}/vehicles/v2/{self.vin}/engine/start') + return self.__requestAndPoll('DELETE', f'{baseUrl}/vehicles/v2/{self.vin}/doors/lock') + + def requestUpdate(self): + #Send request to refresh data from the cars module + self.__acquireToken() + status = self.__makeRequest('PUT', f'{baseUrl}/vehicles/v2/{self.vin}/status', None, None) + return status.json()["status"] + def __makeRequest(self, method, url, data, params): ''' @@ -119,8 +196,8 @@ def __makeRequest(self, method, url, data, params): headers = { **apiHeaders, 'auth-token': self.token - } - + } + return getattr(requests, method.lower())(url, headers=headers, data=data, params=params) def __pollStatus(self, url, id): @@ -148,4 +225,4 @@ def __requestAndPoll(self, method, url): result = command.json() return self.__pollStatus(url, result['commandId']) else: - command.raise_for_status() + command.raise_for_status() \ No newline at end of file diff --git a/setup.py b/setup.py index f64bf81..686f29a 100644 --- a/setup.py +++ b/setup.py @@ -7,11 +7,11 @@ name='fordpass', version='0.0.3', author="Dave Clarke", - author_email="info@daveclarke.me", - description="Python wrapper for the FordPass API for Ford vehicle information and control: start, stop, lock, unlock.", + author_email="steve@itchannel.me", + description="Python wrapper for the FordPass API for Ford vehicle information and control: start, stop, lock, unlock based upon the latest fordpass API", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/clarkd/fordpass-python", + url="https://github.com/itchannel/fordpass-python", license="MIT", packages=['fordpass'], scripts=['fordpass/bin/demo.py'], From 6559bafd219da2eb262662b0b34c5efb88e91db1 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 3 Nov 2020 13:36:18 +1000 Subject: [PATCH 2/5] Updated version info on setup.py --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 686f29a..15a3fdc 100644 --- a/setup.py +++ b/setup.py @@ -5,13 +5,13 @@ setup( name='fordpass', - version='0.0.3', + version='0.0.5', author="Dave Clarke", - author_email="steve@itchannel.me", + author_email="info@daveclarke.me", description="Python wrapper for the FordPass API for Ford vehicle information and control: start, stop, lock, unlock based upon the latest fordpass API", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/itchannel/fordpass-python", + url="https://github.com/clarkd/fordpass-python", license="MIT", packages=['fordpass'], scripts=['fordpass/bin/demo.py'], From 3af8fff645dc19b30af77890d90c3403dc758907 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 3 Nov 2020 13:38:56 +1000 Subject: [PATCH 3/5] Updated readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1368e43..31f4f74 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ It's more or less a straight port of @d4v3y0rk's NPM module [d4v3y0rk/ffpass](ht * Stop the engine (if supported) * Lock the doors * Unlock the doors +* Poll the car for an update +* Save Token to file to be reused (Speeds up the Ford API a lot and prevents timeouts when tokens expire) ## Install Install using pip: @@ -27,13 +29,13 @@ pip install fordpass To test the libary there is a demo script `demo.py`. ``` -demo.py USERNAME PASSWORD VIN +demo.py USERNAME PASSWORD VIN 1 ``` e.g. ``` -demo.py test@test.com mypassword WX12345678901234 +demo.py test@test.com mypassword WX12345678901234 1(True of false to save token in a file for reuse) ``` ## Publishing new versions of this package From f2e67dd67ca458fc9ebba35cbc9769d35e6bef15 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 3 Nov 2020 13:41:57 +1000 Subject: [PATCH 4/5] Replaced demo.py --- .gitignore | 3 +++ fordpass/bin/demo.py | 8 ++++---- fordpass/demo.py | 23 ----------------------- 3 files changed, 7 insertions(+), 27 deletions(-) delete mode 100644 fordpass/demo.py diff --git a/.gitignore b/.gitignore index 3a1aca8..f45c0ea 100644 --- a/.gitignore +++ b/.gitignore @@ -142,4 +142,7 @@ dmypy.json # profiling data .prof +# Temp debug files +fordpass/demo.py + # End of https://www.toptal.com/developers/gitignore/api/python \ No newline at end of file diff --git a/fordpass/bin/demo.py b/fordpass/bin/demo.py index da2dbab..c34f53b 100644 --- a/fordpass/bin/demo.py +++ b/fordpass/bin/demo.py @@ -9,15 +9,15 @@ if __name__ == "__main__": - if len(sys.argv) != 4: + if len(sys.argv) < 4: raise Exception('Must specify Username, Password and VIN as arguments, e.g. demo.py test@test.com password123 WX231231232') else: - r = Vehicle(sys.argv[1], sys.argv[2], sys.argv[3]) # Username, Password, VIN - + r = Vehicle(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) # Username, Password, VIN + r.requestUpdate() # Poll the car for an update print(r.status()) # Print the status of the car # r.unlock() # Unlock the doors # time.sleep(10) # Wait 10 seconds - # r.lock() # Lock the doors \ No newline at end of file + # r.lock() # Lock the doors diff --git a/fordpass/demo.py b/fordpass/demo.py deleted file mode 100644 index c34f53b..0000000 --- a/fordpass/demo.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -""" -Simple script to demo the API -""" - -import sys, os, logging, time -from fordpass import Vehicle - -if __name__ == "__main__": - - if len(sys.argv) < 4: - raise Exception('Must specify Username, Password and VIN as arguments, e.g. demo.py test@test.com password123 WX231231232') - else: - r = Vehicle(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) # Username, Password, VIN - r.requestUpdate() # Poll the car for an update - print(r.status()) # Print the status of the car - - # r.unlock() # Unlock the doors - - # time.sleep(10) # Wait 10 seconds - - # r.lock() # Lock the doors From 896d42f2be71b220daf5dd6536bde9463b0501a7 Mon Sep 17 00:00:00 2001 From: itchannel Date: Tue, 3 Nov 2020 12:58:16 +0000 Subject: [PATCH 5/5] Update fordpass.py Fixed variable name in token refresh function --- fordpass/fordpass.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fordpass/fordpass.py b/fordpass/fordpass.py index 0d4c951..ba35d36 100644 --- a/fordpass/fordpass.py +++ b/fordpass/fordpass.py @@ -88,7 +88,7 @@ def refreshToken(self, token): result["expiry_date"] = time.time() + result['expires_in'] self.writeToken(result) self.token = result['access_token'] - self.refreshToken = result["refresh_token"] + self.refresh_token = result["refresh_token"] self.expiresAt = time.time() + result['expires_in'] def __acquireToken(self): @@ -225,4 +225,4 @@ def __requestAndPoll(self, method, url): result = command.json() return self.__pollStatus(url, result['commandId']) else: - command.raise_for_status() \ No newline at end of file + command.raise_for_status()