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 CHANGED
@@ -10,9 +10,11 @@ import pandas as pd
10
10
  from aiohttp import ClientSession
11
11
  from datetime import datetime
12
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)
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 read_config, json_loads
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
- code_challenge = hashlib.sha256(code_verifier.encode("utf-8")).digest()
291
- code_challenge = urlsafe_b64encode(code_challenge).decode("utf-8")
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
- error = parse_qs(urlparse(location).query).get('error', '')[0]
348
- if error == 'login.error.throttled':
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 error == 'login.errors.password_invalid':
352
+ elif errorTxt == 'login.errors.password_invalid':
352
353
  raise SeatAuthenticationException('Invalid credentials')
353
354
  else:
354
- _LOGGER.warning(f'Login failed: {error}')
355
- raise SeatLoginFailedException(error)
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
- error = self._session_tokens[client].get('error', '')
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'{error} - {error_description}')
438
+ raise SeatException(f'{errorTxt} - {error_description}')
438
439
  else:
439
- raise SeatException(error)
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, data, spin):
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 False
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 False
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
- return error
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 not valid:
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
+ """