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.
- example/PyCupra.py +611 -0
- example/PyCupra_ExportDrivingData.py +113 -0
- pycupra/connection.py +78 -68
- pycupra/dashboard.py +36 -36
- pycupra/firebase.py +11 -8
- pycupra/firebase_messaging/android_checkin_pb2.pyi +257 -257
- 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.14.dist-info/METADATA +63 -0
- {pycupra-0.1.12.dist-info → pycupra-0.1.14.dist-info}/RECORD +15 -14
- pycupra-0.1.14.dist-info/top_level.txt +3 -0
- pycupra/__version__.py +0 -6
- pycupra-0.1.12.dist-info/METADATA +0 -13
- pycupra-0.1.12.dist-info/top_level.txt +0 -1
- {pycupra-0.1.12.dist-info → pycupra-0.1.14.dist-info}/WHEEL +0 -0
- {pycupra-0.1.12.dist-info → pycupra-0.1.14.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
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
|
-
|
289
|
-
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")
|
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
|
-
|
346
|
-
if
|
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
|
352
|
+
elif errorTxt == 'login.errors.password_invalid':
|
350
353
|
raise SeatAuthenticationException('Invalid credentials')
|
351
354
|
else:
|
352
|
-
_LOGGER.warning(f'Login failed: {
|
353
|
-
raise SeatLoginFailedException(
|
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
|
-
|
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'{
|
438
|
+
raise SeatException(f'{errorTxt} - {error_description}')
|
436
439
|
else:
|
437
|
-
raise SeatException(
|
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,
|
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
|
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
|
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
|
-
|
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
|
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
|
+
"""
|