pycupra 0.1.13__py3-none-any.whl → 0.1.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- example/PyCupra.py +12 -4
- example/PyCupra_ExportDrivingData.py +113 -0
- pycupra/connection.py +75 -67
- pycupra/dashboard.py +36 -36
- pycupra/firebase.py +11 -8
- pycupra/firebase_messaging/fcmpushclient.py +8 -7
- pycupra/firebase_messaging/fcmregister.py +3 -3
- pycupra/utilities.py +2 -26
- pycupra/vehicle.py +299 -226
- {pycupra-0.1.13.dist-info → pycupra-0.1.14.dist-info}/METADATA +6 -2
- {pycupra-0.1.13.dist-info → pycupra-0.1.14.dist-info}/RECORD +14 -13
- {pycupra-0.1.13.dist-info → pycupra-0.1.14.dist-info}/top_level.txt +1 -0
- {pycupra-0.1.13.dist-info → pycupra-0.1.14.dist-info}/WHEEL +0 -0
- {pycupra-0.1.13.dist-info → pycupra-0.1.14.dist-info}/licenses/LICENSE +0 -0
example/PyCupra.py
CHANGED
@@ -10,9 +10,11 @@ import pandas as pd
|
|
10
10
|
from aiohttp import ClientSession
|
11
11
|
from datetime import datetime
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
currentframe = inspect.currentframe()
|
14
|
+
if currentframe != None:
|
15
|
+
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(currentframe)))
|
16
|
+
parentdir = os.path.dirname(currentdir)
|
17
|
+
sys.path.insert(0, parentdir)
|
16
18
|
|
17
19
|
try:
|
18
20
|
from pycupra import Connection
|
@@ -383,6 +385,12 @@ async def main():
|
|
383
385
|
if credentials==None or credentials.get('username','')=='' or (credentials.get('password','')==''):
|
384
386
|
_LOGGER.warning('Can not use the credentials read from the credentials file.')
|
385
387
|
raise
|
388
|
+
if credentials.get('brand','')!='':
|
389
|
+
BRAND = credentials.get('brand','')
|
390
|
+
print('Read brand from the credentials file.')
|
391
|
+
else:
|
392
|
+
print('No brand found in the credentials file. Using the default value.')
|
393
|
+
print(f'Now working with brand={BRAND}')
|
386
394
|
async with ClientSession(headers={'Connection': 'keep-alive'}) as session:
|
387
395
|
print('')
|
388
396
|
print('######################################################')
|
@@ -594,7 +602,7 @@ async def main():
|
|
594
602
|
i=i+1
|
595
603
|
_LOGGER.debug(f'Round {i}')
|
596
604
|
|
597
|
-
exit
|
605
|
+
sys.exit(1)
|
598
606
|
|
599
607
|
if __name__ == "__main__":
|
600
608
|
loop = asyncio.new_event_loop()
|
@@ -0,0 +1,113 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
""" Sample program to export the trip statistics as csv file"""
|
3
|
+
import asyncio
|
4
|
+
import logging
|
5
|
+
import inspect
|
6
|
+
import sys
|
7
|
+
import os
|
8
|
+
import json
|
9
|
+
import pandas as pd
|
10
|
+
from aiohttp import ClientSession
|
11
|
+
from datetime import datetime
|
12
|
+
|
13
|
+
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
14
|
+
parentdir = os.path.dirname(currentdir)
|
15
|
+
sys.path.insert(0, parentdir)
|
16
|
+
|
17
|
+
try:
|
18
|
+
from pycupra import Connection
|
19
|
+
except ModuleNotFoundError as e:
|
20
|
+
print(f"Unable to import library: {e}")
|
21
|
+
sys.exit(1)
|
22
|
+
|
23
|
+
logging.basicConfig(level=logging.WARN)
|
24
|
+
_LOGGER = logging.getLogger(__name__)
|
25
|
+
BRAND = 'cupra' # or 'seat' (Default value if no brand is provided via credentials file)
|
26
|
+
|
27
|
+
PRINTRESPONSE = True
|
28
|
+
INTERVAL = 5
|
29
|
+
TOKEN_FILE_NAME_AND_PATH='./pycupra_token.json'
|
30
|
+
CREDENTIALS_FILE_NAME_AND_PATH='./pycupra_credentials.json'
|
31
|
+
|
32
|
+
def readCredentialsFile():
|
33
|
+
try:
|
34
|
+
with open(CREDENTIALS_FILE_NAME_AND_PATH, "r") as f:
|
35
|
+
credentialsString=f.read()
|
36
|
+
credentials=json.loads(credentialsString)
|
37
|
+
return credentials
|
38
|
+
except:
|
39
|
+
_LOGGER.info('readCredentialsFile not successful. Perhaps no credentials file present.')
|
40
|
+
return None
|
41
|
+
|
42
|
+
def exportToCSV(vehicle, csvFileName, dataType='short'):
|
43
|
+
df= pd.DataFrame(vehicle._states['tripstatistics'][dataType])
|
44
|
+
_LOGGER.debug('Exporting trip data to csv')
|
45
|
+
df.to_csv(csvFileName)
|
46
|
+
return True
|
47
|
+
|
48
|
+
async def main():
|
49
|
+
"""Main method."""
|
50
|
+
print('')
|
51
|
+
print('######################################################')
|
52
|
+
print('# Reading credentials file #')
|
53
|
+
print('######################################################')
|
54
|
+
credentials= readCredentialsFile()
|
55
|
+
if credentials==None or credentials.get('username','')=='' or (credentials.get('password','')==''):
|
56
|
+
_LOGGER.warning('Can not use the credentials read from the credentials file.')
|
57
|
+
raise
|
58
|
+
if credentials.get('brand','')!='':
|
59
|
+
BRAND = credentials.get('brand','')
|
60
|
+
print('Read brand from the credentials file.')
|
61
|
+
else:
|
62
|
+
print('No brand found in the credentials file. Using the default value.')
|
63
|
+
print(f'Now working with brand={BRAND}')
|
64
|
+
async with ClientSession(headers={'Connection': 'keep-alive'}) as session:
|
65
|
+
print('')
|
66
|
+
print('######################################################')
|
67
|
+
print('# Logging on to ola.prod.code.seat.cloud.vwgroup.com #')
|
68
|
+
print('######################################################')
|
69
|
+
print(f"Initiating new session to Cupra/Seat Cloud with {credentials.get('username')} as username")
|
70
|
+
connection = Connection(session, BRAND, credentials.get('username'), credentials.get('password'), PRINTRESPONSE, nightlyUpdateReduction=False, anonymise=True, tripStatisticsStartDate='1970-01-01')
|
71
|
+
print("Attempting to login to the Seat Cloud service")
|
72
|
+
if await connection.doLogin(tokenFile=TOKEN_FILE_NAME_AND_PATH, apiKey=credentials.get('apiKey',None)):
|
73
|
+
print('Login or token refresh success!')
|
74
|
+
print(datetime.now())
|
75
|
+
print('Fetching user information for account.')
|
76
|
+
await connection.get_userData()
|
77
|
+
print(f"\tName: {connection._userData.get('name','')}")
|
78
|
+
print(f"\tNickname: {connection._userData.get('nickname','')}")
|
79
|
+
print(f"\tEmail: {connection._userData.get('email','')}")
|
80
|
+
print(f"\tPicture: {connection._userData.get('picture','')}")
|
81
|
+
print("")
|
82
|
+
print('Fetching vehicles associated with account.')
|
83
|
+
await connection.get_vehicles()
|
84
|
+
|
85
|
+
print('')
|
86
|
+
print('########################################')
|
87
|
+
print('# Vehicles discovered #')
|
88
|
+
print('########################################')
|
89
|
+
for vehicle in connection.vehicles:
|
90
|
+
print(f"\tVIN: {vehicle.vin}")
|
91
|
+
print(f"\tModel: {vehicle.model}")
|
92
|
+
print(f"\tManufactured: {vehicle.model_year}")
|
93
|
+
print(f"\tConnect service deactivated: {vehicle.deactivated}")
|
94
|
+
print("")
|
95
|
+
if vehicle.is_nickname_supported: print(f"\tNickname: {vehicle.nickname}")
|
96
|
+
else:
|
97
|
+
return False
|
98
|
+
|
99
|
+
for vehicle in connection.vehicles:
|
100
|
+
txt = vehicle.vin
|
101
|
+
print('########################################')
|
102
|
+
print('# Export driving data to csv #')
|
103
|
+
print(txt.center(40, '#'))
|
104
|
+
exportToCSV(vehicle, credentials.get('csvFileName','./drivingData.csv'), 'short') # possible value: short/cyclic
|
105
|
+
print('')
|
106
|
+
print('Export of driving data to csv complete')
|
107
|
+
sys.exit(1)
|
108
|
+
|
109
|
+
if __name__ == "__main__":
|
110
|
+
loop = asyncio.new_event_loop()
|
111
|
+
asyncio.set_event_loop(loop)
|
112
|
+
loop.run_until_complete(main())
|
113
|
+
|
pycupra/connection.py
CHANGED
@@ -16,6 +16,7 @@ import secrets
|
|
16
16
|
import xmltodict
|
17
17
|
from copy import deepcopy
|
18
18
|
import importlib.metadata
|
19
|
+
from typing import Any
|
19
20
|
|
20
21
|
from PIL import Image
|
21
22
|
from io import BytesIO
|
@@ -28,7 +29,7 @@ import aiohttp
|
|
28
29
|
from bs4 import BeautifulSoup
|
29
30
|
from base64 import b64decode, b64encode, urlsafe_b64decode, urlsafe_b64encode
|
30
31
|
#from .__version__ import __version__ as lib_version
|
31
|
-
from .utilities import
|
32
|
+
from .utilities import json_loads
|
32
33
|
from .vehicle import Vehicle
|
33
34
|
from .exceptions import (
|
34
35
|
SeatConfigException,
|
@@ -227,7 +228,7 @@ class Connection:
|
|
227
228
|
return False
|
228
229
|
|
229
230
|
# API login/logout/authorization
|
230
|
-
async def doLogin(self,**data):
|
231
|
+
async def doLogin(self,**data) -> bool:
|
231
232
|
"""Login method, clean login or use token from file and refresh it"""
|
232
233
|
#if len(self._session_tokens) > 0:
|
233
234
|
# _LOGGER.info('Revoking old tokens.')
|
@@ -241,7 +242,7 @@ class Connection:
|
|
241
242
|
self._clear_cookies()
|
242
243
|
self._vehicles.clear()
|
243
244
|
self._session_tokens = {}
|
244
|
-
self._session_headers = HEADERS_SESSION.get(self._session_auth_brand).copy()
|
245
|
+
self._session_headers = HEADERS_SESSION.get(self._session_auth_brand, HEADERS_SESSION['cupra']).copy()
|
245
246
|
self._session_auth_headers = HEADERS_AUTH.copy()
|
246
247
|
self._session_nonce = self._getNonce()
|
247
248
|
self._session_state = self._getState()
|
@@ -260,7 +261,7 @@ class Connection:
|
|
260
261
|
_LOGGER.info('Initiating new login with user name and password.')
|
261
262
|
return await self._authorize(self._session_auth_brand)
|
262
263
|
|
263
|
-
async def _authorize(self, client=BRAND_CUPRA):
|
264
|
+
async def _authorize(self, client=BRAND_CUPRA) -> bool:
|
264
265
|
""""Login" function. Authorize a certain client type and get tokens."""
|
265
266
|
# Helper functions
|
266
267
|
def extract_csrf(req):
|
@@ -287,8 +288,8 @@ class Connection:
|
|
287
288
|
oauthClient = OAuth2Session(client_id=CLIENT_LIST[client].get('CLIENT_ID'), scope=CLIENT_LIST[client].get('SCOPE'), redirect_uri=CLIENT_LIST[client].get('REDIRECT_URL'))
|
288
289
|
code_verifier = urlsafe_b64encode(os.urandom(40)).decode('utf-8')
|
289
290
|
code_verifier = re.sub('[^a-zA-Z0-9]+', '', code_verifier)
|
290
|
-
|
291
|
-
code_challenge = urlsafe_b64encode(
|
291
|
+
code_challenge_hash = hashlib.sha256(code_verifier.encode("utf-8")).digest()
|
292
|
+
code_challenge = urlsafe_b64encode(code_challenge_hash).decode("utf-8")
|
292
293
|
code_challenge = code_challenge.replace("=", "")
|
293
294
|
authorization_url, state = oauthClient.authorization_url(authorizationEndpoint, code_challenge=code_challenge, code_challenge_method='S256',
|
294
295
|
nonce=self._session_nonce, state=self._session_state)
|
@@ -344,19 +345,19 @@ class Connection:
|
|
344
345
|
if location is None:
|
345
346
|
raise SeatException('Login failed')
|
346
347
|
if 'error' in location:
|
347
|
-
|
348
|
-
if
|
348
|
+
errorTxt = parse_qs(urlparse(location).query).get('error', '')[0]
|
349
|
+
if errorTxt == 'login.error.throttled':
|
349
350
|
timeout = parse_qs(urlparse(location).query).get('enableNextButtonAfterSeconds', '')[0]
|
350
351
|
raise SeatAccountLockedException(f'Account is locked for another {timeout} seconds')
|
351
|
-
elif
|
352
|
+
elif errorTxt == 'login.errors.password_invalid':
|
352
353
|
raise SeatAuthenticationException('Invalid credentials')
|
353
354
|
else:
|
354
|
-
_LOGGER.warning(f'Login failed: {
|
355
|
-
raise SeatLoginFailedException(
|
355
|
+
_LOGGER.warning(f'Login failed: {errorTxt}')
|
356
|
+
raise SeatLoginFailedException(errorTxt)
|
356
357
|
if 'terms-and-conditions' in location:
|
357
358
|
raise SeatEULAException('The terms and conditions must be accepted first at your local SEAT/Cupra site, e.g. "https://cupraofficial.se/"')
|
358
359
|
if 'user_id' in location: # Get the user_id which is needed for some later requests
|
359
|
-
self._user_id=parse_qs(urlparse(location).query).get('user_id')[0]
|
360
|
+
self._user_id=parse_qs(urlparse(location).query).get('user_id', [''])[0]
|
360
361
|
self.addToAnonymisationDict(self._user_id,'[USER_ID_ANONYMISED]')
|
361
362
|
#_LOGGER.debug('Got user_id: %s' % self._user_id)
|
362
363
|
if self._session_fulldebug:
|
@@ -388,7 +389,7 @@ class Connection:
|
|
388
389
|
|
389
390
|
_LOGGER.debug('Received authorization code, exchange for tokens.')
|
390
391
|
# Extract code and tokens
|
391
|
-
auth_code = parse_qs(urlparse(location).query).get('code')[0]
|
392
|
+
auth_code = parse_qs(urlparse(location).query).get('code', [''])[0]
|
392
393
|
# Save access, identity and refresh tokens according to requested client"""
|
393
394
|
if client=='cupra':
|
394
395
|
# oauthClient.fetch_token() does not work in home assistant, using POST request instead
|
@@ -431,12 +432,12 @@ class Connection:
|
|
431
432
|
if '_token' in key:
|
432
433
|
self._session_tokens[client][key] = token_data[key]
|
433
434
|
if 'error' in self._session_tokens[client]:
|
434
|
-
|
435
|
+
errorTxt = self._session_tokens[client].get('error', '')
|
435
436
|
if 'error_description' in self._session_tokens[client]:
|
436
437
|
error_description = self._session_tokens[client].get('error_description', '')
|
437
|
-
raise SeatException(f'{
|
438
|
+
raise SeatException(f'{errorTxt} - {error_description}')
|
438
439
|
else:
|
439
|
-
raise SeatException(
|
440
|
+
raise SeatException(errorTxt)
|
440
441
|
if self._session_fulldebug:
|
441
442
|
for key in self._session_tokens.get(client, {}):
|
442
443
|
if 'token' in key:
|
@@ -559,7 +560,7 @@ class Connection:
|
|
559
560
|
)
|
560
561
|
return req.headers.get('Location', None)
|
561
562
|
|
562
|
-
async def terminate(self):
|
563
|
+
async def terminate(self) -> None:
|
563
564
|
"""Log out from connect services"""
|
564
565
|
for v in self.vehicles:
|
565
566
|
_LOGGER.debug(self.anonymise(f'Calling stopFirebase() for vehicle {v.vin}'))
|
@@ -570,7 +571,7 @@ class Connection:
|
|
570
571
|
v.firebaseStatus = FIREBASE_STATUS_NOT_INITIALISED
|
571
572
|
await self.logout()
|
572
573
|
|
573
|
-
async def logout(self):
|
574
|
+
async def logout(self) -> None:
|
574
575
|
"""Logout, revoke tokens."""
|
575
576
|
_LOGGER.info(f'Initiating logout.')
|
576
577
|
self._session_headers.pop('Authorization', None)
|
@@ -631,6 +632,9 @@ class Connection:
|
|
631
632
|
return data
|
632
633
|
except Exception as e:
|
633
634
|
_LOGGER.debug(f'Got non HTTP related error: {e}')
|
635
|
+
return {
|
636
|
+
'error_description': 'Non HTTP related error'
|
637
|
+
}
|
634
638
|
|
635
639
|
async def post(self, url, **data):
|
636
640
|
"""Perform a HTTP POST."""
|
@@ -748,7 +752,7 @@ class Connection:
|
|
748
752
|
return False
|
749
753
|
|
750
754
|
# Class get data functions
|
751
|
-
async def update_all(self):
|
755
|
+
async def update_all(self) -> bool:
|
752
756
|
"""Update status."""
|
753
757
|
try:
|
754
758
|
# Get all Vehicle objects and update in parallell
|
@@ -773,7 +777,7 @@ class Connection:
|
|
773
777
|
raise
|
774
778
|
return False
|
775
779
|
|
776
|
-
async def get_userData(self):
|
780
|
+
async def get_userData(self) -> dict:
|
777
781
|
"""Fetch user profile."""
|
778
782
|
await self.set_token(self._session_auth_brand)
|
779
783
|
userData={}
|
@@ -798,7 +802,7 @@ class Connection:
|
|
798
802
|
self._userData=userData
|
799
803
|
return userData
|
800
804
|
|
801
|
-
async def get_vehicles(self):
|
805
|
+
async def get_vehicles(self) -> list:
|
802
806
|
"""Fetch vehicle information from user profile."""
|
803
807
|
api_vehicles = []
|
804
808
|
# Check if user needs to update consent
|
@@ -938,7 +942,7 @@ class Connection:
|
|
938
942
|
_LOGGER.debug(f'Could not get consent information, error {error}')
|
939
943
|
return False"""
|
940
944
|
|
941
|
-
async def getBasicCarData(self, vin, baseurl):
|
945
|
+
async def getBasicCarData(self, vin, baseurl) -> dict | bool:
|
942
946
|
"""Get car information from customer profile, VIN, nickname, etc."""
|
943
947
|
await self.set_token(self._session_auth_brand)
|
944
948
|
data={}
|
@@ -956,7 +960,7 @@ class Connection:
|
|
956
960
|
return False
|
957
961
|
return data
|
958
962
|
|
959
|
-
async def getMileage(self, vin, baseurl):
|
963
|
+
async def getMileage(self, vin, baseurl) -> dict | bool:
|
960
964
|
"""Get car information from customer profile, VIN, nickname, etc."""
|
961
965
|
await self.set_token(self._session_auth_brand)
|
962
966
|
data={}
|
@@ -974,7 +978,7 @@ class Connection:
|
|
974
978
|
return False
|
975
979
|
return data
|
976
980
|
|
977
|
-
async def getVehicleHealthWarnings(self, vin, baseurl):
|
981
|
+
async def getVehicleHealthWarnings(self, vin, baseurl) -> dict | bool:
|
978
982
|
"""Get car information from customer profile, VIN, nickname, etc."""
|
979
983
|
await self.set_token(self._session_auth_brand)
|
980
984
|
data={}
|
@@ -1010,7 +1014,7 @@ class Connection:
|
|
1010
1014
|
data = {'error': 'unknown'}
|
1011
1015
|
return data"""
|
1012
1016
|
|
1013
|
-
async def getModelImageURL(self, vin, baseurl):
|
1017
|
+
async def getModelImageURL(self, vin, baseurl) -> dict | None:
|
1014
1018
|
"""Construct the URL for the model image."""
|
1015
1019
|
await self.set_token(self._session_auth_brand)
|
1016
1020
|
try:
|
@@ -1019,7 +1023,7 @@ class Connection:
|
|
1019
1023
|
url=eval(f"f'{API_IMAGE}'"),
|
1020
1024
|
)
|
1021
1025
|
if response.get('front',False):
|
1022
|
-
images={}
|
1026
|
+
images: dict[str, str] ={}
|
1023
1027
|
for pos in {'front', 'side', 'top', 'rear'}:
|
1024
1028
|
if pos in response:
|
1025
1029
|
pic = await self._request(
|
@@ -1060,7 +1064,7 @@ class Connection:
|
|
1060
1064
|
_LOGGER.debug('Could not fetch Model image URL, message signing failed.')
|
1061
1065
|
return None
|
1062
1066
|
|
1063
|
-
async def getVehicleStatusReport(self, vin, baseurl):
|
1067
|
+
async def getVehicleStatusReport(self, vin, baseurl) -> dict | bool:
|
1064
1068
|
"""Get stored vehicle status report (Connect services)."""
|
1065
1069
|
data={}
|
1066
1070
|
await self.set_token(self._session_auth_brand)
|
@@ -1078,7 +1082,7 @@ class Connection:
|
|
1078
1082
|
return False
|
1079
1083
|
return data
|
1080
1084
|
|
1081
|
-
async def getMaintenance(self, vin, baseurl):
|
1085
|
+
async def getMaintenance(self, vin, baseurl) -> dict | bool:
|
1082
1086
|
"""Get stored vehicle status report (Connect services)."""
|
1083
1087
|
data={}
|
1084
1088
|
await self.set_token(self._session_auth_brand)
|
@@ -1096,7 +1100,7 @@ class Connection:
|
|
1096
1100
|
return False
|
1097
1101
|
return data
|
1098
1102
|
|
1099
|
-
async def getTripStatistics(self, vin, baseurl, supportsCyclicTrips):
|
1103
|
+
async def getTripStatistics(self, vin, baseurl, supportsCyclicTrips) -> dict | bool:
|
1100
1104
|
"""Get short term and cyclic trip statistics."""
|
1101
1105
|
await self.set_token(self._session_auth_brand)
|
1102
1106
|
if self._session_tripStatisticsStartDate==None:
|
@@ -1106,7 +1110,7 @@ class Connection:
|
|
1106
1110
|
else:
|
1107
1111
|
startDate = self._session_tripStatisticsStartDate
|
1108
1112
|
try:
|
1109
|
-
data={'tripstatistics': {}}
|
1113
|
+
data: dict[str, dict] ={'tripstatistics': {}}
|
1110
1114
|
if supportsCyclicTrips:
|
1111
1115
|
dataType='CYCLIC'
|
1112
1116
|
response = await self.get(eval(f"f'{API_TRIP}'"))
|
@@ -1136,7 +1140,7 @@ class Connection:
|
|
1136
1140
|
_LOGGER.warning(f'Could not fetch trip statistics, error: {error}')
|
1137
1141
|
return False
|
1138
1142
|
|
1139
|
-
async def getPosition(self, vin, baseurl):
|
1143
|
+
async def getPosition(self, vin, baseurl) -> dict | bool:
|
1140
1144
|
"""Get position data."""
|
1141
1145
|
await self.set_token(self._session_auth_brand)
|
1142
1146
|
try:
|
@@ -1171,7 +1175,7 @@ class Connection:
|
|
1171
1175
|
_LOGGER.warning(f'Could not fetch position, error: {error}')
|
1172
1176
|
return False
|
1173
1177
|
|
1174
|
-
async def getDeparturetimer(self, vin, baseurl):
|
1178
|
+
async def getDeparturetimer(self, vin, baseurl) -> dict | bool:
|
1175
1179
|
"""Get departure timers."""
|
1176
1180
|
await self.set_token(self._session_auth_brand)
|
1177
1181
|
try:
|
@@ -1188,7 +1192,7 @@ class Connection:
|
|
1188
1192
|
_LOGGER.warning(f'Could not fetch departure timers, error: {error}')
|
1189
1193
|
return False
|
1190
1194
|
|
1191
|
-
async def getDepartureprofiles(self, vin, baseurl):
|
1195
|
+
async def getDepartureprofiles(self, vin, baseurl) -> dict | bool:
|
1192
1196
|
"""Get departure timers."""
|
1193
1197
|
await self.set_token(self._session_auth_brand)
|
1194
1198
|
try:
|
@@ -1210,7 +1214,7 @@ class Connection:
|
|
1210
1214
|
_LOGGER.warning(f'Could not fetch departure profiles, error: {error}')
|
1211
1215
|
return False
|
1212
1216
|
|
1213
|
-
async def getClimater(self, vin, baseurl, oldClimatingData):
|
1217
|
+
async def getClimater(self, vin, baseurl, oldClimatingData) -> dict | bool:
|
1214
1218
|
"""Get climatisation data."""
|
1215
1219
|
#data={}
|
1216
1220
|
#data['climater']={}
|
@@ -1240,7 +1244,7 @@ class Connection:
|
|
1240
1244
|
return False
|
1241
1245
|
return data
|
1242
1246
|
|
1243
|
-
async def getCharger(self, vin, baseurl, oldChargingData):
|
1247
|
+
async def getCharger(self, vin, baseurl, oldChargingData) -> dict | bool:
|
1244
1248
|
"""Get charger data."""
|
1245
1249
|
await self.set_token(self._session_auth_brand)
|
1246
1250
|
try:
|
@@ -1298,7 +1302,7 @@ class Connection:
|
|
1298
1302
|
_LOGGER.warning(f'Could not fetch charger, error: {error}')
|
1299
1303
|
return False
|
1300
1304
|
|
1301
|
-
async def getPreHeater(self, vin, baseurl):
|
1305
|
+
async def getPreHeater(self, vin, baseurl) -> dict | bool:
|
1302
1306
|
"""Get parking heater data."""
|
1303
1307
|
await self.set_token(self._session_auth_brand)
|
1304
1308
|
try:
|
@@ -1373,7 +1377,7 @@ class Connection:
|
|
1373
1377
|
_LOGGER.warning(f'Failure during get request status: {error}')
|
1374
1378
|
raise SeatException(f'Failure during get request status: {error}')"""
|
1375
1379
|
|
1376
|
-
async def get_sec_token(self, spin, baseurl):
|
1380
|
+
async def get_sec_token(self, spin, baseurl) -> str:
|
1377
1381
|
"""Get a security token, required for certain set functions."""
|
1378
1382
|
data = {'spin': spin}
|
1379
1383
|
url = eval(f"f'{API_SECTOKEN}'")
|
@@ -1383,7 +1387,7 @@ class Connection:
|
|
1383
1387
|
else:
|
1384
1388
|
raise SeatException('Did not receive a valid security token. Maybewrong SPIN?' )
|
1385
1389
|
|
1386
|
-
async def _setViaAPI(self, endpoint, **data):
|
1390
|
+
async def _setViaAPI(self, endpoint, **data) -> dict | bool:
|
1387
1391
|
"""Data call to API to set a value or to start an action."""
|
1388
1392
|
await self.set_token(self._session_auth_brand)
|
1389
1393
|
try:
|
@@ -1416,7 +1420,7 @@ class Connection:
|
|
1416
1420
|
raise
|
1417
1421
|
return False
|
1418
1422
|
|
1419
|
-
async def _setViaPUTtoAPI(self, endpoint, **data):
|
1423
|
+
async def _setViaPUTtoAPI(self, endpoint, **data) -> dict | bool:
|
1420
1424
|
"""PUT call to API to set a value or to start an action."""
|
1421
1425
|
await self.set_token(self._session_auth_brand)
|
1422
1426
|
try:
|
@@ -1449,7 +1453,7 @@ class Connection:
|
|
1449
1453
|
raise
|
1450
1454
|
return False
|
1451
1455
|
|
1452
|
-
async def subscribe(self, vin, credentials):
|
1456
|
+
async def subscribe(self, vin, credentials) -> dict | bool:
|
1453
1457
|
url = f'{APP_URI}/v2/subscriptions'
|
1454
1458
|
deviceId = credentials.get('gcm',{}).get('app_id','')
|
1455
1459
|
token = credentials.get('fcm',{}).get('registration',{}).get('token','')
|
@@ -1498,7 +1502,7 @@ class Connection:
|
|
1498
1502
|
raise
|
1499
1503
|
return False
|
1500
1504
|
|
1501
|
-
async def setCharger(self, vin, baseurl, mode, data):
|
1505
|
+
async def setCharger(self, vin, baseurl, mode, data) -> dict | bool:
|
1502
1506
|
"""Start/Stop charger."""
|
1503
1507
|
if mode in {'start', 'stop'}:
|
1504
1508
|
capability='charging'
|
@@ -1509,7 +1513,7 @@ class Connection:
|
|
1509
1513
|
_LOGGER.error(f'Not yet implemented. Mode: {mode}. Command ignored')
|
1510
1514
|
raise
|
1511
1515
|
|
1512
|
-
async def setClimater(self, vin, baseurl, mode, data, spin):
|
1516
|
+
async def setClimater(self, vin, baseurl, mode, data, spin) -> dict | bool:
|
1513
1517
|
"""Execute climatisation actions."""
|
1514
1518
|
try:
|
1515
1519
|
# Only get security token if auxiliary heater is to be started
|
@@ -1538,7 +1542,7 @@ class Connection:
|
|
1538
1542
|
raise
|
1539
1543
|
return False
|
1540
1544
|
|
1541
|
-
async def setDeparturetimer(self, vin, baseurl, data, spin):
|
1545
|
+
async def setDeparturetimer(self, vin, baseurl, data, spin) -> dict | bool:
|
1542
1546
|
"""Set departure timers."""
|
1543
1547
|
try:
|
1544
1548
|
url= eval(f"f'{API_DEPARTURE_TIMERS}'")
|
@@ -1550,7 +1554,7 @@ class Connection:
|
|
1550
1554
|
raise
|
1551
1555
|
return False
|
1552
1556
|
|
1553
|
-
async def setDepartureprofile(self, vin, baseurl, data, spin):
|
1557
|
+
async def setDepartureprofile(self, vin, baseurl, data, spin) -> dict | bool:
|
1554
1558
|
"""Set departure profiles."""
|
1555
1559
|
try:
|
1556
1560
|
url= eval(f"f'{API_DEPARTURE_PROFILES}'")
|
@@ -1597,11 +1601,11 @@ class Connection:
|
|
1597
1601
|
raise
|
1598
1602
|
return False
|
1599
1603
|
|
1600
|
-
async def setHonkAndFlash(self, vin, baseurl, data):
|
1604
|
+
async def setHonkAndFlash(self, vin, baseurl, data) -> dict | bool:
|
1601
1605
|
"""Execute honk and flash actions."""
|
1602
1606
|
return await self._setViaAPI(eval(f"f'{API_HONK_AND_FLASH}'"), json = data)
|
1603
1607
|
|
1604
|
-
async def setLock(self, vin, baseurl, action,
|
1608
|
+
async def setLock(self, vin, baseurl, action, spin) -> dict | bool:
|
1605
1609
|
"""Remote lock and unlock actions."""
|
1606
1610
|
try:
|
1607
1611
|
# Fetch security token
|
@@ -1619,7 +1623,7 @@ class Connection:
|
|
1619
1623
|
raise
|
1620
1624
|
return False
|
1621
1625
|
|
1622
|
-
async def setPreHeater(self, vin, baseurl, data, spin):
|
1626
|
+
async def setPreHeater(self, vin, baseurl, data, spin) -> dict | bool:
|
1623
1627
|
"""Petrol/diesel parking heater actions."""
|
1624
1628
|
try:
|
1625
1629
|
# Fetch security token
|
@@ -1637,12 +1641,12 @@ class Connection:
|
|
1637
1641
|
raise
|
1638
1642
|
return False
|
1639
1643
|
|
1640
|
-
async def setRefresh(self, vin, baseurl):
|
1644
|
+
async def setRefresh(self, vin, baseurl) -> dict | bool:
|
1641
1645
|
""""Force vehicle data update."""
|
1642
1646
|
return await self._setViaAPI(eval(f"f'{API_REFRESH}'"))
|
1643
1647
|
|
1644
1648
|
#### Token handling ####
|
1645
|
-
async def validate_token(self, token):
|
1649
|
+
async def validate_token(self, token) -> datetime:
|
1646
1650
|
"""Function to validate a single token."""
|
1647
1651
|
try:
|
1648
1652
|
now = datetime.now()
|
@@ -1665,12 +1669,12 @@ class Connection:
|
|
1665
1669
|
return expires
|
1666
1670
|
else:
|
1667
1671
|
_LOGGER.debug(f'Token expired at {expires.strftime("%Y-%m-%d %H:%M:%S")}')
|
1668
|
-
return
|
1672
|
+
return datetime.min # Return value datetime.min means that the token is not valid
|
1669
1673
|
except Exception as e:
|
1670
1674
|
_LOGGER.info(f'Token validation failed, {e}')
|
1671
|
-
return
|
1675
|
+
return datetime.min # Return value datetime.min means that the token is not valid
|
1672
1676
|
|
1673
|
-
async def verify_token(self, token):
|
1677
|
+
async def verify_token(self, token) -> bool:
|
1674
1678
|
"""Function to verify a single token."""
|
1675
1679
|
try:
|
1676
1680
|
req = None
|
@@ -1696,7 +1700,9 @@ class Connection:
|
|
1696
1700
|
if aud == CLIENT_LIST[client].get('CLIENT_ID', ''):
|
1697
1701
|
req = await self._session.get(url = AUTH_TOKENKEYS)
|
1698
1702
|
break
|
1699
|
-
|
1703
|
+
if req == None:
|
1704
|
+
return False
|
1705
|
+
|
1700
1706
|
# Fetch key list
|
1701
1707
|
keys = await req.json()
|
1702
1708
|
pubkeys = {}
|
@@ -1721,9 +1727,9 @@ class Connection:
|
|
1721
1727
|
return False
|
1722
1728
|
except Exception as error:
|
1723
1729
|
_LOGGER.debug(f'Failed to verify {aud} token, error: {error}')
|
1724
|
-
|
1730
|
+
return False
|
1725
1731
|
|
1726
|
-
async def refresh_token(self, client):
|
1732
|
+
async def refresh_token(self, client) -> bool:
|
1727
1733
|
"""Function to refresh tokens for a client."""
|
1728
1734
|
try:
|
1729
1735
|
# Refresh API tokens
|
@@ -1782,7 +1788,7 @@ class Connection:
|
|
1782
1788
|
_LOGGER.warning(f'Could not refresh tokens: {error}')
|
1783
1789
|
return False
|
1784
1790
|
|
1785
|
-
async def set_token(self, client):
|
1791
|
+
async def set_token(self, client) -> bool:
|
1786
1792
|
"""Switch between tokens."""
|
1787
1793
|
# Lock to prevent multiple instances updating tokens simultaneously
|
1788
1794
|
async with self._lock:
|
@@ -1807,7 +1813,7 @@ class Connection:
|
|
1807
1813
|
try:
|
1808
1814
|
# Validate access token for client, refresh if validation fails
|
1809
1815
|
valid = await self.validate_token(self._session_tokens.get(client, {}).get('access_token', ''))
|
1810
|
-
if
|
1816
|
+
if valid == datetime.min:
|
1811
1817
|
_LOGGER.debug(f'Tokens for "{client}" are invalid')
|
1812
1818
|
# Try to refresh tokens for client
|
1813
1819
|
if await self.refresh_token(client) is not True:
|
@@ -1817,8 +1823,9 @@ class Connection:
|
|
1817
1823
|
pass
|
1818
1824
|
else:
|
1819
1825
|
try:
|
1820
|
-
dt = datetime.fromtimestamp(valid)
|
1821
|
-
_LOGGER.debug(f'Access token for "{client}" is valid until {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
1826
|
+
#dt = datetime.fromtimestamp(valid)
|
1827
|
+
#_LOGGER.debug(f'Access token for "{client}" is valid until {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
1828
|
+
_LOGGER.debug(f'Access token for "{client}" is valid until {valid.strftime("%Y-%m-%d %H:%M:%S")}')
|
1822
1829
|
except:
|
1823
1830
|
pass
|
1824
1831
|
# Assign token to authorization header
|
@@ -1829,11 +1836,11 @@ class Connection:
|
|
1829
1836
|
|
1830
1837
|
#### Class helpers ####
|
1831
1838
|
@property
|
1832
|
-
def vehicles(self):
|
1839
|
+
def vehicles(self) -> list:
|
1833
1840
|
"""Return list of Vehicle objects."""
|
1834
1841
|
return self._vehicles
|
1835
1842
|
|
1836
|
-
def vehicle(self, vin):
|
1843
|
+
def vehicle(self, vin) -> Any:
|
1837
1844
|
"""Return vehicle object for given vin."""
|
1838
1845
|
return next(
|
1839
1846
|
(
|
@@ -1843,20 +1850,20 @@ class Connection:
|
|
1843
1850
|
), None
|
1844
1851
|
)
|
1845
1852
|
|
1846
|
-
def hash_spin(self, challenge, spin):
|
1853
|
+
def hash_spin(self, challenge, spin) -> str:
|
1847
1854
|
"""Convert SPIN and challenge to hash."""
|
1848
1855
|
spinArray = bytearray.fromhex(spin);
|
1849
1856
|
byteChallenge = bytearray.fromhex(challenge);
|
1850
1857
|
spinArray.extend(byteChallenge)
|
1851
1858
|
return hashlib.sha512(spinArray).hexdigest()
|
1852
1859
|
|
1853
|
-
def addToAnonymisationDict(self, keyword, replacement):
|
1860
|
+
def addToAnonymisationDict(self, keyword, replacement) -> None:
|
1854
1861
|
self._anonymisationDict[keyword] = replacement
|
1855
1862
|
|
1856
|
-
def addToAnonymisationKeys(self, keyword):
|
1863
|
+
def addToAnonymisationKeys(self, keyword) -> None:
|
1857
1864
|
self._anonymisationKeys.add(keyword)
|
1858
1865
|
|
1859
|
-
def anonymise(self, inObj):
|
1866
|
+
def anonymise(self, inObj) -> Any:
|
1860
1867
|
if self._session_anonymise:
|
1861
1868
|
if isinstance(inObj, str):
|
1862
1869
|
for key, value in self._anonymisationDict.items():
|
@@ -1872,9 +1879,9 @@ class Connection:
|
|
1872
1879
|
inObj[i]= self.anonymise(inObj[i])
|
1873
1880
|
return inObj
|
1874
1881
|
|
1875
|
-
async def main():
|
1882
|
+
#async def main():
|
1876
1883
|
"""Main method."""
|
1877
|
-
if '-v' in argv:
|
1884
|
+
"""if '-v' in argv:
|
1878
1885
|
logging.basicConfig(level=logging.INFO)
|
1879
1886
|
elif '-vv' in argv:
|
1880
1887
|
logging.basicConfig(level=logging.DEBUG)
|
@@ -1895,3 +1902,4 @@ async def main():
|
|
1895
1902
|
if __name__ == '__main__':
|
1896
1903
|
loop = asyncio.get_event_loop()
|
1897
1904
|
loop.run_until_complete(main())
|
1905
|
+
"""
|