pycupra 0.0.15__py3-none-any.whl → 0.1.0__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.
pycupra/__init__.py CHANGED
@@ -5,3 +5,4 @@ For more details and documentation, visit the github page at https://github.com/
5
5
  """
6
6
 
7
7
  from pycupra.connection import Connection
8
+
pycupra/__version__.py CHANGED
@@ -3,4 +3,4 @@ pycupra - A Python 3 library for interacting with the My Cupra/My Seat portal.
3
3
 
4
4
  For more details and documentation, visit the github page at https://github.com/WulfgarW/pycupra
5
5
  """
6
- __version__ = "0.0.15"
6
+ __version__ = "0.1.0"
pycupra/connection.py CHANGED
@@ -46,7 +46,7 @@ from requests_oauthlib import OAuth2Session
46
46
  from oauthlib.oauth2.rfc6749.parameters import parse_authorization_code_response, parse_token_response, prepare_grant_uri
47
47
 
48
48
  from aiohttp import ClientSession, ClientTimeout
49
- from aiohttp.hdrs import METH_GET, METH_POST, METH_PUT
49
+ from aiohttp.hdrs import METH_GET, METH_POST, METH_PUT, METH_DELETE
50
50
 
51
51
  from .const import (
52
52
  HEADERS_SESSION,
@@ -643,6 +643,8 @@ class Connection:
643
643
  res = {'status_code': response.status}
644
644
  elif response.status == 202 and method==METH_PUT:
645
645
  res = response
646
+ elif response.status == 200 and method==METH_DELETE:
647
+ res = response
646
648
  elif response.status >= 200 or response.status <= 300:
647
649
  # If this is a revoke token url, expect Content-Length 0 and return
648
650
  if int(response.headers.get('Content-Length', 0)) == 0 and 'revoke' in url:
@@ -759,7 +761,7 @@ class Connection:
759
761
  # Check if user needs to update consent
760
762
  try:
761
763
  await self.set_token(self._session_auth_brand)
762
- _LOGGER.debug('Achtung! getConsentInfo auskommentiert')
764
+ #_LOGGER.debug('Achtung! getConsentInfo auskommentiert')
763
765
  response = await self.get(eval(f"f'{API_MBB_STATUSDATA}'"))
764
766
  if response.get('profileCompleted','incomplete'):
765
767
  if response.get('profileCompleted',False):
@@ -1182,6 +1184,8 @@ class Connection:
1182
1184
  """Get charger data."""
1183
1185
  await self.set_token(self._session_auth_brand)
1184
1186
  try:
1187
+ chargingStatus = {}
1188
+ chargingInfo = {}
1185
1189
  response = await self.get(eval(f"f'{API_CHARGING}/status'"))
1186
1190
  if response.get('battery', {}):
1187
1191
  chargingStatus = response
@@ -1203,12 +1207,16 @@ class Connection:
1203
1207
  _LOGGER.warning(f'Could not fetch charging modes, HTTP status code: {response.get("status_code")}')
1204
1208
  else:
1205
1209
  _LOGGER.info('Unhandled error while trying to fetch charging modes')"""
1206
- data = {'charging': {
1207
- 'status': chargingStatus,
1208
- 'info' : chargingInfo,
1209
- #'modes' : chargingModes,
1210
- }
1211
- }
1210
+ if chargingStatus != {} and chargingInfo != {}:
1211
+ data = {'charging': {
1212
+ 'status': chargingStatus,
1213
+ 'info' : chargingInfo,
1214
+ #'modes' : chargingModes,
1215
+ }
1216
+ }
1217
+ else:
1218
+ _LOGGER.warning(f'getCharger() got no valid data. Returning None')
1219
+ return None
1212
1220
  return data
1213
1221
  except Exception as error:
1214
1222
  _LOGGER.warning(f'Could not fetch charger, error: {error}')
@@ -1321,7 +1329,7 @@ class Connection:
1321
1329
  if 'state' in k.lower():
1322
1330
  data['state'] = response.get(key).get(k)
1323
1331
  else:
1324
- if 'Id' in key:
1332
+ if 'Id' in key or 'id' in key:
1325
1333
  data['id'] = str(response.get(key))
1326
1334
  if 'State' in key:
1327
1335
  data['state'] = response.get(key)
@@ -1365,6 +1373,55 @@ class Connection:
1365
1373
  raise
1366
1374
  return False
1367
1375
 
1376
+ async def subscribe(self, vin, credentials):
1377
+ url = f'{APP_URI}/v2/subscriptions'
1378
+ deviceId = credentials.get('gcm',{}).get('app_id','')
1379
+ token = credentials.get('fcm',{}).get('registration',{}).get('token','')
1380
+
1381
+ data = {
1382
+ "deviceId": deviceId,
1383
+ "locale":"en_GB",
1384
+ "services":{"charging":True,"climatisation":True},
1385
+ "token": token,
1386
+ "userId": self._user_id,
1387
+ "vin":vin
1388
+ }
1389
+ return await self._setViaAPI(url, json=data)
1390
+
1391
+ async def deleteSubscription(self, credentials):
1392
+ await self.set_token(self._session_auth_brand)
1393
+ try:
1394
+ id = credentials.get('subscription',{}).get('id','')
1395
+ url = f'{APP_URI}/v1/subscriptions/{id}'
1396
+ response = await self._request(METH_DELETE, url)
1397
+ if response.status==200:
1398
+ _LOGGER.debug(f'Subscription {id} successfully deleted.')
1399
+ return response
1400
+ else:
1401
+ _LOGGER.debug(f'API did not successfully delete subscription.')
1402
+ raise SeatException(f'Invalid or no response for endpoint {url}')
1403
+ return response
1404
+ except aiohttp.client_exceptions.ClientResponseError as error:
1405
+ _LOGGER.debug(f'Request failed. Id: {id}, HTTP request headers: {self._session_headers}')
1406
+ if error.status == 401:
1407
+ _LOGGER.error('Unauthorized')
1408
+ elif error.status == 400:
1409
+ _LOGGER.error(f'Bad request')
1410
+ elif error.status == 429:
1411
+ _LOGGER.warning('Too many requests. Further requests can only be made after the end of next trip in order to protect your vehicles battery.')
1412
+ return 429
1413
+ elif error.status == 500:
1414
+ _LOGGER.error('Internal server error, server might be temporarily unavailable')
1415
+ elif error.status == 502:
1416
+ _LOGGER.error('Bad gateway, this function may not be implemented for this vehicle')
1417
+ else:
1418
+ _LOGGER.error(f'Unhandled HTTP exception: {error}')
1419
+ #return False
1420
+ except Exception as error:
1421
+ _LOGGER.error(f'Error: {error}')
1422
+ raise
1423
+ return False
1424
+
1368
1425
  async def setCharger(self, vin, baseurl, mode, data):
1369
1426
  """Start/Stop charger."""
1370
1427
  if mode in {'start', 'stop'}:
pycupra/const.py CHANGED
@@ -181,3 +181,14 @@ REQ_STATUS = {
181
181
  'vsr': 'fs-car/bs/vsr/v1/{BRAND}/{COUNTRY}/vehicles/{vin}/requests/{id}/jobstatus',
182
182
  'default': 'fs-car/bs/{section}/v1/{BRAND}/{COUNTRY}/vehicles/{vin}/requests/{id}/status'
183
183
  }
184
+
185
+ FCM_PROJECT_ID='ola-apps-prod'
186
+ FCM_APP_ID={
187
+ 'cupra': '1:530284123617:android:9b9ba5a87c7ffd37fbeea0',
188
+ 'seat': '1:530284123617:android:d6187613ac3d7b08fbeea0'
189
+ }
190
+ FCM_API_KEY='AIzaSyCoSp1zitklb1EDj5yQumN0VNhDizJQHLk'
191
+ FIREBASE_STATUS_NOT_INITIALISED= 0
192
+ FIREBASE_STATUS_ACTIVATED= 1
193
+ FIREBASE_STATUS_NOT_WANTED= -2
194
+ FIREBASE_STATUS_ACTIVATION_FAILED= -1
pycupra/dashboard.py CHANGED
@@ -1037,6 +1037,13 @@ def create_instruments():
1037
1037
  unit="%",
1038
1038
  device_class="battery"
1039
1039
  ),
1040
+ Sensor(
1041
+ attr="target_soc",
1042
+ name="Target state of charge",
1043
+ icon="mdi:battery-positive",
1044
+ unit="%",
1045
+ device_class="battery"
1046
+ ),
1040
1047
  Sensor(
1041
1048
  attr="adblue_level",
1042
1049
  name="Adblue level",
pycupra/firebase.py ADDED
@@ -0,0 +1,73 @@
1
+ import logging
2
+ import asyncio
3
+ import os
4
+ import json
5
+ import string
6
+ import secrets
7
+
8
+ from .firebase_messaging import FcmPushClient, FcmRegisterConfig
9
+
10
+ from .const import (
11
+ FCM_PROJECT_ID,
12
+ FCM_API_KEY,
13
+ FCM_APP_ID
14
+ )
15
+
16
+ _LOGGER = logging.getLogger(__name__)
17
+
18
+ class Firebase():
19
+ async def firebaseStart(self, onNotificationFunc, firebaseCredentialsFileName, brand='cupra'):
20
+ """ Starts the firebase cloud messaging receiver """
21
+ loop = asyncio.get_running_loop()
22
+ credentials = await loop.run_in_executor(None, readFCMCredsFile, firebaseCredentialsFileName)
23
+ #credentials = readFCMCredsFile(firebaseCredentialsFileName)
24
+ if credentials == {}:
25
+ credentials =''
26
+
27
+ fcm_project_id=FCM_PROJECT_ID
28
+ fcm_app_id=FCM_APP_ID[brand]
29
+ fcm_api_key=FCM_API_KEY
30
+ chars = string.ascii_letters + string.digits
31
+ fcmMessageSenderId = ''.join(secrets.choice(chars) for i in range(16))
32
+ fcmMessageSenderId= 'fxpWQ_'+fcmMessageSenderId
33
+
34
+
35
+ fcm_config = FcmRegisterConfig(fcm_project_id, fcm_app_id, fcm_api_key, fcmMessageSenderId)
36
+ pc = FcmPushClient(onNotificationFunc, fcm_config, credentials, onFCMCredentialsUpdated)
37
+ fcm_token = await pc.checkin_or_register(firebaseCredentialsFileName)
38
+ _LOGGER.debug(f'Firebase.checkin_or_register() returned a token:{fcm_token}')
39
+ await pc.start()
40
+ await asyncio.sleep(5)
41
+ return pc.is_started()
42
+
43
+ def readFCMCredsFile(credsFile):
44
+ """ Reads the firebase cloud messaging credentials from file"""
45
+ try:
46
+ if os.path.isfile(credsFile):
47
+ with open(credsFile, "r") as f:
48
+ credString=f.read()
49
+ f.close()
50
+ creds=json.loads(credString)
51
+ return creds
52
+ else:
53
+ _LOGGER.debug(f'{credsFile} not found.')
54
+ return {}
55
+ except:
56
+ _LOGGER.warning('readFCMCredsFile() not successful.')
57
+ return ''
58
+
59
+ def writeFCMCredsFile(creds, firebaseCredentialsFileName):
60
+ """ Saves the firebase cloud messaging credentials to a file for future use """
61
+ try:
62
+ with open(firebaseCredentialsFileName, "w") as f:
63
+ f.write(json.dumps(creds))
64
+ f.close()
65
+ except Exception as e:
66
+ _LOGGER.warning(f'writeFCMCredsFile() not successful. Error: {e}')
67
+
68
+ async def onFCMCredentialsUpdated(creds, firebaseCredentialsFileName):
69
+ """ Is called from firebase-messaging package """
70
+ loop = asyncio.get_running_loop()
71
+ await loop.run_in_executor(None, writeFCMCredsFile, creds, firebaseCredentialsFileName)
72
+ #writeFCMCredsFile(creds, firebaseCredentialsFileName)
73
+
@@ -0,0 +1,9 @@
1
+ from .fcmpushclient import FcmPushClient, FcmPushClientConfig, FcmPushClientRunState
2
+ from .fcmregister import FcmRegisterConfig
3
+
4
+ __all__ = [
5
+ "FcmPushClientConfig",
6
+ "FcmPushClient",
7
+ "FcmPushClientRunState",
8
+ "FcmRegisterConfig",
9
+ ]
@@ -0,0 +1,32 @@
1
+ """Constants module."""
2
+
3
+ GCM_REGISTER_URL = "https://android.clients.google.com/c2dm/register3"
4
+ GCM_CHECKIN_URL = "https://android.clients.google.com/checkin"
5
+ GCM_SERVER_KEY_BIN = (
6
+ b"\x04\x33\x94\xf7\xdf\xa1\xeb\xb1\xdc\x03\xa2\x5e\x15\x71\xdb\x48\xd3"
7
+ + b"\x2e\xed\xed\xb2\x34\xdb\xb7\x47\x3a\x0c\x8f\xc4\xcc\xe1\x6f\x3c"
8
+ + b"\x8c\x84\xdf\xab\xb6\x66\x3e\xf2\x0c\xd4\x8b\xfe\xe3\xf9\x76\x2f"
9
+ + b"\x14\x1c\x63\x08\x6a\x6f\x2d\xb1\x1a\x95\xb0\xce\x37\xc0\x9c\x6e"
10
+ )
11
+ # urlsafe b64 encoding of the binary key with = padding removed
12
+ GCM_SERVER_KEY_B64 = (
13
+ "BDOU99-h67HcA6JeFXHbSNMu7e2yNNu3RzoM"
14
+ + "j8TM4W88jITfq7ZmPvIM1Iv-4_l2LxQcYwhqby2xGpWwzjfAnG4"
15
+ )
16
+
17
+ FCM_SUBSCRIBE_URL = "https://fcm.googleapis.com/fcm/connect/subscribe/"
18
+ FCM_SEND_URL = "https://fcm.googleapis.com/fcm/send/"
19
+
20
+ FCM_API = "https://fcm.googleapis.com/v1/"
21
+ FCM_REGISTRATION = "https://fcmregistrations.googleapis.com/v1/"
22
+ FCM_INSTALLATION = "https://firebaseinstallations.googleapis.com/v1/"
23
+ AUTH_VERSION = "FIS_v2"
24
+ SDK_VERSION = "w:0.6.6"
25
+
26
+ DOORBELLS_ENDPOINT = "/clients_api/doorbots/{0}"
27
+
28
+ MCS_VERSION = 41
29
+ MCS_HOST = "mtalk.google.com"
30
+ MCS_PORT = 5228
31
+ MCS_SELECTIVE_ACK_ID = 12
32
+ MCS_STREAM_ACK_ID = 13