Skip to content

Commit 80c22f9

Browse files
committed
Fixed 401 Client Error and implemented token refreshing
1 parent aac8dec commit 80c22f9

File tree

1 file changed

+195
-151
lines changed

1 file changed

+195
-151
lines changed

fordpass/fordpass.py

Lines changed: 195 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,151 +1,195 @@
1-
import requests
2-
import logging
3-
import time
4-
5-
defaultHeaders = {
6-
'Accept': '*/*',
7-
'Accept-Language': 'en-us',
8-
'User-Agent': 'fordpass-na/353 CFNetwork/1121.2.2 Darwin/19.3.0',
9-
'Accept-Encoding': 'gzip, deflate, br',
10-
}
11-
12-
apiHeaders = {
13-
**defaultHeaders,
14-
'Application-Id': '71A3AD0A-CF46-4CCF-B473-FC7FE5BC4592',
15-
'Content-Type': 'application/json',
16-
}
17-
18-
baseUrl = 'https://usapi.cv.ford.com/api'
19-
20-
class Vehicle(object):
21-
'''Represents a Ford vehicle, with methods for status and issuing commands'''
22-
23-
def __init__(self, username, password, vin):
24-
self.username = username
25-
self.password = password
26-
self.vin = vin
27-
self.token = None
28-
self.expires = None
29-
30-
def auth(self):
31-
'''Authenticate and store the token'''
32-
33-
data = {
34-
'client_id': '9fb503e0-715b-47e8-adfd-ad4b7770f73b',
35-
'grant_type': 'password',
36-
'username': self.username,
37-
'password': self.password
38-
}
39-
40-
headers = {
41-
**defaultHeaders,
42-
'Content-Type': 'application/x-www-form-urlencoded'
43-
}
44-
45-
r = requests.post('https://fcis.ice.ibmcloud.com/v1.0/endpoint/default/token', data=data, headers=headers)
46-
47-
if r.status_code == 200:
48-
logging.info('Succesfully fetched token')
49-
result = r.json()
50-
self.token = result['access_token']
51-
self.expiresAt = time.time() + result['expires_in']
52-
return True
53-
else:
54-
r.raise_for_status()
55-
56-
def __acquireToken(self):
57-
'''Fetch and refresh token as needed'''
58-
59-
if (self.token == None) or (time.time() >= self.expiresAt):
60-
logging.info('No token, or has expired, requesting new token')
61-
self.auth()
62-
else:
63-
logging.info('Token is valid, continuing')
64-
pass
65-
66-
def status(self):
67-
'''Get the status of the vehicle'''
68-
69-
self.__acquireToken()
70-
71-
params = {
72-
'lrdt': '01-01-1970 00:00:00'
73-
}
74-
75-
headers = {
76-
**apiHeaders,
77-
'auth-token': self.token
78-
}
79-
80-
r = requests.get(f'{baseUrl}/vehicles/v4/{self.vin}/status', params=params, headers=headers)
81-
82-
if r.status_code == 200:
83-
result = r.json()
84-
return result['vehiclestatus']
85-
else:
86-
r.raise_for_status()
87-
88-
def start(self):
89-
'''
90-
Issue a start command to the engine
91-
'''
92-
return self.__requestAndPoll('PUT', f'{baseUrl}/vehicles/v2/{self.vin}/engine/start')
93-
94-
def stop(self):
95-
'''
96-
Issue a stop command to the engine
97-
'''
98-
return self.__requestAndPoll('DELETE', f'{baseUrl}/vehicles/v2/{self.vin}/engine/start')
99-
100-
101-
def lock(self):
102-
'''
103-
Issue a lock command to the doors
104-
'''
105-
return self.__requestAndPoll('PUT', f'{baseUrl}/vehicles/v2/{self.vin}/doors/lock')
106-
107-
108-
def unlock(self):
109-
'''
110-
Issue an unlock command to the doors
111-
'''
112-
return self.__requestAndPoll('DELETE', f'{baseUrl}/vehicles/v2/{self.vin}/doors/lock')
113-
114-
def __makeRequest(self, method, url, data, params):
115-
'''
116-
Make a request to the given URL, passing data/params as needed
117-
'''
118-
119-
headers = {
120-
**apiHeaders,
121-
'auth-token': self.token
122-
}
123-
124-
return getattr(requests, method.lower())(url, headers=headers, data=data, params=params)
125-
126-
def __pollStatus(self, url, id):
127-
'''
128-
Poll the given URL with the given command ID until the command is completed
129-
'''
130-
status = self.__makeRequest('GET', f'{url}/{id}', None, None)
131-
result = status.json()
132-
if result['status'] == 552:
133-
logging.info('Command is pending')
134-
time.sleep(5)
135-
return self.__pollStatus(url, id) # retry after 5s
136-
elif result['status'] == 200:
137-
logging.info('Command completed succesfully')
138-
return True
139-
else:
140-
logging.info('Command failed')
141-
return False
142-
143-
def __requestAndPoll(self, method, url):
144-
self.__acquireToken()
145-
command = self.__makeRequest(method, url, None, None)
146-
147-
if command.status_code == 200:
148-
result = command.json()
149-
return self.__pollStatus(url, result['commandId'])
150-
else:
151-
command.raise_for_status()
1+
import requests
2+
import logging
3+
import time
4+
import json
5+
6+
defaultHeaders = {
7+
'Accept': '*/*',
8+
'Accept-Language': 'en-US',
9+
'User-Agent': 'FordPass/5 CFNetwork/1197 Darwin/20.0.0',
10+
'Accept-Encoding': 'gzip, deflate, br',
11+
'Content-Type': 'application/x-www-form-urlencoded',
12+
}
13+
14+
apiHeaders = {
15+
**defaultHeaders,
16+
'Application-Id': '1E8C7794-FF5F-49BC-9596-A1E0C86C5B19',
17+
'Content-Type': 'application/json',
18+
}
19+
20+
SSO_URI = 'https://sso.ci.ford.com'
21+
API_URI = 'https://usapi.cv.ford.com'
22+
TOKEN_URL = 'https://api.mps.ford.com/api/oauth2/v1'
23+
24+
class Vehicle(object):
25+
'''Represents a Ford vehicle, with methods for status and issuing commands'''
26+
27+
def __init__(self, username, password, vin):
28+
self.username = username
29+
self.password = password
30+
self.vin = vin
31+
self.access_token = None
32+
self.refresh_token = None
33+
self.access_expire_time = None
34+
self.refresh_expire_time = None
35+
36+
37+
def __auth(self):
38+
'''Authenticate and store the token'''
39+
40+
data = {
41+
'client_id': '9fb503e0-715b-47e8-adfd-ad4b7770f73b',
42+
'grant_type': 'password',
43+
'username': self.username,
44+
'password': self.password
45+
}
46+
47+
r = requests.post(f'{SSO_URI}/oidc/endpoint/default/token', data=data, headers=defaultHeaders)
48+
49+
if r.status_code == 200:
50+
result = r.json()
51+
52+
self.auth_token = result['access_token']
53+
logging.info(f'Succesfully fetched auth token and refresh token')
54+
55+
data = json.dumps({"code": result['access_token']})
56+
57+
logging.info('Requesting access token')
58+
r = self.__makeRequest('PUT', f'{TOKEN_URL}/token', data)
59+
60+
if r.status_code == 200:
61+
62+
result = r.json()
63+
64+
self.access_token = result['access_token']
65+
self.access_expire_time = time.time() + result['expires_in']
66+
logging.info(f'Successfully fetched access token (Expires in {result["expires_in"]} seconds)')
67+
68+
self.refresh_token = result['refresh_token']
69+
self.refresh_expire_time = time.time() + result['refresh_expires_in']
70+
logging.info(f'Successfully fetched refresh token (Expires in {result["refresh_expires_in"]} seconds)')
71+
72+
else:
73+
r.raise_for_status()
74+
else:
75+
r.raise_for_status()
76+
77+
def __fetch_refresh_token(self):
78+
'''
79+
Fetch a new access token using the refresh token
80+
'''
81+
82+
data = json.dumps({"refresh_token": self.refresh_token})
83+
84+
logging.info('Refreshing access token')
85+
r = requests.put(f'{TOKEN_URL}/refresh', headers=apiHeaders, data=data)
86+
87+
if r.status_code == 200:
88+
result = r.json()
89+
self.access_token = result['access_token']
90+
self.access_expire_time = time.time() + result['expires_in']
91+
logging.info(f'Successfully refreshed access token (Expires in {result["expires_in"]} seconds)')
92+
93+
else:
94+
r.raise_for_status()
95+
96+
def __acquireToken(self):
97+
'''
98+
Fetch and refresh token as needed
99+
'''
100+
101+
if self.access_token == None or self.refresh_expire_time < time.time():
102+
logging.info('No token, or refresh token has expired, requesting new token')
103+
self.__auth()
104+
105+
elif self.access_expire_time < time.time():
106+
logging.info('Token has expired, refreshing new token')
107+
self.__fetch_refresh_token()
108+
109+
else:
110+
logging.info('Token is valid, continuing')
111+
logging.info(f'Access token expires in {self.access_expire_time - time.time()} seconds')
112+
logging.info(f'Refresh token expires in {self.refresh_expire_time - time.time()} seconds')
113+
114+
def status(self):
115+
'''Get the status of the vehicle'''
116+
117+
self.__acquireToken()
118+
119+
headers = {
120+
**apiHeaders,
121+
'auth-token': self.access_token
122+
}
123+
124+
r = requests.get(f'{API_URI}/api/vehicles/v4/{self.vin}/status', headers=headers)
125+
126+
if r.status_code == 200:
127+
result = r.json()
128+
return result['vehiclestatus']
129+
else:
130+
r.raise_for_status()
131+
132+
def start(self):
133+
'''
134+
Issue a start command to the engine
135+
'''
136+
return self.__requestAndPoll('PUT', f'{API_URI}/api/vehicles/v2/{self.vin}/engine/start')
137+
138+
def stop(self):
139+
'''
140+
Issue a stop command to the engine
141+
'''
142+
return self.__requestAndPoll('DELETE', f'{API_URI}/api/vehicles/v2/{self.vin}/engine/start')
143+
144+
145+
def lock(self):
146+
'''
147+
Issue a lock command to the doors
148+
'''
149+
return self.__requestAndPoll('PUT', f'{API_URI}/api/vehicles/v2/{self.vin}/doors/lock')
150+
151+
152+
def unlock(self):
153+
'''
154+
Issue an unlock command to the doors
155+
'''
156+
return self.__requestAndPoll('DELETE', f'{API_URI}/api/vehicles/v2/{self.vin}/doors/lock')
157+
158+
def __makeRequest(self, method, url, data=None, params=None):
159+
'''
160+
Make a request to the given URL, passing data/params as needed
161+
'''
162+
163+
headers = {
164+
**apiHeaders,
165+
'auth-token': self.access_token,
166+
}
167+
168+
return getattr(requests, method.lower())(url, headers=headers, data=data, params=params)
169+
170+
def __pollStatus(self, url, id):
171+
'''
172+
Poll the given URL with the given command ID until the command is completed
173+
'''
174+
status = self.__makeRequest('GET', f'{url}/{id}')
175+
result = status.json()
176+
if result['status'] == 552:
177+
logging.info('Command is pending')
178+
time.sleep(5)
179+
return self.__pollStatus(url, id) # retry after 5s
180+
elif result['status'] == 200:
181+
logging.info('Command completed succesfully')
182+
return True
183+
else:
184+
logging.info('Command failed')
185+
return False
186+
187+
def __requestAndPoll(self, method, url):
188+
self.__acquireToken()
189+
command = self.__makeRequest(method, url)
190+
191+
if command.status_code == 200:
192+
result = command.json()
193+
return self.__pollStatus(url, result['commandId'])
194+
else:
195+
command.raise_for_status()

0 commit comments

Comments
 (0)