pycupra 0.1.12__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.
@@ -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
@@ -15,6 +15,8 @@ import string
15
15
  import secrets
16
16
  import xmltodict
17
17
  from copy import deepcopy
18
+ import importlib.metadata
19
+ from typing import Any
18
20
 
19
21
  from PIL import Image
20
22
  from io import BytesIO
@@ -26,8 +28,8 @@ from jwt.exceptions import ExpiredSignatureError
26
28
  import aiohttp
27
29
  from bs4 import BeautifulSoup
28
30
  from base64 import b64decode, b64encode, urlsafe_b64decode, urlsafe_b64encode
29
- from .__version__ import __version__ as lib_version
30
- from .utilities import read_config, json_loads
31
+ #from .__version__ import __version__ as lib_version
32
+ from .utilities import json_loads
31
33
  from .vehicle import Vehicle
32
34
  from .exceptions import (
33
35
  SeatConfigException,
@@ -102,6 +104,7 @@ from .const import (
102
104
  )
103
105
 
104
106
  version_info >= (3, 0) or exit('Python 3 required')
107
+ lib_version = importlib.metadata.version("pycupra")
105
108
 
106
109
  _LOGGER = logging.getLogger(__name__)
107
110
  BRAND_CUPRA = 'cupra'
@@ -225,7 +228,7 @@ class Connection:
225
228
  return False
226
229
 
227
230
  # API login/logout/authorization
228
- async def doLogin(self,**data):
231
+ async def doLogin(self,**data) -> bool:
229
232
  """Login method, clean login or use token from file and refresh it"""
230
233
  #if len(self._session_tokens) > 0:
231
234
  # _LOGGER.info('Revoking old tokens.')
@@ -239,7 +242,7 @@ class Connection:
239
242
  self._clear_cookies()
240
243
  self._vehicles.clear()
241
244
  self._session_tokens = {}
242
- 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()
243
246
  self._session_auth_headers = HEADERS_AUTH.copy()
244
247
  self._session_nonce = self._getNonce()
245
248
  self._session_state = self._getState()
@@ -258,7 +261,7 @@ class Connection:
258
261
  _LOGGER.info('Initiating new login with user name and password.')
259
262
  return await self._authorize(self._session_auth_brand)
260
263
 
261
- async def _authorize(self, client=BRAND_CUPRA):
264
+ async def _authorize(self, client=BRAND_CUPRA) -> bool:
262
265
  """"Login" function. Authorize a certain client type and get tokens."""
263
266
  # Helper functions
264
267
  def extract_csrf(req):
@@ -285,8 +288,8 @@ class Connection:
285
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'))
286
289
  code_verifier = urlsafe_b64encode(os.urandom(40)).decode('utf-8')
287
290
  code_verifier = re.sub('[^a-zA-Z0-9]+', '', code_verifier)
288
- code_challenge = hashlib.sha256(code_verifier.encode("utf-8")).digest()
289
- 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")
290
293
  code_challenge = code_challenge.replace("=", "")
291
294
  authorization_url, state = oauthClient.authorization_url(authorizationEndpoint, code_challenge=code_challenge, code_challenge_method='S256',
292
295
  nonce=self._session_nonce, state=self._session_state)
@@ -342,19 +345,19 @@ class Connection:
342
345
  if location is None:
343
346
  raise SeatException('Login failed')
344
347
  if 'error' in location:
345
- error = parse_qs(urlparse(location).query).get('error', '')[0]
346
- if error == 'login.error.throttled':
348
+ errorTxt = parse_qs(urlparse(location).query).get('error', '')[0]
349
+ if errorTxt == 'login.error.throttled':
347
350
  timeout = parse_qs(urlparse(location).query).get('enableNextButtonAfterSeconds', '')[0]
348
351
  raise SeatAccountLockedException(f'Account is locked for another {timeout} seconds')
349
- elif error == 'login.errors.password_invalid':
352
+ elif errorTxt == 'login.errors.password_invalid':
350
353
  raise SeatAuthenticationException('Invalid credentials')
351
354
  else:
352
- _LOGGER.warning(f'Login failed: {error}')
353
- raise SeatLoginFailedException(error)
355
+ _LOGGER.warning(f'Login failed: {errorTxt}')
356
+ raise SeatLoginFailedException(errorTxt)
354
357
  if 'terms-and-conditions' in location:
355
358
  raise SeatEULAException('The terms and conditions must be accepted first at your local SEAT/Cupra site, e.g. "https://cupraofficial.se/"')
356
359
  if 'user_id' in location: # Get the user_id which is needed for some later requests
357
- 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]
358
361
  self.addToAnonymisationDict(self._user_id,'[USER_ID_ANONYMISED]')
359
362
  #_LOGGER.debug('Got user_id: %s' % self._user_id)
360
363
  if self._session_fulldebug:
@@ -386,7 +389,7 @@ class Connection:
386
389
 
387
390
  _LOGGER.debug('Received authorization code, exchange for tokens.')
388
391
  # Extract code and tokens
389
- auth_code = parse_qs(urlparse(location).query).get('code')[0]
392
+ auth_code = parse_qs(urlparse(location).query).get('code', [''])[0]
390
393
  # Save access, identity and refresh tokens according to requested client"""
391
394
  if client=='cupra':
392
395
  # oauthClient.fetch_token() does not work in home assistant, using POST request instead
@@ -429,12 +432,12 @@ class Connection:
429
432
  if '_token' in key:
430
433
  self._session_tokens[client][key] = token_data[key]
431
434
  if 'error' in self._session_tokens[client]:
432
- error = self._session_tokens[client].get('error', '')
435
+ errorTxt = self._session_tokens[client].get('error', '')
433
436
  if 'error_description' in self._session_tokens[client]:
434
437
  error_description = self._session_tokens[client].get('error_description', '')
435
- raise SeatException(f'{error} - {error_description}')
438
+ raise SeatException(f'{errorTxt} - {error_description}')
436
439
  else:
437
- raise SeatException(error)
440
+ raise SeatException(errorTxt)
438
441
  if self._session_fulldebug:
439
442
  for key in self._session_tokens.get(client, {}):
440
443
  if 'token' in key:
@@ -557,7 +560,7 @@ class Connection:
557
560
  )
558
561
  return req.headers.get('Location', None)
559
562
 
560
- async def terminate(self):
563
+ async def terminate(self) -> None:
561
564
  """Log out from connect services"""
562
565
  for v in self.vehicles:
563
566
  _LOGGER.debug(self.anonymise(f'Calling stopFirebase() for vehicle {v.vin}'))
@@ -568,7 +571,7 @@ class Connection:
568
571
  v.firebaseStatus = FIREBASE_STATUS_NOT_INITIALISED
569
572
  await self.logout()
570
573
 
571
- async def logout(self):
574
+ async def logout(self) -> None:
572
575
  """Logout, revoke tokens."""
573
576
  _LOGGER.info(f'Initiating logout.')
574
577
  self._session_headers.pop('Authorization', None)
@@ -629,6 +632,9 @@ class Connection:
629
632
  return data
630
633
  except Exception as e:
631
634
  _LOGGER.debug(f'Got non HTTP related error: {e}')
635
+ return {
636
+ 'error_description': 'Non HTTP related error'
637
+ }
632
638
 
633
639
  async def post(self, url, **data):
634
640
  """Perform a HTTP POST."""
@@ -746,7 +752,7 @@ class Connection:
746
752
  return False
747
753
 
748
754
  # Class get data functions
749
- async def update_all(self):
755
+ async def update_all(self) -> bool:
750
756
  """Update status."""
751
757
  try:
752
758
  # Get all Vehicle objects and update in parallell
@@ -771,7 +777,7 @@ class Connection:
771
777
  raise
772
778
  return False
773
779
 
774
- async def get_userData(self):
780
+ async def get_userData(self) -> dict:
775
781
  """Fetch user profile."""
776
782
  await self.set_token(self._session_auth_brand)
777
783
  userData={}
@@ -796,7 +802,7 @@ class Connection:
796
802
  self._userData=userData
797
803
  return userData
798
804
 
799
- async def get_vehicles(self):
805
+ async def get_vehicles(self) -> list:
800
806
  """Fetch vehicle information from user profile."""
801
807
  api_vehicles = []
802
808
  # Check if user needs to update consent
@@ -936,7 +942,7 @@ class Connection:
936
942
  _LOGGER.debug(f'Could not get consent information, error {error}')
937
943
  return False"""
938
944
 
939
- async def getBasicCarData(self, vin, baseurl):
945
+ async def getBasicCarData(self, vin, baseurl) -> dict | bool:
940
946
  """Get car information from customer profile, VIN, nickname, etc."""
941
947
  await self.set_token(self._session_auth_brand)
942
948
  data={}
@@ -954,7 +960,7 @@ class Connection:
954
960
  return False
955
961
  return data
956
962
 
957
- async def getMileage(self, vin, baseurl):
963
+ async def getMileage(self, vin, baseurl) -> dict | bool:
958
964
  """Get car information from customer profile, VIN, nickname, etc."""
959
965
  await self.set_token(self._session_auth_brand)
960
966
  data={}
@@ -972,7 +978,7 @@ class Connection:
972
978
  return False
973
979
  return data
974
980
 
975
- async def getVehicleHealthWarnings(self, vin, baseurl):
981
+ async def getVehicleHealthWarnings(self, vin, baseurl) -> dict | bool:
976
982
  """Get car information from customer profile, VIN, nickname, etc."""
977
983
  await self.set_token(self._session_auth_brand)
978
984
  data={}
@@ -1008,7 +1014,7 @@ class Connection:
1008
1014
  data = {'error': 'unknown'}
1009
1015
  return data"""
1010
1016
 
1011
- async def getModelImageURL(self, vin, baseurl):
1017
+ async def getModelImageURL(self, vin, baseurl) -> dict | None:
1012
1018
  """Construct the URL for the model image."""
1013
1019
  await self.set_token(self._session_auth_brand)
1014
1020
  try:
@@ -1017,7 +1023,7 @@ class Connection:
1017
1023
  url=eval(f"f'{API_IMAGE}'"),
1018
1024
  )
1019
1025
  if response.get('front',False):
1020
- images={}
1026
+ images: dict[str, str] ={}
1021
1027
  for pos in {'front', 'side', 'top', 'rear'}:
1022
1028
  if pos in response:
1023
1029
  pic = await self._request(
@@ -1058,7 +1064,7 @@ class Connection:
1058
1064
  _LOGGER.debug('Could not fetch Model image URL, message signing failed.')
1059
1065
  return None
1060
1066
 
1061
- async def getVehicleStatusReport(self, vin, baseurl):
1067
+ async def getVehicleStatusReport(self, vin, baseurl) -> dict | bool:
1062
1068
  """Get stored vehicle status report (Connect services)."""
1063
1069
  data={}
1064
1070
  await self.set_token(self._session_auth_brand)
@@ -1076,7 +1082,7 @@ class Connection:
1076
1082
  return False
1077
1083
  return data
1078
1084
 
1079
- async def getMaintenance(self, vin, baseurl):
1085
+ async def getMaintenance(self, vin, baseurl) -> dict | bool:
1080
1086
  """Get stored vehicle status report (Connect services)."""
1081
1087
  data={}
1082
1088
  await self.set_token(self._session_auth_brand)
@@ -1094,7 +1100,7 @@ class Connection:
1094
1100
  return False
1095
1101
  return data
1096
1102
 
1097
- async def getTripStatistics(self, vin, baseurl, supportsCyclicTrips):
1103
+ async def getTripStatistics(self, vin, baseurl, supportsCyclicTrips) -> dict | bool:
1098
1104
  """Get short term and cyclic trip statistics."""
1099
1105
  await self.set_token(self._session_auth_brand)
1100
1106
  if self._session_tripStatisticsStartDate==None:
@@ -1104,7 +1110,7 @@ class Connection:
1104
1110
  else:
1105
1111
  startDate = self._session_tripStatisticsStartDate
1106
1112
  try:
1107
- data={'tripstatistics': {}}
1113
+ data: dict[str, dict] ={'tripstatistics': {}}
1108
1114
  if supportsCyclicTrips:
1109
1115
  dataType='CYCLIC'
1110
1116
  response = await self.get(eval(f"f'{API_TRIP}'"))
@@ -1134,7 +1140,7 @@ class Connection:
1134
1140
  _LOGGER.warning(f'Could not fetch trip statistics, error: {error}')
1135
1141
  return False
1136
1142
 
1137
- async def getPosition(self, vin, baseurl):
1143
+ async def getPosition(self, vin, baseurl) -> dict | bool:
1138
1144
  """Get position data."""
1139
1145
  await self.set_token(self._session_auth_brand)
1140
1146
  try:
@@ -1169,7 +1175,7 @@ class Connection:
1169
1175
  _LOGGER.warning(f'Could not fetch position, error: {error}')
1170
1176
  return False
1171
1177
 
1172
- async def getDeparturetimer(self, vin, baseurl):
1178
+ async def getDeparturetimer(self, vin, baseurl) -> dict | bool:
1173
1179
  """Get departure timers."""
1174
1180
  await self.set_token(self._session_auth_brand)
1175
1181
  try:
@@ -1186,7 +1192,7 @@ class Connection:
1186
1192
  _LOGGER.warning(f'Could not fetch departure timers, error: {error}')
1187
1193
  return False
1188
1194
 
1189
- async def getDepartureprofiles(self, vin, baseurl):
1195
+ async def getDepartureprofiles(self, vin, baseurl) -> dict | bool:
1190
1196
  """Get departure timers."""
1191
1197
  await self.set_token(self._session_auth_brand)
1192
1198
  try:
@@ -1208,7 +1214,7 @@ class Connection:
1208
1214
  _LOGGER.warning(f'Could not fetch departure profiles, error: {error}')
1209
1215
  return False
1210
1216
 
1211
- async def getClimater(self, vin, baseurl, oldClimatingData):
1217
+ async def getClimater(self, vin, baseurl, oldClimatingData) -> dict | bool:
1212
1218
  """Get climatisation data."""
1213
1219
  #data={}
1214
1220
  #data['climater']={}
@@ -1238,7 +1244,7 @@ class Connection:
1238
1244
  return False
1239
1245
  return data
1240
1246
 
1241
- async def getCharger(self, vin, baseurl, oldChargingData):
1247
+ async def getCharger(self, vin, baseurl, oldChargingData) -> dict | bool:
1242
1248
  """Get charger data."""
1243
1249
  await self.set_token(self._session_auth_brand)
1244
1250
  try:
@@ -1296,7 +1302,7 @@ class Connection:
1296
1302
  _LOGGER.warning(f'Could not fetch charger, error: {error}')
1297
1303
  return False
1298
1304
 
1299
- async def getPreHeater(self, vin, baseurl):
1305
+ async def getPreHeater(self, vin, baseurl) -> dict | bool:
1300
1306
  """Get parking heater data."""
1301
1307
  await self.set_token(self._session_auth_brand)
1302
1308
  try:
@@ -1371,7 +1377,7 @@ class Connection:
1371
1377
  _LOGGER.warning(f'Failure during get request status: {error}')
1372
1378
  raise SeatException(f'Failure during get request status: {error}')"""
1373
1379
 
1374
- async def get_sec_token(self, spin, baseurl):
1380
+ async def get_sec_token(self, spin, baseurl) -> str:
1375
1381
  """Get a security token, required for certain set functions."""
1376
1382
  data = {'spin': spin}
1377
1383
  url = eval(f"f'{API_SECTOKEN}'")
@@ -1381,7 +1387,7 @@ class Connection:
1381
1387
  else:
1382
1388
  raise SeatException('Did not receive a valid security token. Maybewrong SPIN?' )
1383
1389
 
1384
- async def _setViaAPI(self, endpoint, **data):
1390
+ async def _setViaAPI(self, endpoint, **data) -> dict | bool:
1385
1391
  """Data call to API to set a value or to start an action."""
1386
1392
  await self.set_token(self._session_auth_brand)
1387
1393
  try:
@@ -1414,7 +1420,7 @@ class Connection:
1414
1420
  raise
1415
1421
  return False
1416
1422
 
1417
- async def _setViaPUTtoAPI(self, endpoint, **data):
1423
+ async def _setViaPUTtoAPI(self, endpoint, **data) -> dict | bool:
1418
1424
  """PUT call to API to set a value or to start an action."""
1419
1425
  await self.set_token(self._session_auth_brand)
1420
1426
  try:
@@ -1447,7 +1453,7 @@ class Connection:
1447
1453
  raise
1448
1454
  return False
1449
1455
 
1450
- async def subscribe(self, vin, credentials):
1456
+ async def subscribe(self, vin, credentials) -> dict | bool:
1451
1457
  url = f'{APP_URI}/v2/subscriptions'
1452
1458
  deviceId = credentials.get('gcm',{}).get('app_id','')
1453
1459
  token = credentials.get('fcm',{}).get('registration',{}).get('token','')
@@ -1496,7 +1502,7 @@ class Connection:
1496
1502
  raise
1497
1503
  return False
1498
1504
 
1499
- async def setCharger(self, vin, baseurl, mode, data):
1505
+ async def setCharger(self, vin, baseurl, mode, data) -> dict | bool:
1500
1506
  """Start/Stop charger."""
1501
1507
  if mode in {'start', 'stop'}:
1502
1508
  capability='charging'
@@ -1507,7 +1513,7 @@ class Connection:
1507
1513
  _LOGGER.error(f'Not yet implemented. Mode: {mode}. Command ignored')
1508
1514
  raise
1509
1515
 
1510
- async def setClimater(self, vin, baseurl, mode, data, spin):
1516
+ async def setClimater(self, vin, baseurl, mode, data, spin) -> dict | bool:
1511
1517
  """Execute climatisation actions."""
1512
1518
  try:
1513
1519
  # Only get security token if auxiliary heater is to be started
@@ -1536,7 +1542,7 @@ class Connection:
1536
1542
  raise
1537
1543
  return False
1538
1544
 
1539
- async def setDeparturetimer(self, vin, baseurl, data, spin):
1545
+ async def setDeparturetimer(self, vin, baseurl, data, spin) -> dict | bool:
1540
1546
  """Set departure timers."""
1541
1547
  try:
1542
1548
  url= eval(f"f'{API_DEPARTURE_TIMERS}'")
@@ -1548,7 +1554,7 @@ class Connection:
1548
1554
  raise
1549
1555
  return False
1550
1556
 
1551
- async def setDepartureprofile(self, vin, baseurl, data, spin):
1557
+ async def setDepartureprofile(self, vin, baseurl, data, spin) -> dict | bool:
1552
1558
  """Set departure profiles."""
1553
1559
  try:
1554
1560
  url= eval(f"f'{API_DEPARTURE_PROFILES}'")
@@ -1595,11 +1601,11 @@ class Connection:
1595
1601
  raise
1596
1602
  return False
1597
1603
 
1598
- async def setHonkAndFlash(self, vin, baseurl, data):
1604
+ async def setHonkAndFlash(self, vin, baseurl, data) -> dict | bool:
1599
1605
  """Execute honk and flash actions."""
1600
1606
  return await self._setViaAPI(eval(f"f'{API_HONK_AND_FLASH}'"), json = data)
1601
1607
 
1602
- async def setLock(self, vin, baseurl, action, data, spin):
1608
+ async def setLock(self, vin, baseurl, action, spin) -> dict | bool:
1603
1609
  """Remote lock and unlock actions."""
1604
1610
  try:
1605
1611
  # Fetch security token
@@ -1617,7 +1623,7 @@ class Connection:
1617
1623
  raise
1618
1624
  return False
1619
1625
 
1620
- async def setPreHeater(self, vin, baseurl, data, spin):
1626
+ async def setPreHeater(self, vin, baseurl, data, spin) -> dict | bool:
1621
1627
  """Petrol/diesel parking heater actions."""
1622
1628
  try:
1623
1629
  # Fetch security token
@@ -1635,12 +1641,12 @@ class Connection:
1635
1641
  raise
1636
1642
  return False
1637
1643
 
1638
- async def setRefresh(self, vin, baseurl):
1644
+ async def setRefresh(self, vin, baseurl) -> dict | bool:
1639
1645
  """"Force vehicle data update."""
1640
1646
  return await self._setViaAPI(eval(f"f'{API_REFRESH}'"))
1641
1647
 
1642
1648
  #### Token handling ####
1643
- async def validate_token(self, token):
1649
+ async def validate_token(self, token) -> datetime:
1644
1650
  """Function to validate a single token."""
1645
1651
  try:
1646
1652
  now = datetime.now()
@@ -1663,12 +1669,12 @@ class Connection:
1663
1669
  return expires
1664
1670
  else:
1665
1671
  _LOGGER.debug(f'Token expired at {expires.strftime("%Y-%m-%d %H:%M:%S")}')
1666
- return False
1672
+ return datetime.min # Return value datetime.min means that the token is not valid
1667
1673
  except Exception as e:
1668
1674
  _LOGGER.info(f'Token validation failed, {e}')
1669
- return False
1675
+ return datetime.min # Return value datetime.min means that the token is not valid
1670
1676
 
1671
- async def verify_token(self, token):
1677
+ async def verify_token(self, token) -> bool:
1672
1678
  """Function to verify a single token."""
1673
1679
  try:
1674
1680
  req = None
@@ -1694,7 +1700,9 @@ class Connection:
1694
1700
  if aud == CLIENT_LIST[client].get('CLIENT_ID', ''):
1695
1701
  req = await self._session.get(url = AUTH_TOKENKEYS)
1696
1702
  break
1697
-
1703
+ if req == None:
1704
+ return False
1705
+
1698
1706
  # Fetch key list
1699
1707
  keys = await req.json()
1700
1708
  pubkeys = {}
@@ -1719,9 +1727,9 @@ class Connection:
1719
1727
  return False
1720
1728
  except Exception as error:
1721
1729
  _LOGGER.debug(f'Failed to verify {aud} token, error: {error}')
1722
- return error
1730
+ return False
1723
1731
 
1724
- async def refresh_token(self, client):
1732
+ async def refresh_token(self, client) -> bool:
1725
1733
  """Function to refresh tokens for a client."""
1726
1734
  try:
1727
1735
  # Refresh API tokens
@@ -1780,7 +1788,7 @@ class Connection:
1780
1788
  _LOGGER.warning(f'Could not refresh tokens: {error}')
1781
1789
  return False
1782
1790
 
1783
- async def set_token(self, client):
1791
+ async def set_token(self, client) -> bool:
1784
1792
  """Switch between tokens."""
1785
1793
  # Lock to prevent multiple instances updating tokens simultaneously
1786
1794
  async with self._lock:
@@ -1805,7 +1813,7 @@ class Connection:
1805
1813
  try:
1806
1814
  # Validate access token for client, refresh if validation fails
1807
1815
  valid = await self.validate_token(self._session_tokens.get(client, {}).get('access_token', ''))
1808
- if not valid:
1816
+ if valid == datetime.min:
1809
1817
  _LOGGER.debug(f'Tokens for "{client}" are invalid')
1810
1818
  # Try to refresh tokens for client
1811
1819
  if await self.refresh_token(client) is not True:
@@ -1815,8 +1823,9 @@ class Connection:
1815
1823
  pass
1816
1824
  else:
1817
1825
  try:
1818
- dt = datetime.fromtimestamp(valid)
1819
- _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")}')
1820
1829
  except:
1821
1830
  pass
1822
1831
  # Assign token to authorization header
@@ -1827,11 +1836,11 @@ class Connection:
1827
1836
 
1828
1837
  #### Class helpers ####
1829
1838
  @property
1830
- def vehicles(self):
1839
+ def vehicles(self) -> list:
1831
1840
  """Return list of Vehicle objects."""
1832
1841
  return self._vehicles
1833
1842
 
1834
- def vehicle(self, vin):
1843
+ def vehicle(self, vin) -> Any:
1835
1844
  """Return vehicle object for given vin."""
1836
1845
  return next(
1837
1846
  (
@@ -1841,20 +1850,20 @@ class Connection:
1841
1850
  ), None
1842
1851
  )
1843
1852
 
1844
- def hash_spin(self, challenge, spin):
1853
+ def hash_spin(self, challenge, spin) -> str:
1845
1854
  """Convert SPIN and challenge to hash."""
1846
1855
  spinArray = bytearray.fromhex(spin);
1847
1856
  byteChallenge = bytearray.fromhex(challenge);
1848
1857
  spinArray.extend(byteChallenge)
1849
1858
  return hashlib.sha512(spinArray).hexdigest()
1850
1859
 
1851
- def addToAnonymisationDict(self, keyword, replacement):
1860
+ def addToAnonymisationDict(self, keyword, replacement) -> None:
1852
1861
  self._anonymisationDict[keyword] = replacement
1853
1862
 
1854
- def addToAnonymisationKeys(self, keyword):
1863
+ def addToAnonymisationKeys(self, keyword) -> None:
1855
1864
  self._anonymisationKeys.add(keyword)
1856
1865
 
1857
- def anonymise(self, inObj):
1866
+ def anonymise(self, inObj) -> Any:
1858
1867
  if self._session_anonymise:
1859
1868
  if isinstance(inObj, str):
1860
1869
  for key, value in self._anonymisationDict.items():
@@ -1870,9 +1879,9 @@ class Connection:
1870
1879
  inObj[i]= self.anonymise(inObj[i])
1871
1880
  return inObj
1872
1881
 
1873
- async def main():
1882
+ #async def main():
1874
1883
  """Main method."""
1875
- if '-v' in argv:
1884
+ """if '-v' in argv:
1876
1885
  logging.basicConfig(level=logging.INFO)
1877
1886
  elif '-vv' in argv:
1878
1887
  logging.basicConfig(level=logging.DEBUG)
@@ -1893,3 +1902,4 @@ async def main():
1893
1902
  if __name__ == '__main__':
1894
1903
  loop = asyncio.get_event_loop()
1895
1904
  loop.run_until_complete(main())
1905
+ """