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/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 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/fordpass.py b/fordpass/fordpass.py index 02a67e4..ba35d36 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.refresh_token = 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): diff --git a/setup.py b/setup.py index f64bf81..15a3fdc 100644 --- a/setup.py +++ b/setup.py @@ -5,10 +5,10 @@ setup( name='fordpass', - version='0.0.3', + version='0.0.5', 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.", + 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",