pycupra 0.1.11__py3-2ndver-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/vehicle.py ADDED
@@ -0,0 +1,3453 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Vehicle class for pycupra."""
4
+ import re
5
+ import logging
6
+ import asyncio
7
+ import json
8
+
9
+ from copy import deepcopy
10
+ from datetime import datetime, timedelta, timezone
11
+ from json import dumps as to_json
12
+ from collections import OrderedDict
13
+ from .utilities import find_path, is_valid_path
14
+ from .exceptions import (
15
+ SeatConfigException,
16
+ SeatException,
17
+ SeatEULAException,
18
+ SeatServiceUnavailable,
19
+ SeatThrottledException,
20
+ SeatInvalidRequestException,
21
+ SeatRequestInProgressException
22
+ )
23
+ from .const import (
24
+ APP_URI,
25
+ FIREBASE_STATUS_NOT_INITIALISED,
26
+ FIREBASE_STATUS_ACTIVATED,
27
+ FIREBASE_STATUS_ACTIVATION_FAILED,
28
+ FIREBASE_STATUS_ACTIVATION_STOPPED,
29
+ FIREBASE_STATUS_NOT_WANTED,
30
+ )
31
+
32
+ from .firebase import Firebase, readFCMCredsFile, writeFCMCredsFile
33
+
34
+ _LOGGER = logging.getLogger(__name__)
35
+
36
+ DATEZERO = datetime(1970,1,1)
37
+ class Vehicle:
38
+ def __init__(self, conn, data):
39
+ _LOGGER.debug(conn.anonymise(f'Creating Vehicle class object with data {data}'))
40
+ self._connection = conn
41
+ self._url = data.get('vin', '')
42
+ self._connectivities = data.get('connectivities', '')
43
+ self._capabilities = data.get('capabilities', [])
44
+ self._specification = data.get('specification', {})
45
+ self._properties = data.get('properties', {})
46
+ self._apibase = APP_URI
47
+ self._secbase = 'https://msg.volkswagen.de'
48
+ self._modelimages = None
49
+ self._discovered = False
50
+ self._dashboard = None
51
+ self._states = {}
52
+ self._firebaseCredentialsFileName = None
53
+ self._firebaseLastMessageId = ''
54
+ self.firebaseStatus = FIREBASE_STATUS_NOT_INITIALISED
55
+ self.firebase = None
56
+ self.updateCallback = None
57
+
58
+ self._requests = {
59
+ 'departuretimer': {'status': '', 'timestamp': DATEZERO},
60
+ 'departureprofile': {'status': '', 'timestamp': DATEZERO},
61
+ 'batterycharge': {'status': '', 'timestamp': DATEZERO},
62
+ 'climatisation': {'status': '', 'timestamp': DATEZERO},
63
+ 'refresh': {'status': '', 'timestamp': DATEZERO},
64
+ 'lock': {'status': '', 'timestamp': DATEZERO},
65
+ 'honkandflash': {'status': '', 'timestamp': DATEZERO},
66
+ 'preheater': {'status': '', 'timestamp': DATEZERO},
67
+ 'remaining': -1,
68
+ 'latest': '',
69
+ 'state': ''
70
+ }
71
+ self._climate_duration = 30
72
+
73
+ self._relevantCapabilties = {
74
+ 'measurements': {'active': False, 'reason': 'not supported', },
75
+ 'climatisation': {'active': False, 'reason': 'not supported'},
76
+ 'tripStatistics': {'active': False, 'reason': 'not supported', 'supportsCyclicTrips': False},
77
+ 'vehicleHealthInspection': {'active': False, 'reason': 'not supported'},
78
+ 'vehicleHealthWarnings': {'active': False, 'reason': 'not supported'},
79
+ 'state': {'active': False, 'reason': 'not supported'},
80
+ 'charging': {'active': False, 'reason': 'not supported', 'supportsTargetStateOfCharge': False},
81
+ 'chargingProfiles': {'active': False, 'reason': 'not supported', "supportsTimerClimatisation": False,"supportsVehiclePositionedInProfileID": False,"supportsSingleTimer": False},
82
+ 'honkAndFlash': {'active': False, 'reason': 'not supported'},
83
+ 'parkingPosition': {'active': False, 'reason': 'not supported'},
84
+ 'departureTimers': {'active': False, 'reason': 'not supported', 'supportsSingleTimer': False},
85
+ 'departureProfiles': {'active': False, 'reason': 'not supported', 'supportsSingleTimer': False},
86
+ 'transactionHistoryLockUnlock': {'active': False, 'reason': 'not supported'},
87
+ 'transactionHistoryHonkFlash': {'active': False, 'reason': 'not supported'},
88
+ }
89
+
90
+ self._last_full_update = datetime.now(tz=None) - timedelta(seconds=1200)
91
+ # Timestamps for the last API calls
92
+ self._last_get_statusreport = datetime.now(tz=None) - timedelta(seconds=600)
93
+ self._last_get_departure_timers = datetime.now(tz=None) - timedelta(seconds=600)
94
+ self._last_get_departure_profiles = datetime.now(tz=None) - timedelta(seconds=600)
95
+ self._last_get_charger = datetime.now(tz=None) - timedelta(seconds=600)
96
+ self._last_get_climater = datetime.now(tz=None) - timedelta(seconds=600)
97
+ self._last_get_mileage = datetime.now(tz=None) - timedelta(seconds=600)
98
+ self._last_get_position = datetime.now(tz=None) - timedelta(seconds=600)
99
+
100
+
101
+ #### API get and set functions ####
102
+ # Init and update vehicle data
103
+ async def discover(self):
104
+ """Discover vehicle and initial data."""
105
+ #await asyncio.gather(
106
+ # self.get_basiccardata(),
107
+ # return_exceptions=True
108
+ #)
109
+ # Extract information of relevant capabilities
110
+ if self._capabilities != None:
111
+ for capa in self._capabilities:
112
+ id=capa.get('id', '')
113
+ if self._relevantCapabilties.get(id, False):
114
+ data={}
115
+ data['active']=capa.get('active', False)
116
+ if capa.get('user-enabled', False):
117
+ data['reason']='user-enabled'
118
+ else:
119
+ data['reason']=capa.get('user-enabled', False)
120
+ if capa.get('status', False):
121
+ data['reason']=capa.get('status', '')
122
+ if capa.get('parameters', False):
123
+ if capa['parameters'].get('supportsCyclicTrips',False)==True or capa['parameters'].get('supportsCyclicTrips',False)=='true':
124
+ data['supportsCyclicTrips']=True
125
+ if capa['parameters'].get('supportsTargetStateOfCharge',False)==True or capa['parameters'].get('supportsTargetStateOfCharge',False)=='true':
126
+ data['supportsTargetStateOfCharge']=True
127
+ if capa['parameters'].get('supportsSingleTimer',False)==True or capa['parameters'].get('supportsSingleTimer',False)=='true':
128
+ data['supportsSingleTimer']=True
129
+ if capa['parameters'].get('supportsVehiclePositionedInProfileID',False)==True or capa['parameters'].get('supportsVehiclePositionedInProfileID',False)=='true':
130
+ data['supportsVehiclePositionedInProfileID']=True
131
+ if capa['parameters'].get('supportsTimerClimatisation',False)==True or capa['parameters'].get('supportsTimerClimatisation',False)=='true':
132
+ data['supportsTimerClimatisation']=True
133
+ self._relevantCapabilties[id].update(data)
134
+ else:
135
+ _LOGGER.warning(f"No capabilities information stored for vehicle with VIN {self.vin}")
136
+
137
+
138
+
139
+
140
+ # Get URLs for model image
141
+ self._modelimages = await self.get_modelimageurl()
142
+
143
+ self._discovered = datetime.now()
144
+
145
+ async def update(self, updateType=0) -> bool:
146
+ """Try to fetch data for all known API endpoints."""
147
+ # Update vehicle information if not discovered or stale information
148
+ if not self._discovered:
149
+ await self.discover()
150
+ else:
151
+ # Rediscover if data is older than 2 hours
152
+ hourago = datetime.now() - timedelta(hours = 2)
153
+ if self._discovered < hourago:
154
+ await self.discover()
155
+
156
+ # Fetch all data if car is not deactivated
157
+ if not self.deactivated:
158
+ try:
159
+ if self.attrs.get('areaAlarm', {}) !={}:
160
+ # Delete an area alarm if it is older than 900 seconds
161
+ alarmTimestamp = self.attrs.get('areaAlarm', {}).get('timestamp', 0)
162
+ if alarmTimestamp < datetime.now(tz=None) - timedelta(seconds= 900):
163
+ self.attrs.pop("areaAlarm")
164
+
165
+ if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
166
+ # Check, if fcmpushclient still started
167
+ if not self.firebase._pushClient.is_started():
168
+ _LOGGER.warning(f'firebaseStatus={self.firebaseStatus}, but state of push client is not started. Changing firebaseStatus to {FIREBASE_STATUS_ACTIVATION_STOPPED}')
169
+ self.firebaseStatus = FIREBASE_STATUS_ACTIVATION_STOPPED
170
+
171
+ fullUpdateExpired = datetime.now(tz=None) - timedelta(seconds= 1700)
172
+ oldMileage = self.distance
173
+ if self._last_get_mileage < datetime.now(tz=None) - timedelta(seconds= 300):
174
+ await self.get_mileage()
175
+ if self.distance > oldMileage:
176
+ # self.distance has changed. So it's time for a full update
177
+ _LOGGER.debug(f'Mileage has changed. Old value: {oldMileage}, new value {self.distance}. This calls for a full update.')
178
+ updateType = 1
179
+ else:
180
+ fullUpdateExpired = datetime.now(tz=None) - timedelta(seconds= 1100)
181
+
182
+ if self.firebaseStatus == FIREBASE_STATUS_ACTIVATION_STOPPED:
183
+ # Trying to activate firebase connection again
184
+ """_LOGGER.debug(f'As firebase status={self.firebaseStatus}, fcmpushclient.start() is called.')
185
+ await self.firebase._pushClient.start()
186
+ #await asyncio.sleep(5)
187
+ if self.firebase._pushClient.is_started():
188
+ self.firebaseStatus = FIREBASE_STATUS_ACTIVATED
189
+ _LOGGER.debug(f'Successfully restarted push client. New firebase status={self.firebaseStatus}')
190
+ else:
191
+ _LOGGER.warning(f'Restart of push client failed. Firebase status={self.firebaseStatus}')"""
192
+ newStatus = await self.stopFirebase()
193
+ if newStatus != FIREBASE_STATUS_NOT_INITIALISED:
194
+ _LOGGER.debug(f'stopFirebase() not successful.')
195
+ # Although stopFirebase() was not successful, the firebase status is reset to FIREBASE_STATUS_NOT_INITIALISED to allow a new initialisation
196
+ self.firebaseStatus = FIREBASE_STATUS_NOT_INITIALISED
197
+ newStatus = await self.initialiseFirebase(self._firebaseCredentialsFileName, self.updateCallback)
198
+ if newStatus == FIREBASE_STATUS_ACTIVATED:
199
+ _LOGGER.debug(f'Reinitialisation of firebase successful.New firebase status={self.firebaseStatus}.')
200
+ else:
201
+ self.firebaseStatus = FIREBASE_STATUS_ACTIVATION_STOPPED
202
+ _LOGGER.warning(f'Reinitialisation of firebase failed. New firebase status={self.firebaseStatus}.')
203
+
204
+ if self._connection._session_nightlyUpdateReduction:
205
+ # nightlyUpdateReduction is activated
206
+ if datetime.now(tz=None).hour<5 or datetime.now(tz=None).hour>=22:
207
+ # current time is within the night interval
208
+ if hasattr(self, '_last_full_update'):
209
+ _LOGGER.debug(f'last_full_update= {self._last_full_update}, fullUpdateExpired= {fullUpdateExpired}.')
210
+ if updateType<1 and (hasattr(self, '_last_full_update') and self._last_full_update>fullUpdateExpired):
211
+ _LOGGER.debug('Nightly update reduction is active and current time within 22:00 and 5:00. So we skip small update.')
212
+ return True
213
+
214
+ # Data to be updated most often
215
+ await asyncio.gather(
216
+ #self.get_charger(),
217
+ self.get_basiccardata(),
218
+ self.get_statusreport(),
219
+ return_exceptions=True
220
+ )
221
+
222
+ if hasattr(self, '_last_full_update'):
223
+ _LOGGER.debug(f'last_full_update= {self._last_full_update}, fullUpdateExpired= {fullUpdateExpired}.')
224
+ if updateType!=1 and (hasattr(self, '_last_full_update') and self._last_full_update>fullUpdateExpired):
225
+ _LOGGER.debug(f'Just performed small update for vehicle with VIN {self._connection.anonymise(self.vin)}.')
226
+ return True
227
+
228
+ # Data to be updated less often
229
+ if self.firebaseStatus != FIREBASE_STATUS_ACTIVATED:
230
+ await self.get_mileage()
231
+
232
+
233
+ await asyncio.gather(
234
+ #self.get_statusreport(),
235
+ self.get_charger(),
236
+ self.get_preheater(),
237
+ self.get_climater(),
238
+ self.get_trip_statistic(),
239
+ self.get_position(),
240
+ self.get_maintenance(),
241
+ self.get_vehicleHealthWarnings(),
242
+ self.get_departure_timers(),
243
+ self.get_departure_profiles(),
244
+ #self.get_modelimageurl(), #commented out, because getting the images discover() should be sufficient
245
+ return_exceptions=True
246
+ )
247
+ self._last_full_update = datetime.now(tz=None)
248
+ _LOGGER.debug(f'Performed full update for vehicle with VIN {self._connection.anonymise(self.vin)}.')
249
+ _LOGGER.debug(f'So far about {self._connection._sessionRequestCounter} API calls since {self._connection._sessionRequestTimestamp}.')
250
+ except:
251
+ raise SeatException("Update failed")
252
+ return True
253
+ else:
254
+ _LOGGER.info(f'Vehicle with VIN {self._connection.anonymise(self.vin)} is deactivated.')
255
+ return False
256
+ return True
257
+
258
+ # Data collection functions
259
+ async def get_modelimageurl(self):
260
+ """Fetch the URL for model image."""
261
+ return await self._connection.getModelImageURL(self.vin, self._apibase)
262
+
263
+ async def get_basiccardata(self):
264
+ """Fetch basic car data."""
265
+ data = await self._connection.getBasicCarData(self.vin, self._apibase)
266
+ if data:
267
+ self._states.update(data)
268
+ return True
269
+ else:
270
+ _LOGGER.debug('Could not fetch basic car data')
271
+ return False
272
+
273
+ async def get_mileage(self):
274
+ """Fetch basic car data."""
275
+ data = await self._connection.getMileage(self.vin, self._apibase)
276
+ if data:
277
+ self._states.update(data)
278
+ self._last_get_mileage = datetime.now(tz=None)
279
+ return True
280
+ else:
281
+ _LOGGER.debug('Could not fetch mileage data')
282
+ return False
283
+
284
+ async def get_preheater(self):
285
+ """Fetch pre-heater data if function is enabled."""
286
+ _LOGGER.info('get_preheater() not implemented yet')
287
+ #if self._relevantCapabilties.get('#dont know the name for the preheater capability', {}).get('active', False):
288
+ # if not await self.expired('rheating_v1'):
289
+ # data = await self._connection.getPreHeater(self.vin, self._apibase)
290
+ # if data:
291
+ # self._states.update(data)
292
+ # else:
293
+ # _LOGGER.debug('Could not fetch preheater data')
294
+ #else:
295
+ # self._requests.pop('preheater', None)
296
+
297
+ async def get_climater(self):
298
+ """Fetch climater data if function is enabled."""
299
+ if self._relevantCapabilties.get('climatisation', {}).get('active', False):
300
+ data = await self._connection.getClimater(self.vin, self._apibase, deepcopy(self.attrs.get('climater',{})))
301
+ if data:
302
+ self._states.update(data)
303
+ self._last_get_climater = datetime.now(tz=None)
304
+ return True
305
+ else:
306
+ _LOGGER.debug('Could not fetch climater data')
307
+ return False
308
+ #else:
309
+ # self._requests.pop('climatisation', None)
310
+
311
+ async def get_trip_statistic(self):
312
+ """Fetch trip data if function is enabled."""
313
+ if self._relevantCapabilties.get('tripStatistics', {}).get('active', False):
314
+ data = await self._connection.getTripStatistics(self.vin, self._apibase, self._relevantCapabilties['tripStatistics'].get('supportsCyclicTrips', False))
315
+ if data:
316
+ self._states.update(data)
317
+ return True
318
+ else:
319
+ _LOGGER.debug('Could not fetch trip statistics')
320
+ return False
321
+
322
+ async def get_position(self):
323
+ """Fetch position data if function is enabled."""
324
+ if self._relevantCapabilties.get('parkingPosition', {}).get('active', False):
325
+ data = await self._connection.getPosition(self.vin, self._apibase)
326
+ if data:
327
+ # Reset requests remaining to 15 if parking time has been updated
328
+ if data.get('findCarResponse', {}).get('parkingTimeUTC', False):
329
+ try:
330
+ newTime = data.get('findCarResponse').get('parkingTimeUTC')
331
+ oldTime = self.attrs.get('findCarResponse').get('parkingTimeUTC')
332
+ if newTime > oldTime:
333
+ self.requests_remaining = 15
334
+ except:
335
+ pass
336
+ self._states.update(data)
337
+ self._last_get_position = datetime.now(tz=None)
338
+ return True
339
+ else:
340
+ _LOGGER.debug('Could not fetch any positional data')
341
+ return False
342
+
343
+ async def get_vehicleHealthWarnings(self):
344
+ if self._relevantCapabilties.get('vehicleHealthWarnings', {}).get('active', False):
345
+ data = await self._connection.getVehicleHealthWarnings(self.vin, self._apibase)
346
+ if data:
347
+ self._states.update(data)
348
+ return True
349
+ else:
350
+ _LOGGER.debug('Could not fetch vehicle health warnings')
351
+ return False
352
+
353
+ async def get_statusreport(self):
354
+ """Fetch status data if function is enabled."""
355
+ if self._relevantCapabilties.get('state', {}).get('active', False):
356
+ data = await self._connection.getVehicleStatusReport(self.vin, self._apibase)
357
+ if data:
358
+ self._states.update(data)
359
+ self._last_get_statusreport = datetime.now(tz=None)
360
+ return True
361
+ else:
362
+ _LOGGER.debug('Could not fetch status report')
363
+ return False
364
+
365
+ async def get_maintenance(self):
366
+ """Fetch maintenance data if function is enabled."""
367
+ if self._relevantCapabilties.get('vehicleHealthInspection', {}).get('active', False):
368
+ data = await self._connection.getMaintenance(self.vin, self._apibase)
369
+ if data:
370
+ self._states.update(data)
371
+ return True
372
+ else:
373
+ _LOGGER.debug('Could not fetch status report')
374
+ return False
375
+
376
+ async def get_charger(self):
377
+ """Fetch charger data if function is enabled."""
378
+ if self._relevantCapabilties.get('charging', {}).get('active', False):
379
+ data = await self._connection.getCharger(self.vin, self._apibase, deepcopy(self.attrs.get('charging',{})))
380
+ if data:
381
+ self._states.update(data)
382
+ self._last_get_charger = datetime.now(tz=None)
383
+ return True
384
+ else:
385
+ _LOGGER.debug('Could not fetch charger data')
386
+ return False
387
+
388
+ async def get_departure_timers(self):
389
+ """Fetch timer data if function is enabled."""
390
+ if self._relevantCapabilties.get('departureTimers', {}).get('active', False):
391
+ data = await self._connection.getDeparturetimer(self.vin, self._apibase)
392
+ if data:
393
+ self._states.update(data)
394
+ self._last_get_departure_timers = datetime.now(tz=None)
395
+ return True
396
+ else:
397
+ _LOGGER.debug('Could not fetch timers')
398
+ return False
399
+
400
+ async def get_departure_profiles(self):
401
+ """Fetch timer data if function is enabled."""
402
+ if self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
403
+ data = await self._connection.getDepartureprofiles(self.vin, self._apibase)
404
+ if data:
405
+ self._states.update(data)
406
+ self._last_get_departure_profiles = datetime.now(tz=None)
407
+ return True
408
+ else:
409
+ _LOGGER.debug('Could not fetch timers')
410
+ return False
411
+
412
+ #async def wait_for_request(self, section, request, retryCount=36):
413
+ """Update status of outstanding requests."""
414
+ """retryCount -= 1
415
+ if (retryCount == 0):
416
+ _LOGGER.info(f'Timeout while waiting for result of {request}.')
417
+ return 'Timeout'
418
+ try:
419
+ status = await self._connection.get_request_status(self.vin, section, request, self._apibase)
420
+ _LOGGER.info(f'Request for {section} with ID {request}: {status}')
421
+ if status == 'In progress':
422
+ self._requests['state'] = 'In progress'
423
+ await asyncio.sleep(5)
424
+ return await self.wait_for_request(section, request, retryCount)
425
+ else:
426
+ self._requests['state'] = status
427
+ return status
428
+ except Exception as error:
429
+ _LOGGER.warning(f'Exception encountered while waiting for request status: {error}')
430
+ return 'Exception'"""
431
+
432
+ # Data set functions
433
+ # API endpoint charging
434
+ async def set_charger_current(self, value):
435
+ """Set charger current"""
436
+ if self.is_charging_supported:
437
+ # Set charger max ampere to integer value
438
+ if isinstance(value, int):
439
+ if 1 <= int(value) <= 255:
440
+ # VW-Group API charger current request
441
+ if self._relevantCapabilties.get('charging', {}).get('active', False):
442
+ data = {'maxChargeCurrentAc': int(value)}
443
+ if int(value)==252:
444
+ data = {'maxChargeCurrentAc': 'reduced'}
445
+ elif int(value)==254:
446
+ data = {'maxChargeCurrentAc': 'maximum'}
447
+ else:
448
+ data = {'maxChargeCurrentAcInAmperes': int(value)}
449
+ else:
450
+ _LOGGER.error(f'Set charger maximum current to {value} is not supported.')
451
+ raise SeatInvalidRequestException(f'Set charger maximum current to {value} is not supported.')
452
+ # Mimick app and set charger max ampere to Maximum/Reduced
453
+ elif isinstance(value, str):
454
+ if value in ['Maximum', 'maximum', 'Max', 'max', 'Minimum', 'minimum', 'Min', 'min', 'Reduced', 'reduced']:
455
+ # VW-Group API charger current request
456
+ if self._relevantCapabilties.get('charging', {}).get('active', False):
457
+ value = 'maximum' if value in ['Maximum', 'maximum', 'Max', 'max'] else 'reduced'
458
+ data = {'maxChargeCurrentAc': value}
459
+ else:
460
+ _LOGGER.error(f'Set charger maximum current to {value} is not supported.')
461
+ raise SeatInvalidRequestException(f'Set charger maximum current to {value} is not supported.')
462
+ else:
463
+ _LOGGER.error(f'Data type passed is invalid.')
464
+ raise SeatInvalidRequestException(f'Invalid data type.')
465
+ return await self.set_charger('settings', data)
466
+ else:
467
+ _LOGGER.error('No charger support.')
468
+ raise SeatInvalidRequestException('No charger support.')
469
+
470
+ async def set_charger_target_soc(self, value):
471
+ """Set target state of charge"""
472
+ if self.is_charging_supported:
473
+ if isinstance(value, int):
474
+ if 1 <= int(value) <= 100:
475
+ # VW-Group API charger current request
476
+ if self._relevantCapabilties.get('charging', {}).get('active', False) and self._relevantCapabilties.get('charging', {}).get('supportsTargetStateOfCharge', False):
477
+ data= deepcopy(self.attrs.get('charging',{}).get('info',{}).get('settings',{}))
478
+ if data=={}:
479
+ _LOGGER.error(f'Can not set target soc, because currently no charging settings are present.')
480
+ raise SeatInvalidRequestException(f'Set target soc not possible. Charging settings not present.')
481
+ data['targetSoc'] = int(value)
482
+ else:
483
+ _LOGGER.warning(f'Can not set target soc, because vehicle does not support this feature.')
484
+ return False
485
+ else:
486
+ _LOGGER.error(f'Set target soc to {value} is not supported.')
487
+ raise SeatInvalidRequestException(f'Set target soc to {value} is not supported.')
488
+ # Mimick app and set charger max ampere to Maximum/Reduced
489
+ else:
490
+ _LOGGER.error(f'Data type passed is invalid.')
491
+ raise SeatInvalidRequestException(f'Invalid data type.')
492
+ return await self.set_charger('settings', data)
493
+ else:
494
+ _LOGGER.error('No charger support.')
495
+ raise SeatInvalidRequestException('No charger support.')
496
+
497
+ async def set_charger(self, action, data=None):
498
+ """Charging actions."""
499
+ if not self._relevantCapabilties.get('charging', {}).get('active', False):
500
+ _LOGGER.info('Remote start/stop of charger is not supported.')
501
+ raise SeatInvalidRequestException('Remote start/stop of charger is not supported.')
502
+ if self._requests['batterycharge'].get('id', False):
503
+ timestamp = self._requests.get('batterycharge', {}).get('timestamp', datetime.now())
504
+ expired = datetime.now() - timedelta(minutes=1)
505
+ if expired > timestamp:
506
+ self._requests.get('batterycharge', {}).pop('id')
507
+ else:
508
+ raise SeatRequestInProgressException('Charging action already in progress')
509
+ if self._relevantCapabilties.get('charging', {}).get('active', False):
510
+ if action in ['start', 'Start', 'On', 'on']:
511
+ mode='start'
512
+ elif action in ['stop', 'Stop', 'Off', 'off']:
513
+ mode='stop'
514
+ elif action=='settings':
515
+ mode=action
516
+ else:
517
+ _LOGGER.error(f'Invalid charger action: {action}. Must be either start, stop or setSettings')
518
+ raise SeatInvalidRequestException(f'Invalid charger action: {action}. Must be either start, stop or setSettings')
519
+ try:
520
+ self._requests['latest'] = 'Charger'
521
+ response = await self._connection.setCharger(self.vin, self._apibase, mode, data)
522
+ if not response:
523
+ self._requests['batterycharge'] = {'status': 'Failed'}
524
+ _LOGGER.error(f'Failed to {action} charging')
525
+ raise SeatException(f'Failed to {action} charging')
526
+ else:
527
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
528
+ self._requests['batterycharge'] = {
529
+ 'timestamp': datetime.now(),
530
+ 'status': response.get('state', 'Unknown'),
531
+ 'id': response.get('id', 0)
532
+ }
533
+ # if firebaseStatus is FIREBASE_STATUS_ACTIVATED, the request is assumed successful. Waiting for push notification before rereading status
534
+ if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
535
+ _LOGGER.debug('POST request for charger assumed successful. Waiting for push notification')
536
+ return True
537
+ # Update the charger data and check, if they have changed as expected
538
+ retry = 0
539
+ actionSuccessful = False
540
+ while not actionSuccessful and retry < 2:
541
+ await asyncio.sleep(15)
542
+ await self.get_charger()
543
+ await self.get_basiccardata() # We get both, get_charger() and get_basiccardata()
544
+ if mode == 'start':
545
+ if self.charging:
546
+ actionSuccessful = True
547
+ elif mode == 'stop':
548
+ if not self.charging:
549
+ actionSuccessful = True
550
+ elif mode == 'settings':
551
+ if data.get('targetSoc',0) == self.target_soc: # In case targetSoc is changed
552
+ actionSuccessful = True
553
+ if data.get('maxChargeCurrentAc','') == self.charge_max_ampere: # In case 'maximum', 'reduced'
554
+ actionSuccessful = True
555
+ if data.get('maxChargeCurrentAcInAmperes',0) == self.charge_max_ampere: # In case of a numerical value for charge current
556
+ actionSuccessful = True
557
+ else:
558
+ _LOGGER.error(f'Missing code in vehicle._set_charger() for mode {mode}')
559
+ raise
560
+ retry = retry +1
561
+ if actionSuccessful:
562
+ _LOGGER.debug('POST request for charger successful. New status as expected.')
563
+ self._requests.get('batterycharge', {}).pop('id')
564
+ return True
565
+ _LOGGER.error('Response to POST request seemed successful but the charging status did not change as expected.')
566
+ return False
567
+ except (SeatInvalidRequestException, SeatException):
568
+ raise
569
+ except Exception as error:
570
+ _LOGGER.warning(f'Failed to {action} charging - {error}')
571
+ self._requests['batterycharge'] = {'status': 'Exception'}
572
+ raise SeatException(f'Failed to execute set charger - {error}')
573
+
574
+ # API endpoint departuretimer
575
+ async def set_charge_limit(self, limit=50):
576
+ """ Set minimum state of charge limit for departure timers or departure profiles. """
577
+ if (not self._relevantCapabilties.get('departureTimers', {}).get('active', False) and
578
+ not self._relevantCapabilties.get('departureProfiles', {}).get('active', False) and
579
+ not self._relevantCapabilties.get('charging', {}).get('active', False)):
580
+ _LOGGER.info('Set charging limit is not supported.')
581
+ raise SeatInvalidRequestException('Set charging limit is not supported.')
582
+ if self._relevantCapabilties.get('departureTimers', {}).get('active', False) :
583
+ # Vehicle has departure timers
584
+ data = {}
585
+ if isinstance(limit, int):
586
+ if limit in [0, 10, 20, 30, 40, 50]:
587
+ data['minSocPercentage'] = limit
588
+ else:
589
+ raise SeatInvalidRequestException(f'Charge limit must be one of 0, 10, 20, 30, 40 or 50.')
590
+ else:
591
+ raise SeatInvalidRequestException(f'Charge limit "{limit}" is not supported.')
592
+ return await self._set_timers(data)
593
+ elif self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
594
+ # Vehicle has departure profiles
595
+ data= deepcopy(self.attrs.get('departureProfiles'))
596
+ if isinstance(limit, int):
597
+ if limit in [0, 10, 20, 30, 40, 50]:
598
+ data['minSocPercentage'] = limit
599
+ else:
600
+ raise SeatInvalidRequestException(f'Charge limit must be one of 0, 10, 20, 30, 40 or 50.')
601
+ else:
602
+ raise SeatInvalidRequestException(f'Charge limit "{limit}" is not supported.')
603
+ return await self._set_departure_profiles(data, action='minSocPercentage')
604
+
605
+ async def set_timer_active(self, id=1, action='off'):
606
+ """ Activate/deactivate departure timers. """
607
+ data = {}
608
+ supported = "is_departure" + str(id) + "_supported"
609
+ if getattr(self, supported) is not True:
610
+ raise SeatConfigException(f'This vehicle does not support timer id {id}.')
611
+ if self._relevantCapabilties.get('departureTimers', {}).get('active', False):
612
+ allTimers= self.attrs.get('departureTimers').get('timers', [])
613
+ for singleTimer in allTimers:
614
+ if singleTimer.get('id',-1)==id:
615
+ if action in ['on', 'off']:
616
+ if action=='on':
617
+ enabled=True
618
+ else:
619
+ enabled=False
620
+ singleTimer['enabled'] = enabled
621
+ data = {
622
+ 'timers' : []
623
+ }
624
+ data['timers'].append(singleTimer)
625
+ else:
626
+ raise SeatInvalidRequestException(f'Timer action "{action}" is not supported.')
627
+ return await self._set_timers(data)
628
+ raise SeatInvalidRequestException(f'Departure timer id {id} not found.')
629
+ else:
630
+ raise SeatInvalidRequestException('Departure timers are not supported.')
631
+
632
+ async def set_timer_schedule(self, id, schedule={}):
633
+ """ Set departure timer schedule. """
634
+ data = {}
635
+ # Validate required user inputs
636
+ supported = "is_departure" + str(id) + "_supported"
637
+ if getattr(self, supported) is not True:
638
+ raise SeatConfigException(f'Timer id {id} is not supported for this vehicle.')
639
+ else:
640
+ _LOGGER.debug(f'Timer id {id} is supported')
641
+ if not schedule:
642
+ raise SeatInvalidRequestException('A schedule must be set.')
643
+ if not isinstance(schedule.get('enabled', ''), bool):
644
+ raise SeatInvalidRequestException('The enabled variable must be set to True or False.')
645
+ if not isinstance(schedule.get('recurring', ''), bool):
646
+ raise SeatInvalidRequestException('The recurring variable must be set to True or False.')
647
+ if not re.match('^[0-9]{2}:[0-9]{2}$', schedule.get('time', '')):
648
+ raise SeatInvalidRequestException('The time for departure must be set in 24h format HH:MM.')
649
+
650
+ # Validate optional inputs
651
+ if schedule.get('recurring', False):
652
+ if not re.match('^[yn]{7}$', schedule.get('days', '')):
653
+ raise SeatInvalidRequestException('For recurring schedules the days variable must be set to y/n mask (mon-sun with only wed enabled): nnynnnn.')
654
+ elif not schedule.get('recurring'):
655
+ if not re.match('^[0-9]{4}-[0-9]{2}-[0-9]{2}$', schedule.get('date', '')):
656
+ raise SeatInvalidRequestException('For single departure schedule the date variable must be set to YYYY-mm-dd.')
657
+
658
+ if self._relevantCapabilties.get('departureTimers', {}).get('active', False):
659
+ # Sanity check for off-peak hours
660
+ if not isinstance(schedule.get('nightRateActive', False), bool):
661
+ raise SeatInvalidRequestException('The off-peak active variable must be set to True or False')
662
+ if schedule.get('nightRateStart', None) is not None:
663
+ if not re.match('^[0-9]{2}:[0-9]{2}$', schedule.get('nightRateStart', '')):
664
+ raise SeatInvalidRequestException('The start time for off-peak hours must be set in 24h format HH:MM.')
665
+ if schedule.get('nightRateEnd', None) is not None:
666
+ if not re.match('^[0-9]{2}:[0-9]{2}$', schedule.get('nightRateEnd', '')):
667
+ raise SeatInvalidRequestException('The start time for off-peak hours must be set in 24h format HH:MM.')
668
+
669
+ # Check if charging/climatisation is set and correct
670
+ if not isinstance(schedule.get('operationClimatisation', False), bool):
671
+ raise SeatInvalidRequestException('The climatisation enable variable must be set to True or False')
672
+ if not isinstance(schedule.get('operationCharging', False), bool):
673
+ raise SeatInvalidRequestException('The charging variable must be set to True or False')
674
+
675
+ # Validate temp setting, if set
676
+ if schedule.get("targetTemp", None) is not None:
677
+ if not 16 <= int(schedule.get("targetTemp", None)) <= 30:
678
+ raise SeatInvalidRequestException('Target temp must be integer value from 16 to 30')
679
+ else:
680
+ data['temp'] = int(schedule.get('targetTemp'))
681
+ raise SeatInvalidRequestException('Target temp (yet) not supported.')
682
+
683
+ # Validate charge target and current
684
+ if schedule.get("targetChargeLevel", None) is not None:
685
+ if not 0 <= int(schedule.get("targetChargeLevel", None)) <= 100:
686
+ raise SeatInvalidRequestException('Target charge level must be 0 to 100')
687
+ else:
688
+ raise SeatInvalidRequestException('targetChargeLevel (yet) not supported.')
689
+ if schedule.get("chargeMaxCurrent", None) is not None:
690
+ raise SeatInvalidRequestException('chargeMaxCurrent (yet) not supported.')
691
+ if isinstance(schedule.get('chargeMaxCurrent', None), str):
692
+ if not schedule.get("chargeMaxCurrent", None) in ['Maximum', 'maximum', 'Max', 'max', 'Minimum', 'minimum', 'Min', 'min', 'Reduced', 'reduced']:
693
+ raise SeatInvalidRequestException('Charge current must be one of Maximum/Minimum/Reduced')
694
+ elif isinstance(schedule.get('chargeMaxCurrent', None), int):
695
+ if not 1 <= int(schedule.get("chargeMaxCurrent", 254)) < 255:
696
+ raise SeatInvalidRequestException('Charge current must be set from 1 to 254')
697
+ else:
698
+ raise SeatInvalidRequestException('Invalid type for charge max current variable')
699
+
700
+ # Prepare data and execute
701
+ data['id'] = id
702
+ # Converting schedule to data map
703
+ if schedule.get("enabled",False):
704
+ data['enabled']=True
705
+ else:
706
+ data['enabled']=False
707
+ if schedule.get("operationCharging",False):
708
+ data['charging']=True
709
+ else:
710
+ data['charging']=False
711
+ if schedule.get("operationClimatisation",False):
712
+ data['climatisation']=True
713
+ else:
714
+ data['climatisation']=False
715
+ if schedule.get("nightRateAcvtive", False):
716
+ preferedChargingTimes= [{
717
+ "id" : 1,
718
+ "enabled" : True,
719
+ "startTimeLocal" : schedule.get('nightRateStart',"00:00"),
720
+ "endTimeLocal" : schedule.get('nightRateStop',"00:00")
721
+ }]
722
+ else:
723
+ preferedChargingTimes= [{
724
+ "id" : 1,
725
+ "enabled" : False,
726
+ "startTimeLocal" : "00:00",
727
+ "endTimeLocal" : "00:00"
728
+ }]
729
+ if schedule.get("recurring",False):
730
+ data['recurringTimer']= {
731
+ "startTimeLocal": schedule.get('time',"00:00"),
732
+ "recurringOn":{""
733
+ "mondays":(schedule.get('days',"nnnnnnn")[0]=='y'),
734
+ "tuesdays":(schedule.get('days',"nnnnnnn")[1]=='y'),
735
+ "wednesdays":(schedule.get('days',"nnnnnnn")[2]=='y'),
736
+ "thursdays":(schedule.get('days',"nnnnnnn")[3]=='y'),
737
+ "fridays":(schedule.get('days',"nnnnnnn")[4]=='y'),
738
+ "saturdays":(schedule.get('days',"nnnnnnn")[5]=='y'),
739
+ "sundays":(schedule.get('days',"nnnnnnn")[6]=='y'),
740
+ #"preferredChargingTimes": preferedChargingTimes
741
+ }
742
+ }
743
+ else:
744
+ startDateTime = datetime.fromisoformat(schedule.get('date',"2025-01-01")+'T'+schedule.get('time',"00:00"))
745
+ _LOGGER.info(f'startDateTime={startDateTime.isoformat()}')
746
+ data['singleTimer']= {
747
+ "startDateTimeLocal": startDateTime.isoformat(),
748
+ #"preferredChargingTimes": preferedChargingTimes
749
+ }
750
+ data["preferredChargingTimes"]= preferedChargingTimes
751
+
752
+ # Now we have to embed the data for the timer 'id' in timers[]
753
+ data={
754
+ 'timers' : [data]
755
+ }
756
+ return await self._set_timers(data)
757
+ else:
758
+ _LOGGER.info('Departure timers are not supported.')
759
+ raise SeatInvalidRequestException('Departure timers are not supported.')
760
+
761
+ async def _set_timers(self, data=None):
762
+ """ Set departure timers. """
763
+ if not self._relevantCapabilties.get('departureTimers', {}).get('active', False):
764
+ raise SeatInvalidRequestException('Departure timers are not supported.')
765
+ if self._requests['departuretimer'].get('id', False):
766
+ timestamp = self._requests.get('departuretimer', {}).get('timestamp', datetime.now())
767
+ expired = datetime.now() - timedelta(minutes=1)
768
+ if expired > timestamp:
769
+ self._requests.get('departuretimer', {}).pop('id')
770
+ else:
771
+ raise SeatRequestInProgressException('Scheduling of departure timer is already in progress')
772
+
773
+ try:
774
+ self._requests['latest'] = 'Departuretimer'
775
+ response = await self._connection.setDeparturetimer(self.vin, self._apibase, data, spin=False)
776
+ if not response:
777
+ self._requests['departuretimer'] = {'status': 'Failed'}
778
+ _LOGGER.error('Failed to execute departure timer request')
779
+ raise SeatException('Failed to execute departure timer request')
780
+ else:
781
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
782
+ self._requests['departuretimer'] = {
783
+ 'timestamp': datetime.now(),
784
+ 'status': response.get('state', 'Unknown'),
785
+ 'id': response.get('id', 0),
786
+ }
787
+ # if firebaseStatus is FIREBASE_STATUS_ACTIVATED, the request is assumed successful. Waiting for push notification before rereading status
788
+ if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
789
+ _LOGGER.debug('POST request for change of departure timers assumed successful. Waiting for push notification')
790
+ return True
791
+ # Update the departure timers data and check, if they have changed as expected
792
+ retry = 0
793
+ actionSuccessful = False
794
+ while not actionSuccessful and retry < 2:
795
+ await asyncio.sleep(15)
796
+ await self.get_departure_timers()
797
+ if data.get('minSocPercentage',False):
798
+ if data.get('minSocPercentage',-2)==self.attrs.get('departureTimers',{}).get('minSocPercentage',-1):
799
+ actionSuccessful=True
800
+ else:
801
+ _LOGGER.debug('Checking if new departure timer is as expected:')
802
+ timerData = data.get('timers',[])[0]
803
+ timerDataId = timerData.get('id',False)
804
+ timerDataCopy = deepcopy(timerData)
805
+ timerDataCopy['enabled']=True
806
+ if timerDataId:
807
+ newTimers = self.attrs.get('departureTimers',{}).get('timers',[])
808
+ for newTimer in newTimers:
809
+ if newTimer.get('id',-1)==timerDataId:
810
+ _LOGGER.debug(f'Value of timer sent:{timerData}')
811
+ _LOGGER.debug(f'Value of timer read:{newTimer}')
812
+ if timerData==newTimer:
813
+ actionSuccessful=True
814
+ elif timerDataCopy==newTimer:
815
+ _LOGGER.debug('Data written and data read are the same, but the timer is activated.')
816
+ actionSuccessful=True
817
+ break
818
+ retry = retry +1
819
+ if True: #actionSuccessful:
820
+ #_LOGGER.debug('POST request for departure timers successful. New status as expected.')
821
+ self._requests.get('departuretimer', {}).pop('id')
822
+ return True
823
+ _LOGGER.error('Response to POST request seemed successful but the departure timers status did not change as expected.')
824
+ return False
825
+ except (SeatInvalidRequestException, SeatException):
826
+ raise
827
+ except Exception as error:
828
+ _LOGGER.warning(f'Failed to execute departure timer request - {error}')
829
+ self._requests['departuretimer'] = {'status': 'Exception'}
830
+ raise SeatException('Failed to set departure timer schedule')
831
+
832
+ async def set_departure_profile_schedule(self, id, schedule={}):
833
+ """ Set departure profile schedule. """
834
+ data = {}
835
+ # Validate required user inputs
836
+ supported = "is_departure_profile" + str(id) + "_supported"
837
+ if getattr(self, supported) is not True:
838
+ raise SeatConfigException(f'Departure profile id {id} is not supported for this vehicle.')
839
+ else:
840
+ _LOGGER.debug(f'Departure profile id {id} is supported')
841
+ if not schedule:
842
+ raise SeatInvalidRequestException('A schedule must be set.')
843
+ if not isinstance(schedule.get('enabled', ''), bool):
844
+ raise SeatInvalidRequestException('The enabled variable must be set to True or False.')
845
+ if not isinstance(schedule.get('recurring', ''), bool):
846
+ raise SeatInvalidRequestException('The recurring variable must be set to True or False.')
847
+ if not re.match('^[0-9]{2}:[0-9]{2}$', schedule.get('time', '')):
848
+ raise SeatInvalidRequestException('The time for departure must be set in 24h format HH:MM.')
849
+
850
+ # Validate optional inputs
851
+ if schedule.get('recurring', False):
852
+ if not re.match('^[yn]{7}$', schedule.get('days', '')):
853
+ raise SeatInvalidRequestException('For recurring schedules the days variable must be set to y/n mask (mon-sun with only wed enabled): nnynnnn.')
854
+ elif not schedule.get('recurring'):
855
+ if not re.match('^[0-9]{4}-[0-9]{2}-[0-9]{2}$', schedule.get('date', '')):
856
+ raise SeatInvalidRequestException('For single departure profile schedule the date variable must be set to YYYY-mm-dd.')
857
+
858
+ if self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
859
+ # Check if profileIds is set and correct
860
+ if schedule.get('chargingProgramId', False):
861
+ # At the moment, only one charging program id is supported
862
+ chargingProgramId = int(schedule.get('chargingProgramId', False))
863
+ found = False
864
+ for chargingProgram in self.attrs.get('departureProfiles', {}).get('profileIds', []):
865
+ if chargingProgram.get('id',None) == chargingProgramId:
866
+ found = True
867
+ break
868
+ if not found:
869
+ raise SeatInvalidRequestException('The charging program id provided for the departure profile schedule is unknown.')
870
+ else:
871
+ profileIds = []
872
+ profileIds.append(chargingProgramId)
873
+ else:
874
+ raise SeatInvalidRequestException('No charging program id provided for departure profile schedule.')
875
+
876
+ newDepProfileSchedule = {}
877
+ # Prepare data and execute
878
+ newDepProfileSchedule['id'] = id
879
+ # Converting schedule to data map
880
+ if schedule.get("enabled",False):
881
+ newDepProfileSchedule['enabled']=True
882
+ else:
883
+ newDepProfileSchedule['enabled']=False
884
+ if schedule.get("recurring",False):
885
+ newDepProfileSchedule['recurringTimer']= {
886
+ "startTime": schedule.get('time',"00:00"),
887
+ "recurringOn":{""
888
+ "mondays":(schedule.get('days',"nnnnnnn")[0]=='y'),
889
+ "tuesdays":(schedule.get('days',"nnnnnnn")[1]=='y'),
890
+ "wednesdays":(schedule.get('days',"nnnnnnn")[2]=='y'),
891
+ "thursdays":(schedule.get('days',"nnnnnnn")[3]=='y'),
892
+ "fridays":(schedule.get('days',"nnnnnnn")[4]=='y'),
893
+ "saturdays":(schedule.get('days',"nnnnnnn")[5]=='y'),
894
+ "sundays":(schedule.get('days',"nnnnnnn")[6]=='y'),
895
+ }
896
+ }
897
+ else:
898
+ if self._relevantCapabilties.get('departureProfiles', {}).get('supportsSingleTimer', False):
899
+ startDateTime = datetime.fromisoformat(schedule.get('date',"2025-01-01")+'T'+schedule.get('time',"00:00"))
900
+ _LOGGER.info(f'startDateTime={startDateTime.isoformat()}')
901
+ newDepProfileSchedule['singleTimer']= {
902
+ "startDateTimeLocal": startDateTime.isoformat(),
903
+ }
904
+ else:
905
+ raise SeatInvalidRequestException('Vehicle does not support single timer.')
906
+ newDepProfileSchedule["profileIds"]= profileIds
907
+
908
+ # Now we have to substitute the current departure profile schedule with the given id by the new one
909
+ data= deepcopy(self.attrs.get('departureProfiles'))
910
+ if len(data.get('timers', []))<1:
911
+ raise SeatInvalidRequestException(f'No timers found in departure profile: {data}.')
912
+ idFound=False
913
+ for e in range(len(data.get('timers', []))):
914
+ if data['timers'][e].get('id',-1)==id:
915
+ data['timers'][e] = newDepProfileSchedule
916
+ idFound=True
917
+ if idFound:
918
+ return await self._set_departure_profiles(data, action='set')
919
+ raise SeatInvalidRequestException(f'Departure profile id {id} not found in {data.get('timers',[])}.')
920
+ else:
921
+ _LOGGER.info('Departure profiles are not supported.')
922
+ raise SeatInvalidRequestException('Departure profiles are not supported.')
923
+
924
+ async def set_departure_profile_active(self, id=1, action='off'):
925
+ """ Activate/deactivate departure profiles. """
926
+ data = {}
927
+ supported = "is_departure_profile" + str(id) + "_supported"
928
+ if getattr(self, supported) is not True:
929
+ raise SeatConfigException(f'This vehicle does not support departure profile id "{id}".')
930
+ if self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
931
+ data= deepcopy(self.attrs.get('departureProfiles'))
932
+ if len(data.get('timers', []))<1:
933
+ raise SeatInvalidRequestException(f'No timers found in departure profile: {data}.')
934
+ idFound=False
935
+ for e in range(len(data.get('timers', []))):
936
+ if data['timers'][e].get('id',-1)==id:
937
+ if action in ['on', 'off']:
938
+ if action=='on':
939
+ enabled=True
940
+ else:
941
+ enabled=False
942
+ data['timers'][e]['enabled'] = enabled
943
+ idFound=True
944
+ _LOGGER.debug(f'Changing departure profile {id} to {action}.')
945
+ else:
946
+ raise SeatInvalidRequestException(f'Profile action "{action}" is not supported.')
947
+ if idFound:
948
+ return await self._set_departure_profiles(data, action=action)
949
+ raise SeatInvalidRequestException(f'Departure profile id {id} not found in {data.get('timers',[])}.')
950
+ else:
951
+ raise SeatInvalidRequestException('Departure profiles are not supported.')
952
+
953
+ async def _set_departure_profiles(self, data=None, action=None):
954
+ """ Set departure profiles. """
955
+ if not self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
956
+ raise SeatInvalidRequestException('Departure profiles are not supported.')
957
+ if self._requests['departureprofile'].get('id', False):
958
+ timestamp = self._requests.get('departureprofile', {}).get('timestamp', datetime.now())
959
+ expired = datetime.now() - timedelta(minutes=1)
960
+ if expired > timestamp:
961
+ self._requests.get('departureprofile', {}).pop('id')
962
+ else:
963
+ raise SeatRequestInProgressException('Scheduling of departure profile is already in progress')
964
+ try:
965
+ self._requests['latest'] = 'Departureprofile'
966
+ response = await self._connection.setDepartureprofile(self.vin, self._apibase, data, spin=False)
967
+ if not response:
968
+ self._requests['departureprofile'] = {'status': 'Failed'}
969
+ _LOGGER.error('Failed to execute departure profile request')
970
+ raise SeatException('Failed to execute departure profile request')
971
+ else:
972
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
973
+ self._requests['departureprofile'] = {
974
+ 'timestamp': datetime.now(),
975
+ 'status': response.get('state', 'Unknown'),
976
+ 'id': response.get('id', 0),
977
+ }
978
+ # if firebaseStatus is FIREBASE_STATUS_ACTIVATED, the request is assumed successful. Waiting for push notification before rereading status
979
+ if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
980
+ _LOGGER.debug('POST request for change of departure profiles assumed successful. Waiting for push notification')
981
+ return True
982
+ # Update the departure profile data and check, if they have changed as expected
983
+ retry = 0
984
+ actionSuccessful = False
985
+ while not actionSuccessful and retry < 2:
986
+ await asyncio.sleep(15)
987
+ await self.get_departure_profiles()
988
+ if action=='minSocPercentage':
989
+ _LOGGER.debug('Checking if new minSocPercentage is as expected:')
990
+ _LOGGER.debug(f'Value of minSocPercentage sent:{data.get('minSocPercentage',-2)}')
991
+ _LOGGER.debug(f'Value of minSocPercentage read:{self.attrs.get('departureTimers',{}).get('minSocPercentage',-1)}')
992
+ if data.get('minSocPercentage',-2)==self.attrs.get('departureTimers',{}).get('minSocPercentage',-1):
993
+ actionSuccessful=True
994
+ else:
995
+ sendData = data.get('timers',[])
996
+ newData = self.attrs.get('departureProfiles',{}).get('timers',[])
997
+ _LOGGER.debug('Checking if new departure profiles are as expected:')
998
+ _LOGGER.debug(f'Value of data sent:{sendData}')
999
+ _LOGGER.debug(f'Value of data read:{newData}')
1000
+ if sendData==newData:
1001
+ actionSuccessful=True
1002
+ retry = retry +1
1003
+ if actionSuccessful:
1004
+ self._requests.get('departureprofile', {}).pop('id')
1005
+ return True
1006
+ _LOGGER.error('Response to PUT request seemed successful but the departure profiles status did not change as expected.')
1007
+ return False
1008
+ except (SeatInvalidRequestException, SeatException):
1009
+ raise
1010
+ except Exception as error:
1011
+ _LOGGER.warning(f'Failed to execute departure profile request - {error}')
1012
+ self._requests['departureprofile'] = {'status': 'Exception'}
1013
+ raise SeatException('Failed to set departure profile schedule')
1014
+
1015
+
1016
+ # Send a destination to vehicle
1017
+ async def send_destination(self, destination=None):
1018
+ """ Send destination to vehicle. """
1019
+
1020
+ if destination==None:
1021
+ _LOGGER.error('No destination provided')
1022
+ raise
1023
+ else:
1024
+ data=[]
1025
+ data.append(destination)
1026
+ try:
1027
+ response = await self._connection.sendDestination(self.vin, self._apibase, data, spin=False)
1028
+ if not response:
1029
+ _LOGGER.error('Failed to execute send destination request')
1030
+ raise SeatException('Failed to execute send destination request')
1031
+ else:
1032
+ return True
1033
+ except (SeatInvalidRequestException, SeatException):
1034
+ raise
1035
+ except Exception as error:
1036
+ _LOGGER.warning(f'Failed to execute send destination request - {error}')
1037
+ raise SeatException('Failed to send destination to vehicle')
1038
+
1039
+ # Climatisation electric/auxiliary/windows (CLIMATISATION)
1040
+ async def set_climatisation_temp(self, temperature=20):
1041
+ """Set climatisation target temp."""
1042
+ if self.is_electric_climatisation_supported or self.is_auxiliary_climatisation_supported:
1043
+ if 16 <= float(temperature) <= 30:
1044
+ data = {
1045
+ 'climatisationWithoutExternalPower': self.climatisation_without_external_power,
1046
+ 'targetTemperature': temperature,
1047
+ 'targetTemperatureUnit': 'celsius'
1048
+ }
1049
+ mode = 'settings'
1050
+ else:
1051
+ _LOGGER.error(f'Set climatisation target temp to {temperature} is not supported.')
1052
+ raise SeatInvalidRequestException(f'Set climatisation target temp to {temperature} is not supported.')
1053
+ return await self._set_climater(mode, data)
1054
+ else:
1055
+ _LOGGER.error('No climatisation support.')
1056
+ raise SeatInvalidRequestException('No climatisation support.')
1057
+
1058
+ async def set_window_heating(self, action = 'stop'):
1059
+ """Turn on/off window heater."""
1060
+ if self.is_window_heater_supported:
1061
+ if action in ['start', 'stop']:
1062
+ data = {'action': {'type': action + 'WindowHeating'}}
1063
+ else:
1064
+ _LOGGER.error(f'Window heater action "{action}" is not supported.')
1065
+ raise SeatInvalidRequestException(f'Window heater action "{action}" is not supported.')
1066
+ return await self._set_climater(f'windowHeater {action}', data)
1067
+ else:
1068
+ _LOGGER.error('No climatisation support.')
1069
+ raise SeatInvalidRequestException('No climatisation support.')
1070
+
1071
+ async def set_battery_climatisation(self, mode = False):
1072
+ """Turn on/off electric climatisation from battery."""
1073
+ if self.is_electric_climatisation_supported:
1074
+ if mode in [True, False]:
1075
+ data = {
1076
+ 'climatisationWithoutExternalPower': mode,
1077
+ 'targetTemperature': self.climatisation_target_temperature, #keep the current target temperature
1078
+ 'targetTemperatureUnit': 'celsius'
1079
+ }
1080
+ mode = 'settings'
1081
+ else:
1082
+ _LOGGER.error(f'Set climatisation without external power to "{mode}" is not supported.')
1083
+ raise SeatInvalidRequestException(f'Set climatisation without external power to "{mode}" is not supported.')
1084
+ return await self._set_climater(mode, data)
1085
+ else:
1086
+ _LOGGER.error('No climatisation support.')
1087
+ raise SeatInvalidRequestException('No climatisation support.')
1088
+
1089
+ async def set_climatisation(self, mode = 'off', temp = None, hvpower = None, spin = None):
1090
+ """Turn on/off climatisation with electric/auxiliary heater."""
1091
+ data = {}
1092
+ # Validate user input
1093
+ if mode.lower() not in ['electric', 'auxiliary', 'start', 'stop', 'on', 'off']:
1094
+ _LOGGER.error(f"Invalid mode for 'set_climatisation': {mode}")
1095
+ raise SeatInvalidRequestException(f"Invalid mode for set_climatisation: {mode}")
1096
+ elif mode == 'auxiliary' and spin is None:
1097
+ raise SeatInvalidRequestException("Starting auxiliary heater requires provided S-PIN")
1098
+ if temp is not None:
1099
+ if not isinstance(temp, float) and not isinstance(temp, int):
1100
+ _LOGGER.error(f"Invalid type for temp. type={type(temp)}")
1101
+ raise SeatInvalidRequestException(f"Invalid type for temp")
1102
+ elif not 16 <= float(temp) <=30:
1103
+ raise SeatInvalidRequestException(f"Invalid value for temp")
1104
+ else:
1105
+ temp = self.climatisation_target_temperature
1106
+ #if hvpower is not None:
1107
+ # if not isinstance(hvpower, bool):
1108
+ # raise SeatInvalidRequestException(f"Invalid type for hvpower")
1109
+ if self.is_electric_climatisation_supported:
1110
+ if self._relevantCapabilties.get('climatisation', {}).get('active', False):
1111
+ if mode in ['Start', 'start', 'Electric', 'electric', 'On', 'on']:
1112
+ mode = 'start'
1113
+ if mode in ['start', 'auxiliary']:
1114
+ #if hvpower is not None:
1115
+ # withoutHVPower = hvpower
1116
+ #else:
1117
+ # withoutHVPower = self.climatisation_without_external_power
1118
+ data = {
1119
+ 'targetTemperature': temp,
1120
+ 'targetTemperatureUnit': 'celsius',
1121
+ }
1122
+ return await self._set_climater(mode, data, spin)
1123
+ else:
1124
+ if self._requests['climatisation'].get('id', False) or self.electric_climatisation:
1125
+ request_id=self._requests.get('climatisation', 0)
1126
+ mode = 'stop'
1127
+ data={}
1128
+ return await self._set_climater(mode, data, spin)
1129
+ else:
1130
+ _LOGGER.error('Can not stop climatisation because no running request was found')
1131
+ return None
1132
+ else:
1133
+ _LOGGER.error('No climatisation support.')
1134
+ raise SeatInvalidRequestException('No climatisation support.')
1135
+
1136
+ async def _set_climater(self, mode, data, spin = False):
1137
+ """Climater actions."""
1138
+ if not self._relevantCapabilties.get('climatisation', {}).get('active', False):
1139
+ _LOGGER.info('Remote control of climatisation functions is not supported.')
1140
+ raise SeatInvalidRequestException('Remote control of climatisation functions is not supported.')
1141
+ if self._requests['climatisation'].get('id', False):
1142
+ timestamp = self._requests.get('climatisation', {}).get('timestamp', datetime.now())
1143
+ expired = datetime.now() - timedelta(minutes=1)
1144
+ if expired > timestamp:
1145
+ self._requests.get('climatisation', {}).pop('id')
1146
+ else:
1147
+ raise SeatRequestInProgressException('A climatisation action is already in progress')
1148
+ try:
1149
+ self._requests['latest'] = 'Climatisation'
1150
+ response = await self._connection.setClimater(self.vin, self._apibase, mode, data, spin)
1151
+ if not response:
1152
+ self._requests['climatisation'] = {'status': 'Failed'}
1153
+ _LOGGER.error('Failed to execute climatisation request')
1154
+ raise SeatException('Failed to execute climatisation request')
1155
+ else:
1156
+ #self._requests['remaining'] = response.get('rate_limit_remaining', -1)
1157
+ self._requests['climatisation'] = {
1158
+ 'timestamp': datetime.now(),
1159
+ 'status': response.get('state', 'Unknown'),
1160
+ 'id': response.get('id', 0),
1161
+ }
1162
+ # if firebaseStatus is FIREBASE_STATUS_ACTIVATED, the request is assumed successful. Waiting for push notification before rereading status
1163
+ if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
1164
+ _LOGGER.debug('POST request for climater assumed successful. Waiting for push notification')
1165
+ return True
1166
+ # Update the climater data and check, if they have changed as expected
1167
+ retry = 0
1168
+ actionSuccessful = False
1169
+ while not actionSuccessful and retry < 2:
1170
+ await asyncio.sleep(15)
1171
+ await self.get_climater()
1172
+ if mode == 'start':
1173
+ if self.electric_climatisation:
1174
+ actionSuccessful = True
1175
+ elif mode == 'stop':
1176
+ if not self.electric_climatisation:
1177
+ actionSuccessful = True
1178
+ elif mode == 'settings':
1179
+ if data.get('targetTemperature',0)== self.climatisation_target_temperature and data.get('climatisationWithoutExternalPower',False)== self.climatisation_without_external_power:
1180
+ actionSuccessful = True
1181
+ elif mode == 'windowHeater start':
1182
+ if self.window_heater:
1183
+ actionSuccessful = True
1184
+ elif mode == 'windowHeater stop':
1185
+ if not self.window_heater:
1186
+ actionSuccessful = True
1187
+ else:
1188
+ _LOGGER.error(f'Missing code in vehicle._set_climater() for mode {mode}')
1189
+ raise
1190
+ retry = retry +1
1191
+ if actionSuccessful:
1192
+ _LOGGER.debug('POST request for climater successful. New status as expected.')
1193
+ self._requests.get('climatisation', {}).pop('id')
1194
+ return True
1195
+ _LOGGER.error('Response to POST request seemed successful but the climater status did not change as expected.')
1196
+ return False
1197
+ except (SeatInvalidRequestException, SeatException):
1198
+ raise
1199
+ except Exception as error:
1200
+ _LOGGER.warning(f'Failed to execute climatisation request - {error}')
1201
+ self._requests['climatisation'] = {'status': 'Exception'}
1202
+ raise SeatException('Climatisation action failed')
1203
+
1204
+ # Parking heater heating/ventilation (RS)
1205
+ async def set_pheater(self, mode, spin):
1206
+ """Set the mode for the parking heater."""
1207
+ if not self.is_pheater_heating_supported:
1208
+ _LOGGER.error('No parking heater support.')
1209
+ raise SeatInvalidRequestException('No parking heater support.')
1210
+ if self._requests['preheater'].get('id', False):
1211
+ timestamp = self._requests.get('preheater', {}).get('timestamp', datetime.now())
1212
+ expired = datetime.now() - timedelta(minutes=1)
1213
+ if expired > timestamp:
1214
+ self._requests.get('preheater', {}).pop('id')
1215
+ else:
1216
+ raise SeatRequestInProgressException('A parking heater action is already in progress')
1217
+ if not mode in ['heating', 'ventilation', 'off']:
1218
+ _LOGGER.error(f'{mode} is an invalid action for parking heater')
1219
+ raise SeatInvalidRequestException(f'{mode} is an invalid action for parking heater')
1220
+ if mode == 'off':
1221
+ data = {'performAction': {'quickstop': {'active': False }}}
1222
+ else:
1223
+ data = {'performAction': {'quickstart': {'climatisationDuration': self.pheater_duration, 'startMode': mode, 'active': True }}}
1224
+ try:
1225
+ self._requests['latest'] = 'Preheater'
1226
+ _LOGGER.debug(f'Executing setPreHeater with data: {data}')
1227
+ response = await self._connection.setPreHeater(self.vin, self._apibase, data, spin)
1228
+ if not response:
1229
+ self._requests['preheater'] = {'status': 'Failed'}
1230
+ _LOGGER.error(f'Failed to set parking heater to {mode}')
1231
+ raise SeatException(f'setPreHeater returned "{response}"')
1232
+ else:
1233
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
1234
+ self._requests['preheater'] = {
1235
+ 'timestamp': datetime.now(),
1236
+ 'status': response.get('state', 'Unknown'),
1237
+ 'id': response.get('id', 0),
1238
+ }
1239
+ return True
1240
+ except (SeatInvalidRequestException, SeatException):
1241
+ raise
1242
+ except Exception as error:
1243
+ _LOGGER.warning(f'Failed to set parking heater mode to {mode} - {error}')
1244
+ self._requests['preheater'] = {'status': 'Exception'}
1245
+ raise SeatException('Pre-heater action failed')
1246
+
1247
+ # Lock
1248
+ async def set_lock(self, action, spin):
1249
+ """Remote lock and unlock actions."""
1250
+ #if not self._services.get('rlu_v1', False):
1251
+ if not self._relevantCapabilties.get('transactionHistoryLockUnlock', {}).get('active', False):
1252
+ _LOGGER.info('Remote lock/unlock is not supported.')
1253
+ raise SeatInvalidRequestException('Remote lock/unlock is not supported.')
1254
+ if self._requests['lock'].get('id', False):
1255
+ timestamp = self._requests.get('lock', {}).get('timestamp', datetime.now() - timedelta(minutes=5))
1256
+ expired = datetime.now() - timedelta(minutes=1)
1257
+ if expired > timestamp:
1258
+ self._requests.get('lock', {}).pop('id')
1259
+ else:
1260
+ raise SeatRequestInProgressException('A lock action is already in progress')
1261
+ if action not in ['lock', 'unlock']:
1262
+ _LOGGER.error(f'Invalid lock action: {action}')
1263
+ raise SeatInvalidRequestException(f'Invalid lock action: {action}')
1264
+ try:
1265
+ self._requests['latest'] = 'Lock'
1266
+ data = {}
1267
+ response = await self._connection.setLock(self.vin, self._apibase, action, data, spin)
1268
+ if not response:
1269
+ self._requests['lock'] = {'status': 'Failed'}
1270
+ _LOGGER.error(f'Failed to {action} vehicle')
1271
+ raise SeatException(f'Failed to {action} vehicle')
1272
+ else:
1273
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
1274
+ self._requests['lock'] = {
1275
+ 'timestamp': datetime.now(),
1276
+ 'status': response.get('state', 'Unknown'),
1277
+ 'id': response.get('id', 0),
1278
+ }
1279
+ # if firebaseStatus is FIREBASE_STATUS_ACTIVATED, the request is assumed successful. Waiting for push notification before rereading status
1280
+ if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
1281
+ _LOGGER.debug('POST request for lock/unlock assumed successful. Waiting for push notification')
1282
+ return True
1283
+ # Update the lock data and check, if they have changed as expected
1284
+ retry = 0
1285
+ actionSuccessful = False
1286
+ while not actionSuccessful and retry < 2:
1287
+ await asyncio.sleep(15)
1288
+ await self.get_statusreport()
1289
+ if action == 'lock':
1290
+ if self.door_locked:
1291
+ actionSuccessful = True
1292
+ else:
1293
+ if not self.door_locked:
1294
+ actionSuccessful = True
1295
+ retry = retry +1
1296
+ if actionSuccessful:
1297
+ _LOGGER.debug('POST request for lock/unlock successful. New status as expected.')
1298
+ self._requests.get('lock', {}).pop('id')
1299
+ return True
1300
+ _LOGGER.error('Response to POST request seemed successful but the lock status did not change as expected.')
1301
+ return False
1302
+ except (SeatInvalidRequestException, SeatException):
1303
+ raise
1304
+ except Exception as error:
1305
+ _LOGGER.warning(f'Failed to {action} vehicle - {error}')
1306
+ self._requests['lock'] = {'status': 'Exception'}
1307
+ raise SeatException('Lock action failed')
1308
+
1309
+ # Honk and flash (RHF)
1310
+ async def set_honkandflash(self, action, lat=None, lng=None):
1311
+ """Turn on/off honk and flash."""
1312
+ if not self._relevantCapabilties.get('honkAndFlash', {}).get('active', False):
1313
+ _LOGGER.info('Remote honk and flash is not supported.')
1314
+ raise SeatInvalidRequestException('Remote honk and flash is not supported.')
1315
+ if self._requests['honkandflash'].get('id', False):
1316
+ timestamp = self._requests.get('honkandflash', {}).get('timestamp', datetime.now() - timedelta(minutes=2))
1317
+ expired = datetime.now() - timedelta(minutes=1)
1318
+ if expired > timestamp:
1319
+ self._requests.get('honkandflash', {}).pop('id')
1320
+ else:
1321
+ raise SeatRequestInProgressException('A honk and flash action is already in progress')
1322
+ if action == 'flash':
1323
+ operationCode = 'flash'
1324
+ elif action == 'honkandflash':
1325
+ operationCode = 'honkandflash'
1326
+ else:
1327
+ raise SeatInvalidRequestException(f'Invalid action "{action}", must be one of "flash" or "honkandflash"')
1328
+ try:
1329
+ # Get car position
1330
+ if lat is None:
1331
+ lat = self.attrs.get('findCarResponse', {}).get('lat', None)
1332
+ if lng is None:
1333
+ lng = self.attrs.get('findCarResponse', {}).get('lon', None)
1334
+ if lat is None or lng is None:
1335
+ raise SeatConfigException('No location available, location information is needed for this action')
1336
+ lat = int(lat*10000.0)/10000.0
1337
+ lng = int(lng*10000.0)/10000.0
1338
+ data = {
1339
+ 'mode': operationCode,
1340
+ 'userPosition': {
1341
+ 'latitude': lat,
1342
+ 'longitude': lng
1343
+ }
1344
+ }
1345
+ self._requests['latest'] = 'HonkAndFlash'
1346
+ response = await self._connection.setHonkAndFlash(self.vin, self._apibase, data)
1347
+ if not response:
1348
+ self._requests['honkandflash'] = {'status': 'Failed'}
1349
+ _LOGGER.error(f'Failed to execute honk and flash action')
1350
+ raise SeatException(f'Failed to execute honk and flash action')
1351
+ else:
1352
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
1353
+ self._requests['honkandflash'] = {
1354
+ 'timestamp': datetime.now(),
1355
+ 'status': response.get('state', 'Unknown'),
1356
+ 'id': response.get('id', 0),
1357
+ }
1358
+ return True
1359
+ except (SeatInvalidRequestException, SeatException):
1360
+ raise
1361
+ except Exception as error:
1362
+ _LOGGER.warning(f'Failed to {action} vehicle - {error}')
1363
+ self._requests['honkandflash'] = {'status': 'Exception'}
1364
+ raise SeatException('Honk and flash action failed')
1365
+
1366
+ # Refresh vehicle data (VSR)
1367
+ async def set_refresh(self):
1368
+ """Wake up vehicle and update status data."""
1369
+ if not self._relevantCapabilties.get('state', {}).get('active', False):
1370
+ _LOGGER.info('Data refresh is not supported.')
1371
+ raise SeatInvalidRequestException('Data refresh is not supported.')
1372
+ if self._requests['refresh'].get('id', False):
1373
+ timestamp = self._requests.get('refresh', {}).get('timestamp', datetime.now() - timedelta(minutes=5))
1374
+ expired = datetime.now() - timedelta(minutes=1)
1375
+ if expired > timestamp:
1376
+ self._requests.get('refresh', {}).pop('id')
1377
+ else:
1378
+ raise SeatRequestInProgressException('Last data refresh request less than 3 minutes ago')
1379
+ try:
1380
+ self._requests['latest'] = 'Refresh'
1381
+ response = await self._connection.setRefresh(self.vin, self._apibase)
1382
+ if not response:
1383
+ _LOGGER.error('Failed to request vehicle update')
1384
+ self._requests['refresh'] = {'status': 'Failed'}
1385
+ raise SeatException('Failed to execute data refresh')
1386
+ else:
1387
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
1388
+ self._requests['refresh'] = {
1389
+ 'timestamp': datetime.now(),
1390
+ 'status': response.get('status', 'Unknown'),
1391
+ 'id': response.get('id', 0)
1392
+ }
1393
+ # if firebaseStatus is FIREBASE_STATUS_ACTIVATED, the request is assumed successful. Waiting for push notification before rereading status
1394
+ if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
1395
+ _LOGGER.debug('POST request for wakeup vehicle assumed successful. Waiting for push notification')
1396
+ return True
1397
+ await self.update(updateType=1) #full update after set_refresh
1398
+ return True
1399
+ except(SeatInvalidRequestException, SeatException):
1400
+ raise
1401
+ except Exception as error:
1402
+ _LOGGER.warning(f'Failed to execute data refresh - {error}')
1403
+ self._requests['refresh'] = {'status': 'Exception'}
1404
+ raise SeatException('Data refresh failed')
1405
+
1406
+ #### Vehicle class helpers ####
1407
+ # Vehicle info
1408
+ @property
1409
+ def attrs(self):
1410
+ return self._states
1411
+
1412
+ def has_attr(self, attr):
1413
+ return is_valid_path(self.attrs, attr)
1414
+
1415
+ def get_attr(self, attr):
1416
+ return find_path(self.attrs, attr)
1417
+
1418
+ def dashboard(self, **config):
1419
+ """Returns dashboard, creates new if none exist."""
1420
+ if self._dashboard is None:
1421
+ # Init new dashboard if none exist
1422
+ from .dashboard import Dashboard
1423
+ self._dashboard = Dashboard(self, **config)
1424
+ elif config != self._dashboard._config:
1425
+ # Init new dashboard on config change
1426
+ from .dashboard import Dashboard
1427
+ self._dashboard = Dashboard(self, **config)
1428
+ return self._dashboard
1429
+
1430
+ @property
1431
+ def vin(self):
1432
+ return self._url
1433
+
1434
+ @property
1435
+ def unique_id(self):
1436
+ return self.vin
1437
+
1438
+
1439
+ #### Information from vehicle states ####
1440
+ # Car information
1441
+ @property
1442
+ def nickname(self):
1443
+ return self._properties.get('vehicleNickname', '')
1444
+
1445
+ @property
1446
+ def is_nickname_supported(self):
1447
+ if self._properties.get('vehicleNickname', False):
1448
+ return True
1449
+
1450
+ @property
1451
+ def deactivated(self):
1452
+ if 'mode' in self._connectivities:
1453
+ if self._connectivities.get('mode','')=='online':
1454
+ return False
1455
+ return True
1456
+ #for car in self.attrs.get('realCars', []):
1457
+ # if self.vin == car.get('vehicleIdentificationNumber', ''):
1458
+ # return car.get('deactivated', False)
1459
+
1460
+ @property
1461
+ def is_deactivated_supported(self):
1462
+ if 'mode' in self._connectivities:
1463
+ return True
1464
+ return False
1465
+ #for car in self.attrs.get('realCars', []):
1466
+ # if self.vin == car.get('vehicleIdentificationNumber', ''):
1467
+ # if car.get('deactivated', False):
1468
+ # return True
1469
+
1470
+ @property
1471
+ def brand(self):
1472
+ """Return brand"""
1473
+ return self._specification.get('factoryModel', False).get('vehicleBrand', 'Unknown')
1474
+
1475
+ @property
1476
+ def is_brand_supported(self):
1477
+ """Return true if brand is supported."""
1478
+ if self._specification.get('factoryModel', False).get('vehicleBrand', False):
1479
+ return True
1480
+
1481
+ @property
1482
+ def model(self):
1483
+ """Return model"""
1484
+ if self._specification.get('carBody', False):
1485
+ model = self._specification.get('factoryModel', False).get('vehicleModel', 'Unknown') + ' ' + self._specification.get('carBody', '')
1486
+ return model
1487
+ return self._specification.get('factoryModel', False).get('vehicleModel', 'Unknown')
1488
+
1489
+ @property
1490
+ def is_model_supported(self):
1491
+ """Return true if model is supported."""
1492
+ if self._specification.get('factoryModel', False).get('vehicleModel', False):
1493
+ return True
1494
+
1495
+ @property
1496
+ def model_year(self):
1497
+ """Return model year"""
1498
+ return self._specification.get('factoryModel', False).get('modYear', 'Unknown')
1499
+
1500
+ @property
1501
+ def is_model_year_supported(self):
1502
+ """Return true if model year is supported."""
1503
+ if self._specification.get('factoryModel', False).get('modYear', False):
1504
+ return True
1505
+
1506
+ @property
1507
+ def model_image_small(self):
1508
+ """Return URL for model image"""
1509
+ return self._modelimages.get('images','').get('front_cropped','')
1510
+
1511
+ @property
1512
+ def is_model_image_small_supported(self):
1513
+ """Return true if model image url is not None."""
1514
+ if self._modelimages is not None:
1515
+ if self._modelimages.get('images','').get('front_cropped','')!='':
1516
+ return True
1517
+
1518
+ @property
1519
+ def model_image_large(self):
1520
+ """Return URL for model image"""
1521
+ return self._modelimages.get('images','').get('front', '')
1522
+
1523
+ @property
1524
+ def is_model_image_large_supported(self):
1525
+ """Return true if model image url is not None."""
1526
+ if self._modelimages is not None:
1527
+ return True
1528
+
1529
+ # Lights
1530
+ @property
1531
+ def parking_light(self):
1532
+ """Return true if parking light is on"""
1533
+ response = self.attrs.get('status').get('lights', 0)
1534
+ if response == 'on':
1535
+ return True
1536
+ else:
1537
+ return False
1538
+
1539
+ @property
1540
+ def is_parking_light_supported(self):
1541
+ """Return true if parking light is supported"""
1542
+ if self.attrs.get('status', False):
1543
+ if 'lights' in self.attrs.get('status'):
1544
+ return True
1545
+ else:
1546
+ return False
1547
+
1548
+ # Connection status
1549
+ @property
1550
+ def last_connected(self):
1551
+ """Return when vehicle was last connected to connect servers."""
1552
+ last_connected_utc = self.attrs.get('status').get('updatedAt','')
1553
+ if isinstance(last_connected_utc, datetime):
1554
+ last_connected = last_connected_utc.replace(tzinfo=timezone.utc).astimezone(tz=None)
1555
+ else:
1556
+ last_connected = datetime.strptime(last_connected_utc,'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc).astimezone(tz=None)
1557
+ return last_connected #.strftime('%Y-%m-%d %H:%M:%S')
1558
+
1559
+ @property
1560
+ def is_last_connected_supported(self):
1561
+ """Return when vehicle was last connected to connect servers."""
1562
+ if 'updatedAt' in self.attrs.get('status', {}):
1563
+ return True
1564
+
1565
+ # Update status
1566
+ @property
1567
+ def last_full_update(self):
1568
+ """Return when the last full update for the vehicle took place."""
1569
+ return self._last_full_update.astimezone(tz=None)
1570
+
1571
+ @property
1572
+ def is_last_full_update_supported(self):
1573
+ """Return when last full update for vehicle took place."""
1574
+ if hasattr(self,'_last_full_update'):
1575
+ return True
1576
+
1577
+ # Service information
1578
+ @property
1579
+ def distance(self):
1580
+ """Return vehicle odometer."""
1581
+ value = self.attrs.get('mileage').get('mileageKm', 0)
1582
+ return int(value)
1583
+
1584
+ @property
1585
+ def is_distance_supported(self):
1586
+ """Return true if odometer is supported"""
1587
+ if self.attrs.get('mileage', False):
1588
+ if 'mileageKm' in self.attrs.get('mileage'):
1589
+ return True
1590
+ return False
1591
+
1592
+ @property
1593
+ def service_inspection(self):
1594
+ """Return time left until service inspection"""
1595
+ value = -1
1596
+ value = int(self.attrs.get('maintenance', {}).get('inspectionDueDays', 0))
1597
+ return int(value)
1598
+
1599
+ @property
1600
+ def is_service_inspection_supported(self):
1601
+ if self.attrs.get('maintenance', False):
1602
+ if 'inspectionDueDays' in self.attrs.get('maintenance'):
1603
+ return True
1604
+ return False
1605
+
1606
+ @property
1607
+ def service_inspection_distance(self):
1608
+ """Return time left until service inspection"""
1609
+ value = -1
1610
+ value = int(self.attrs.get('maintenance').get('inspectionDueKm', 0))
1611
+ return int(value)
1612
+
1613
+ @property
1614
+ def is_service_inspection_distance_supported(self):
1615
+ if self.attrs.get('maintenance', False):
1616
+ if 'inspectionDueKm' in self.attrs.get('maintenance'):
1617
+ return True
1618
+ return False
1619
+
1620
+ @property
1621
+ def oil_inspection(self):
1622
+ """Return time left until oil inspection"""
1623
+ value = -1
1624
+ value = int(self.attrs.get('maintenance', {}).get('oilServiceDueDays', 0))
1625
+ return int(value)
1626
+
1627
+ @property
1628
+ def is_oil_inspection_supported(self):
1629
+ if self.attrs.get('maintenance', False):
1630
+ if 'oilServiceDueDays' in self.attrs.get('maintenance'):
1631
+ if self.attrs.get('maintenance').get('oilServiceDueDays', None) is not None:
1632
+ return True
1633
+ return False
1634
+
1635
+ @property
1636
+ def oil_inspection_distance(self):
1637
+ """Return distance left until oil inspection"""
1638
+ value = -1
1639
+ value = int(self.attrs.get('maintenance').get('oilServiceDueKm', 0))
1640
+ return int(value)
1641
+
1642
+ @property
1643
+ def is_oil_inspection_distance_supported(self):
1644
+ if self.attrs.get('maintenance', False):
1645
+ if 'oilServiceDueKm' in self.attrs.get('maintenance'):
1646
+ if self.attrs.get('maintenance').get('oilServiceDueKm', None) is not None:
1647
+ return True
1648
+ return False
1649
+
1650
+ @property
1651
+ def adblue_level(self):
1652
+ """Return adblue level."""
1653
+ return int(self.attrs.get('maintenance', {}).get('0x02040C0001', {}).get('value', 0))
1654
+
1655
+ @property
1656
+ def is_adblue_level_supported(self):
1657
+ """Return true if adblue level is supported."""
1658
+ if self.attrs.get('maintenance', False):
1659
+ if '0x02040C0001' in self.attrs.get('maintenance'):
1660
+ if 'value' in self.attrs.get('maintenance')['0x02040C0001']:
1661
+ if self.attrs.get('maintenance')['0x02040C0001'].get('value', 0) is not None:
1662
+ return True
1663
+ return False
1664
+
1665
+ # Charger related states for EV and PHEV
1666
+ @property
1667
+ def charging(self):
1668
+ """Return battery level"""
1669
+ #cstate = self.attrs.get('charging').get('status').get('charging').get('state','')
1670
+ cstate = self.attrs.get('mycar',{}).get('services',{}).get('charging',{}).get('status','')
1671
+ return 1 if cstate in ['charging', 'Charging'] else 0
1672
+
1673
+ @property
1674
+ def is_charging_supported(self):
1675
+ """Return true if charging is supported"""
1676
+ #if self.attrs.get('charging', False):
1677
+ # if 'status' in self.attrs.get('charging', {}):
1678
+ # if 'charging' in self.attrs.get('charging')['status']:
1679
+ # if 'state' in self.attrs.get('charging')['status']['charging']:
1680
+ # return True
1681
+ if self.attrs.get('mycar', False):
1682
+ if 'services' in self.attrs.get('mycar', {}):
1683
+ if 'charging' in self.attrs.get('mycar')['services']:
1684
+ if 'status' in self.attrs.get('mycar')['services']['charging']:
1685
+ return True
1686
+ return False
1687
+
1688
+ @property
1689
+ def min_charge_level(self):
1690
+ """Return the charge level that car charges directly to"""
1691
+ if self.attrs.get('departuretimers', {}):
1692
+ return self.attrs.get('departuretimers', {}).get('minSocPercentage', 0)
1693
+ else:
1694
+ return 0
1695
+
1696
+ @property
1697
+ def is_min_charge_level_supported(self):
1698
+ """Return true if car supports setting the min charge level"""
1699
+ if self.attrs.get('departuretimers', {}).get('minSocPercentage', False):
1700
+ return True
1701
+ return False
1702
+
1703
+ @property
1704
+ def battery_level(self):
1705
+ """Return battery level"""
1706
+ #if self.attrs.get('charging', False):
1707
+ # return int(self.attrs.get('charging').get('status', {}).get('battery', {}).get('currentSocPercentage', 0))
1708
+ if self.attrs.get('mycar', False):
1709
+ return int(self.attrs.get('mycar',{}).get('services', {}).get('charging', {}).get('currentPct', 0))
1710
+ else:
1711
+ return 0
1712
+
1713
+ @property
1714
+ def is_battery_level_supported(self):
1715
+ """Return true if battery level is supported"""
1716
+ #if self.attrs.get('charging', False):
1717
+ # if 'status' in self.attrs.get('charging'):
1718
+ # if 'battery' in self.attrs.get('charging')['status']:
1719
+ # if 'currentSocPercentage' in self.attrs.get('charging')['status']['battery']:
1720
+ # return True
1721
+ if self.attrs.get('mycar', False):
1722
+ if 'services' in self.attrs.get('mycar'):
1723
+ if 'charging' in self.attrs.get('mycar')['services']:
1724
+ if 'currentPct' in self.attrs.get('mycar')['services']['charging']:
1725
+ return True
1726
+ return False
1727
+
1728
+ @property
1729
+ def charge_max_ampere(self):
1730
+ """Return charger max ampere setting."""
1731
+ if self.attrs.get('charging', False):
1732
+ if self.attrs.get('charging',{}).get('info',{}).get('settings',{}).get('maxChargeCurrentAcInAmperes', None):
1733
+ return self.attrs.get('charging',{}).get('info',{}).get('settings',{}).get('maxChargeCurrentAcInAmperes', 0)
1734
+ else:
1735
+ return self.attrs.get('charging').get('info').get('settings').get('maxChargeCurrentAc')
1736
+ return 0
1737
+
1738
+ @property
1739
+ def is_charge_max_ampere_supported(self):
1740
+ """Return true if Charger Max Ampere is supported"""
1741
+ if self.attrs.get('charging', False):
1742
+ if 'info' in self.attrs.get('charging', {}):
1743
+ if 'settings' in self.attrs.get('charging')['info']:
1744
+ if 'maxChargeCurrentAc' in self.attrs.get('charging', {})['info']['settings']:
1745
+ return True
1746
+ return False
1747
+
1748
+ @property
1749
+ def slow_charge(self):
1750
+ """Return charger max ampere setting."""
1751
+ if self.charge_max_ampere=='reduced':
1752
+ return True
1753
+ return False
1754
+
1755
+ @property
1756
+ def is_slow_charge_supported(self):
1757
+ """Return true if Slow Charge is supported"""
1758
+ if self.is_charge_max_ampere_supported:
1759
+ if self.charge_max_ampere in ('reduced', 'maximum'):
1760
+ return True
1761
+ return False
1762
+
1763
+ @property
1764
+ def charging_cable_locked(self):
1765
+ """Return plug locked state"""
1766
+ response = ''
1767
+ if self.attrs.get('charging', False):
1768
+ response = self.attrs.get('charging').get('status', {}).get('plug', {}).get('lock', '')
1769
+ return True if response in ['Locked', 'locked'] else False
1770
+
1771
+ @property
1772
+ def is_charging_cable_locked_supported(self):
1773
+ """Return true if plug locked state is supported"""
1774
+ if self.attrs.get('charging', False):
1775
+ if 'status' in self.attrs.get('charging'):
1776
+ if 'plug' in self.attrs.get('charging')['status']:
1777
+ if 'lock' in self.attrs.get('charging')['status']['plug']:
1778
+ return True
1779
+ return False
1780
+
1781
+ @property
1782
+ def charging_cable_connected(self):
1783
+ """Return plug locked state"""
1784
+ response = ''
1785
+ if self.attrs.get('charging', False):
1786
+ response = self.attrs.get('charging', {}).get('status', {}).get('plug').get('connection', 0)
1787
+ return True if response in ['Connected', 'connected'] else False
1788
+
1789
+ @property
1790
+ def is_charging_cable_connected_supported(self):
1791
+ """Return true if charging cable connected is supported"""
1792
+ if self.attrs.get('charging', False):
1793
+ if 'status' in self.attrs.get('charging', {}):
1794
+ if 'plug' in self.attrs.get('charging').get('status', {}):
1795
+ if 'connection' in self.attrs.get('charging')['status'].get('plug', {}):
1796
+ return True
1797
+ return False
1798
+
1799
+ @property
1800
+ def charging_time_left(self):
1801
+ """Return minutes to charging complete"""
1802
+ #if self.external_power:
1803
+ if self.charging:
1804
+ #if self.attrs.get('charging', {}).get('status', {}).get('charging', {}).get('remainingTimeInMinutes', False):
1805
+ # minutes = int(self.attrs.get('charging', {}).get('status', {}).get('charging', {}).get('remainingTimeInMinutes', 0))
1806
+ if self.attrs.get('mycar', {}).get('services', {}).get('charging', {}).get('remainingTime', False):
1807
+ minutes = int(self.attrs.get('mycar', {}).get('services', {}).get('charging', {}).get('remainingTime', 0))
1808
+ else:
1809
+ minutes = 0
1810
+ return minutes
1811
+ #try:
1812
+ # if minutes == -1: return '00:00'
1813
+ # if minutes == 65535: return '00:00'
1814
+ # return "%02d:%02d" % divmod(minutes, 60)
1815
+ #except Exception:
1816
+ # pass
1817
+ #return '00:00'
1818
+ return 0
1819
+
1820
+ @property
1821
+ def is_charging_time_left_supported(self):
1822
+ """Return true if charging is supported"""
1823
+ return self.is_charging_supported
1824
+
1825
+ @property
1826
+ def charging_power(self):
1827
+ """Return charging power in watts."""
1828
+ if self.attrs.get('charging', False):
1829
+ return int(self.attrs.get('charging', {}).get('chargingPowerInWatts', 0))
1830
+ else:
1831
+ return 0
1832
+
1833
+ @property
1834
+ def is_charging_power_supported(self):
1835
+ """Return true if charging power is supported."""
1836
+ if self.attrs.get('charging', False):
1837
+ if self.attrs.get('charging', {}).get('chargingPowerInWatts', False) is not False:
1838
+ return True
1839
+ return False
1840
+
1841
+ @property
1842
+ def charge_rate(self):
1843
+ """Return charge rate in km per h."""
1844
+ if self.attrs.get('charging', False):
1845
+ return int(self.attrs.get('charging', {}).get('chargingRateInKilometersPerHour', 0))
1846
+ else:
1847
+ return 0
1848
+
1849
+ @property
1850
+ def is_charge_rate_supported(self):
1851
+ """Return true if charge rate is supported."""
1852
+ if self.attrs.get('charging', False):
1853
+ if self.attrs.get('charging', {}).get('chargingRateInKilometersPerHour', False) is not False:
1854
+ return True
1855
+ return False
1856
+
1857
+ @property
1858
+ def external_power(self):
1859
+ """Return true if external power is connected."""
1860
+ response = ''
1861
+ if self.attrs.get('charging', False):
1862
+ response = self.attrs.get('charging', {}).get('status', {}).get('plug', {}).get('externalPower', '')
1863
+ else:
1864
+ response = ''
1865
+ return True if response in ['stationConnected', 'available', 'Charging', 'ready'] else False
1866
+
1867
+ @property
1868
+ def is_external_power_supported(self):
1869
+ """External power supported."""
1870
+ if self.attrs.get('charging', {}).get('status', {}).get('plug, {}').get('externalPower', False):
1871
+ return True
1872
+
1873
+ @property
1874
+ def charging_state(self):
1875
+ """Return true if vehicle is charging."""
1876
+ #check = self.attrs.get('charging', {}).get('status', {}).get('state', '')
1877
+ check = self.attrs.get('mycar',{}).get('services',{}).get('charging',{}).get('status','')
1878
+ if check in ('charging','Charging'):
1879
+ return True
1880
+ else:
1881
+ return False
1882
+
1883
+ @property
1884
+ def is_charging_state_supported(self):
1885
+ """Charging state supported."""
1886
+ #if self.attrs.get('charging', {}).get('status', {}).get('state', False):
1887
+ # return True
1888
+ if self.attrs.get('mycar', False):
1889
+ if 'services' in self.attrs.get('mycar', {}):
1890
+ if 'charging' in self.attrs.get('mycar')['services']:
1891
+ if 'status' in self.attrs.get('mycar')['services']['charging']:
1892
+ return True
1893
+
1894
+ @property
1895
+ def energy_flow(self):
1896
+ """Return true if energy is flowing to (i.e. charging) or from (i.e. climating with battery power) the battery."""
1897
+ if self.charging_state:
1898
+ return True
1899
+ #check = self.attrs.get('charging', {}).get('status', {}).get('state', '')
1900
+ check = self.attrs.get('mycar',{}).get('services',{}).get('charging',{}).get('status','')
1901
+ if self.is_electric_climatisation_supported:
1902
+ if self.electric_climatisation and check not in {'charging','Charging', 'conservation','Conservation'}:
1903
+ # electric climatisation is on and car is not charging or conserving power
1904
+ return True
1905
+ return False
1906
+
1907
+ @property
1908
+ def is_energy_flow_supported(self):
1909
+ """Energy flow supported."""
1910
+ if self.is_charging_state_supported:
1911
+ return True
1912
+
1913
+ @property
1914
+ def target_soc(self):
1915
+ """Return the target soc."""
1916
+ return self.attrs.get('charging', {}).get('info', {}).get('settings', {}).get('targetSoc', 0)
1917
+
1918
+ @property
1919
+ def is_target_soc_supported(self):
1920
+ """Target state of charge supported."""
1921
+ if self.attrs.get('charging', {}).get('info', {}).get('settings', {}).get('targetSoc', False):
1922
+ return True
1923
+
1924
+ # Vehicle location states
1925
+ @property
1926
+ def position(self):
1927
+ """Return position."""
1928
+ output = {}
1929
+ try:
1930
+ if self.vehicle_moving:
1931
+ output = {
1932
+ 'lat': None,
1933
+ 'lng': None,
1934
+ 'address': None,
1935
+ 'timestamp': None
1936
+ }
1937
+ else:
1938
+ posObj = self.attrs.get('findCarResponse', {})
1939
+ lat = posObj.get('lat')
1940
+ lng = posObj.get('lon')
1941
+ position_to_address = posObj.get('position_to_address')
1942
+ parkingTime = posObj.get('parkingTimeUTC', None)
1943
+ output = {
1944
+ 'lat' : lat,
1945
+ 'lng' : lng,
1946
+ 'address': position_to_address,
1947
+ 'timestamp' : parkingTime
1948
+ }
1949
+ except:
1950
+ output = {
1951
+ 'lat': '?',
1952
+ 'lng': '?',
1953
+ }
1954
+ return output
1955
+
1956
+ @property
1957
+ def is_position_supported(self):
1958
+ """Return true if carfinder_v1 service is active."""
1959
+ if self.attrs.get('findCarResponse', {}).get('lat', False):
1960
+ return True
1961
+ elif self.attrs.get('isMoving', False):
1962
+ return True
1963
+
1964
+ @property
1965
+ def vehicle_moving(self):
1966
+ """Return true if vehicle is moving."""
1967
+ return self.attrs.get('isMoving', False)
1968
+
1969
+ @property
1970
+ def is_vehicle_moving_supported(self):
1971
+ """Return true if vehicle supports position."""
1972
+ if self.is_position_supported:
1973
+ return True
1974
+
1975
+ @property
1976
+ def parking_time(self):
1977
+ """Return timestamp of last parking time."""
1978
+ parkTime_utc = self.attrs.get('findCarResponse', {}).get('parkingTimeUTC', 'Unknown')
1979
+ if isinstance(parkTime_utc, datetime):
1980
+ parkTime = parkTime_utc.replace(tzinfo=timezone.utc).astimezone(tz=None)
1981
+ else:
1982
+ parkTime = datetime.strptime(parkTime_utc,'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc).astimezone(tz=None)
1983
+ return parkTime.strftime('%Y-%m-%d %H:%M:%S')
1984
+
1985
+ @property
1986
+ def is_parking_time_supported(self):
1987
+ """Return true if vehicle parking timestamp is supported."""
1988
+ if 'parkingTimeUTC' in self.attrs.get('findCarResponse', {}):
1989
+ return True
1990
+
1991
+ # Vehicle fuel level and range
1992
+ @property
1993
+ def primary_range(self):
1994
+ value = -1
1995
+ if 'engines' in self.attrs.get('mycar'):
1996
+ value = self.attrs.get('mycar')['engines']['primary'].get('rangeKm', 0)
1997
+ return int(value)
1998
+
1999
+ @property
2000
+ def is_primary_range_supported(self):
2001
+ if self.attrs.get('mycar', False):
2002
+ if 'engines' in self.attrs.get('mycar', {}):
2003
+ if 'primary' in self.attrs.get('mycar')['engines']:
2004
+ if 'rangeKm' in self.attrs.get('mycar')['engines']['primary']:
2005
+ return True
2006
+ return False
2007
+
2008
+ @property
2009
+ def primary_drive(self):
2010
+ value=''
2011
+ if 'engines' in self.attrs.get('mycar'):
2012
+ value = self.attrs.get('mycar')['engines']['primary'].get('fuelType', '')
2013
+ return value
2014
+
2015
+ @property
2016
+ def is_primary_drive_supported(self):
2017
+ if self.attrs.get('mycar', False):
2018
+ if 'engines' in self.attrs.get('mycar', {}):
2019
+ if 'primary' in self.attrs.get('mycar')['engines']:
2020
+ if 'fuelType' in self.attrs.get('mycar')['engines']['primary']:
2021
+ return True
2022
+ return False
2023
+
2024
+ @property
2025
+ def secondary_range(self):
2026
+ value = -1
2027
+ if 'engines' in self.attrs.get('mycar'):
2028
+ value = self.attrs.get('mycar')['engines']['secondary'].get('rangeKm', 0)
2029
+ return int(value)
2030
+
2031
+ @property
2032
+ def is_secondary_range_supported(self):
2033
+ if self.attrs.get('mycar', False):
2034
+ if 'engines' in self.attrs.get('mycar', {}):
2035
+ if 'secondary' in self.attrs.get('mycar')['engines']:
2036
+ if 'rangeKm' in self.attrs.get('mycar')['engines']['secondary']:
2037
+ return True
2038
+ return False
2039
+
2040
+ @property
2041
+ def secondary_drive(self):
2042
+ value=''
2043
+ if 'engines' in self.attrs.get('mycar'):
2044
+ value = self.attrs.get('mycar')['engines']['secondary'].get('fuelType', '')
2045
+ return value
2046
+
2047
+ @property
2048
+ def is_secondary_drive_supported(self):
2049
+ if self.attrs.get('mycar', False):
2050
+ if 'engines' in self.attrs.get('mycar', {}):
2051
+ if 'secondary' in self.attrs.get('mycar')['engines']:
2052
+ if 'fuelType' in self.attrs.get('mycar')['engines']['secondary']:
2053
+ return True
2054
+ return False
2055
+
2056
+ @property
2057
+ def electric_range(self):
2058
+ value = -1
2059
+ if self.is_secondary_drive_supported:
2060
+ if self.secondary_drive == 'electric':
2061
+ return self.secondary_range
2062
+ elif self.is_primary_drive_supported:
2063
+ if self.primary_drive == 'electric':
2064
+ return self.primary_range
2065
+ return -1
2066
+
2067
+ @property
2068
+ def is_electric_range_supported(self):
2069
+ if self.is_secondary_drive_supported:
2070
+ if self.secondary_drive == 'electric':
2071
+ return self.is_secondary_range_supported
2072
+ elif self.is_primary_drive_supported:
2073
+ if self.primary_drive == 'electric':
2074
+ return self.is_primary_range_supported
2075
+ return False
2076
+
2077
+ @property
2078
+ def combustion_range(self):
2079
+ value = -1
2080
+ if self.is_primary_drive_supported:
2081
+ if not self.primary_drive == 'electric':
2082
+ return self.primary_range
2083
+ elif self.is_secondary_drive_supported:
2084
+ if not self.secondary_drive == 'electric':
2085
+ return self.secondary_range
2086
+ return -1
2087
+
2088
+ @property
2089
+ def is_combustion_range_supported(self):
2090
+ if self.is_primary_drive_supported:
2091
+ if not self.primary_drive == 'electric':
2092
+ return self.is_primary_range_supported
2093
+ elif self.is_secondary_drive_supported:
2094
+ if not self.secondary_drive == 'electric':
2095
+ return self.is_secondary_range_supported
2096
+ return False
2097
+
2098
+ @property
2099
+ def combined_range(self):
2100
+ return int(self.combustion_range)+int(self.electric_range)
2101
+
2102
+ @property
2103
+ def is_combined_range_supported(self):
2104
+ if self.is_combustion_range_supported and self.is_electric_range_supported:
2105
+ return True
2106
+ return False
2107
+
2108
+ @property
2109
+ def fuel_level(self):
2110
+ value = -1
2111
+ if self.is_fuel_level_supported:
2112
+ if not self.primary_drive == 'electric':
2113
+ value= self.attrs.get('mycar')['engines']['primary'].get('levelPct',0)
2114
+ elif not self.secondary_drive == 'electric':
2115
+ value= self.attrs.get('mycar')['engines']['primary'].get('levelPct',0)
2116
+ return int(value)
2117
+
2118
+ @property
2119
+ def is_fuel_level_supported(self):
2120
+ if self.is_primary_drive_supported:
2121
+ if not self.primary_drive == 'electric':
2122
+ if "levelPct" in self.attrs.get('mycar')['engines']['primary']:
2123
+ return self.is_primary_range_supported
2124
+ elif self.is_secondary_drive_supported:
2125
+ if not self.secondary_drive == 'electric':
2126
+ if "levelPct" in self.attrs.get('mycar')['engines']['secondary']:
2127
+ return self.is_secondary_range_supported
2128
+ return False
2129
+
2130
+ # Climatisation settings
2131
+ @property
2132
+ def climatisation_target_temperature(self):
2133
+ """Return the target temperature from climater."""
2134
+ if self.attrs.get('climater', False):
2135
+ value = self.attrs.get('climater').get('settings', {}).get('targetTemperatureInCelsius', 0)
2136
+ return value
2137
+ return False
2138
+
2139
+ @property
2140
+ def is_climatisation_target_temperature_supported(self):
2141
+ """Return true if climatisation target temperature is supported."""
2142
+ if self.attrs.get('climater', False):
2143
+ if 'settings' in self.attrs.get('climater', {}):
2144
+ if 'targetTemperatureInCelsius' in self.attrs.get('climater', {})['settings']:
2145
+ return True
2146
+ return False
2147
+
2148
+ @property
2149
+ def climatisation_time_left(self):
2150
+ """Return time left for climatisation in hours:minutes."""
2151
+ if self.attrs.get('airConditioning', {}).get('remainingTimeToReachTargetTemperatureInSeconds', False):
2152
+ minutes = int(self.attrs.get('airConditioning', {}).get('remainingTimeToReachTargetTemperatureInSeconds', 0))/60
2153
+ try:
2154
+ if not 0 <= minutes <= 65535:
2155
+ return "00:00"
2156
+ return "%02d:%02d" % divmod(minutes, 60)
2157
+ except Exception:
2158
+ pass
2159
+ return "00:00"
2160
+
2161
+ @property
2162
+ def is_climatisation_time_left_supported(self):
2163
+ """Return true if remainingTimeToReachTargetTemperatureInSeconds is supported."""
2164
+ if self.attrs.get('airConditioning', {}).get('remainingTimeToReachTargetTemperatureInSeconds', False):
2165
+ return True
2166
+ return False
2167
+
2168
+ @property
2169
+ def climatisation_without_external_power(self):
2170
+ """Return state of climatisation from battery power."""
2171
+ return self.attrs.get('climater').get('settings').get('climatisationWithoutExternalPower', False)
2172
+
2173
+ @property
2174
+ def is_climatisation_without_external_power_supported(self):
2175
+ """Return true if climatisation on battery power is supported."""
2176
+ if self.attrs.get('climater', False):
2177
+ if 'settings' in self.attrs.get('climater', {}):
2178
+ if 'climatisationWithoutExternalPower' in self.attrs.get('climater', {})['settings']:
2179
+ return True
2180
+ else:
2181
+ return False
2182
+
2183
+ @property
2184
+ def outside_temperature(self):
2185
+ """Return outside temperature."""
2186
+ response = int(self.attrs.get('StoredVehicleDataResponseParsed')['0x0301020001'].get('value', 0))
2187
+ if response:
2188
+ return round(float((response / 10) - 273.15), 1)
2189
+ else:
2190
+ return False
2191
+
2192
+ @property
2193
+ def is_outside_temperature_supported(self):
2194
+ """Return true if outside temp is supported"""
2195
+ if self.attrs.get('StoredVehicleDataResponseParsed', False):
2196
+ if '0x0301020001' in self.attrs.get('StoredVehicleDataResponseParsed'):
2197
+ if "value" in self.attrs.get('StoredVehicleDataResponseParsed')['0x0301020001']:
2198
+ return True
2199
+ else:
2200
+ return False
2201
+ else:
2202
+ return False
2203
+
2204
+ # Climatisation, electric
2205
+ @property
2206
+ def electric_climatisation_attributes(self):
2207
+ """Return climatisation attributes."""
2208
+ data = {
2209
+ 'source': self.attrs.get('climater', {}).get('settings', {}).get('heaterSource', {}).get('content', ''),
2210
+ 'status': self.attrs.get('climater', {}).get('status', {}).get('climatisationStatus', {}).get('climatisationState', '')
2211
+ }
2212
+ return data
2213
+
2214
+ @property
2215
+ def is_electric_climatisation_attributes_supported(self):
2216
+ """Return true if vehichle has climater."""
2217
+ return self.is_climatisation_supported
2218
+
2219
+ @property
2220
+ def electric_climatisation(self):
2221
+ """Return status of climatisation."""
2222
+ if self.attrs.get('climater', {}).get('status', {}).get('climatisationStatus', {}).get('climatisationState', False):
2223
+ climatisation_type = self.attrs.get('climater', {}).get('settings', {}).get('heaterSource', '')
2224
+ status = self.attrs.get('climater', {}).get('status', {}).get('climatisationStatus', {}).get('climatisationState', '')
2225
+ if status in ['heating', 'cooling', 'on']: #and climatisation_type == 'electric':
2226
+ return True
2227
+ return False
2228
+
2229
+ @property
2230
+ def is_electric_climatisation_supported(self):
2231
+ """Return true if vehichle has climater."""
2232
+ return self.is_climatisation_supported
2233
+
2234
+ @property
2235
+ def auxiliary_climatisation(self):
2236
+ """Return status of auxiliary climatisation."""
2237
+ climatisation_type = self.attrs.get('climater', {}).get('settings', {}).get('heaterSource', {}).get('content', '')
2238
+ status = self.attrs.get('climater', {}).get('status', {}).get('climatisationStatus', {}).get('climatisationState', '')
2239
+ if status in ['heating', 'cooling', 'ventilation', 'heatingAuxiliary', 'on'] and climatisation_type == 'auxiliary':
2240
+ return True
2241
+ elif status in ['heatingAuxiliary'] and climatisation_type == 'electric':
2242
+ return True
2243
+ else:
2244
+ return False
2245
+
2246
+ @property
2247
+ def is_auxiliary_climatisation_supported(self):
2248
+ """Return true if vehicle has auxiliary climatisation."""
2249
+ #if self._services.get('rclima_v1', False):
2250
+ if self._relevantCapabilties.get('climatisation', {}).get('active', False):
2251
+ functions = self._services.get('rclima_v1', {}).get('operations', [])
2252
+ #for operation in functions:
2253
+ # if operation['id'] == 'P_START_CLIMA_AU':
2254
+ if 'P_START_CLIMA_AU' in functions:
2255
+ return True
2256
+ return False
2257
+
2258
+ @property
2259
+ def is_climatisation_supported(self):
2260
+ """Return true if climatisation has State."""
2261
+ if self.attrs.get('climater', {}).get('status', {}).get('climatisationStatus', {}).get('climatisationState', False):
2262
+ return True
2263
+ return False
2264
+
2265
+ @property
2266
+ def window_heater(self):
2267
+ """Return status of window heater."""
2268
+ if self.attrs.get('climater', False):
2269
+ for elem in self.attrs.get('climater', {}).get('status', {}).get('windowHeatingStatus', {}).get('windowHeatingStatus', []):
2270
+ if elem.get('windowHeatingState','off')=='on':
2271
+ return True
2272
+ return False
2273
+
2274
+ @property
2275
+ def is_window_heater_supported(self):
2276
+ """Return true if vehichle has heater."""
2277
+ if self.is_electric_climatisation_supported:
2278
+ if self.attrs.get('climater', False):
2279
+ if self.attrs.get('climater', {}).get('status', {}).get('windowHeatingStatus', {}).get('windowHeatingStatus', []):
2280
+ if len(self.attrs.get('climater', {}).get('status', {}).get('windowHeatingStatus', {}).get('windowHeatingStatus', []))>0:
2281
+ return True
2282
+ return False
2283
+
2284
+ @property
2285
+ def seat_heating(self):
2286
+ """Return status of seat heating."""
2287
+ if self.attrs.get('airConditioning', {}).get('seatHeatingSupport', False):
2288
+ for element in self.attrs.get('airConditioning', {}).get('seatHeatingSupport', {}):
2289
+ if self.attrs.get('airConditioning', {}).get('seatHeatingSupport', {}).get(element, False):
2290
+ return True
2291
+ return False
2292
+
2293
+ @property
2294
+ def is_seat_heating_supported(self):
2295
+ """Return true if vehichle has seat heating."""
2296
+ if self.attrs.get('airConditioning', {}).get('seatHeatingSupport', False):
2297
+ return True
2298
+ return False
2299
+
2300
+ @property
2301
+ def warnings(self):
2302
+ """Return warnings."""
2303
+ return len(self.attrs.get('warninglights', {}).get('statuses',[]))
2304
+
2305
+ @property
2306
+ def is_warnings_supported(self):
2307
+ """Return true if vehichle has warnings."""
2308
+ if self.attrs.get('warninglights', False):
2309
+ return True
2310
+ return False
2311
+
2312
+ # Parking heater, "legacy" auxiliary climatisation
2313
+ @property
2314
+ def pheater_duration(self):
2315
+ return self._climate_duration
2316
+
2317
+ @pheater_duration.setter
2318
+ def pheater_duration(self, value):
2319
+ if value in [10, 20, 30, 40, 50, 60]:
2320
+ self._climate_duration = value
2321
+ else:
2322
+ _LOGGER.warning(f'Invalid value for duration: {value}')
2323
+
2324
+ @property
2325
+ def is_pheater_duration_supported(self):
2326
+ return self.is_pheater_heating_supported
2327
+
2328
+ @property
2329
+ def pheater_ventilation(self):
2330
+ """Return status of combustion climatisation."""
2331
+ return self.attrs.get('heating', {}).get('climatisationStateReport', {}).get('climatisationState', False) == 'ventilation'
2332
+
2333
+ @property
2334
+ def is_pheater_ventilation_supported(self):
2335
+ """Return true if vehichle has combustion climatisation."""
2336
+ return self.is_pheater_heating_supported
2337
+
2338
+ @property
2339
+ def pheater_heating(self):
2340
+ """Return status of combustion engine heating."""
2341
+ return self.attrs.get('heating', {}).get('climatisationStateReport', {}).get('climatisationState', False) == 'heating'
2342
+
2343
+ @property
2344
+ def is_pheater_heating_supported(self):
2345
+ """Return true if vehichle has combustion engine heating."""
2346
+ if self.attrs.get('heating', {}).get('climatisationStateReport', {}).get('climatisationState', False):
2347
+ return True
2348
+
2349
+ @property
2350
+ def pheater_status(self):
2351
+ """Return status of combustion engine heating/ventilation."""
2352
+ return self.attrs.get('heating', {}).get('climatisationStateReport', {}).get('climatisationState', 'Unknown')
2353
+
2354
+ @property
2355
+ def is_pheater_status_supported(self):
2356
+ """Return true if vehichle has combustion engine heating/ventilation."""
2357
+ if self.attrs.get('heating', {}).get('climatisationStateReport', {}).get('climatisationState', False):
2358
+ return True
2359
+
2360
+ # Windows
2361
+ @property
2362
+ def windows_closed(self):
2363
+ return (self.window_closed_left_front and self.window_closed_left_back and self.window_closed_right_front and self.window_closed_right_back)
2364
+
2365
+ @property
2366
+ def is_windows_closed_supported(self):
2367
+ """Return true if window state is supported"""
2368
+ response = ""
2369
+ if self.attrs.get('status', False):
2370
+ if 'windows' in self.attrs.get('status'):
2371
+ response = self.attrs.get('status')['windows'].get('frontLeft', '')
2372
+ return True if response != '' else False
2373
+
2374
+ @property
2375
+ def window_closed_left_front(self):
2376
+ response = self.attrs.get('status')['windows'].get('frontLeft', '')
2377
+ if response == 'closed':
2378
+ return True
2379
+ else:
2380
+ return False
2381
+
2382
+ @property
2383
+ def is_window_closed_left_front_supported(self):
2384
+ """Return true if window state is supported"""
2385
+ response = ""
2386
+ if self.attrs.get('status', False):
2387
+ if 'windows' in self.attrs.get('status'):
2388
+ response = self.attrs.get('status')['windows'].get('frontLeft', '')
2389
+ return True if response != "" else False
2390
+
2391
+ @property
2392
+ def window_closed_right_front(self):
2393
+ response = self.attrs.get('status')['windows'].get('frontRight', '')
2394
+ if response == 'closed':
2395
+ return True
2396
+ else:
2397
+ return False
2398
+
2399
+ @property
2400
+ def is_window_closed_right_front_supported(self):
2401
+ """Return true if window state is supported"""
2402
+ response = ""
2403
+ if self.attrs.get('status', False):
2404
+ if 'windows' in self.attrs.get('status'):
2405
+ response = self.attrs.get('status')['windows'].get('frontRight', '')
2406
+ return True if response != "" else False
2407
+
2408
+ @property
2409
+ def window_closed_left_back(self):
2410
+ response = self.attrs.get('status')['windows'].get('rearLeft', '')
2411
+ if response == 'closed':
2412
+ return True
2413
+ else:
2414
+ return False
2415
+
2416
+ @property
2417
+ def is_window_closed_left_back_supported(self):
2418
+ """Return true if window state is supported"""
2419
+ response = ""
2420
+ if self.attrs.get('status', False):
2421
+ if 'windows' in self.attrs.get('status'):
2422
+ response = self.attrs.get('status')['windows'].get('rearLeft', '')
2423
+ return True if response != "" else False
2424
+
2425
+ @property
2426
+ def window_closed_right_back(self):
2427
+ response = self.attrs.get('status')['windows'].get('rearRight', '')
2428
+ if response == 'closed':
2429
+ return True
2430
+ else:
2431
+ return False
2432
+
2433
+ @property
2434
+ def is_window_closed_right_back_supported(self):
2435
+ """Return true if window state is supported"""
2436
+ response = ""
2437
+ if self.attrs.get('status', False):
2438
+ if 'windows' in self.attrs.get('status'):
2439
+ response = self.attrs.get('status')['windows'].get('rearRight', '')
2440
+ return True if response != "" else False
2441
+
2442
+ @property
2443
+ def sunroof_closed(self):
2444
+ # Due to missing test objects, it is yet unclear, if 'sunroof' is direct subentry of 'status' or a subentry of 'windows'. So both are checked.
2445
+ response = ""
2446
+ if 'sunRoof' in self.attrs.get('status'):
2447
+ response = self.attrs.get('status').get('sunRoof', '')
2448
+ #else:
2449
+ # response = self.attrs.get('status')['windows'].get('sunRoof', '')
2450
+ if response == 'closed':
2451
+ return True
2452
+ else:
2453
+ return False
2454
+
2455
+ @property
2456
+ def is_sunroof_closed_supported(self):
2457
+ """Return true if sunroof state is supported"""
2458
+ # Due to missing test objects, it is yet unclear, if 'sunroof' is direct subentry of 'status' or a subentry of 'windows'. So both are checked.
2459
+ response = ""
2460
+ if self.attrs.get('status', False):
2461
+ if 'sunRoof' in self.attrs.get('status'):
2462
+ response = self.attrs.get('status').get('sunRoof', '')
2463
+ #elif 'sunRoof' in self.attrs.get('status')['windows']:
2464
+ # response = self.attrs.get('status')['windows'].get('sunRoof', '')
2465
+ return True if response != '' else False
2466
+
2467
+ # Locks
2468
+ @property
2469
+ def door_locked(self):
2470
+ # LEFT FRONT
2471
+ response = self.attrs.get('status')['doors']['frontLeft'].get('locked', 'false')
2472
+ if response != 'true':
2473
+ return False
2474
+ # LEFT REAR
2475
+ response = self.attrs.get('status')['doors']['rearLeft'].get('locked', 'false')
2476
+ if response != 'true':
2477
+ return False
2478
+ # RIGHT FRONT
2479
+ response = self.attrs.get('status')['doors']['frontRight'].get('locked', 'false')
2480
+ if response != 'true':
2481
+ return False
2482
+ # RIGHT REAR
2483
+ response = self.attrs.get('status')['doors']['rearRight'].get('locked', 'false')
2484
+ if response != 'true':
2485
+ return False
2486
+
2487
+ return True
2488
+
2489
+ @property
2490
+ def is_door_locked_supported(self):
2491
+ response = 0
2492
+ if self.attrs.get('status', False):
2493
+ if 'doors' in self.attrs.get('status'):
2494
+ response = self.attrs.get('status')['doors'].get('frontLeft', {}).get('locked', 0)
2495
+ return True if response != 0 else False
2496
+
2497
+ @property
2498
+ def trunk_locked(self):
2499
+ locked=self.attrs.get('status')['trunk'].get('locked', 'false')
2500
+ return True if locked == 'true' else False
2501
+
2502
+ @property
2503
+ def is_trunk_locked_supported(self):
2504
+ if self.attrs.get('status', False):
2505
+ if 'trunk' in self.attrs.get('status'):
2506
+ if 'locked' in self.attrs.get('status').get('trunk'):
2507
+ return True
2508
+ return False
2509
+
2510
+ # Doors, hood and trunk
2511
+ @property
2512
+ def hood_closed(self):
2513
+ """Return true if hood is closed"""
2514
+ open = self.attrs.get('status')['hood'].get('open', 'false')
2515
+ return True if open == 'false' else False
2516
+
2517
+ @property
2518
+ def is_hood_closed_supported(self):
2519
+ """Return true if hood state is supported"""
2520
+ response = 0
2521
+ if self.attrs.get('status', False):
2522
+ if 'hood' in self.attrs.get('status', {}):
2523
+ response = self.attrs.get('status')['hood'].get('open', 0)
2524
+ return True if response != 0 else False
2525
+
2526
+ @property
2527
+ def door_closed_left_front(self):
2528
+ open=self.attrs.get('status')['doors']['frontLeft'].get('open', 'false')
2529
+ return True if open == 'false' else False
2530
+
2531
+ @property
2532
+ def is_door_closed_left_front_supported(self):
2533
+ """Return true if window state is supported"""
2534
+ if self.attrs.get('status', False):
2535
+ if 'doors' in self.attrs.get('status'):
2536
+ if 'frontLeft' in self.attrs.get('status').get('doors', {}):
2537
+ return True
2538
+ return False
2539
+
2540
+ @property
2541
+ def door_closed_right_front(self):
2542
+ open=self.attrs.get('status')['doors']['frontRight'].get('open', 'false')
2543
+ return True if open == 'false' else False
2544
+
2545
+ @property
2546
+ def is_door_closed_right_front_supported(self):
2547
+ """Return true if window state is supported"""
2548
+ if self.attrs.get('status', False):
2549
+ if 'doors' in self.attrs.get('status'):
2550
+ if 'frontRight' in self.attrs.get('status').get('doors', {}):
2551
+ return True
2552
+ return False
2553
+
2554
+ @property
2555
+ def door_closed_left_back(self):
2556
+ open=self.attrs.get('status')['doors']['rearLeft'].get('open', 'false')
2557
+ return True if open == 'false' else False
2558
+
2559
+ @property
2560
+ def is_door_closed_left_back_supported(self):
2561
+ if self.attrs.get('status', False):
2562
+ if 'doors' in self.attrs.get('status'):
2563
+ if 'rearLeft' in self.attrs.get('status').get('doors', {}):
2564
+ return True
2565
+ return False
2566
+
2567
+ @property
2568
+ def door_closed_right_back(self):
2569
+ open=self.attrs.get('status')['doors']['rearRight'].get('open', 'false')
2570
+ return True if open == 'false' else False
2571
+
2572
+ @property
2573
+ def is_door_closed_right_back_supported(self):
2574
+ """Return true if window state is supported"""
2575
+ if self.attrs.get('status', False):
2576
+ if 'doors' in self.attrs.get('status'):
2577
+ if 'rearRight' in self.attrs.get('status').get('doors', {}):
2578
+ return True
2579
+ return False
2580
+
2581
+ @property
2582
+ def trunk_closed(self):
2583
+ open = self.attrs.get('status')['trunk'].get('open', 'false')
2584
+ return True if open == 'false' else False
2585
+
2586
+ @property
2587
+ def is_trunk_closed_supported(self):
2588
+ """Return true if window state is supported"""
2589
+ response = 0
2590
+ if self.attrs.get('status', False):
2591
+ if 'trunk' in self.attrs.get('status', {}):
2592
+ response = self.attrs.get('status')['trunk'].get('open', 0)
2593
+ return True if response != 0 else False
2594
+
2595
+ # Departure timers
2596
+ @property
2597
+ def departure1(self):
2598
+ """Return timer status and attributes."""
2599
+ if self.attrs.get('departureTimers', False):
2600
+ try:
2601
+ data = {}
2602
+ timerdata = self.attrs.get('departureTimers', {}).get('timers', [])
2603
+ timer = timerdata[0]
2604
+ timer.pop('timestamp', None)
2605
+ timer.pop('timerID', None)
2606
+ timer.pop('profileID', None)
2607
+ data.update(timer)
2608
+ return data
2609
+ except:
2610
+ pass
2611
+ elif self.attrs.get('timers', False):
2612
+ try:
2613
+ response = self.attrs.get('timers', [])
2614
+ if len(self.attrs.get('timers', [])) >= 1:
2615
+ timer = response[0]
2616
+ timer.pop('id', None)
2617
+ else:
2618
+ timer = {}
2619
+ return timer
2620
+ except:
2621
+ pass
2622
+ return None
2623
+
2624
+ @property
2625
+ def is_departure1_supported(self):
2626
+ """Return true if timer 1 is supported."""
2627
+ if len(self.attrs.get('departureTimers', {}).get('timers', [])) >= 1:
2628
+ return True
2629
+ elif len(self.attrs.get('timers', [])) >= 1:
2630
+ return True
2631
+ return False
2632
+
2633
+ @property
2634
+ def departure2(self):
2635
+ """Return timer status and attributes."""
2636
+ if self.attrs.get('departureTimers', False):
2637
+ try:
2638
+ data = {}
2639
+ timerdata = self.attrs.get('departureTimers', {}).get('timers', [])
2640
+ timer = timerdata[1]
2641
+ timer.pop('timestamp', None)
2642
+ timer.pop('timerID', None)
2643
+ timer.pop('profileID', None)
2644
+ data.update(timer)
2645
+ return data
2646
+ except:
2647
+ pass
2648
+ elif self.attrs.get('timers', False):
2649
+ try:
2650
+ response = self.attrs.get('timers', [])
2651
+ if len(self.attrs.get('timers', [])) >= 2:
2652
+ timer = response[1]
2653
+ timer.pop('id', None)
2654
+ else:
2655
+ timer = {}
2656
+ return timer
2657
+ except:
2658
+ pass
2659
+ return None
2660
+
2661
+ @property
2662
+ def is_departure2_supported(self):
2663
+ """Return true if timer 2 is supported."""
2664
+ if len(self.attrs.get('departureTimers', {}).get('timers', [])) >= 2:
2665
+ return True
2666
+ elif len(self.attrs.get('timers', [])) >= 2:
2667
+ return True
2668
+ return False
2669
+
2670
+ @property
2671
+ def departure3(self):
2672
+ """Return timer status and attributes."""
2673
+ if self.attrs.get('departureTimers', False):
2674
+ try:
2675
+ data = {}
2676
+ timerdata = self.attrs.get('departureTimers', {}).get('timers', [])
2677
+ timer = timerdata[2]
2678
+ timer.pop('timestamp', None)
2679
+ timer.pop('timerID', None)
2680
+ timer.pop('profileID', None)
2681
+ data.update(timer)
2682
+ return data
2683
+ except:
2684
+ pass
2685
+ elif self.attrs.get('timers', False):
2686
+ try:
2687
+ response = self.attrs.get('timers', [])
2688
+ if len(self.attrs.get('timers', [])) >= 3:
2689
+ timer = response[2]
2690
+ timer.pop('id', None)
2691
+ else:
2692
+ timer = {}
2693
+ return timer
2694
+ except:
2695
+ pass
2696
+ return None
2697
+
2698
+ @property
2699
+ def is_departure3_supported(self):
2700
+ """Return true if timer 3 is supported."""
2701
+ if len(self.attrs.get('departureTimers', {}).get('timers', [])) >= 3:
2702
+ return True
2703
+ elif len(self.attrs.get('timers', [])) >= 3:
2704
+ return True
2705
+ return False
2706
+
2707
+ # Departure profiles
2708
+ @property
2709
+ def departure_profile1(self):
2710
+ """Return profile status and attributes."""
2711
+ if self.attrs.get('departureProfiles', False):
2712
+ try:
2713
+ data = {}
2714
+ timerdata = self.attrs.get('departureProfiles', {}).get('timers', [])
2715
+ timer = timerdata[0]
2716
+ #timer.pop('timestamp', None)
2717
+ #timer.pop('timerID', None)
2718
+ #timer.pop('profileID', None)
2719
+ data.update(timer)
2720
+ return data
2721
+ except:
2722
+ pass
2723
+ return None
2724
+
2725
+ @property
2726
+ def is_departure_profile1_supported(self):
2727
+ """Return true if profile 1 is supported."""
2728
+ if len(self.attrs.get('departureProfiles', {}).get('timers', [])) >= 1:
2729
+ return True
2730
+ return False
2731
+
2732
+ @property
2733
+ def departure_profile2(self):
2734
+ """Return profile status and attributes."""
2735
+ if self.attrs.get('departureProfiles', False):
2736
+ try:
2737
+ data = {}
2738
+ timerdata = self.attrs.get('departureProfiles', {}).get('timers', [])
2739
+ timer = timerdata[1]
2740
+ #timer.pop('timestamp', None)
2741
+ #timer.pop('timerID', None)
2742
+ #timer.pop('profileID', None)
2743
+ data.update(timer)
2744
+ return data
2745
+ except:
2746
+ pass
2747
+ return None
2748
+
2749
+ @property
2750
+ def is_departure_profile2_supported(self):
2751
+ """Return true if profile 2 is supported."""
2752
+ if len(self.attrs.get('departureProfiles', {}).get('timers', [])) >= 2:
2753
+ return True
2754
+ return False
2755
+
2756
+ @property
2757
+ def departure_profile3(self):
2758
+ """Return profile status and attributes."""
2759
+ if self.attrs.get('departureProfiles', False):
2760
+ try:
2761
+ data = {}
2762
+ timerdata = self.attrs.get('departureProfiles', {}).get('timers', [])
2763
+ timer = timerdata[2]
2764
+ #timer.pop('timestamp', None)
2765
+ #timer.pop('timerID', None)
2766
+ #timer.pop('profileID', None)
2767
+ data.update(timer)
2768
+ return data
2769
+ except:
2770
+ pass
2771
+ return None
2772
+
2773
+ @property
2774
+ def is_departure_profile3_supported(self):
2775
+ """Return true if profile 3 is supported."""
2776
+ if len(self.attrs.get('departureProfiles', {}).get('timers', [])) >= 3:
2777
+ return True
2778
+ return False
2779
+
2780
+ # Trip data
2781
+ @property
2782
+ def trip_last_entry(self):
2783
+ return self.attrs.get('tripstatistics', {}).get('short', [{},{}])[-1]
2784
+
2785
+ @property
2786
+ def trip_last_average_speed(self):
2787
+ return self.trip_last_entry.get('averageSpeedKmph')
2788
+
2789
+ @property
2790
+ def is_trip_last_average_speed_supported(self):
2791
+ response = self.trip_last_entry
2792
+ if response and type(response.get('averageSpeedKmph', None)) in (float, int):
2793
+ return True
2794
+
2795
+ @property
2796
+ def trip_last_average_electric_consumption(self):
2797
+ return self.trip_last_entry.get('averageElectricConsumption')
2798
+
2799
+ @property
2800
+ def is_trip_last_average_electric_consumption_supported(self):
2801
+ response = self.trip_last_entry
2802
+ if response and type(response.get('averageElectricConsumption', None)) in (float, int):
2803
+ return True
2804
+
2805
+ @property
2806
+ def trip_last_average_fuel_consumption(self):
2807
+ return self.trip_last_entry.get('averageFuelConsumption')
2808
+
2809
+ @property
2810
+ def is_trip_last_average_fuel_consumption_supported(self):
2811
+ response = self.trip_last_entry
2812
+ if response and type(response.get('averageFuelConsumption', None)) in (float, int):
2813
+ return True
2814
+
2815
+ @property
2816
+ def trip_last_average_auxillary_consumption(self):
2817
+ return self.trip_last_entry.get('averageAuxConsumption')
2818
+
2819
+ @property
2820
+ def is_trip_last_average_auxillary_consumption_supported(self):
2821
+ response = self.trip_last_entry
2822
+ if response and type(response.get('averageAuxConsumption', None)) in (float, int):
2823
+ return True
2824
+
2825
+ @property
2826
+ def trip_last_average_aux_consumer_consumption(self):
2827
+ value = self.trip_last_entry.get('averageAuxConsumerConsumption')
2828
+ return value
2829
+
2830
+ @property
2831
+ def is_trip_last_average_aux_consumer_consumption_supported(self):
2832
+ response = self.trip_last_entry
2833
+ if response and type(response.get('averageAuxConsumerConsumption', None)) in (float, int):
2834
+ return True
2835
+
2836
+ @property
2837
+ def trip_last_duration(self):
2838
+ return self.trip_last_entry.get('travelTime')
2839
+
2840
+ @property
2841
+ def is_trip_last_duration_supported(self):
2842
+ response = self.trip_last_entry
2843
+ if response and type(response.get('travelTime', None)) in (float, int):
2844
+ return True
2845
+
2846
+ @property
2847
+ def trip_last_length(self):
2848
+ return self.trip_last_entry.get('mileageKm')
2849
+
2850
+ @property
2851
+ def is_trip_last_length_supported(self):
2852
+ response = self.trip_last_entry
2853
+ if response and type(response.get('mileageKm', None)) in (float, int):
2854
+ return True
2855
+
2856
+ @property
2857
+ def trip_last_recuperation(self):
2858
+ #Not implemented
2859
+ return self.trip_last_entry.get('recuperation')
2860
+
2861
+ @property
2862
+ def is_trip_last_recuperation_supported(self):
2863
+ #Not implemented
2864
+ response = self.trip_last_entry
2865
+ if response and type(response.get('recuperation', None)) in (float, int):
2866
+ return True
2867
+
2868
+ @property
2869
+ def trip_last_average_recuperation(self):
2870
+ #Not implemented
2871
+ value = self.trip_last_entry.get('averageRecuperation')
2872
+ return value
2873
+
2874
+ @property
2875
+ def is_trip_last_average_recuperation_supported(self):
2876
+ #Not implemented
2877
+ response = self.trip_last_entry
2878
+ if response and type(response.get('averageRecuperation', None)) in (float, int):
2879
+ return True
2880
+
2881
+ @property
2882
+ def trip_last_total_electric_consumption(self):
2883
+ #Not implemented
2884
+ return self.trip_last_entry.get('totalElectricConsumption')
2885
+
2886
+ @property
2887
+ def is_trip_last_total_electric_consumption_supported(self):
2888
+ #Not implemented
2889
+ response = self.trip_last_entry
2890
+ if response and type(response.get('totalElectricConsumption', None)) in (float, int):
2891
+ return True
2892
+
2893
+ @property
2894
+ def trip_last_cycle_entry(self):
2895
+ return self.attrs.get('tripstatistics', {}).get('cyclic', [{},{}])[-1]
2896
+
2897
+ @property
2898
+ def trip_last_cycle_average_speed(self):
2899
+ return self.trip_last_cycle_entry.get('averageSpeedKmph')
2900
+
2901
+ @property
2902
+ def is_trip_last_cycle_average_speed_supported(self):
2903
+ response = self.trip_last_cycle_entry
2904
+ if response and type(response.get('averageSpeedKmph', None)) in (float, int):
2905
+ return True
2906
+
2907
+ @property
2908
+ def trip_last_cycle_average_electric_consumption(self):
2909
+ return self.trip_last_cycle_entry.get('averageElectricConsumption')
2910
+
2911
+ @property
2912
+ def is_trip_last_cycle_average_electric_consumption_supported(self):
2913
+ response = self.trip_last_cycle_entry
2914
+ if response and type(response.get('averageElectricConsumption', None)) in (float, int):
2915
+ return True
2916
+
2917
+ @property
2918
+ def trip_last_cycle_average_fuel_consumption(self):
2919
+ return self.trip_last_cycle_entry.get('averageFuelConsumption')
2920
+
2921
+ @property
2922
+ def is_trip_last_cycle_average_fuel_consumption_supported(self):
2923
+ response = self.trip_last_cycle_entry
2924
+ if response and type(response.get('averageFuelConsumption', None)) in (float, int):
2925
+ return True
2926
+
2927
+ @property
2928
+ def trip_last_cycle_average_auxillary_consumption(self):
2929
+ return self.trip_last_cycle_entry.get('averageAuxConsumption')
2930
+
2931
+ @property
2932
+ def is_trip_last_cycle_average_auxillary_consumption_supported(self):
2933
+ response = self.trip_last_cycle_entry
2934
+ if response and type(response.get('averageAuxConsumption', None)) in (float, int):
2935
+ return True
2936
+
2937
+ @property
2938
+ def trip_last_cycle_average_aux_consumer_consumption(self):
2939
+ value = self.trip_last_cycle_entry.get('averageAuxConsumerConsumption')
2940
+ return value
2941
+
2942
+ @property
2943
+ def is_trip_last_cycle_average_aux_consumer_consumption_supported(self):
2944
+ response = self.trip_last_cycle_entry
2945
+ if response and type(response.get('averageAuxConsumerConsumption', None)) in (float, int):
2946
+ return True
2947
+
2948
+ @property
2949
+ def trip_last_cycle_duration(self):
2950
+ return self.trip_last_cycle_entry.get('travelTime')
2951
+
2952
+ @property
2953
+ def is_trip_last_cycle_duration_supported(self):
2954
+ response = self.trip_last_cycle_entry
2955
+ if response and type(response.get('travelTime', None)) in (float, int):
2956
+ return True
2957
+
2958
+ @property
2959
+ def trip_last_cycle_length(self):
2960
+ return self.trip_last_cycle_entry.get('mileageKm')
2961
+
2962
+ @property
2963
+ def is_trip_last_cycle_length_supported(self):
2964
+ response = self.trip_last_cycle_entry
2965
+ if response and type(response.get('mileageKm', None)) in (float, int):
2966
+ return True
2967
+
2968
+ @property
2969
+ def trip_last_cycle_recuperation(self):
2970
+ #Not implemented
2971
+ return self.trip_last_cycle_entry.get('recuperation')
2972
+
2973
+ @property
2974
+ def is_trip_last_cycle_recuperation_supported(self):
2975
+ #Not implemented
2976
+ response = self.trip_last_cycle_entry
2977
+ if response and type(response.get('recuperation', None)) in (float, int):
2978
+ return True
2979
+
2980
+ @property
2981
+ def trip_last_cycle_average_recuperation(self):
2982
+ #Not implemented
2983
+ value = self.trip_last_cycle_entry.get('averageRecuperation')
2984
+ return value
2985
+
2986
+ @property
2987
+ def is_trip_last_cycle_average_recuperation_supported(self):
2988
+ #Not implemented
2989
+ response = self.trip_last_cycle_entry
2990
+ if response and type(response.get('averageRecuperation', None)) in (float, int):
2991
+ return True
2992
+
2993
+ @property
2994
+ def trip_last_cycle_total_electric_consumption(self):
2995
+ #Not implemented
2996
+ return self.trip_last_cycle_entry.get('totalElectricConsumption')
2997
+
2998
+ @property
2999
+ def is_trip_last_cycle_total_electric_consumption_supported(self):
3000
+ #Not implemented
3001
+ response = self.trip_last_cycle_entry
3002
+ if response and type(response.get('totalElectricConsumption', None)) in (float, int):
3003
+ return True
3004
+
3005
+ # Area alarm
3006
+ @property
3007
+ def area_alarm(self):
3008
+ """Return True, if attribute areaAlarm is not {}"""
3009
+ alarmPresent = self.attrs.get('areaAlarm', {})
3010
+ if alarmPresent !={}:
3011
+ # Delete an area alarm if it is older than 900 seconds
3012
+ alarmTimestamp = self.attrs.get('areaAlarm', {}).get('timestamp', 0)
3013
+ if alarmTimestamp < datetime.now(tz=None) - timedelta(seconds= 900):
3014
+ self.attrs.pop("areaAlarm")
3015
+ alarmPresent = {}
3016
+ return False if alarmPresent == {} else True
3017
+
3018
+ @property
3019
+ def is_area_alarm_supported(self):
3020
+ """Return True, if vehicle supports area alarm (always True at the moment)"""
3021
+ # Always True at the moment. Have to check, if the geofence capability is a necessary condition
3022
+ return True
3023
+
3024
+ # Status of set data requests
3025
+ @property
3026
+ def refresh_action_status(self):
3027
+ """Return latest status of data refresh request."""
3028
+ return self._requests.get('refresh', {}).get('status', 'None')
3029
+
3030
+ @property
3031
+ def refresh_action_timestamp(self):
3032
+ """Return timestamp of latest data refresh request."""
3033
+ timestamp = self._requests.get('refresh', {}).get('timestamp', DATEZERO)
3034
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
3035
+
3036
+ @property
3037
+ def charger_action_status(self):
3038
+ """Return latest status of charger request."""
3039
+ return self._requests.get('batterycharge', {}).get('status', 'None')
3040
+
3041
+ @property
3042
+ def charger_action_timestamp(self):
3043
+ """Return timestamp of latest charger request."""
3044
+ timestamp = self._requests.get('charger', {}).get('timestamp', DATEZERO)
3045
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
3046
+
3047
+ @property
3048
+ def climater_action_status(self):
3049
+ """Return latest status of climater request."""
3050
+ return self._requests.get('climatisation', {}).get('status', 'None')
3051
+
3052
+ @property
3053
+ def climater_action_timestamp(self):
3054
+ """Return timestamp of latest climater request."""
3055
+ timestamp = self._requests.get('climatisation', {}).get('timestamp', DATEZERO)
3056
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
3057
+
3058
+ @property
3059
+ def pheater_action_status(self):
3060
+ """Return latest status of parking heater request."""
3061
+ return self._requests.get('preheater', {}).get('status', 'None')
3062
+
3063
+ @property
3064
+ def pheater_action_timestamp(self):
3065
+ """Return timestamp of latest parking heater request."""
3066
+ timestamp = self._requests.get('preheater', {}).get('timestamp', DATEZERO)
3067
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
3068
+
3069
+ @property
3070
+ def honkandflash_action_status(self):
3071
+ """Return latest status of honk and flash action request."""
3072
+ return self._requests.get('honkandflash', {}).get('status', 'None')
3073
+
3074
+ @property
3075
+ def honkandflash_action_timestamp(self):
3076
+ """Return timestamp of latest honk and flash request."""
3077
+ timestamp = self._requests.get('honkandflash', {}).get('timestamp', DATEZERO)
3078
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
3079
+
3080
+ @property
3081
+ def lock_action_status(self):
3082
+ """Return latest status of lock action request."""
3083
+ return self._requests.get('lock', {}).get('status', 'None')
3084
+
3085
+ @property
3086
+ def lock_action_timestamp(self):
3087
+ """Return timestamp of latest lock action request."""
3088
+ timestamp = self._requests.get('lock', {}).get('timestamp', DATEZERO)
3089
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
3090
+
3091
+ @property
3092
+ def timer_action_status(self):
3093
+ """Return latest status of departure timer request."""
3094
+ return self._requests.get('departuretimer', {}).get('status', 'None')
3095
+
3096
+ @property
3097
+ def timer_action_timestamp(self):
3098
+ """Return timestamp of latest departure timer request."""
3099
+ timestamp = self._requests.get('departuretimer', {}).get('timestamp', DATEZERO)
3100
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
3101
+
3102
+ @property
3103
+ def refresh_data(self):
3104
+ """Get state of data refresh"""
3105
+ #if self._requests.get('refresh', {}).get('id', False):
3106
+ # timestamp = self._requests.get('refresh', {}).get('timestamp', DATEZERO)
3107
+ # expired = datetime.now() - timedelta(minutes=2)
3108
+ # if expired < timestamp:
3109
+ # return True
3110
+ #State is always false
3111
+ return False
3112
+
3113
+ @property
3114
+ def is_refresh_data_supported(self):
3115
+ """Data refresh is supported."""
3116
+ if self._connectivities.get('mode', '') == 'online':
3117
+ return True
3118
+
3119
+ @property
3120
+ def update_data(self):
3121
+ """Get state of data update"""
3122
+ return False
3123
+
3124
+ @property
3125
+ def is_update_data_supported(self):
3126
+ """Data update is supported."""
3127
+ return True
3128
+
3129
+ # Honk and flash
3130
+ @property
3131
+ def request_honkandflash(self):
3132
+ """State is always False"""
3133
+ return False
3134
+
3135
+ @property
3136
+ def is_request_honkandflash_supported(self):
3137
+ """Honk and flash is supported if service is enabled."""
3138
+ if self._relevantCapabilties.get('honkAndFlash', {}).get('active', False):
3139
+ return True
3140
+
3141
+ @property
3142
+ def request_flash(self):
3143
+ """State is always False"""
3144
+ return False
3145
+
3146
+ @property
3147
+ def is_request_flash_supported(self):
3148
+ """Honk and flash is supported if service is enabled."""
3149
+ if self._relevantCapabilties.get('honkAndFlash', {}).get('active', False):
3150
+ return True
3151
+
3152
+ # Requests data
3153
+ @property
3154
+ def request_in_progress(self):
3155
+ """Request in progress is always supported."""
3156
+ try:
3157
+ for section in self._requests:
3158
+ if self._requests[section].get('id', False):
3159
+ return True
3160
+ except:
3161
+ pass
3162
+ return False
3163
+
3164
+ @property
3165
+ def is_request_in_progress_supported(self):
3166
+ """Request in progress is always supported."""
3167
+ return False
3168
+
3169
+ @property
3170
+ def request_results(self):
3171
+ """Get last request result."""
3172
+ data = {
3173
+ 'latest': self._requests.get('latest', 'N/A'),
3174
+ 'state': self._requests.get('state', 'N/A'),
3175
+ }
3176
+ for section in self._requests:
3177
+ if section in ['departuretimer', 'departureprofiles', 'batterycharge', 'climatisation', 'refresh', 'lock', 'preheater']:
3178
+ timestamp = self._requests.get(section, {}).get('timestamp', DATEZERO)
3179
+ data[section] = self._requests[section].get('status', 'N/A')
3180
+ data[section+'_timestamp'] = timestamp.strftime("%Y-%m-%d %H:%M:%S")
3181
+ return data
3182
+
3183
+ @property
3184
+ def is_request_results_supported(self):
3185
+ """Request results is supported if in progress is supported."""
3186
+ return False # deactivated because it provides no usefull information
3187
+ #return self.is_request_in_progress_supported
3188
+
3189
+ @property
3190
+ def requests_remaining(self):
3191
+ """Get remaining requests before throttled."""
3192
+ if self.attrs.get('rate_limit_remaining', False):
3193
+ self.requests_remaining = self.attrs.get('rate_limit_remaining')
3194
+ self.attrs.pop('rate_limit_remaining')
3195
+ return self._requests['remaining']
3196
+
3197
+ @requests_remaining.setter
3198
+ def requests_remaining(self, value):
3199
+ self._requests['remaining'] = value
3200
+
3201
+ @property
3202
+ def is_requests_remaining_supported(self):
3203
+ return False # deactivated because it provides no usefull information
3204
+ #if self.is_request_in_progress_supported:
3205
+ # return True if self._requests.get('remaining', False) else False
3206
+
3207
+ #### Helper functions ####
3208
+ def __str__(self):
3209
+ return self.vin
3210
+
3211
+ @property
3212
+ def json(self):
3213
+ def serialize(obj):
3214
+ if isinstance(obj, datetime):
3215
+ return obj.isoformat()
3216
+
3217
+ return to_json(
3218
+ OrderedDict(sorted(self.attrs.items())),
3219
+ indent=4,
3220
+ default=serialize
3221
+ )
3222
+
3223
+
3224
+ async def stopFirebase(self):
3225
+ # Check if firebase is activated
3226
+ if self.firebaseStatus not in (FIREBASE_STATUS_ACTIVATED, FIREBASE_STATUS_ACTIVATION_STOPPED):
3227
+ _LOGGER.info(f'No need to stop firebase. Firebase status={self.firebaseStatus}')
3228
+ return self.firebaseStatus
3229
+
3230
+ if self.firebase == None:
3231
+ _LOGGER.error(f'Internal error: Firebase status={self.firebaseStatus} but firebase variable not set. Setting firebase status back to not initialised.')
3232
+ self.firebaseStatus = FIREBASE_STATUS_NOT_INITIALISED
3233
+ return self.firebaseStatus
3234
+
3235
+ success = await self.firebase.firebaseStop()
3236
+ if not success:
3237
+ _LOGGER.warning('Stopping of firebase messaging failed.')
3238
+ return self.firebaseStatus
3239
+
3240
+ #await asyncio.sleep(5)
3241
+ self.firebaseStatus = FIREBASE_STATUS_NOT_INITIALISED
3242
+ _LOGGER.info('Stopping of firebase messaging was successful.')
3243
+ return self.firebaseStatus
3244
+
3245
+ async def initialiseFirebase(self, firebaseCredentialsFileName=None, updateCallback=None):
3246
+ # Check if firebase shall be used
3247
+ if firebaseCredentialsFileName == None:
3248
+ _LOGGER.debug('No use of firebase wanted.')
3249
+ self.firebaseStatus = FIREBASE_STATUS_NOT_WANTED
3250
+ return self.firebaseStatus
3251
+ self._firebaseCredentialsFileName = firebaseCredentialsFileName
3252
+
3253
+ # Check if firebase not already initialised
3254
+ if self.firebaseStatus!= FIREBASE_STATUS_NOT_INITIALISED:
3255
+ _LOGGER.debug(f'No need to initialise firebase anymore. Firebase status={self.firebaseStatus}')
3256
+ return self.firebaseStatus
3257
+
3258
+ # Read the firebase credentials file and check if an existing subscription has to be deleted
3259
+ loop = asyncio.get_running_loop()
3260
+ credentials = await loop.run_in_executor(None, readFCMCredsFile, firebaseCredentialsFileName)
3261
+ subscribedVin = credentials.get('subscription',{}).get('vin','')
3262
+ subscribedUserId = credentials.get('subscription',{}).get('userId','')
3263
+ subscribedBrand = credentials.get('subscription',{}).get('brand','')
3264
+ if subscribedVin != '' and subscribedUserId != '':
3265
+ if subscribedVin != self.vin or subscribedUserId != self._connection._user_id or subscribedBrand != self._connection._session_auth_brand:
3266
+ _LOGGER.debug(self._connection.anonymise(f'Change of vin, userId or brand. Deleting subscription for vin={subscribedVin} and userId={subscribedUserId}.'))
3267
+ result = await self._connection.deleteSubscription(credentials)
3268
+
3269
+ # Start firebase
3270
+ if self.firebase == None:
3271
+ self.firebase = Firebase()
3272
+ success = await self.firebase.firebaseStart(self.onNotification, firebaseCredentialsFileName, brand=self._connection._session_auth_brand)
3273
+ if not success:
3274
+ self.firebaseStatus = FIREBASE_STATUS_ACTIVATION_FAILED
3275
+ _LOGGER.warning('Activation of firebase messaging failed.')
3276
+ return self.firebaseStatus
3277
+
3278
+ self.updateCallback = updateCallback
3279
+ # Read possibly new credentials and subscribe vin and userId for push notifications
3280
+ loop = asyncio.get_running_loop()
3281
+ credentials = await loop.run_in_executor(None, readFCMCredsFile, firebaseCredentialsFileName)
3282
+ result = await self._connection.subscribe(self.vin, credentials)
3283
+ _LOGGER.debug(f'Result of subscription={result}.')
3284
+ credentials['subscription']= {
3285
+ 'vin' : self.vin,
3286
+ 'userId' : self._connection._user_id,
3287
+ 'brand' : self._connection._session_auth_brand,
3288
+ 'id' : result.get('id', ''),
3289
+ }
3290
+ loop = asyncio.get_running_loop()
3291
+ await loop.run_in_executor(None, writeFCMCredsFile, credentials, firebaseCredentialsFileName)
3292
+
3293
+ await asyncio.sleep(5) # Wait to ignore the first notifications
3294
+ self.firebaseStatus = FIREBASE_STATUS_ACTIVATED
3295
+ _LOGGER.info('Activation of firebase messaging was successful.')
3296
+ return self.firebaseStatus
3297
+
3298
+
3299
+
3300
+ async def onNotification(self, obj, notification, data_message):
3301
+ # Do something with the notification
3302
+ _LOGGER.debug(f'Received push notification: notification id={notification}, type={obj.get('data',{}).get('type','')}, requestId={obj.get('data',{}).get('requestId','[None]')}')
3303
+ _LOGGER.debug(f' data_message={data_message}, payload={obj.get('data',{}).get('payload','[None]')}')
3304
+
3305
+ if self.firebaseStatus != FIREBASE_STATUS_ACTIVATED:
3306
+ if self.firebaseStatus != FIREBASE_STATUS_ACTIVATION_STOPPED:
3307
+ _LOGGER.info(f'While firebase is not fully activated, received notifications are just acknowledged.')
3308
+ # As long as the firebase status is not set to activated, ignore the notifications
3309
+ return False
3310
+ else:
3311
+ # It seems that the firebase connection still works although fcmpushclient.is_started() returned False some time ago
3312
+ _LOGGER.info(f'Firebase status={self.firebaseStatus}, but PyCupra still receives push notifications.')
3313
+ self.firebaseStatus = FIREBASE_STATUS_ACTIVATED
3314
+ _LOGGER.info(f'Set firebase status back to {self.firebaseStatus}.')
3315
+
3316
+
3317
+ type = obj.get('data',{}).get('type','')
3318
+ requestId = obj.get('data',{}).get('requestId','')
3319
+ payload = obj.get('data',{}).get('payload','')
3320
+ openRequest = -1
3321
+ if requestId != '':
3322
+ _LOGGER.info(f'Received notification of type \'{type}\', request id={requestId} ')
3323
+ else:
3324
+ _LOGGER.info(f'Received notification of type \'{type}\' ')
3325
+
3326
+ if notification == self._firebaseLastMessageId:
3327
+ _LOGGER.info(f'Received notification {notification} again. Just acknoledging it, nothing to do.')
3328
+ return False
3329
+
3330
+ self._firebaseLastMessageId = notification # save the id of the last notification
3331
+ if type in ('vehicle-access-locked-successful', 'vehicle-access-unlocked-successful'): # vehicle was locked/unlocked
3332
+ if self._requests.get('lock', {}).get('id', None):
3333
+ openRequest= self._requests.get('lock', {}).get('id', None)
3334
+ if openRequest == requestId:
3335
+ _LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
3336
+ self._requests.get('lock', {}).pop('id')
3337
+ if (self._last_get_statusreport < datetime.now(tz=None) - timedelta(seconds= 10)) or openRequest == requestId:
3338
+ # 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
3339
+ #await self.get_statusreport() # Call not needed because it's part of updateCallback(2)
3340
+ if self.updateCallback:
3341
+ await self.updateCallback(2)
3342
+ else:
3343
+ _LOGGER.debug(f'It is now {datetime.now(tz=None)}. Last update of status report was at {self._last_get_statusreport}. So no need to update.')
3344
+ # Wait 2 seconds
3345
+ await asyncio.sleep(2)
3346
+ elif type == 'departure-times-updated':
3347
+ if self._requests.get('departuretimer', {}).get('id', None):
3348
+ openRequest= self._requests.get('departuretimer', {}).get('id', None)
3349
+ if openRequest == requestId:
3350
+ _LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
3351
+ self._requests.get('departuretimer', {}).pop('id')
3352
+ if (self._last_get_departure_timers < datetime.now(tz=None) - timedelta(seconds= 30)) or openRequest == requestId:
3353
+ # 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
3354
+ await self.get_departure_timers()
3355
+ if self.updateCallback:
3356
+ await self.updateCallback(2)
3357
+ else:
3358
+ _LOGGER.debug(f'It is now {datetime.now(tz=None)}. Last update of departure timers was at {self._last_get_departure_timers}. So no need to update.')
3359
+ # Wait 5 seconds
3360
+ await asyncio.sleep(5)
3361
+ elif type == 'departure-profiles-updated': # !!! Is this the right type?
3362
+ if self._requests.get('departureprofile', {}).get('id', None):
3363
+ openRequest= self._requests.get('departureprofile', {}).get('id', None)
3364
+ if openRequest == requestId:
3365
+ _LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
3366
+ self._requests.get('departureprofile', {}).pop('id')
3367
+ if (self._last_get_departure_profiles < datetime.now(tz=None) - timedelta(seconds= 30)) or openRequest == requestId:
3368
+ # 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
3369
+ await self.get_departure_profiles()
3370
+ if self.updateCallback:
3371
+ await self.updateCallback(2)
3372
+ else:
3373
+ _LOGGER.debug(f'It is now {datetime.now(tz=None)}. Last update of departure profiles was at {self._last_get_departure_profiles}. So no need to update.')
3374
+ # Wait 5 seconds
3375
+ await asyncio.sleep(5)
3376
+ elif type in ('charging-status-changed', 'charging-started', 'charging-stopped', 'charging-settings-updated'):
3377
+ if self._requests.get('batterycharge', {}).get('id', None):
3378
+ openRequest= self._requests.get('batterycharge', {}).get('id', None)
3379
+ if openRequest == requestId:
3380
+ _LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
3381
+ self._requests.get('batterycharge', {}).pop('id')
3382
+ if (self._last_get_charger < datetime.now(tz=None) - timedelta(seconds= 10)) or openRequest == requestId:
3383
+ # 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
3384
+ await self.get_charger()
3385
+ if self.updateCallback:
3386
+ await self.updateCallback(2)
3387
+ else:
3388
+ _LOGGER.debug(f'It is now {datetime.now(tz=None)}. Last get_charger was at {self._last_get_charger}. So no need to update.')
3389
+ # Wait 5 seconds
3390
+ await asyncio.sleep(5)
3391
+ elif type in ('climatisation-status-changed','climatisation-started', 'climatisation-stopped', 'climatisation-settings-updated'):
3392
+ if self._requests.get('climatisation', {}).get('id', None):
3393
+ openRequest= self._requests.get('climatisation', {}).get('id', None)
3394
+ if openRequest == requestId:
3395
+ _LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
3396
+ self._requests.get('climatisation', {}).pop('id')
3397
+ if (self._last_get_climater < datetime.now(tz=None) - timedelta(seconds= 10)) or openRequest == requestId:
3398
+ # 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
3399
+ await self.get_climater()
3400
+ if self.updateCallback:
3401
+ await self.updateCallback(2)
3402
+ else:
3403
+ _LOGGER.debug(f'It is now {datetime.now(tz=None)}. Last get_climater was at {self._last_get_climater}. So no need to update.')
3404
+ # Wait 5 seconds
3405
+ await asyncio.sleep(5)
3406
+ elif type in ('vehicle-area-alarm-vehicle-exits-zone-triggered', 'vehicle-area-alarm-vehicle-enters-zone-triggered'):
3407
+ #if self._last_get_position < datetime.now(tz=None) - timedelta(seconds= 30):
3408
+ # # Update position data only if the last one is older than timedelta
3409
+ # await self.get_position()
3410
+ #else:
3411
+ # _LOGGER.debug(f'It is now {datetime.now(tz=None)}. Last get_position was at {self._last_get_position}. So no need to update.')
3412
+ if payload != '':
3413
+ payloadDict = json.loads(payload) # Convert json string to dict
3414
+ #_LOGGER.debug(f'payloadDict is dict: {isinstance(payloadDict, dict)}')
3415
+ zones = payloadDict.get('description',{}).get('values',[])
3416
+ else:
3417
+ _LOGGER.warning(f'Missing information about areas. Payload ={payload}')
3418
+ zones = []
3419
+ areaAlarm = {'areaAlarm' : {
3420
+ 'type': 'vehicle-exits-zone' if type=='vehicle-area-alarm-vehicle-exits-zone-triggered' else 'vehicle-enters-zone',
3421
+ 'timestamp': datetime.now(tz=None),
3422
+ 'zones': zones
3423
+ }
3424
+ }
3425
+ self._states.update(areaAlarm)
3426
+ if self.updateCallback:
3427
+ await self.updateCallback(2)
3428
+ elif type in ('vehicle-wake-up-succeeded', 'vehicle-wakeup-succeeded'):
3429
+ if self._requests.get('refresh', {}).get('id', None):
3430
+ openRequest= self._requests.get('refresh', {}).get('id', None)
3431
+ if openRequest == requestId:
3432
+ _LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
3433
+ self._requests.get('refresh', {}).pop('id')
3434
+ if (self._last_full_update < datetime.now(tz=None) - timedelta(seconds= 30)) or openRequest == requestId:
3435
+ # Do full update only if the last one is older than timedelta or if the notification belongs to an open request initiated by PyCupra
3436
+ if self.updateCallback:
3437
+ await self.updateCallback(1)
3438
+ else:
3439
+ _LOGGER.debug(f'It is now {datetime.now(tz=None)}. Last full update was at {self._last_full_update}. So no need to update.')
3440
+ # Wait 5 seconds
3441
+ await asyncio.sleep(2)
3442
+ elif type == 'vehicle-honk-and-flash-started':
3443
+ if self._requests.get('refresh', {}).get('id', None):
3444
+ openRequest= self._requests.get('refresh', {}).get('id', None)
3445
+ if openRequest == requestId:
3446
+ _LOGGER.debug(f'The notification closes an open request initiated by PyCupra.')
3447
+ self._requests.get('refresh', {}).pop('id')
3448
+ # Nothing else to do
3449
+ elif type in ('vehicle-area-alert-added', 'vehicle-area-alert-updated'):
3450
+ _LOGGER.info(f' Intentionally ignoring a notification of type \'{type}\')')
3451
+ else:
3452
+ _LOGGER.warning(f' Don\'t know what to do with a notification of type \'{type}\'. Please open an issue.)')
3453
+