pycupra 0.0.14__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 +1 -0
- pycupra/__version__.py +1 -1
- pycupra/connection.py +66 -9
- pycupra/const.py +11 -0
- pycupra/dashboard.py +34 -0
- pycupra/firebase.py +73 -0
- pycupra/firebase_messaging/__init__.py +9 -0
- pycupra/firebase_messaging/const.py +32 -0
- pycupra/firebase_messaging/fcmpushclient.py +796 -0
- pycupra/firebase_messaging/fcmregister.py +519 -0
- pycupra/firebase_messaging/py.typed +0 -0
- pycupra/vehicle.py +272 -13
- {pycupra-0.0.14.dist-info → pycupra-0.1.0.dist-info}/METADATA +12 -2
- pycupra-0.1.0.dist-info/RECORD +19 -0
- pycupra-0.0.14.dist-info/RECORD +0 -13
- {pycupra-0.0.14.dist-info → pycupra-0.1.0.dist-info}/WHEEL +0 -0
- {pycupra-0.0.14.dist-info → pycupra-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {pycupra-0.0.14.dist-info → pycupra-0.1.0.dist-info}/top_level.txt +0 -0
pycupra/vehicle.py
CHANGED
@@ -20,9 +20,15 @@ from .exceptions import (
|
|
20
20
|
SeatRequestInProgressException
|
21
21
|
)
|
22
22
|
from .const import (
|
23
|
-
APP_URI
|
23
|
+
APP_URI,
|
24
|
+
FIREBASE_STATUS_NOT_INITIALISED,
|
25
|
+
FIREBASE_STATUS_ACTIVATED,
|
26
|
+
FIREBASE_STATUS_ACTIVATION_FAILED,
|
27
|
+
FIREBASE_STATUS_NOT_WANTED,
|
24
28
|
)
|
25
29
|
|
30
|
+
from .firebase import Firebase, readFCMCredsFile, writeFCMCredsFile
|
31
|
+
|
26
32
|
_LOGGER = logging.getLogger(__name__)
|
27
33
|
|
28
34
|
DATEZERO = datetime(1970,1,1)
|
@@ -41,6 +47,10 @@ class Vehicle:
|
|
41
47
|
self._discovered = False
|
42
48
|
self._dashboard = None
|
43
49
|
self._states = {}
|
50
|
+
self._firebaseCredentialsFileName = None
|
51
|
+
self._firebaseLastMessageId = ''
|
52
|
+
self.firebaseStatus = FIREBASE_STATUS_NOT_INITIALISED
|
53
|
+
self.updateCallback = None
|
44
54
|
|
45
55
|
self._requests = {
|
46
56
|
'departuretimer': {'status': '', 'timestamp': DATEZERO},
|
@@ -75,6 +85,13 @@ class Vehicle:
|
|
75
85
|
}
|
76
86
|
|
77
87
|
self._last_full_update = datetime.now(tz=None) - timedelta(seconds=1200)
|
88
|
+
# Timestamps for the last API calls
|
89
|
+
self._last_get_statusreport = datetime.now(tz=None) - timedelta(seconds=600)
|
90
|
+
self._last_get_departure_timers = datetime.now(tz=None) - timedelta(seconds=600)
|
91
|
+
self._last_get_departure_profiles = datetime.now(tz=None) - timedelta(seconds=600)
|
92
|
+
self._last_get_charger = datetime.now(tz=None) - timedelta(seconds=600)
|
93
|
+
self._last_get_climater = datetime.now(tz=None) - timedelta(seconds=600)
|
94
|
+
self._last_get_mileage = datetime.now(tz=None) - timedelta(seconds=600)
|
78
95
|
|
79
96
|
|
80
97
|
#### API get and set functions ####
|
@@ -114,7 +131,7 @@ class Vehicle:
|
|
114
131
|
|
115
132
|
self._discovered = datetime.now()
|
116
133
|
|
117
|
-
async def update(self, updateType=0):
|
134
|
+
async def update(self, updateType=0) -> bool:
|
118
135
|
"""Try to fetch data for all known API endpoints."""
|
119
136
|
# Update vehicle information if not discovered or stale information
|
120
137
|
if not self._discovered:
|
@@ -129,14 +146,25 @@ class Vehicle:
|
|
129
146
|
# Fetch all data if car is not deactivated
|
130
147
|
if not self.deactivated:
|
131
148
|
try:
|
149
|
+
if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
|
150
|
+
fullUpdateExpired = datetime.now(tz=None) - timedelta(seconds= 1700)
|
151
|
+
oldMileage = self.distance
|
152
|
+
if self._last_get_mileage < datetime.now(tz=None) - timedelta(seconds= 300):
|
153
|
+
await self.get_mileage()
|
154
|
+
if self.distance > oldMileage:
|
155
|
+
# self.distance has changed. So it's time for a full update
|
156
|
+
_LOGGER.debug(f'Mileage has changed. Old value: {oldMileage}, new value {self.distance}. This calls for a full update.')
|
157
|
+
updateType = 1
|
158
|
+
else:
|
159
|
+
fullUpdateExpired = datetime.now(tz=None) - timedelta(seconds= 1100)
|
160
|
+
|
132
161
|
if self._connection._session_nightlyUpdateReduction:
|
133
162
|
# nightlyUpdateReduction is activated
|
134
163
|
if datetime.now(tz=None).hour<5 or datetime.now(tz=None).hour>=22:
|
135
164
|
# current time is within the night interval
|
136
|
-
fullUpdateExpired = datetime.now(tz=None) - timedelta(seconds= 1100)
|
137
165
|
if hasattr(self, '_last_full_update'):
|
138
166
|
_LOGGER.debug(f'last_full_update= {self._last_full_update}, fullUpdateExpired= {fullUpdateExpired}.')
|
139
|
-
if updateType
|
167
|
+
if updateType<1 and (hasattr(self, '_last_full_update') and self._last_full_update>fullUpdateExpired):
|
140
168
|
_LOGGER.debug('Nightly update reduction is active and current time within 22:00 and 5:00. So we skip small update.')
|
141
169
|
return True
|
142
170
|
|
@@ -148,7 +176,6 @@ class Vehicle:
|
|
148
176
|
return_exceptions=True
|
149
177
|
)
|
150
178
|
|
151
|
-
fullUpdateExpired = datetime.now(tz=None) - timedelta(seconds= 1100)
|
152
179
|
if hasattr(self, '_last_full_update'):
|
153
180
|
_LOGGER.debug(f'last_full_update= {self._last_full_update}, fullUpdateExpired= {fullUpdateExpired}.')
|
154
181
|
if updateType!=1 and (hasattr(self, '_last_full_update') and self._last_full_update>fullUpdateExpired):
|
@@ -156,6 +183,9 @@ class Vehicle:
|
|
156
183
|
return True
|
157
184
|
|
158
185
|
# Data to be updated less often
|
186
|
+
if self.firebaseStatus != FIREBASE_STATUS_ACTIVATED:
|
187
|
+
await self.get_mileage()
|
188
|
+
|
159
189
|
await asyncio.gather(
|
160
190
|
#self.get_statusreport(),
|
161
191
|
self.get_charger(),
|
@@ -167,7 +197,6 @@ class Vehicle:
|
|
167
197
|
self.get_vehicleHealthWarnings(),
|
168
198
|
self.get_departure_timers(),
|
169
199
|
self.get_departure_profiles(),
|
170
|
-
self.get_mileage(),
|
171
200
|
#self.get_modelimageurl(), #commented out, because getting the images discover() should be sufficient
|
172
201
|
return_exceptions=True
|
173
202
|
)
|
@@ -198,6 +227,7 @@ class Vehicle:
|
|
198
227
|
data = await self._connection.getMileage(self.vin, self._apibase)
|
199
228
|
if data:
|
200
229
|
self._states.update(data)
|
230
|
+
self._last_get_mileage = datetime.now(tz=None)
|
201
231
|
|
202
232
|
async def get_preheater(self):
|
203
233
|
"""Fetch pre-heater data if function is enabled."""
|
@@ -218,10 +248,11 @@ class Vehicle:
|
|
218
248
|
data = await self._connection.getClimater(self.vin, self._apibase)
|
219
249
|
if data:
|
220
250
|
self._states.update(data)
|
251
|
+
self._last_get_climater = datetime.now(tz=None)
|
221
252
|
else:
|
222
253
|
_LOGGER.debug('Could not fetch climater data')
|
223
|
-
else:
|
224
|
-
|
254
|
+
#else:
|
255
|
+
# self._requests.pop('climatisation', None)
|
225
256
|
|
226
257
|
async def get_trip_statistic(self):
|
227
258
|
"""Fetch trip data if function is enabled."""
|
@@ -264,6 +295,7 @@ class Vehicle:
|
|
264
295
|
data = await self._connection.getVehicleStatusReport(self.vin, self._apibase)
|
265
296
|
if data:
|
266
297
|
self._states.update(data)
|
298
|
+
self._last_get_statusreport = datetime.now(tz=None)
|
267
299
|
else:
|
268
300
|
_LOGGER.debug('Could not fetch status report')
|
269
301
|
|
@@ -282,6 +314,7 @@ class Vehicle:
|
|
282
314
|
data = await self._connection.getCharger(self.vin, self._apibase)
|
283
315
|
if data:
|
284
316
|
self._states.update(data)
|
317
|
+
self._last_get_charger = datetime.now(tz=None)
|
285
318
|
else:
|
286
319
|
_LOGGER.debug('Could not fetch charger data')
|
287
320
|
|
@@ -291,6 +324,7 @@ class Vehicle:
|
|
291
324
|
data = await self._connection.getDeparturetimer(self.vin, self._apibase)
|
292
325
|
if data:
|
293
326
|
self._states.update(data)
|
327
|
+
self._last_get_departure_timers = datetime.now(tz=None)
|
294
328
|
else:
|
295
329
|
_LOGGER.debug('Could not fetch timers')
|
296
330
|
|
@@ -300,6 +334,7 @@ class Vehicle:
|
|
300
334
|
data = await self._connection.getDepartureprofiles(self.vin, self._apibase)
|
301
335
|
if data:
|
302
336
|
self._states.update(data)
|
337
|
+
self._last_get_departure_profiles = datetime.now(tz=None)
|
303
338
|
else:
|
304
339
|
_LOGGER.debug('Could not fetch timers')
|
305
340
|
|
@@ -336,8 +371,10 @@ class Vehicle:
|
|
336
371
|
data = {'maxChargeCurrentAc': int(value)}
|
337
372
|
if int(value)==252:
|
338
373
|
data = {'maxChargeCurrentAc': 'reduced'}
|
339
|
-
|
374
|
+
elif int(value)==254:
|
340
375
|
data = {'maxChargeCurrentAc': 'maximum'}
|
376
|
+
else:
|
377
|
+
data = {'maxChargeCurrentAcInAmperes': int(value)}
|
341
378
|
else:
|
342
379
|
_LOGGER.error(f'Set charger maximum current to {value} is not supported.')
|
343
380
|
raise SeatInvalidRequestException(f'Set charger maximum current to {value} is not supported.')
|
@@ -395,6 +432,10 @@ class Vehicle:
|
|
395
432
|
'status': response.get('state', 'Unknown'),
|
396
433
|
'id': response.get('id', 0)
|
397
434
|
}
|
435
|
+
# if firebaseStatus is FIREBASE_STATUS_ACTIVATED, the request is assumed successful. Waiting for push notification before rereading status
|
436
|
+
if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
|
437
|
+
_LOGGER.debug('POST request for charger assumed successful. Waiting for push notification')
|
438
|
+
return True
|
398
439
|
# Update the charger data and check, if they have changed as expected
|
399
440
|
retry = 0
|
400
441
|
actionSuccessful = False
|
@@ -409,7 +450,9 @@ class Vehicle:
|
|
409
450
|
if not self.charging:
|
410
451
|
actionSuccessful = True
|
411
452
|
elif mode == 'settings':
|
412
|
-
if data.get('maxChargeCurrentAc','') == self.charge_max_ampere:
|
453
|
+
if data.get('maxChargeCurrentAc','') == self.charge_max_ampere: # In case 'maximum', 'reduced'
|
454
|
+
actionSuccessful = True
|
455
|
+
if data.get('maxChargeCurrentAcInAmperes',0) == self.charge_max_ampere: # In case of a numerical value
|
413
456
|
actionSuccessful = True
|
414
457
|
else:
|
415
458
|
_LOGGER.error(f'Missing code in vehicle._set_charger() for mode {mode}')
|
@@ -641,6 +684,10 @@ class Vehicle:
|
|
641
684
|
'status': response.get('state', 'Unknown'),
|
642
685
|
'id': response.get('id', 0),
|
643
686
|
}
|
687
|
+
# if firebaseStatus is FIREBASE_STATUS_ACTIVATED, the request is assumed successful. Waiting for push notification before rereading status
|
688
|
+
if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
|
689
|
+
_LOGGER.debug('POST request for change of departure timers assumed successful. Waiting for push notification')
|
690
|
+
return True
|
644
691
|
# Update the departure timers data and check, if they have changed as expected
|
645
692
|
retry = 0
|
646
693
|
actionSuccessful = False
|
@@ -828,6 +875,10 @@ class Vehicle:
|
|
828
875
|
'status': response.get('state', 'Unknown'),
|
829
876
|
'id': response.get('id', 0),
|
830
877
|
}
|
878
|
+
# if firebaseStatus is FIREBASE_STATUS_ACTIVATED, the request is assumed successful. Waiting for push notification before rereading status
|
879
|
+
if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
|
880
|
+
_LOGGER.debug('POST request for change of departure profiles assumed successful. Waiting for push notification')
|
881
|
+
return True
|
831
882
|
# Update the departure profile data and check, if they have changed as expected
|
832
883
|
retry = 0
|
833
884
|
actionSuccessful = False
|
@@ -1006,6 +1057,10 @@ class Vehicle:
|
|
1006
1057
|
'status': response.get('state', 'Unknown'),
|
1007
1058
|
'id': response.get('id', 0),
|
1008
1059
|
}
|
1060
|
+
# if firebaseStatus is FIREBASE_STATUS_ACTIVATED, the request is assumed successful. Waiting for push notification before rereading status
|
1061
|
+
if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
|
1062
|
+
_LOGGER.debug('POST request for climater assumed successful. Waiting for push notification')
|
1063
|
+
return True
|
1009
1064
|
# Update the climater data and check, if they have changed as expected
|
1010
1065
|
retry = 0
|
1011
1066
|
actionSuccessful = False
|
@@ -1119,6 +1174,10 @@ class Vehicle:
|
|
1119
1174
|
'status': response.get('state', 'Unknown'),
|
1120
1175
|
'id': response.get('id', 0),
|
1121
1176
|
}
|
1177
|
+
# if firebaseStatus is FIREBASE_STATUS_ACTIVATED, the request is assumed successful. Waiting for push notification before rereading status
|
1178
|
+
if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
|
1179
|
+
_LOGGER.debug('POST request for lock/unlock assumed successful. Waiting for push notification')
|
1180
|
+
return True
|
1122
1181
|
# Update the lock data and check, if they have changed as expected
|
1123
1182
|
retry = 0
|
1124
1183
|
actionSuccessful = False
|
@@ -1552,7 +1611,10 @@ class Vehicle:
|
|
1552
1611
|
def charge_max_ampere(self):
|
1553
1612
|
"""Return charger max ampere setting."""
|
1554
1613
|
if self.attrs.get('charging', False):
|
1555
|
-
|
1614
|
+
if self.attrs.get('charging',{}).get('info',{}).get('settings',{}).get('maxChargeCurrentAcInAmperes', None):
|
1615
|
+
return self.attrs.get('charging',{}).get('info',{}).get('settings',{}).get('maxChargeCurrentAcInAmperes', 0)
|
1616
|
+
else:
|
1617
|
+
return self.attrs.get('charging').get('info').get('settings').get('maxChargeCurrentAc')
|
1556
1618
|
return 0
|
1557
1619
|
|
1558
1620
|
@property
|
@@ -1565,6 +1627,21 @@ class Vehicle:
|
|
1565
1627
|
return True
|
1566
1628
|
return False
|
1567
1629
|
|
1630
|
+
@property
|
1631
|
+
def slow_charge(self):
|
1632
|
+
"""Return charger max ampere setting."""
|
1633
|
+
if self.charge_max_ampere=='reduced':
|
1634
|
+
return True
|
1635
|
+
return False
|
1636
|
+
|
1637
|
+
@property
|
1638
|
+
def is_slow_charge_supported(self):
|
1639
|
+
"""Return true if Slow Charge is supported"""
|
1640
|
+
if self.is_charge_max_ampere_supported:
|
1641
|
+
if self.charge_max_ampere in ('reduced', 'maximum'):
|
1642
|
+
return True
|
1643
|
+
return False
|
1644
|
+
|
1568
1645
|
@property
|
1569
1646
|
def charging_cable_locked(self):
|
1570
1647
|
"""Return plug locked state"""
|
@@ -1715,6 +1792,17 @@ class Vehicle:
|
|
1715
1792
|
if self.is_charging_state_supported:
|
1716
1793
|
return True
|
1717
1794
|
|
1795
|
+
@property
|
1796
|
+
def target_soc(self):
|
1797
|
+
"""Return the target soc."""
|
1798
|
+
return self.attrs.get('charging', {}).get('info', {}).get('settings', {}).get('targetSoc', 0)
|
1799
|
+
|
1800
|
+
@property
|
1801
|
+
def is_target_soc_supported(self):
|
1802
|
+
"""Target state of charge supported."""
|
1803
|
+
if self.attrs.get('charging', {}).get('info', {}).get('settings', {}).get('targetSoc', False):
|
1804
|
+
return True
|
1805
|
+
|
1718
1806
|
# Vehicle location states
|
1719
1807
|
@property
|
1720
1808
|
def position(self):
|
@@ -2941,7 +3029,7 @@ class Vehicle:
|
|
2941
3029
|
@property
|
2942
3030
|
def is_request_in_progress_supported(self):
|
2943
3031
|
"""Request in progress is always supported."""
|
2944
|
-
return
|
3032
|
+
return False
|
2945
3033
|
|
2946
3034
|
@property
|
2947
3035
|
def request_results(self):
|
@@ -2951,7 +3039,7 @@ class Vehicle:
|
|
2951
3039
|
'state': self._requests.get('state', 'N/A'),
|
2952
3040
|
}
|
2953
3041
|
for section in self._requests:
|
2954
|
-
if section in ['departuretimer', 'batterycharge', 'climatisation', 'refresh', 'lock', 'preheater']:
|
3042
|
+
if section in ['departuretimer', 'departureprofiles', 'batterycharge', 'climatisation', 'refresh', 'lock', 'preheater']:
|
2955
3043
|
timestamp = self._requests.get(section, {}).get('timestamp', DATEZERO)
|
2956
3044
|
data[section] = self._requests[section].get('status', 'N/A')
|
2957
3045
|
data[section+'_timestamp'] = timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
@@ -2996,3 +3084,174 @@ class Vehicle:
|
|
2996
3084
|
indent=4,
|
2997
3085
|
default=serialize
|
2998
3086
|
)
|
3087
|
+
|
3088
|
+
|
3089
|
+
async def initialiseFirebase(self, firebaseCredentialsFileName=None, updateCallback=None):
|
3090
|
+
# Check if firebase shall be used
|
3091
|
+
if firebaseCredentialsFileName == None:
|
3092
|
+
_LOGGER.debug('No use of firebase wanted.')
|
3093
|
+
self.firebaseStatus = FIREBASE_STATUS_NOT_WANTED
|
3094
|
+
return self.firebaseStatus
|
3095
|
+
self._firebaseCredentialsFileName = firebaseCredentialsFileName
|
3096
|
+
|
3097
|
+
# Check if firebase not already initialised
|
3098
|
+
if self.firebaseStatus!= FIREBASE_STATUS_NOT_INITIALISED:
|
3099
|
+
_LOGGER.debug(f'No need to initialise firebase anymore. Firebase status={self.firebaseStatus}')
|
3100
|
+
return self.firebaseStatus
|
3101
|
+
|
3102
|
+
# Read the firebase credentials file and check if an existing subscription has to be deleted
|
3103
|
+
loop = asyncio.get_running_loop()
|
3104
|
+
credentials = await loop.run_in_executor(None, readFCMCredsFile, firebaseCredentialsFileName)
|
3105
|
+
subscribedVin = credentials.get('subscription',{}).get('vin','')
|
3106
|
+
subscribedUserId = credentials.get('subscription',{}).get('userId','')
|
3107
|
+
subscribedBrand = credentials.get('subscription',{}).get('brand','')
|
3108
|
+
if subscribedVin != '' and subscribedUserId != '':
|
3109
|
+
if subscribedVin != self.vin or subscribedUserId != self._connection._user_id or subscribedBrand != self._connection._session_auth_brand:
|
3110
|
+
_LOGGER.debug(f'Change of vin, userId or brand. Deleting subscription for vin={subscribedVin} and userId={subscribedUserId}.')
|
3111
|
+
result = await self._connection.deleteSubscription(credentials)
|
3112
|
+
|
3113
|
+
# Start firebase
|
3114
|
+
fb = Firebase()
|
3115
|
+
success = await fb.firebaseStart(self.onNotification, firebaseCredentialsFileName, brand=self._connection._session_auth_brand)
|
3116
|
+
if not success:
|
3117
|
+
self.firebaseStatus = FIREBASE_STATUS_ACTIVATION_FAILED
|
3118
|
+
_LOGGER.warning('Activation of firebase messaging failed.')
|
3119
|
+
return self.firebaseStatus
|
3120
|
+
|
3121
|
+
self.updateCallback = updateCallback
|
3122
|
+
# Read possibly new credentials and subscribe vin and userId for push notifications
|
3123
|
+
loop = asyncio.get_running_loop()
|
3124
|
+
credentials = await loop.run_in_executor(None, readFCMCredsFile, firebaseCredentialsFileName)
|
3125
|
+
result = await self._connection.subscribe(self.vin, credentials)
|
3126
|
+
_LOGGER.debug(f'Result of subscription={result}.')
|
3127
|
+
credentials['subscription']= {
|
3128
|
+
'vin' : self.vin,
|
3129
|
+
'userId' : self._connection._user_id,
|
3130
|
+
'brand' : self._connection._session_auth_brand,
|
3131
|
+
'id' : result.get('id', ''),
|
3132
|
+
}
|
3133
|
+
loop = asyncio.get_running_loop()
|
3134
|
+
await loop.run_in_executor(None, writeFCMCredsFile, credentials, firebaseCredentialsFileName)
|
3135
|
+
|
3136
|
+
await asyncio.sleep(5) # Wait to let the first notifications
|
3137
|
+
self.firebaseStatus = FIREBASE_STATUS_ACTIVATED
|
3138
|
+
_LOGGER.info('Activation of firebase messaging was successful.')
|
3139
|
+
return self.firebaseStatus
|
3140
|
+
|
3141
|
+
|
3142
|
+
|
3143
|
+
async def onNotification(self, obj, notification, data_message):
|
3144
|
+
# Do something with the notification
|
3145
|
+
_LOGGER.debug(f'Received push notification: notification id={notification}, type={obj.get('data',{}).get('type','')}, requestId={obj.get('data',{}).get('requestId','[None]')}')
|
3146
|
+
_LOGGER.debug(f' data_message={data_message}, payload={obj.get('data',{}).get('payload','[None]')}')
|
3147
|
+
|
3148
|
+
#temporary output of notifications in a file
|
3149
|
+
if self.updateCallback == self.update:
|
3150
|
+
self.storeFirebaseNotifications(obj, notification, data_message)
|
3151
|
+
|
3152
|
+
if self.firebaseStatus != FIREBASE_STATUS_ACTIVATED:
|
3153
|
+
_LOGGER.info(f'While firebase is not fully activated, received notifications are just acknoledged.')
|
3154
|
+
# As long as the firebase status is not set to activated, ignore the notifications
|
3155
|
+
return False
|
3156
|
+
|
3157
|
+
type = obj.get('data',{}).get('type','')
|
3158
|
+
requestId = obj.get('data',{}).get('requestId','')
|
3159
|
+
openRequest = -1
|
3160
|
+
if requestId != '':
|
3161
|
+
_LOGGER.info(f'Received notification of type \'{type}\', request id={requestId} ')
|
3162
|
+
else:
|
3163
|
+
_LOGGER.info(f'Received notification of type \'{type}\' ')
|
3164
|
+
|
3165
|
+
if notification == self._firebaseLastMessageId:
|
3166
|
+
_LOGGER.info(f'Received notification {notification} again. Just acknoledging it, nothing to do.')
|
3167
|
+
return False
|
3168
|
+
|
3169
|
+
self._firebaseLastMessageId = notification # save the id of the last notification
|
3170
|
+
if type in ('vehicle-access-locked-successful', 'vehicle-access-unlocked-successful'): # vehicle was locked/unlocked
|
3171
|
+
if self._requests.get('lock', {}).get('id', None):
|
3172
|
+
openRequest= self._requests.get('lock', {}).get('id', None)
|
3173
|
+
if openRequest == requestId:
|
3174
|
+
_LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
|
3175
|
+
self._requests.get('lock', {}).pop('id')
|
3176
|
+
if (self._last_get_statusreport < datetime.now(tz=None) - timedelta(seconds= 10)) or openRequest == requestId:
|
3177
|
+
# Update the status report only if the last one is older than timedelta or if the notification belongs to an open request initiated by PyCupra
|
3178
|
+
#await self.get_statusreport() # Call not needed because it's part of updateCallback(2)
|
3179
|
+
if self.updateCallback:
|
3180
|
+
await self.updateCallback(2)
|
3181
|
+
elif type == 'departure-times-updated':
|
3182
|
+
if self._requests.get('departuretimer', {}).get('id', None):
|
3183
|
+
openRequest= self._requests.get('departuretimer', {}).get('id', None)
|
3184
|
+
if openRequest == requestId:
|
3185
|
+
_LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
|
3186
|
+
self._requests.get('departuretimer', {}).pop('id')
|
3187
|
+
if (self._last_get_departure_timers < datetime.now(tz=None) - timedelta(seconds= 30)) or openRequest == requestId:
|
3188
|
+
# Update the departure timers only if the last one is older than timedelta or if the notification belongs to an open request initiated by PyCupra
|
3189
|
+
await self.get_departure_timers()
|
3190
|
+
if self.updateCallback:
|
3191
|
+
await self.updateCallback(2)
|
3192
|
+
elif type == 'departure-profiles-updated': # !!! Is this the right type?
|
3193
|
+
if self._requests.get('departureprofile', {}).get('id', None):
|
3194
|
+
openRequest= self._requests.get('departureprofile', {}).get('id', None)
|
3195
|
+
if openRequest == requestId:
|
3196
|
+
_LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
|
3197
|
+
self._requests.get('departureprofile', {}).pop('id')
|
3198
|
+
if (self._last_get_departure_profiles < datetime.now(tz=None) - timedelta(seconds= 30)) or openRequest == requestId:
|
3199
|
+
# Update the departure profiles only if the last one is older than timedelta or if the notification belongs to an open request initiated by PyCupra
|
3200
|
+
await self.get_departure_profiles()
|
3201
|
+
if self.updateCallback:
|
3202
|
+
await self.updateCallback(2)
|
3203
|
+
elif type in ('charging-status-changed', 'charging-started', 'charging-stopped'):
|
3204
|
+
if self._requests.get('batterycharge', {}).get('id', None):
|
3205
|
+
openRequest= self._requests.get('batterycharge', {}).get('id', None)
|
3206
|
+
if openRequest == requestId:
|
3207
|
+
_LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
|
3208
|
+
self._requests.get('batterycharge', {}).pop('id')
|
3209
|
+
if (self._last_get_charger < datetime.now(tz=None) - timedelta(seconds= 10)) or openRequest == requestId:
|
3210
|
+
# Update the charging data only if the last one is older than timedelta or if the notification belongs to an open request initiated by PyCupra
|
3211
|
+
await self.get_charger()
|
3212
|
+
if self.updateCallback:
|
3213
|
+
await self.updateCallback(2)
|
3214
|
+
else:
|
3215
|
+
_LOGGER.debug(f'It is now {datetime.now(tz=None)}. Last get_charger was at {self._last_get_charger}. So no need to update.')
|
3216
|
+
elif type in ('climatisation-status-changed','climatisation-started', 'climatisation-stopped'):
|
3217
|
+
if self._requests.get('climatisation', {}).get('id', None):
|
3218
|
+
openRequest= self._requests.get('climatisation', {}).get('id', None)
|
3219
|
+
if openRequest == requestId:
|
3220
|
+
_LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
|
3221
|
+
self._requests.get('climatisation', {}).pop('id')
|
3222
|
+
if (self._last_get_climater < datetime.now(tz=None) - timedelta(seconds= 10)) or openRequest == requestId:
|
3223
|
+
# Update the climatisation data only if the last one is older than timedelta or if the notification belongs to an open request initiated by PyCupra
|
3224
|
+
await self.get_climater()
|
3225
|
+
if self.updateCallback:
|
3226
|
+
await self.updateCallback(2)
|
3227
|
+
else:
|
3228
|
+
_LOGGER.debug(f'It is now {datetime.now(tz=None)}. Last get_climater was at {self._last_get_climater}. So no need to update.')
|
3229
|
+
elif type == 'vehicle-wakeup-succeeded':
|
3230
|
+
if self._requests.get('refresh', {}).get('id', None):
|
3231
|
+
openRequest= self._requests.get('refresh', {}).get('id', None)
|
3232
|
+
if openRequest == requestId:
|
3233
|
+
_LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
|
3234
|
+
self._requests.get('refresh', {}).pop('id')
|
3235
|
+
if (self._last_full_update < datetime.now(tz=None) - timedelta(seconds= 30)) or openRequest == requestId:
|
3236
|
+
# Do full update only if the last one is older than timedelta or if the notification belongs to an open request initiated by PyCupra
|
3237
|
+
if self.updateCallback:
|
3238
|
+
await self.updateCallback(1)
|
3239
|
+
else:
|
3240
|
+
_LOGGER.warning(f' Don\'t know what to do with a notification of type \'{type}\')')
|
3241
|
+
|
3242
|
+
|
3243
|
+
|
3244
|
+
|
3245
|
+
def storeFirebaseNotifications(self, obj, notification, data_message):
|
3246
|
+
_LOGGER.debug(f'In storeFirebaseNotifications. notification={notification}')
|
3247
|
+
fName = self._firebaseCredentialsFileName
|
3248
|
+
fName = fName.replace('pycupra_firebase_credentials.json', 'pycupra_firebasenotifications.txt')
|
3249
|
+
|
3250
|
+
with open(fName, "a") as ofile:
|
3251
|
+
ofile.write(f'{datetime.now()}\n')
|
3252
|
+
ofile.write(f' notification id={notification}, data_message={data_message}\n')
|
3253
|
+
ofile.write(f' obj={obj}\n')
|
3254
|
+
ofile.write("----------------------------------------------------------------\n")
|
3255
|
+
|
3256
|
+
|
3257
|
+
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pycupra
|
3
|
-
Version: 0.0
|
3
|
+
Version: 0.1.0
|
4
4
|
Summary: A library to read and send vehicle data via Cupra/Seat portal using the same API calls as the MyCupra/MySeat mobile app.
|
5
5
|
Home-page: https://github.com/WulfgarW/pycupra
|
6
6
|
Author: WulfgarW
|
@@ -39,11 +39,21 @@ No licence, public domain, no guarantees, feel free to use for anything. Please
|
|
39
39
|
|
40
40
|
## Breaking changes
|
41
41
|
|
42
|
+
- The method vehicle.update(updateType) supports 3 different update types:
|
43
|
+
- updateType=0: Small update (=only get_basiccardata() and get_statusreport are called). If the last full update is more than 1100 seconds ago, then a full update is performed.
|
44
|
+
- updateType=1: Full update (nearly all get-methods are called. The model images and the capabilitites are refreshed only every 2 hours.)
|
45
|
+
- updateType=2: Like updateType=0, but ignoring the nightly update reduction
|
46
|
+
|
47
|
+
- Nightly update reduction: If nightly reduction is activated and the current time is within the time frame between 22:00 and 05:00, then vehicle.update(0) performs a full update, if the last full update is more than 1700 seconds ago. If that's not the case, vehicle.update(0) does nothing.
|
48
|
+
|
49
|
+
- PyCupra can ask the Seat/Cupra portal to send push notifications to PyCupra if the charging status or the climatisation status has changed or when the API has finished a request like lock or unlock vehicle, start or stop charging or change departure timers or ....
|
50
|
+
|
42
51
|
## Thanks to
|
43
52
|
|
44
53
|
- [RobinostLund](https://github.com/robinostlund/volkswagencarnet) for initial project for Volkswagen Carnet I was able to fork
|
45
54
|
- [Farfar](https://github.com/Farfar) for modifications related to electric engines
|
46
55
|
- [tanelvakker](https://github.com/tanelvakker) for modifications related to correct SPIN handling for various actions and using correct URLs also for MY2021
|
56
|
+
- [sdb9696](https://github.com/sdb9696) for the firebase-messaging package that is used in PyCupra with only minor modifications
|
47
57
|
|
48
58
|
### Example
|
49
59
|
|
@@ -52,6 +62,6 @@ When logged in the library will automatically create a vehicle object for every
|
|
52
62
|
Method get_vehicles will fetch vehicle basic information and create Vehicle class objects for all associated vehicles in account.
|
53
63
|
To update all available data use the update_all method of the Connect class. This will call the update function for all registered vehicles, which in turn will fetch data from all available API endpoints.
|
54
64
|
|
55
|
-
The file *
|
65
|
+
The file *pycupra_credentials.json.demo* explains the data structure of the credentials file.
|
56
66
|
|
57
67
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
pycupra/__init__.py,sha256=p0880jPkLqOErX3u3qaLboBLOsEHFpe44axApdaGeqI,231
|
2
|
+
pycupra/__version__.py,sha256=In9TmRdT6ZDVmATS3I2cl6CVsy_2A7P8a5W09AsPF84,207
|
3
|
+
pycupra/connection.py,sha256=r02CHAhRrZwu90AIsfTnzAhRn9dZjK8WuZvhUa6_DcA,85314
|
4
|
+
pycupra/const.py,sha256=cJJ9xrof6HZ7ZE7nXnB6tU4qMbSadlN8mgs43Igy7Mo,10591
|
5
|
+
pycupra/dashboard.py,sha256=hFHhCDrCNufQR1I85AlR4kPgSlFSinCegZ0w8F4FvUk,43703
|
6
|
+
pycupra/exceptions.py,sha256=Nq_F79GP8wjHf5lpvPy9TbSIrRHAJrFMo0T1N9TcgSQ,2917
|
7
|
+
pycupra/firebase.py,sha256=V3Ico6FZzEn0-5-CnqaDP9Mg9LpVU-_qLyZQwiRBbD0,2725
|
8
|
+
pycupra/utilities.py,sha256=cH4MiIzT2WlHgmnl_E7rR0R5LvCXfDNvirJolct50V8,2563
|
9
|
+
pycupra/vehicle.py,sha256=armeMQHxOZyyQJ9Iu7G26pq_BNc8oahPFxOe7daEip8,151994
|
10
|
+
pycupra/firebase_messaging/__init__.py,sha256=_ch_LNoKWQHCAgn-DGQiftU3MCNtcW8MsJMZBJILkTs,245
|
11
|
+
pycupra/firebase_messaging/const.py,sha256=XMy8kJ37uBSkTpVpdLeSjxk5UIPuvDuo-rxYdgmo2G8,1191
|
12
|
+
pycupra/firebase_messaging/fcmpushclient.py,sha256=FGttEKCnOlnBWkFiVSo-xkl7RAc7Dgi25lY3lEvMAa8,29549
|
13
|
+
pycupra/firebase_messaging/fcmregister.py,sha256=lDgk0TZjW2dvF6ogEKc45rHT30iv8DxHzB9MlekZtO4,18103
|
14
|
+
pycupra/firebase_messaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
pycupra-0.1.0.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
16
|
+
pycupra-0.1.0.dist-info/METADATA,sha256=fl7RIBu5ZpGieSr3yH-X1kGrV3f01Pfq9bNsxK1rCQs,3757
|
17
|
+
pycupra-0.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
18
|
+
pycupra-0.1.0.dist-info/top_level.txt,sha256=9Lbj_jG4JvpGwt6K3AwhWFc0XieDnuHFOP4x44wSXSQ,8
|
19
|
+
pycupra-0.1.0.dist-info/RECORD,,
|
pycupra-0.0.14.dist-info/RECORD
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
pycupra/__init__.py,sha256=VPzUfKd5mBFD1UERNV61FbGHih5dQPupLgIfYtmIUi4,230
|
2
|
-
pycupra/__version__.py,sha256=qY5o8F0qE8xC3RpWzlzulBbikrc7tq6TO-V5LlAJV5s,208
|
3
|
-
pycupra/connection.py,sha256=0SA-2UcL_uJhKMfluzyw_N4ZCqmsNEg3jU18XacMm5w,82641
|
4
|
-
pycupra/const.py,sha256=mgl29DcZz_J5hSzxknteu0ocDOXmQAgP0x17kvVSSi0,10234
|
5
|
-
pycupra/dashboard.py,sha256=3f4yBBBOMrXGiLdjO-2-1Ts2BVuQBw0wKa-2bhwid0c,42807
|
6
|
-
pycupra/exceptions.py,sha256=Nq_F79GP8wjHf5lpvPy9TbSIrRHAJrFMo0T1N9TcgSQ,2917
|
7
|
-
pycupra/utilities.py,sha256=cH4MiIzT2WlHgmnl_E7rR0R5LvCXfDNvirJolct50V8,2563
|
8
|
-
pycupra/vehicle.py,sha256=W1pQ8IkEmirSY6nha9OwSDFJyE2fcPGgeO_cVZAhgqg,135505
|
9
|
-
pycupra-0.0.14.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
10
|
-
pycupra-0.0.14.dist-info/METADATA,sha256=9txUYXQVhWep4acCAthlWizfMKKy19BCaiLPCdg0Wdk,2579
|
11
|
-
pycupra-0.0.14.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
12
|
-
pycupra-0.0.14.dist-info/top_level.txt,sha256=9Lbj_jG4JvpGwt6K3AwhWFc0XieDnuHFOP4x44wSXSQ,8
|
13
|
-
pycupra-0.0.14.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|