pycupra 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pycupra/vehicle.py ADDED
@@ -0,0 +1,2556 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Vehicle class for pycupra."""
4
+ import re
5
+ import logging
6
+ import asyncio
7
+
8
+ from datetime import datetime, timedelta, timezone
9
+ from json import dumps as to_json
10
+ from collections import OrderedDict
11
+ from .utilities import find_path, is_valid_path
12
+ from .exceptions import (
13
+ SeatConfigException,
14
+ SeatException,
15
+ SeatEULAException,
16
+ SeatServiceUnavailable,
17
+ SeatThrottledException,
18
+ SeatInvalidRequestException,
19
+ SeatRequestInProgressException
20
+ )
21
+ from .const import (
22
+ APP_URI
23
+ )
24
+
25
+ _LOGGER = logging.getLogger(__name__)
26
+
27
+ DATEZERO = datetime(1970,1,1)
28
+ class Vehicle:
29
+ def __init__(self, conn, data):
30
+ _LOGGER.debug(f'Creating Vehicle class object with data {data}')
31
+ self._connection = conn
32
+ self._url = data.get('vin', '')
33
+ self._connectivities = data.get('connectivities', '')
34
+ self._capabilities = data.get('capabilities', [])
35
+ self._specification = data.get('specification', {})
36
+ self._properties = data.get('properties', {})
37
+ self._apibase = APP_URI
38
+ self._secbase = 'https://msg.volkswagen.de'
39
+ self._modelimages = None
40
+ self._discovered = False
41
+ self._dashboard = None
42
+ self._states = {}
43
+
44
+ self._requests = {
45
+ 'departuretimer': {'status': '', 'timestamp': DATEZERO},
46
+ 'batterycharge': {'status': '', 'timestamp': DATEZERO},
47
+ 'climatisation': {'status': '', 'timestamp': DATEZERO},
48
+ 'refresh': {'status': '', 'timestamp': DATEZERO},
49
+ 'lock': {'status': '', 'timestamp': DATEZERO},
50
+ 'honkandflash': {'status': '', 'timestamp': DATEZERO},
51
+ 'preheater': {'status': '', 'timestamp': DATEZERO},
52
+ 'remaining': -1,
53
+ 'latest': '',
54
+ 'state': ''
55
+ }
56
+ self._climate_duration = 30
57
+
58
+ self._relevantCapabilties = {
59
+ 'measurements': {'active': False, 'reason': 'not supported'},
60
+ 'climatisation': {'active': False, 'reason': 'not supported'},
61
+ #'parkingInformation': {'active': False, 'reason': 'not supported'},
62
+ 'tripStatistics': {'active': False, 'reason': 'not supported'},
63
+ 'vehicleHealthWarnings': {'active': False, 'reason': 'not supported'},
64
+ 'state': {'active': False, 'reason': 'not supported'},
65
+ 'charging': {'active': False, 'reason': 'not supported'},
66
+ 'honkAndFlash': {'active': False, 'reason': 'not supported'},
67
+ 'parkingPosition': {'active': False, 'reason': 'not supported'},
68
+ 'departureTimers': {'active': False, 'reason': 'not supported'},
69
+ 'transactionHistoryLockUnlock': {'active': False, 'reason': 'not supported'},
70
+ 'transactionHistoryHonkFlash': {'active': False, 'reason': 'not supported'},
71
+ }
72
+
73
+ #### API get and set functions ####
74
+ # Init and update vehicle data
75
+ async def discover(self):
76
+ """Discover vehicle and initial data."""
77
+ await asyncio.gather(
78
+ self.get_basiccardata(),
79
+ return_exceptions=True
80
+ )
81
+ # Extract information of relevant capabilities
82
+ for capa in self._capabilities:
83
+ id=capa.get('id', '')
84
+ if self._relevantCapabilties.get(id, False):
85
+ data={}
86
+ data['active']=capa.get('active', False)
87
+ if capa.get('user-enabled', False):
88
+ data['reason']='user-enabled'
89
+ else:
90
+ data['reason']=capa.get('user-enabled', False)
91
+ if capa.get('status', False):
92
+ data['reason']=capa.get('status', '')
93
+ self._relevantCapabilties[id].update(data)
94
+
95
+
96
+ # Get URLs for model image
97
+ self._modelimages = await self.get_modelimageurl()
98
+
99
+ self._discovered = datetime.now()
100
+
101
+ async def update(self):
102
+ """Try to fetch data for all known API endpoints."""
103
+ # Update vehicle information if not discovered or stale information
104
+ if not self._discovered:
105
+ await self.discover()
106
+ else:
107
+ # Rediscover if data is older than 1 hour
108
+ hourago = datetime.now() - timedelta(hours = 1)
109
+ if self._discovered < hourago:
110
+ #await self.discover()
111
+ _LOGGER.debug('Achtung! self.discover() auskommentiert')
112
+
113
+ # Fetch all data if car is not deactivated
114
+ if not self.deactivated:
115
+ try:
116
+ await asyncio.gather(
117
+ self.get_preheater(),
118
+ self.get_climater(),
119
+ self.get_trip_statistic(),
120
+ self.get_position(),
121
+ self.get_statusreport(),
122
+ self.get_charger(),
123
+ self.get_timerprogramming(),
124
+ self.get_basiccardata(),
125
+ self.get_modelimageurl(),
126
+ return_exceptions=True
127
+ )
128
+ except:
129
+ raise SeatException("Update failed")
130
+ return True
131
+ else:
132
+ _LOGGER.info(f'Vehicle with VIN {self.vin} is deactivated.')
133
+ return False
134
+ return True
135
+
136
+ # Data collection functions
137
+ async def get_modelimageurl(self):
138
+ """Fetch the URL for model image."""
139
+ return await self._connection.getModelImageURL(self.vin, self._apibase)
140
+
141
+ async def get_basiccardata(self):
142
+ """Fetch basic car data."""
143
+ data = await self._connection.getBasicCarData(self.vin, self._apibase)
144
+ if data:
145
+ self._states.update(data)
146
+
147
+ async def get_preheater(self):
148
+ """Fetch pre-heater data if function is enabled."""
149
+ _LOGGER.info('get_preheater() not implemented yet')
150
+ raise
151
+ if self._relevantCapabilties.get('#dont know the name for the preheater capability', {}).get('active', False):
152
+ if not await self.expired('rheating_v1'):
153
+ data = await self._connection.getPreHeater(self.vin, self._apibase)
154
+ if data:
155
+ self._states.update(data)
156
+ else:
157
+ _LOGGER.debug('Could not fetch preheater data')
158
+ else:
159
+ self._requests.pop('preheater', None)
160
+
161
+ async def get_climater(self):
162
+ """Fetch climater data if function is enabled."""
163
+ if self._relevantCapabilties.get('climatisation', {}).get('active', False):
164
+ data = await self._connection.getClimater(self.vin, self._apibase)
165
+ if data:
166
+ self._states.update(data)
167
+ else:
168
+ _LOGGER.debug('Could not fetch climater data')
169
+ else:
170
+ self._requests.pop('climatisation', None)
171
+
172
+ async def get_trip_statistic(self):
173
+ """Fetch trip data if function is enabled."""
174
+ if self._relevantCapabilties.get('tripStatistics', {}).get('active', False):
175
+ data = await self._connection.getTripStatistics(self.vin, self._apibase)
176
+ if data:
177
+ self._states.update(data)
178
+ else:
179
+ _LOGGER.debug('Could not fetch trip statistics')
180
+
181
+ async def get_position(self):
182
+ """Fetch position data if function is enabled."""
183
+ if self._relevantCapabilties.get('parkingPosition', {}).get('active', False):
184
+ data = await self._connection.getPosition(self.vin, self._apibase)
185
+ if data:
186
+ # Reset requests remaining to 15 if parking time has been updated
187
+ if data.get('findCarResponse', {}).get('parkingTimeUTC', False):
188
+ try:
189
+ newTime = data.get('findCarResponse').get('parkingTimeUTC')
190
+ oldTime = self.attrs.get('findCarResponse').get('parkingTimeUTC')
191
+ if newTime > oldTime:
192
+ self.requests_remaining = 15
193
+ except:
194
+ pass
195
+ self._states.update(data)
196
+ else:
197
+ _LOGGER.debug('Could not fetch any positional data')
198
+
199
+ async def get_statusreport(self):
200
+ """Fetch status data if function is enabled."""
201
+ if self._relevantCapabilties.get('state', {}).get('active', False):
202
+ data = await self._connection.getVehicleStatusReport(self.vin, self._apibase)
203
+ if data:
204
+ self._states.update(data)
205
+ else:
206
+ _LOGGER.debug('Could not fetch status report')
207
+ if self._relevantCapabilties.get('vehicleHealthWarnings', {}).get('active', False):
208
+ data = await self._connection.getMaintenance(self.vin, self._apibase)
209
+ if data:
210
+ self._states.update(data)
211
+ else:
212
+ _LOGGER.debug('Could not fetch status report')
213
+
214
+ async def get_charger(self):
215
+ """Fetch charger data if function is enabled."""
216
+ if self._relevantCapabilties.get('charging', {}).get('active', False):
217
+ data = await self._connection.getCharger(self.vin, self._apibase)
218
+ if data:
219
+ self._states.update(data)
220
+ else:
221
+ _LOGGER.debug('Could not fetch charger data')
222
+
223
+ async def get_timerprogramming(self):
224
+ """Fetch timer data if function is enabled."""
225
+ if self._relevantCapabilties.get('departureTimers', {}).get('active', False):
226
+ data = await self._connection.getDeparturetimer(self.vin, self._apibase)
227
+ if data:
228
+ self._states.update(data)
229
+ else:
230
+ _LOGGER.debug('Could not fetch timers')
231
+
232
+ #async def wait_for_request(self, section, request, retryCount=36):
233
+ """Update status of outstanding requests."""
234
+ """retryCount -= 1
235
+ if (retryCount == 0):
236
+ _LOGGER.info(f'Timeout while waiting for result of {request}.')
237
+ return 'Timeout'
238
+ try:
239
+ status = await self._connection.get_request_status(self.vin, section, request, self._apibase)
240
+ _LOGGER.info(f'Request for {section} with ID {request}: {status}')
241
+ if status == 'In progress':
242
+ self._requests['state'] = 'In progress'
243
+ await asyncio.sleep(5)
244
+ return await self.wait_for_request(section, request, retryCount)
245
+ else:
246
+ self._requests['state'] = status
247
+ return status
248
+ except Exception as error:
249
+ _LOGGER.warning(f'Exception encountered while waiting for request status: {error}')
250
+ return 'Exception'"""
251
+
252
+ # Data set functions
253
+ # API endpoint charging
254
+ async def set_charger_current(self, value):
255
+ """Set charger current"""
256
+ if self.is_charging_supported:
257
+ # Set charger max ampere to integer value
258
+ if isinstance(value, int):
259
+ if 1 <= int(value) <= 255:
260
+ # VW-Group API charger current request
261
+ if self._relevantCapabilties.get('charging', {}).get('active', False):
262
+ data = {'action': {'settings': {'maxChargeCurrentAC': int(value)}, 'type': 'setSettings'}}
263
+ else:
264
+ _LOGGER.error(f'Set charger maximum current to {value} is not supported.')
265
+ raise SeatInvalidRequestException(f'Set charger maximum current to {value} is not supported.')
266
+ # Mimick app and set charger max ampere to Maximum/Reduced
267
+ elif isinstance(value, str):
268
+ if value in ['Maximum', 'maximum', 'Max', 'max', 'Minimum', 'minimum', 'Min', 'min', 'Reduced', 'reduced']:
269
+ # VW-Group API charger current request
270
+ if self._relevantCapabilties.get('charging', {}).get('active', False):
271
+ value = 'maximum' if value in ['Maximum', 'maximum', 'Max', 'max'] else 'reduced'
272
+ data = {'settings':
273
+ {'maxChargeCurrentAC': value}
274
+ }
275
+ else:
276
+ _LOGGER.error(f'Set charger maximum current to {value} is not supported.')
277
+ raise SeatInvalidRequestException(f'Set charger maximum current to {value} is not supported.')
278
+ else:
279
+ _LOGGER.error(f'Data type passed is invalid.')
280
+ raise SeatInvalidRequestException(f'Invalid data type.')
281
+ return await self.set_charger('settings', data)
282
+ else:
283
+ _LOGGER.error('No charger support.')
284
+ raise SeatInvalidRequestException('No charger support.')
285
+
286
+ async def set_charger(self, action, **data):
287
+ """Charging actions."""
288
+ if not self._relevantCapabilties.get('charging', {}).get('active', False):
289
+ _LOGGER.info('Remote start/stop of charger is not supported.')
290
+ raise SeatInvalidRequestException('Remote start/stop of charger is not supported.')
291
+ if self._requests['batterycharge'].get('id', False):
292
+ timestamp = self._requests.get('batterycharge', {}).get('timestamp', datetime.now())
293
+ expired = datetime.now() - timedelta(minutes=3)
294
+ if expired > timestamp:
295
+ self._requests.get('batterycharge', {}).pop('id')
296
+ else:
297
+ raise SeatRequestInProgressException('Charging action already in progress')
298
+ if self._relevantCapabilties.get('charging', {}).get('active', False):
299
+ if action in ['start', 'Start', 'On', 'on']:
300
+ mode='start'
301
+ elif action in ['stop', 'Stop', 'Off', 'off']:
302
+ mode='stop'
303
+ elif isinstance(action.get('action', None), dict):
304
+ mode=action
305
+ else:
306
+ _LOGGER.error(f'Invalid charger action: {action}. Must be either start, stop or setSettings')
307
+ raise SeatInvalidRequestException(f'Invalid charger action: {action}. Must be either start, stop or setSettings')
308
+ try:
309
+ self._requests['latest'] = 'Charger'
310
+ response = await self._connection.setCharger(self.vin, self._apibase, mode, data)
311
+ if not response:
312
+ self._requests['batterycharge'] = {'status': 'Failed'}
313
+ _LOGGER.error(f'Failed to {action} charging')
314
+ raise SeatException(f'Failed to {action} charging')
315
+ else:
316
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
317
+ self._requests['batterycharge'] = {
318
+ 'timestamp': datetime.now(),
319
+ 'status': response.get('state', 'Unknown'),
320
+ 'id': response.get('id', 0)
321
+ }
322
+ # Update the charger data and check, if they have changed as expected
323
+ retry = 0
324
+ actionSuccessful = False
325
+ while not actionSuccessful and retry < 3:
326
+ await asyncio.sleep(5)
327
+ await self.get_charger()
328
+ if mode == 'start':
329
+ if self.charging:
330
+ actionSuccessful = True
331
+ elif mode == 'stop':
332
+ if not self.charging:
333
+ actionSuccessful = True
334
+ elif mode == 'settings':
335
+ if data.get('settings',0).get('maxChargeCurrentAC','') == self.charge_max_ampere:
336
+ actionSuccessful = True
337
+ else:
338
+ _LOGGER.error(f'Missing code in vehicle._set_charger() for mode {mode}')
339
+ raise
340
+ retry = retry +1
341
+ if actionSuccessful:
342
+ self._requests.get('batterycharge', {}).pop('id')
343
+ return True
344
+ _LOGGER.error('Response to POST request seemed successful but the charging status did not change as expected.')
345
+ return False
346
+ except (SeatInvalidRequestException, SeatException):
347
+ raise
348
+ except Exception as error:
349
+ _LOGGER.warning(f'Failed to {action} charging - {error}')
350
+ self._requests['batterycharge'] = {'status': 'Exception'}
351
+ raise SeatException(f'Failed to execute set charger - {error}')
352
+
353
+ # API endpoint departuretimer
354
+ async def set_charge_limit(self, limit=50):
355
+ """ Set charging limit. """
356
+ if not self._relevantCapabilties.get('departureTimers', {}).get('active', False) and not self._relevantCapabilties.get('charging', {}).get('active', False):
357
+ _LOGGER.info('Set charging limit is not supported.')
358
+ raise SeatInvalidRequestException('Set charging limit is not supported.')
359
+ data = {}
360
+ if self._relevantCapabilties.get('departureTimers', {}).get('active', False):
361
+ if isinstance(limit, int):
362
+ if limit in [0, 10, 20, 30, 40, 50]:
363
+ data['minSocPercentage'] = limit
364
+ else:
365
+ raise SeatInvalidRequestException(f'Charge limit must be one of 0, 10, 20, 30, 40 or 50.')
366
+ else:
367
+ raise SeatInvalidRequestException(f'Charge limit "{limit}" is not supported.')
368
+ return await self._set_timers(data)
369
+
370
+ async def set_timer_active(self, id=1, action='off'):
371
+ """ Activate/deactivate departure timers. """
372
+ data = {}
373
+ supported = 'is_departure' + str(id) + "_supported"
374
+ if getattr(self, supported) is not True:
375
+ raise SeatConfigException(f'This vehicle does not support timer id "{id}".')
376
+ if self._relevantCapabilties.get('departureTimers', {}).get('active', False):
377
+ allTimers= self.attrs.get('departureTimers').get('timers', [])
378
+ for singleTimer in allTimers:
379
+ if singleTimer.get('id',-1)==id:
380
+ if action in ['on', 'off']:
381
+ if action=='on':
382
+ enabled=True
383
+ else:
384
+ enabled=False
385
+ singleTimer['enabled'] = enabled
386
+ data = {
387
+ 'timers' : []
388
+ }
389
+ data['timers'].append(singleTimer)
390
+ else:
391
+ raise SeatInvalidRequestException(f'Timer action "{action}" is not supported.')
392
+ return await self._set_timers(data)
393
+ raise SeatInvalidRequestException(f'Departure timers id {id} not found.')
394
+ else:
395
+ raise SeatInvalidRequestException('Departure timers are not supported.')
396
+
397
+ async def set_timer_schedule(self, id, schedule={}):
398
+ """ Set departure schedules. """
399
+ data = {}
400
+ # Validate required user inputs
401
+ supported = 'is_departure' + str(id) + "_supported"
402
+ if getattr(self, supported) is not True:
403
+ raise SeatConfigException(f'Timer id "{id}" is not supported for this vehicle.')
404
+ else:
405
+ _LOGGER.debug(f'Timer id {id} is supported')
406
+ if not schedule:
407
+ raise SeatInvalidRequestException('A schedule must be set.')
408
+ if not isinstance(schedule.get('enabled', ''), bool):
409
+ raise SeatInvalidRequestException('The enabled variable must be set to True or False.')
410
+ if not isinstance(schedule.get('recurring', ''), bool):
411
+ raise SeatInvalidRequestException('The recurring variable must be set to True or False.')
412
+ if not re.match('^[0-9]{2}:[0-9]{2}$', schedule.get('time', '')):
413
+ raise SeatInvalidRequestException('The time for departure must be set in 24h format HH:MM.')
414
+
415
+ # Validate optional inputs
416
+ if schedule.get('recurring', False):
417
+ if not re.match('^[yn]{7}$', schedule.get('days', '')):
418
+ raise SeatInvalidRequestException('For recurring schedules the days variable must be set to y/n mask (mon-sun with only wed enabled): nnynnnn.')
419
+ elif not schedule.get('recurring'):
420
+ if not re.match('^[0-9]{4}-[0-9]{2}-[0-9]{2}$', schedule.get('date', '')):
421
+ raise SeatInvalidRequestException('For single departure schedule the date variable must be set to YYYY-mm-dd.')
422
+
423
+ if self._relevantCapabilties.get('departureTimers', {}).get('active', False):
424
+ # Sanity check for off-peak hours
425
+ if not isinstance(schedule.get('nightRateActive', False), bool):
426
+ raise SeatInvalidRequestException('The off-peak active variable must be set to True or False')
427
+ if schedule.get('nightRateStart', None) is not None:
428
+ if not re.match('^[0-9]{2}:[0-9]{2}$', schedule.get('nightRateStart', '')):
429
+ raise SeatInvalidRequestException('The start time for off-peak hours must be set in 24h format HH:MM.')
430
+ if schedule.get('nightRateEnd', None) is not None:
431
+ if not re.match('^[0-9]{2}:[0-9]{2}$', schedule.get('nightRateEnd', '')):
432
+ raise SeatInvalidRequestException('The start time for off-peak hours must be set in 24h format HH:MM.')
433
+
434
+ # Check if charging/climatisation is set and correct
435
+ if not isinstance(schedule.get('operationClimatisation', False), bool):
436
+ raise SeatInvalidRequestException('The climatisation enable variable must be set to True or False')
437
+ if not isinstance(schedule.get('operationCharging', False), bool):
438
+ raise SeatInvalidRequestException('The charging variable must be set to True or False')
439
+
440
+ # Validate temp setting, if set
441
+ if schedule.get("targetTemp", None) is not None:
442
+ if not 16 <= int(schedule.get("targetTemp", None)) <= 30:
443
+ raise SeatInvalidRequestException('Target temp must be integer value from 16 to 30')
444
+ else:
445
+ data['temp'] = schedule.get('targetTemp')
446
+ raise SeatInvalidRequestException('Target temp (yet) not supported.')
447
+
448
+ # Validate charge target and current
449
+ if schedule.get("targetChargeLevel", None) is not None:
450
+ if not 0 <= int(schedule.get("targetChargeLevel", None)) <= 100:
451
+ raise SeatInvalidRequestException('Target charge level must be 0 to 100')
452
+ else:
453
+ raise SeatInvalidRequestException('targetChargeLevel (yet) not supported.')
454
+ if schedule.get("chargeMaxCurrent", None) is not None:
455
+ raise SeatInvalidRequestException('chargeMaxCurrent (yet) not supported.')
456
+ if isinstance(schedule.get('chargeMaxCurrent', None), str):
457
+ if not schedule.get("chargeMaxCurrent", None) in ['Maximum', 'maximum', 'Max', 'max', 'Minimum', 'minimum', 'Min', 'min', 'Reduced', 'reduced']:
458
+ raise SeatInvalidRequestException('Charge current must be one of Maximum/Minimum/Reduced')
459
+ elif isinstance(schedule.get('chargeMaxCurrent', None), int):
460
+ if not 1 <= int(schedule.get("chargeMaxCurrent", 254)) < 255:
461
+ raise SeatInvalidRequestException('Charge current must be set from 1 to 254')
462
+ else:
463
+ raise SeatInvalidRequestException('Invalid type for charge max current variable')
464
+
465
+ # Converting schedule to data map
466
+ if schedule.get("enabled",False):
467
+ data['enabled']=True
468
+ else:
469
+ data['enabled']=False
470
+ if schedule.get("operationCharging",False):
471
+ data['charging']=True
472
+ else:
473
+ data['charging']=False
474
+ if schedule.get("operationClimatisation",False):
475
+ data['climatisation']=True
476
+ else:
477
+ data['climatisation']=False
478
+ if schedule.get("nightRateAcvtive", False):
479
+ preferedChargingTimes= [{
480
+ "id" : 1,
481
+ "enabled" : True,
482
+ "startTimeLocal" : schedule.get('nightRateStart',"00:00"),
483
+ "endTimeLocal" : schedule.get('nightRateStop',"00:00")
484
+ }]
485
+ else:
486
+ preferedChargingTimes= [{
487
+ "id" : 1,
488
+ "enabled" : True,
489
+ "startTimeLocal" : "00:00",
490
+ "endTimeLocal" : "00:00"
491
+ }]
492
+ if schedule.get("recurring",False):
493
+ data['recurringTimer']= {
494
+ "startTimeLocal": schedule.get('time',"00:00"),
495
+ "recurringOn":{""
496
+ "mondays":(schedule.get('days',"nnnnnnn")[0]=='y'),
497
+ "tuesdays":(schedule.get('days',"nnnnnnn")[1]=='y'),
498
+ "wednesdays":(schedule.get('days',"nnnnnnn")[2]=='y'),
499
+ "thursdays":(schedule.get('days',"nnnnnnn")[3]=='y'),
500
+ "fridays":(schedule.get('days',"nnnnnnn")[4]=='y'),
501
+ "saturdays":(schedule.get('days',"nnnnnnn")[5]=='y'),
502
+ "sundays":(schedule.get('days',"nnnnnnn")[6]=='y'),
503
+ "preferredChargingTimes": preferedChargingTimes
504
+ }
505
+ }
506
+ else:
507
+ startDateTime = datetime.fromisoformat(schedule.get('date',"2025-01-01")+'T'+schedule.get('time',"00:00"))
508
+ _LOGGER.info(f'startDateTime={startDateTime.isoformat()}')
509
+ data['singleTimer']= {
510
+ "startDateTimeLocal": startDateTime.isoformat(),
511
+ "preferredChargingTimes": preferedChargingTimes
512
+ }
513
+
514
+ # Prepare data and execute
515
+ data['id'] = id
516
+ # Now we have to embed the data for the timer 'id' in timers[]
517
+ data={
518
+ 'timers' : [data]
519
+ }
520
+ return await self._set_timers(data)
521
+ else:
522
+ _LOGGER.info('Departure timers are not supported.')
523
+ raise SeatInvalidRequestException('Departure timers are not supported.')
524
+
525
+ async def _set_timers(self, data=None):
526
+ """ Set departure timers. """
527
+ if not self._relevantCapabilties.get('departureTimers', {}).get('active', False):
528
+ raise SeatInvalidRequestException('Departure timers are not supported.')
529
+ if self._requests['departuretimer'].get('id', False):
530
+ timestamp = self._requests.get('departuretimer', {}).get('timestamp', datetime.now())
531
+ expired = datetime.now() - timedelta(minutes=3)
532
+ if expired > timestamp:
533
+ self._requests.get('departuretimer', {}).pop('id')
534
+ else:
535
+ raise SeatRequestInProgressException('Scheduling of departure timer is already in progress')
536
+
537
+ try:
538
+ self._requests['latest'] = 'Departuretimer'
539
+ response = await self._connection.setDeparturetimer(self.vin, self._apibase, data, spin=False)
540
+ if not response:
541
+ self._requests['departuretimer'] = {'status': 'Failed'}
542
+ _LOGGER.error('Failed to execute departure timer request')
543
+ raise SeatException('Failed to execute departure timer request')
544
+ else:
545
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
546
+ self._requests['departuretimer'] = {
547
+ 'timestamp': datetime.now(),
548
+ 'status': response.get('state', 'Unknown'),
549
+ 'id': response.get('id', 0),
550
+ }
551
+ # Update the departure timers data and check, if they have changed as expected
552
+ retry = 0
553
+ actionSuccessful = False
554
+ while not actionSuccessful and retry < 3:
555
+ await asyncio.sleep(5)
556
+ await self.get_timerprogramming()
557
+ if data.get('minSocPercentage',False):
558
+ if data.get('minSocPercentage',-2)==self.attrs.get('departureTimers',{}).get('minSocPercentage',-1):
559
+ actionSuccessful=True
560
+ else:
561
+ timerData = data.get('timers',[])[0]
562
+ timerDataId = timerData.get('id',False)
563
+ if timerDataId:
564
+ newTimers = self.attrs.get('departureTimers',{}).get('timers',[])
565
+ for newTimer in newTimers:
566
+ if newTimer.get('id',-1)==timerDataId:
567
+ if timerData==newTimer:
568
+ actionSuccessful=True
569
+ break
570
+ retry = retry +1
571
+ if actionSuccessful:
572
+ self._requests.get('departuretimer', {}).pop('id')
573
+ return True
574
+ _LOGGER.error('Response to POST request seemed successful but the departure timers status did not change as expected.')
575
+ return False
576
+ except (SeatInvalidRequestException, SeatException):
577
+ raise
578
+ except Exception as error:
579
+ _LOGGER.warning(f'Failed to execute departure timer request - {error}')
580
+ self._requests['departuretimer'] = {'status': 'Exception'}
581
+ raise SeatException('Failed to set departure timer schedule')
582
+
583
+ # Send a destination to vehicle
584
+ async def send_destination(self, destination=None):
585
+ """ Send destination to vehicle. """
586
+
587
+ if destination==None:
588
+ _LOGGER.error('No destination provided')
589
+ raise
590
+ else:
591
+ data=[]
592
+ data.append(destination)
593
+ try:
594
+ response = await self._connection.sendDestination(self.vin, self._apibase, data, spin=False)
595
+ if not response:
596
+ _LOGGER.error('Failed to execute send destination request')
597
+ raise SeatException('Failed to execute send destination request')
598
+ else:
599
+ return True
600
+ except (SeatInvalidRequestException, SeatException):
601
+ raise
602
+ except Exception as error:
603
+ _LOGGER.warning(f'Failed to execute send destination request - {error}')
604
+ raise SeatException('Failed to send destination to vehicle')
605
+
606
+ # Climatisation electric/auxiliary/windows (CLIMATISATION)
607
+ async def set_climatisation_temp(self, temperature=20):
608
+ """Set climatisation target temp."""
609
+ if self.is_electric_climatisation_supported or self.is_auxiliary_climatisation_supported:
610
+ if 16 <= int(temperature) <= 30:
611
+ data = {
612
+ 'climatisationWithoutExternalPower': self.climatisation_without_external_power,
613
+ 'targetTemperature': temperature,
614
+ 'targetTemperatureUnit': 'celsius'
615
+ }
616
+ mode = 'settings'
617
+ else:
618
+ _LOGGER.error(f'Set climatisation target temp to {temperature} is not supported.')
619
+ raise SeatInvalidRequestException(f'Set climatisation target temp to {temperature} is not supported.')
620
+ return await self._set_climater(mode, data)
621
+ else:
622
+ _LOGGER.error('No climatisation support.')
623
+ raise SeatInvalidRequestException('No climatisation support.')
624
+
625
+ async def set_window_heating(self, action = 'stop'):
626
+ """Turn on/off window heater."""
627
+ if self.is_window_heater_supported:
628
+ if action in ['start', 'stop']:
629
+ data = {'action': {'type': action + 'WindowHeating'}}
630
+ else:
631
+ _LOGGER.error(f'Window heater action "{action}" is not supported.')
632
+ raise SeatInvalidRequestException(f'Window heater action "{action}" is not supported.')
633
+ return await self._set_climater(f'windowHeater {action}', data)
634
+ else:
635
+ _LOGGER.error('No climatisation support.')
636
+ raise SeatInvalidRequestException('No climatisation support.')
637
+
638
+ async def set_battery_climatisation(self, mode = False):
639
+ """Turn on/off electric climatisation from battery."""
640
+ if self.is_electric_climatisation_supported:
641
+ if mode in [True, False]:
642
+ data = {
643
+ 'climatisationWithoutExternalPower': mode,
644
+ 'targetTemperature': self.climatisation_target_temperature, #keep the current target temperature
645
+ 'targetTemperatureUnit': 'celsius'
646
+ }
647
+ mode = 'settings'
648
+ else:
649
+ _LOGGER.error(f'Set climatisation without external power to "{mode}" is not supported.')
650
+ raise SeatInvalidRequestException(f'Set climatisation without external power to "{mode}" is not supported.')
651
+ return await self._set_climater(mode, data)
652
+ else:
653
+ _LOGGER.error('No climatisation support.')
654
+ raise SeatInvalidRequestException('No climatisation support.')
655
+
656
+ async def set_climatisation(self, mode = 'off', temp = None, hvpower = None, spin = None):
657
+ """Turn on/off climatisation with electric/auxiliary heater."""
658
+ data = {}
659
+ # Validate user input
660
+ if mode.lower() not in ['electric', 'auxiliary', 'start', 'stop', 'on', 'off']:
661
+ raise SeatInvalidRequestException(f"Invalid mode for set_climatisation: {mode}")
662
+ elif mode == 'auxiliary' and spin is None:
663
+ raise SeatInvalidRequestException("Starting auxiliary heater requires provided S-PIN")
664
+ if temp is not None:
665
+ if not isinstance(temp, float):
666
+ raise SeatInvalidRequestException(f"Invalid type for temp")
667
+ elif not 16 <= float(temp) <=30:
668
+ raise SeatInvalidRequestException(f"Invalid value for temp")
669
+ else:
670
+ temp = self.climatisation_target_temperature
671
+ if hvpower is not None:
672
+ if not isinstance(hvpower, bool):
673
+ raise SeatInvalidRequestException(f"Invalid type for hvpower")
674
+ if self.is_electric_climatisation_supported:
675
+ if self._relevantCapabilties.get('climatisation', {}).get('active', False):
676
+ if mode in ['Start', 'start', 'Electric', 'electric', 'On', 'on']:
677
+ mode = 'start'
678
+ if mode in ['start', 'auxiliary']:
679
+ if hvpower is not None:
680
+ withoutHVPower = hvpower
681
+ else:
682
+ withoutHVPower = self.climatisation_without_external_power
683
+ data = {
684
+ 'targetTemperature': temp,
685
+ 'targetTemperatureUnit': 'celsius',
686
+ }
687
+ return await self._set_climater(mode, data, spin)
688
+ else:
689
+ if self._requests['climatisation'].get('id', False) or self.electric_climatisation:
690
+ request_id=self._requests.get('climatisation', 0)
691
+ mode = 'stop'
692
+ data={}
693
+ return await self._set_climater(mode, data, spin)
694
+ else:
695
+ _LOGGER.error('Can not stop climatisation because no running request was found')
696
+ return None
697
+ else:
698
+ _LOGGER.error('No climatisation support.')
699
+ raise SeatInvalidRequestException('No climatisation support.')
700
+
701
+ async def _set_climater(self, mode, data, spin = False):
702
+ """Climater actions."""
703
+ if not self._relevantCapabilties.get('climatisation', {}).get('active', False):
704
+ _LOGGER.info('Remote control of climatisation functions is not supported.')
705
+ raise SeatInvalidRequestException('Remote control of climatisation functions is not supported.')
706
+ if self._requests['climatisation'].get('id', False):
707
+ timestamp = self._requests.get('climatisation', {}).get('timestamp', datetime.now())
708
+ expired = datetime.now() - timedelta(minutes=3)
709
+ if expired > timestamp:
710
+ self._requests.get('climatisation', {}).pop('id')
711
+ else:
712
+ raise SeatRequestInProgressException('A climatisation action is already in progress')
713
+ try:
714
+ self._requests['latest'] = 'Climatisation'
715
+ response = await self._connection.setClimater(self.vin, self._apibase, mode, data, spin)
716
+ if not response:
717
+ self._requests['climatisation'] = {'status': 'Failed'}
718
+ _LOGGER.error('Failed to execute climatisation request')
719
+ raise SeatException('Failed to execute climatisation request')
720
+ else:
721
+ #self._requests['remaining'] = response.get('rate_limit_remaining', -1)
722
+ self._requests['climatisation'] = {
723
+ 'timestamp': datetime.now(),
724
+ 'status': response.get('state', 'Unknown'),
725
+ 'id': response.get('id', 0),
726
+ }
727
+ # Update the climater data and check, if they have changed as expected
728
+ retry = 0
729
+ actionSuccessful = False
730
+ while not actionSuccessful and retry < 3:
731
+ await asyncio.sleep(5)
732
+ await self.get_climater()
733
+ if mode == 'start':
734
+ if self.electric_climatisation:
735
+ actionSuccessful = True
736
+ elif mode == 'stop':
737
+ if not self.electric_climatisation:
738
+ actionSuccessful = True
739
+ elif mode == 'settings':
740
+ if data.get('targetTemperature',0)== self.climatisation_target_temperature and data.get('climatisationWithoutExternalPower',False)== self.climatisation_without_external_power:
741
+ actionSuccessful = True
742
+ elif mode == 'windowHeater start':
743
+ if self.window_heater:
744
+ actionSuccessful = True
745
+ elif mode == 'windowHeater stop':
746
+ if not self.window_heater:
747
+ actionSuccessful = True
748
+ else:
749
+ _LOGGER.error(f'Missing code in vehicle._set_climater() for mode {mode}')
750
+ raise
751
+ retry = retry +1
752
+ if actionSuccessful:
753
+ self._requests.get('climatisation', {}).pop('id')
754
+ return True
755
+ _LOGGER.error('Response to POST request seemed successful but the climater status did not change as expected.')
756
+ return False
757
+ except (SeatInvalidRequestException, SeatException):
758
+ raise
759
+ except Exception as error:
760
+ _LOGGER.warning(f'Failed to execute climatisation request - {error}')
761
+ self._requests['climatisation'] = {'status': 'Exception'}
762
+ raise SeatException('Climatisation action failed')
763
+
764
+ # Parking heater heating/ventilation (RS)
765
+ async def set_pheater(self, mode, spin):
766
+ """Set the mode for the parking heater."""
767
+ if not self.is_pheater_heating_supported:
768
+ _LOGGER.error('No parking heater support.')
769
+ raise SeatInvalidRequestException('No parking heater support.')
770
+ if self._requests['preheater'].get('id', False):
771
+ timestamp = self._requests.get('preheater', {}).get('timestamp', datetime.now())
772
+ expired = datetime.now() - timedelta(minutes=3)
773
+ if expired > timestamp:
774
+ self._requests.get('preheater', {}).pop('id')
775
+ else:
776
+ raise SeatRequestInProgressException('A parking heater action is already in progress')
777
+ if not mode in ['heating', 'ventilation', 'off']:
778
+ _LOGGER.error(f'{mode} is an invalid action for parking heater')
779
+ raise SeatInvalidRequestException(f'{mode} is an invalid action for parking heater')
780
+ if mode == 'off':
781
+ data = {'performAction': {'quickstop': {'active': False }}}
782
+ else:
783
+ data = {'performAction': {'quickstart': {'climatisationDuration': self.pheater_duration, 'startMode': mode, 'active': True }}}
784
+ try:
785
+ self._requests['latest'] = 'Preheater'
786
+ _LOGGER.debug(f'Executing setPreHeater with data: {data}')
787
+ response = await self._connection.setPreHeater(self.vin, self._apibase, data, spin)
788
+ if not response:
789
+ self._requests['preheater'] = {'status': 'Failed'}
790
+ _LOGGER.error(f'Failed to set parking heater to {mode}')
791
+ raise SeatException(f'setPreHeater returned "{response}"')
792
+ else:
793
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
794
+ self._requests['preheater'] = {
795
+ 'timestamp': datetime.now(),
796
+ 'status': response.get('state', 'Unknown'),
797
+ 'id': response.get('id', 0),
798
+ }
799
+ return True
800
+ except (SeatInvalidRequestException, SeatException):
801
+ raise
802
+ except Exception as error:
803
+ _LOGGER.warning(f'Failed to set parking heater mode to {mode} - {error}')
804
+ self._requests['preheater'] = {'status': 'Exception'}
805
+ raise SeatException('Pre-heater action failed')
806
+
807
+ # Lock
808
+ async def set_lock(self, action, spin):
809
+ """Remote lock and unlock actions."""
810
+ #if not self._services.get('rlu_v1', False):
811
+ if not self._relevantCapabilties.get('transactionHistoryLockUnlock', {}).get('active', False):
812
+ _LOGGER.info('Remote lock/unlock is not supported.')
813
+ raise SeatInvalidRequestException('Remote lock/unlock is not supported.')
814
+ if self._requests['lock'].get('id', False):
815
+ timestamp = self._requests.get('lock', {}).get('timestamp', datetime.now() - timedelta(minutes=5))
816
+ expired = datetime.now() - timedelta(minutes=1)
817
+ if expired > timestamp:
818
+ self._requests.get('lock', {}).pop('id')
819
+ else:
820
+ raise SeatRequestInProgressException('A lock action is already in progress')
821
+ if action not in ['lock', 'unlock']:
822
+ _LOGGER.error(f'Invalid lock action: {action}')
823
+ raise SeatInvalidRequestException(f'Invalid lock action: {action}')
824
+ try:
825
+ self._requests['latest'] = 'Lock'
826
+ data = {}
827
+ response = await self._connection.setLock(self.vin, self._apibase, action, data, spin)
828
+ if not response:
829
+ self._requests['lock'] = {'status': 'Failed'}
830
+ _LOGGER.error(f'Failed to {action} vehicle')
831
+ raise SeatException(f'Failed to {action} vehicle')
832
+ else:
833
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
834
+ self._requests['lock'] = {
835
+ 'timestamp': datetime.now(),
836
+ 'status': response.get('state', 'Unknown'),
837
+ 'id': response.get('id', 0),
838
+ }
839
+ return True
840
+ except (SeatInvalidRequestException, SeatException):
841
+ raise
842
+ except Exception as error:
843
+ _LOGGER.warning(f'Failed to {action} vehicle - {error}')
844
+ self._requests['lock'] = {'status': 'Exception'}
845
+ raise SeatException('Lock action failed')
846
+
847
+ # Honk and flash (RHF)
848
+ async def set_honkandflash(self, action, lat=None, lng=None):
849
+ """Turn on/off honk and flash."""
850
+ if not self._relevantCapabilties.get('honkAndFlash', {}).get('active', False):
851
+ _LOGGER.info('Remote honk and flash is not supported.')
852
+ raise SeatInvalidRequestException('Remote honk and flash is not supported.')
853
+ if self._requests['honkandflash'].get('id', False):
854
+ timestamp = self._requests.get('honkandflash', {}).get('timestamp', datetime.now() - timedelta(minutes=5))
855
+ expired = datetime.now() - timedelta(minutes=1)
856
+ if expired > timestamp:
857
+ self._requests.get('honkandflash', {}).pop('id')
858
+ else:
859
+ raise SeatRequestInProgressException('A honk and flash action is already in progress')
860
+ if action == 'flash':
861
+ operationCode = 'flash'
862
+ elif action == 'honkandflash':
863
+ operationCode = 'honkandflash'
864
+ else:
865
+ raise SeatInvalidRequestException(f'Invalid action "{action}", must be one of "flash" or "honkandflash"')
866
+ try:
867
+ # Get car position
868
+ if lat is None:
869
+ lat = self.attrs.get('findCarResponse', {}).get('lat', None)
870
+ if lng is None:
871
+ lng = self.attrs.get('findCarResponse', {}).get('lon', None)
872
+ if lat is None or lng is None:
873
+ raise SeatConfigException('No location available, location information is needed for this action')
874
+ lat = int(lat*10000.0)/10000.0
875
+ lng = int(lng*10000.0)/10000.0
876
+ data = {
877
+ 'mode': operationCode,
878
+ 'userPosition': {
879
+ 'latitude': lat,
880
+ 'longitude': lng
881
+ }
882
+ }
883
+ self._requests['latest'] = 'HonkAndFlash'
884
+ response = await self._connection.setHonkAndFlash(self.vin, self._apibase, data)
885
+ if not response:
886
+ self._requests['honkandflash'] = {'status': 'Failed'}
887
+ _LOGGER.error(f'Failed to execute honk and flash action')
888
+ raise SeatException(f'Failed to execute honk and flash action')
889
+ else:
890
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
891
+ self._requests['honkandflash'] = {
892
+ 'timestamp': datetime.now(),
893
+ 'status': response.get('state', 'Unknown'),
894
+ 'id': response.get('id', 0),
895
+ }
896
+ return True
897
+ except (SeatInvalidRequestException, SeatException):
898
+ raise
899
+ except Exception as error:
900
+ _LOGGER.warning(f'Failed to {action} vehicle - {error}')
901
+ self._requests['honkandflash'] = {'status': 'Exception'}
902
+ raise SeatException('Honk and flash action failed')
903
+
904
+ # Refresh vehicle data (VSR)
905
+ async def set_refresh(self):
906
+ """Wake up vehicle and update status data."""
907
+ if not self._relevantCapabilties.get('state', {}).get('active', False):
908
+ _LOGGER.info('Data refresh is not supported.')
909
+ raise SeatInvalidRequestException('Data refresh is not supported.')
910
+ if self._requests['refresh'].get('id', False):
911
+ timestamp = self._requests.get('refresh', {}).get('timestamp', datetime.now() - timedelta(minutes=5))
912
+ expired = datetime.now() - timedelta(minutes=3)
913
+ if expired > timestamp:
914
+ self._requests.get('refresh', {}).pop('id')
915
+ else:
916
+ raise SeatRequestInProgressException('A data refresh request is already in progress')
917
+ try:
918
+ self._requests['latest'] = 'Refresh'
919
+ response = await self._connection.setRefresh(self.vin, self._apibase)
920
+ if not response:
921
+ _LOGGER.error('Failed to request vehicle update')
922
+ self._requests['refresh'] = {'status': 'Failed'}
923
+ raise SeatException('Failed to execute data refresh')
924
+ else:
925
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
926
+ self._requests['refresh'] = {
927
+ 'timestamp': datetime.now(),
928
+ 'status': response.get('status', 'Unknown'),
929
+ 'id': response.get('id', 0)
930
+ }
931
+ return True
932
+ except(SeatInvalidRequestException, SeatException):
933
+ raise
934
+ except Exception as error:
935
+ _LOGGER.warning(f'Failed to execute data refresh - {error}')
936
+ self._requests['refresh'] = {'status': 'Exception'}
937
+ raise SeatException('Data refresh failed')
938
+
939
+ #### Vehicle class helpers ####
940
+ # Vehicle info
941
+ @property
942
+ def attrs(self):
943
+ return self._states
944
+
945
+ def has_attr(self, attr):
946
+ return is_valid_path(self.attrs, attr)
947
+
948
+ def get_attr(self, attr):
949
+ return find_path(self.attrs, attr)
950
+
951
+ def dashboard(self, **config):
952
+ """Returns dashboard, creates new if none exist."""
953
+ if self._dashboard is None:
954
+ # Init new dashboard if none exist
955
+ from .dashboard import Dashboard
956
+ self._dashboard = Dashboard(self, **config)
957
+ elif config != self._dashboard._config:
958
+ # Init new dashboard on config change
959
+ from .dashboard import Dashboard
960
+ self._dashboard = Dashboard(self, **config)
961
+ return self._dashboard
962
+
963
+ @property
964
+ def vin(self):
965
+ return self._url
966
+
967
+ @property
968
+ def unique_id(self):
969
+ return self.vin
970
+
971
+
972
+ #### Information from vehicle states ####
973
+ # Car information
974
+ @property
975
+ def nickname(self):
976
+ return self._properties.get('vehicleNickname', '')
977
+
978
+ @property
979
+ def is_nickname_supported(self):
980
+ if self._properties.get('vehicleNickname', False):
981
+ return True
982
+
983
+ @property
984
+ def deactivated(self):
985
+ if 'mode' in self._connectivities:
986
+ if self._connectivities.get('mode','')=='online':
987
+ return False
988
+ return True
989
+ #for car in self.attrs.get('realCars', []):
990
+ # if self.vin == car.get('vehicleIdentificationNumber', ''):
991
+ # return car.get('deactivated', False)
992
+
993
+ @property
994
+ def is_deactivated_supported(self):
995
+ if 'mode' in self._connectivities:
996
+ return True
997
+ return False
998
+ #for car in self.attrs.get('realCars', []):
999
+ # if self.vin == car.get('vehicleIdentificationNumber', ''):
1000
+ # if car.get('deactivated', False):
1001
+ # return True
1002
+
1003
+ @property
1004
+ def model(self):
1005
+ """Return model"""
1006
+ if self._specification.get('carBody', False):
1007
+ model = self._specification.get('factoryModel', False).get('vehicleModel', 'Unknown') + ' ' + self._specification.get('carBody', '')
1008
+ return model
1009
+ return self._specification.get('factoryModel', False).get('vehicleModel', 'Unknown')
1010
+
1011
+ @property
1012
+ def is_model_supported(self):
1013
+ """Return true if model is supported."""
1014
+ if self._specification.get('factoryModel', False).get('vehicleModel', False):
1015
+ return True
1016
+
1017
+ @property
1018
+ def model_year(self):
1019
+ """Return model year"""
1020
+ return self._specification.get('factoryModel', False).get('modYear', 'Unknown')
1021
+
1022
+ @property
1023
+ def is_model_year_supported(self):
1024
+ """Return true if model year is supported."""
1025
+ if self._specification.get('factoryModel', False).get('modYear', False):
1026
+ return True
1027
+
1028
+ @property
1029
+ def model_image_small(self):
1030
+ """Return URL for model image"""
1031
+ return self._modelimages.get('images','').get('front','')
1032
+
1033
+ @property
1034
+ def is_model_image_small_supported(self):
1035
+ """Return true if model image url is not None."""
1036
+ if self._modelimages is not None:
1037
+ return True
1038
+
1039
+ @property
1040
+ def model_image_large(self):
1041
+ """Return URL for model image"""
1042
+ return self._modelimages.get('images','').get('side', '')
1043
+
1044
+ @property
1045
+ def is_model_image_large_supported(self):
1046
+ """Return true if model image url is not None."""
1047
+ if self._modelimages is not None:
1048
+ return True
1049
+
1050
+ # Lights
1051
+ @property
1052
+ def parking_light(self):
1053
+ """Return true if parking light is on"""
1054
+ response = self.attrs.get('status').get('lights', 0)
1055
+ if response == 'On':
1056
+ return True
1057
+ else:
1058
+ return False
1059
+
1060
+ @property
1061
+ def is_parking_light_supported(self):
1062
+ """Return true if parking light is supported"""
1063
+ if self.attrs.get('status', False):
1064
+ if 'lights' in self.attrs.get('status'):
1065
+ return True
1066
+ else:
1067
+ return False
1068
+
1069
+ # Connection status
1070
+ @property
1071
+ def last_connected(self):
1072
+ """Return when vehicle was last connected to connect servers."""
1073
+ last_connected_utc = self.attrs.get('status').get('updatedAt','')
1074
+ if isinstance(last_connected_utc, datetime):
1075
+ last_connected = last_connected_utc.replace(tzinfo=timezone.utc).astimezone(tz=None)
1076
+ else:
1077
+ last_connected = datetime.strptime(last_connected_utc,'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc).astimezone(tz=None)
1078
+ return last_connected.strftime('%Y-%m-%d %H:%M:%S')
1079
+
1080
+ @property
1081
+ def is_last_connected_supported(self):
1082
+ """Return when vehicle was last connected to connect servers."""
1083
+ if 'updatedAt' in self.attrs.get('status', {}):
1084
+ return True
1085
+
1086
+ # Service information
1087
+ @property
1088
+ def distance(self):
1089
+ """Return vehicle odometer."""
1090
+ value = self.attrs.get('mileage').get('mileageKm', 0)
1091
+ return int(value)
1092
+
1093
+ @property
1094
+ def is_distance_supported(self):
1095
+ """Return true if odometer is supported"""
1096
+ if self.attrs.get('mileage', False):
1097
+ if 'mileageKm' in self.attrs.get('mileage'):
1098
+ return True
1099
+ return False
1100
+
1101
+ @property
1102
+ def service_inspection(self):
1103
+ """Return time left until service inspection"""
1104
+ value = -1
1105
+ value = int(self.attrs.get('maintenance', {}).get('inspectionDueDays', 0))
1106
+ return int(value)
1107
+
1108
+ @property
1109
+ def is_service_inspection_supported(self):
1110
+ if self.attrs.get('maintenance', False):
1111
+ if 'inspectionDueDays' in self.attrs.get('maintenance'):
1112
+ return True
1113
+ return False
1114
+
1115
+ @property
1116
+ def service_inspection_distance(self):
1117
+ """Return time left until service inspection"""
1118
+ value = -1
1119
+ value = int(self.attrs.get('maintenance').get('inspectionDueKm', 0))
1120
+ return int(value)
1121
+
1122
+ @property
1123
+ def is_service_inspection_distance_supported(self):
1124
+ if self.attrs.get('maintenance', False):
1125
+ if 'inspectionDueKm' in self.attrs.get('maintenance'):
1126
+ return True
1127
+ return False
1128
+
1129
+ @property
1130
+ def oil_inspection(self):
1131
+ """Return time left until oil inspection"""
1132
+ value = -1
1133
+ value = int(self.attrs.get('maintenance', {}).get('oilServiceDueDays', 0))
1134
+ return int(value)
1135
+
1136
+ @property
1137
+ def is_oil_inspection_supported(self):
1138
+ if self.attrs.get('maintenance', False):
1139
+ if 'oilServiceDueDays' in self.attrs.get('maintenance'):
1140
+ if self.attrs.get('maintenance').get('oilServiceDueDays', None) is not None:
1141
+ return True
1142
+ return False
1143
+
1144
+ @property
1145
+ def oil_inspection_distance(self):
1146
+ """Return distance left until oil inspection"""
1147
+ value = -1
1148
+ value = int(self.attrs.get('maintenance').get('oilServiceDueKm', 0))
1149
+ return int(value)
1150
+
1151
+ @property
1152
+ def is_oil_inspection_distance_supported(self):
1153
+ if self.attrs.get('maintenance', False):
1154
+ if 'oilServiceDueKm' in self.attrs.get('maintenance'):
1155
+ if self.attrs.get('maintenance').get('oilServiceDueKm', None) is not None:
1156
+ return True
1157
+ return False
1158
+
1159
+ @property
1160
+ def adblue_level(self):
1161
+ """Return adblue level."""
1162
+ return int(self.attrs.get('maintenance', {}).get('0x02040C0001', {}).get('value', 0))
1163
+
1164
+ @property
1165
+ def is_adblue_level_supported(self):
1166
+ """Return true if adblue level is supported."""
1167
+ if self.attrs.get('maintenance', False):
1168
+ if '0x02040C0001' in self.attrs.get('maintenance'):
1169
+ if 'value' in self.attrs.get('maintenance')['0x02040C0001']:
1170
+ if self.attrs.get('maintenance')['0x02040C0001'].get('value', 0) is not None:
1171
+ return True
1172
+ return False
1173
+
1174
+ # Charger related states for EV and PHEV
1175
+ @property
1176
+ def charging(self):
1177
+ """Return battery level"""
1178
+ cstate = self.attrs.get('charging').get('status').get('charging').get('state','')
1179
+ return 1 if cstate in ['charging', 'Charging'] else 0
1180
+
1181
+ @property
1182
+ def is_charging_supported(self):
1183
+ """Return true if charging is supported"""
1184
+ if self.attrs.get('charging', False):
1185
+ if 'status' in self.attrs.get('charging', {}):
1186
+ if 'charging' in self.attrs.get('charging')['status']:
1187
+ if 'state' in self.attrs.get('charging')['status']['charging']:
1188
+ return True
1189
+ return False
1190
+
1191
+ @property
1192
+ def min_charge_level(self):
1193
+ """Return the charge level that car charges directly to"""
1194
+ if self.attrs.get('departuretimers', {}):
1195
+ return self.attrs.get('departuretimers', {}).get('minSocPercentage', 0)
1196
+ else:
1197
+ return 0
1198
+
1199
+ @property
1200
+ def is_min_charge_level_supported(self):
1201
+ """Return true if car supports setting the min charge level"""
1202
+ if self.attrs.get('departuretimers', {}).get('minSocPercentage', False):
1203
+ return True
1204
+ return False
1205
+
1206
+ @property
1207
+ def battery_level(self):
1208
+ """Return battery level"""
1209
+ if self.attrs.get('charging', False):
1210
+ return int(self.attrs.get('charging').get('status', {}).get('battery', {}).get('currentSocPercentage', 0))
1211
+ else:
1212
+ return 0
1213
+
1214
+ @property
1215
+ def is_battery_level_supported(self):
1216
+ """Return true if battery level is supported"""
1217
+ if self.attrs.get('charging', False):
1218
+ if 'status' in self.attrs.get('charging'):
1219
+ if 'battery' in self.attrs.get('charging')['status']:
1220
+ if 'currentSocPercentage' in self.attrs.get('charging')['status']['battery']:
1221
+ return True
1222
+ return False
1223
+
1224
+ @property
1225
+ def charge_max_ampere(self):
1226
+ """Return charger max ampere setting."""
1227
+ if self.attrs.get('charger', False):
1228
+ return self.attrs.get('charger').get('info').get('settings').get('maxChargeCurrentAC')
1229
+ return 0
1230
+
1231
+ @property
1232
+ def is_charge_max_ampere_supported(self):
1233
+ """Return true if Charger Max Ampere is supported"""
1234
+ if self.attrs.get('charging', False):
1235
+ if 'info' in self.attrs.get('charging', {}):
1236
+ if 'settings' in self.attrs.get('charging')['info']:
1237
+ if 'maxChargeCurrentAC' in self.attrs.get('charging', {})['info']['settings']:
1238
+ return True
1239
+ return False
1240
+
1241
+ @property
1242
+ def charging_cable_locked(self):
1243
+ """Return plug locked state"""
1244
+ response = ''
1245
+ if self.attrs.get('charging', False):
1246
+ response = self.attrs.get('charging').get('status', {}).get('plug', {}).get('lock', '')
1247
+ return True if response in ['Locked', 'locked'] else False
1248
+
1249
+ @property
1250
+ def is_charging_cable_locked_supported(self):
1251
+ """Return true if plug locked state is supported"""
1252
+ if self.attrs.get('charging', False):
1253
+ if 'status' in self.attrs.get('charging'):
1254
+ if 'plug' in self.attrs.get('charging')['status']:
1255
+ if 'lock' in self.attrs.get('charging')['status']['plug']:
1256
+ return True
1257
+ return False
1258
+
1259
+ @property
1260
+ def charging_cable_connected(self):
1261
+ """Return plug locked state"""
1262
+ response = ''
1263
+ if self.attrs.get('charging', False):
1264
+ response = self.attrs.get('charging', {}).get('status', {}).get('plug').get('connection', 0)
1265
+ return True if response in ['Connected', 'connected'] else False
1266
+
1267
+ @property
1268
+ def is_charging_cable_connected_supported(self):
1269
+ """Return true if charging cable connected is supported"""
1270
+ if self.attrs.get('charging', False):
1271
+ if 'status' in self.attrs.get('charging', {}):
1272
+ if 'plug' in self.attrs.get('charging').get('status', {}):
1273
+ if 'connection' in self.attrs.get('charging')['status'].get('plug', {}):
1274
+ return True
1275
+ return False
1276
+
1277
+ @property
1278
+ def charging_time_left(self):
1279
+ """Return minutes to charging complete"""
1280
+ if self.external_power:
1281
+ if self.attrs.get('charging', {}).get('status', {}).get('charging', {}).get('remainingTimeInMinutes', False):
1282
+ minutes = int(self.attrs.get('charging', {}).get('status', {}).get('charging', {}).get('remainingTimeInMinutes', 0))
1283
+ else:
1284
+ minutes = 0
1285
+ try:
1286
+ if minutes == -1: return '00:00'
1287
+ if minutes == 65535: return '00:00'
1288
+ return "%02d:%02d" % divmod(minutes, 60)
1289
+ except Exception:
1290
+ pass
1291
+ return '00:00'
1292
+
1293
+ @property
1294
+ def is_charging_time_left_supported(self):
1295
+ """Return true if charging is supported"""
1296
+ return self.is_charging_supported
1297
+
1298
+ @property
1299
+ def charging_power(self):
1300
+ """Return charging power in watts."""
1301
+ if self.attrs.get('charging', False):
1302
+ return int(self.attrs.get('charging', {}).get('chargingPowerInWatts', 0))
1303
+ else:
1304
+ return 0
1305
+
1306
+ @property
1307
+ def is_charging_power_supported(self):
1308
+ """Return true if charging power is supported."""
1309
+ if self.attrs.get('charging', False):
1310
+ if self.attrs.get('charging', {}).get('chargingPowerInWatts', False) is not False:
1311
+ return True
1312
+ return False
1313
+
1314
+ @property
1315
+ def charge_rate(self):
1316
+ """Return charge rate in km per h."""
1317
+ if self.attrs.get('charging', False):
1318
+ return int(self.attrs.get('charging', {}).get('chargingRateInKilometersPerHour', 0))
1319
+ else:
1320
+ return 0
1321
+
1322
+ @property
1323
+ def is_charge_rate_supported(self):
1324
+ """Return true if charge rate is supported."""
1325
+ if self.attrs.get('charging', False):
1326
+ if self.attrs.get('charging', {}).get('chargingRateInKilometersPerHour', False) is not False:
1327
+ return True
1328
+ return False
1329
+
1330
+ @property
1331
+ def external_power(self):
1332
+ """Return true if external power is connected."""
1333
+ response = ''
1334
+ if self.attrs.get('charging', False):
1335
+ response = self.attrs.get('charging', {}).get('status', {}).get('charging', {}).get('plug', {}).get('externalPower', '')
1336
+ else:
1337
+ response = ''
1338
+ return True if response in ['stationConnected', 'available', 'Charging'] else False
1339
+
1340
+ @property
1341
+ def is_external_power_supported(self):
1342
+ """External power supported."""
1343
+ if self.attrs.get('charging', {}).get('status', {}).get('charging', {}).get('plug, {}').get('externalPower', False):
1344
+ return True
1345
+
1346
+ @property
1347
+ def energy_flow(self):
1348
+ """Return true if energy is flowing through charging port."""
1349
+ check = self.attrs.get('charger', {}).get('status', {}).get('chargingStatusData', {}).get('energyFlow', {}).get('content', 'off')
1350
+ if check == 'on':
1351
+ return True
1352
+ else:
1353
+ return False
1354
+
1355
+ @property
1356
+ def is_energy_flow_supported(self):
1357
+ """Energy flow supported."""
1358
+ if self.attrs.get('charger', {}).get('status', {}).get('chargingStatusData', {}).get('energyFlow', False):
1359
+ return True
1360
+
1361
+ # Vehicle location states
1362
+ @property
1363
+ def position(self):
1364
+ """Return position."""
1365
+ output = {}
1366
+ try:
1367
+ if self.vehicle_moving:
1368
+ output = {
1369
+ 'lat': None,
1370
+ 'lng': None,
1371
+ 'address': None,
1372
+ 'timestamp': None
1373
+ }
1374
+ else:
1375
+ posObj = self.attrs.get('findCarResponse', {})
1376
+ lat = posObj.get('lat')
1377
+ lng = posObj.get('lon')
1378
+ position_to_address = posObj.get('position_to_address')
1379
+ parkingTime = posObj.get('parkingTimeUTC', None)
1380
+ output = {
1381
+ 'lat' : lat,
1382
+ 'lng' : lng,
1383
+ 'address': position_to_address,
1384
+ 'timestamp' : parkingTime
1385
+ }
1386
+ except:
1387
+ output = {
1388
+ 'lat': '?',
1389
+ 'lng': '?',
1390
+ }
1391
+ return output
1392
+
1393
+ @property
1394
+ def is_position_supported(self):
1395
+ """Return true if carfinder_v1 service is active."""
1396
+ if self.attrs.get('findCarResponse', {}).get('lat', False):
1397
+ return True
1398
+ elif self.attrs.get('isMoving', False):
1399
+ return True
1400
+
1401
+ @property
1402
+ def vehicle_moving(self):
1403
+ """Return true if vehicle is moving."""
1404
+ return self.attrs.get('isMoving', False)
1405
+
1406
+ @property
1407
+ def is_vehicle_moving_supported(self):
1408
+ """Return true if vehicle supports position."""
1409
+ if self.is_position_supported:
1410
+ return True
1411
+
1412
+ @property
1413
+ def parking_time(self):
1414
+ """Return timestamp of last parking time."""
1415
+ parkTime_utc = self.attrs.get('findCarResponse', {}).get('parkingTimeUTC', 'Unknown')
1416
+ if isinstance(parkTime_utc, datetime):
1417
+ parkTime = parkTime_utc.replace(tzinfo=timezone.utc).astimezone(tz=None)
1418
+ else:
1419
+ parkTime = datetime.strptime(parkTime_utc,'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc).astimezone(tz=None)
1420
+ return parkTime.strftime('%Y-%m-%d %H:%M:%S')
1421
+
1422
+ @property
1423
+ def is_parking_time_supported(self):
1424
+ """Return true if vehicle parking timestamp is supported."""
1425
+ if 'parkingTimeUTC' in self.attrs.get('findCarResponse', {}):
1426
+ return True
1427
+
1428
+ # Vehicle fuel level and range
1429
+ @property
1430
+ def primary_range(self):
1431
+ value = -1
1432
+ if 'engines' in self.attrs.get('mycar'):
1433
+ value = self.attrs.get('mycar')['engines']['primary'].get('rangeKm', 0)
1434
+ return int(value)
1435
+
1436
+ @property
1437
+ def is_primary_range_supported(self):
1438
+ if self.attrs.get('mycar', False):
1439
+ if 'engines' in self.attrs.get('mycar', {}):
1440
+ if 'primary' in self.attrs.get('mycar')['engines']:
1441
+ if 'rangeKm' in self.attrs.get('mycar')['engines']['primary']:
1442
+ return True
1443
+ return False
1444
+
1445
+ @property
1446
+ def primary_drive(self):
1447
+ value=''
1448
+ if 'engines' in self.attrs.get('mycar'):
1449
+ value = self.attrs.get('mycar')['engines']['primary'].get('fuelType', '')
1450
+ return value
1451
+
1452
+ @property
1453
+ def is_primary_drive_supported(self):
1454
+ if self.attrs.get('mycar', False):
1455
+ if 'engines' in self.attrs.get('mycar', {}):
1456
+ if 'primary' in self.attrs.get('mycar')['engines']:
1457
+ if 'fuelType' in self.attrs.get('mycar')['engines']['primary']:
1458
+ return True
1459
+ return False
1460
+
1461
+ @property
1462
+ def secondary_range(self):
1463
+ value = -1
1464
+ if 'engines' in self.attrs.get('mycar'):
1465
+ value = self.attrs.get('mycar')['engines']['secondary'].get('rangeKm', 0)
1466
+ return int(value)
1467
+
1468
+ @property
1469
+ def is_secondary_range_supported(self):
1470
+ if self.attrs.get('mycar', False):
1471
+ if 'engines' in self.attrs.get('mycar', {}):
1472
+ if 'secondary' in self.attrs.get('mycar')['engines']:
1473
+ if 'rangeKm' in self.attrs.get('mycar')['engines']['secondary']:
1474
+ return True
1475
+ return False
1476
+
1477
+ @property
1478
+ def secondary_drive(self):
1479
+ value=''
1480
+ if 'engines' in self.attrs.get('mycar'):
1481
+ value = self.attrs.get('mycar')['engines']['secondary'].get('fuelType', '')
1482
+ return value
1483
+
1484
+ @property
1485
+ def is_secondary_drive_supported(self):
1486
+ if self.attrs.get('mycar', False):
1487
+ if 'engines' in self.attrs.get('mycar', {}):
1488
+ if 'secondary' in self.attrs.get('mycar')['engines']:
1489
+ if 'fuelType' in self.attrs.get('mycar')['engines']['secondary']:
1490
+ return True
1491
+ return False
1492
+
1493
+ @property
1494
+ def electric_range(self):
1495
+ value = -1
1496
+ if self.is_secondary_drive_supported:
1497
+ if self.secondary_drive == 'electric':
1498
+ return self.secondary_range
1499
+ elif self.is_primary_drive_supported:
1500
+ if self.primary_drive == 'electric':
1501
+ return self.primary_range
1502
+ return -1
1503
+
1504
+ @property
1505
+ def is_electric_range_supported(self):
1506
+ if self.is_secondary_drive_supported:
1507
+ if self.secondary_drive == 'electric':
1508
+ return self.is_secondary_range_supported
1509
+ elif self.is_primary_drive_supported:
1510
+ if self.primary_drive == 'electric':
1511
+ return self.is_primary_range_supported
1512
+ return False
1513
+
1514
+ @property
1515
+ def combustion_range(self):
1516
+ value = -1
1517
+ if self.is_primary_drive_supported:
1518
+ if not self.primary_drive == 'electric':
1519
+ return self.primary_range
1520
+ elif self.is_secondary_drive_supported:
1521
+ if not self.secondary_drive == 'electric':
1522
+ return self.secondary_range
1523
+ return -1
1524
+
1525
+ @property
1526
+ def is_combustion_range_supported(self):
1527
+ if self.is_primary_drive_supported:
1528
+ if not self.primary_drive == 'electric':
1529
+ return self.is_primary_range_supported
1530
+ elif self.is_secondary_drive_supported:
1531
+ if not self.secondary_drive == 'electric':
1532
+ return self.is_secondary_range_supported
1533
+ return False
1534
+
1535
+ @property
1536
+ def combined_range(self):
1537
+ return int(self.combustion_range)+int(self.electric_range)
1538
+
1539
+ @property
1540
+ def is_combined_range_supported(self):
1541
+ if self.is_combustion_range_supported and self.is_electric_range_supported:
1542
+ return True
1543
+ return False
1544
+
1545
+ @property
1546
+ def fuel_level(self):
1547
+ value = -1
1548
+ if self.is_fuel_level_supported:
1549
+ if not self.primary_drive == 'electric':
1550
+ value= self.attrs.get('mycar')['engines']['primary'].get('levelPct',0)
1551
+ elif not self.secondary_drive == 'electric':
1552
+ value= self.attrs.get('mycar')['engines']['primary'].get('levelPct',0)
1553
+ return int(value)
1554
+
1555
+ @property
1556
+ def is_fuel_level_supported(self):
1557
+ if self.is_primary_drive_supported:
1558
+ if not self.primary_drive == 'electric':
1559
+ if "levelPct" in self.attrs.get('mycar')['engines']['primary']:
1560
+ return self.is_primary_range_supported
1561
+ elif self.is_secondary_drive_supported:
1562
+ if not self.secondary_drive == 'electric':
1563
+ if "levelPct" in self.attrs.get('mycar')['engines']['secondary']:
1564
+ return self.is_secondary_range_supported
1565
+ return False
1566
+
1567
+ # Climatisation settings
1568
+ @property
1569
+ def climatisation_target_temperature(self):
1570
+ """Return the target temperature from climater."""
1571
+ if self.attrs.get('climater', False):
1572
+ value = self.attrs.get('climater').get('settings', {}).get('targetTemperatureInCelsius', 0)
1573
+ return value
1574
+ return False
1575
+
1576
+ @property
1577
+ def is_climatisation_target_temperature_supported(self):
1578
+ """Return true if climatisation target temperature is supported."""
1579
+ if self.attrs.get('climater', False):
1580
+ if 'settings' in self.attrs.get('climater', {}):
1581
+ if 'targetTemperatureInCelsius' in self.attrs.get('climater', {})['settings']:
1582
+ return True
1583
+ return False
1584
+
1585
+ @property
1586
+ def climatisation_time_left(self):
1587
+ """Return time left for climatisation in hours:minutes."""
1588
+ if self.attrs.get('airConditioning', {}).get('remainingTimeToReachTargetTemperatureInSeconds', False):
1589
+ minutes = int(self.attrs.get('airConditioning', {}).get('remainingTimeToReachTargetTemperatureInSeconds', 0))/60
1590
+ try:
1591
+ if not 0 <= minutes <= 65535:
1592
+ return "00:00"
1593
+ return "%02d:%02d" % divmod(minutes, 60)
1594
+ except Exception:
1595
+ pass
1596
+ return "00:00"
1597
+
1598
+ @property
1599
+ def is_climatisation_time_left_supported(self):
1600
+ """Return true if remainingTimeToReachTargetTemperatureInSeconds is supported."""
1601
+ if self.attrs.get('airConditioning', {}).get('remainingTimeToReachTargetTemperatureInSeconds', False):
1602
+ return True
1603
+ return False
1604
+
1605
+ @property
1606
+ def climatisation_without_external_power(self):
1607
+ """Return state of climatisation from battery power."""
1608
+ return self.attrs.get('climater').get('settings').get('climatisationWithoutExternalPower', False)
1609
+
1610
+ @property
1611
+ def is_climatisation_without_external_power_supported(self):
1612
+ """Return true if climatisation on battery power is supported."""
1613
+ if self.attrs.get('climater', False):
1614
+ if 'settings' in self.attrs.get('climater', {}):
1615
+ if 'climatisationWithoutExternalPower' in self.attrs.get('climater', {})['settings']:
1616
+ return True
1617
+ else:
1618
+ return False
1619
+
1620
+ @property
1621
+ def outside_temperature(self):
1622
+ """Return outside temperature."""
1623
+ response = int(self.attrs.get('StoredVehicleDataResponseParsed')['0x0301020001'].get('value', 0))
1624
+ if response:
1625
+ return round(float((response / 10) - 273.15), 1)
1626
+ else:
1627
+ return False
1628
+
1629
+ @property
1630
+ def is_outside_temperature_supported(self):
1631
+ """Return true if outside temp is supported"""
1632
+ if self.attrs.get('StoredVehicleDataResponseParsed', False):
1633
+ if '0x0301020001' in self.attrs.get('StoredVehicleDataResponseParsed'):
1634
+ if "value" in self.attrs.get('StoredVehicleDataResponseParsed')['0x0301020001']:
1635
+ return True
1636
+ else:
1637
+ return False
1638
+ else:
1639
+ return False
1640
+
1641
+ # Climatisation, electric
1642
+ @property
1643
+ def electric_climatisation_attributes(self):
1644
+ """Return climatisation attributes."""
1645
+ data = {
1646
+ 'source': self.attrs.get('climater', {}).get('settings', {}).get('heaterSource', {}).get('content', ''),
1647
+ 'status': self.attrs.get('climater', {}).get('status', {}).get('climatisationStatus', {}).get('climatisationState', '')
1648
+ }
1649
+ return data
1650
+
1651
+ @property
1652
+ def is_electric_climatisation_attributes_supported(self):
1653
+ """Return true if vehichle has climater."""
1654
+ return self.is_climatisation_supported
1655
+
1656
+ @property
1657
+ def electric_climatisation(self):
1658
+ """Return status of climatisation."""
1659
+ if self.attrs.get('climater', {}).get('status', {}).get('climatisationStatus', {}).get('climatisationState', False):
1660
+ climatisation_type = self.attrs.get('climater', {}).get('settings', {}).get('heaterSource', '')
1661
+ status = self.attrs.get('climater', {}).get('status', {}).get('climatisationStatus', {}).get('climatisationState', '')
1662
+ if status in ['heating', 'cooling', 'on']: #and climatisation_type == 'electric':
1663
+ return True
1664
+ return False
1665
+
1666
+ @property
1667
+ def is_electric_climatisation_supported(self):
1668
+ """Return true if vehichle has climater."""
1669
+ return self.is_climatisation_supported
1670
+
1671
+ @property
1672
+ def auxiliary_climatisation(self):
1673
+ """Return status of auxiliary climatisation."""
1674
+ climatisation_type = self.attrs.get('climater', {}).get('settings', {}).get('heaterSource', {}).get('content', '')
1675
+ status = self.attrs.get('climater', {}).get('status', {}).get('climatisationStatus', {}).get('climatisationState', '')
1676
+ if status in ['heating', 'cooling', 'ventilation', 'heatingAuxiliary', 'on'] and climatisation_type == 'auxiliary':
1677
+ return True
1678
+ elif status in ['heatingAuxiliary'] and climatisation_type == 'electric':
1679
+ return True
1680
+ else:
1681
+ return False
1682
+
1683
+ @property
1684
+ def is_auxiliary_climatisation_supported(self):
1685
+ """Return true if vehicle has auxiliary climatisation."""
1686
+ #if self._services.get('rclima_v1', False):
1687
+ if self._relevantCapabilties.get('climatisation', {}).get('active', False):
1688
+ functions = self._services.get('rclima_v1', {}).get('operations', [])
1689
+ #for operation in functions:
1690
+ # if operation['id'] == 'P_START_CLIMA_AU':
1691
+ if 'P_START_CLIMA_AU' in functions:
1692
+ return True
1693
+ return False
1694
+
1695
+ @property
1696
+ def is_climatisation_supported(self):
1697
+ """Return true if climatisation has State."""
1698
+ if self.attrs.get('climater', {}).get('status', {}).get('climatisationStatus', {}).get('climatisationState', False):
1699
+ return True
1700
+ return False
1701
+
1702
+ @property
1703
+ def window_heater(self):
1704
+ """Return status of window heater."""
1705
+ if self.attrs.get('climater', False):
1706
+ for elem in self.attrs.get('climater', {}).get('status', {}).get('windowHeatingStatus', {}).get('windowHeatingStatus', []):
1707
+ if elem.get('windowHeatingState','off')=='on':
1708
+ return True
1709
+ return False
1710
+
1711
+ @property
1712
+ def is_window_heater_supported(self):
1713
+ """Return true if vehichle has heater."""
1714
+ if self.is_electric_climatisation_supported:
1715
+ if self.attrs.get('climater', False):
1716
+ if self.attrs.get('climater', {}).get('status', {}).get('windowHeatingStatus', {}).get('windowHeatingStatus', []):
1717
+ if len(self.attrs.get('climater', {}).get('status', {}).get('windowHeatingStatus', {}).get('windowHeatingStatus', []))>0:
1718
+ return True
1719
+ return False
1720
+
1721
+ @property
1722
+ def seat_heating(self):
1723
+ """Return status of seat heating."""
1724
+ if self.attrs.get('airConditioning', {}).get('seatHeatingSupport', False):
1725
+ for element in self.attrs.get('airConditioning', {}).get('seatHeatingSupport', {}):
1726
+ if self.attrs.get('airConditioning', {}).get('seatHeatingSupport', {}).get(element, False):
1727
+ return True
1728
+ return False
1729
+
1730
+ @property
1731
+ def is_seat_heating_supported(self):
1732
+ """Return true if vehichle has seat heating."""
1733
+ if self.attrs.get('airConditioning', {}).get('seatHeatingSupport', False):
1734
+ return True
1735
+ return False
1736
+
1737
+ @property
1738
+ def warnings(self):
1739
+ """Return warnings."""
1740
+ if self.attrs.get('warninglights', {}).get('statuses',[]):
1741
+ return self.attrs.get('warninglights', {}).get('statuses',[])
1742
+ return 'No warnings'
1743
+
1744
+ @property
1745
+ def is_warnings_supported(self):
1746
+ """Return true if vehichle has warnings."""
1747
+ if self.attrs.get('warninglights', False):
1748
+ return True
1749
+ return False
1750
+
1751
+ # Parking heater, "legacy" auxiliary climatisation
1752
+ @property
1753
+ def pheater_duration(self):
1754
+ return self._climate_duration
1755
+
1756
+ @pheater_duration.setter
1757
+ def pheater_duration(self, value):
1758
+ if value in [10, 20, 30, 40, 50, 60]:
1759
+ self._climate_duration = value
1760
+ else:
1761
+ _LOGGER.warning(f'Invalid value for duration: {value}')
1762
+
1763
+ @property
1764
+ def is_pheater_duration_supported(self):
1765
+ return self.is_pheater_heating_supported
1766
+
1767
+ @property
1768
+ def pheater_ventilation(self):
1769
+ """Return status of combustion climatisation."""
1770
+ return self.attrs.get('heating', {}).get('climatisationStateReport', {}).get('climatisationState', False) == 'ventilation'
1771
+
1772
+ @property
1773
+ def is_pheater_ventilation_supported(self):
1774
+ """Return true if vehichle has combustion climatisation."""
1775
+ return self.is_pheater_heating_supported
1776
+
1777
+ @property
1778
+ def pheater_heating(self):
1779
+ """Return status of combustion engine heating."""
1780
+ return self.attrs.get('heating', {}).get('climatisationStateReport', {}).get('climatisationState', False) == 'heating'
1781
+
1782
+ @property
1783
+ def is_pheater_heating_supported(self):
1784
+ """Return true if vehichle has combustion engine heating."""
1785
+ if self.attrs.get('heating', {}).get('climatisationStateReport', {}).get('climatisationState', False):
1786
+ return True
1787
+
1788
+ @property
1789
+ def pheater_status(self):
1790
+ """Return status of combustion engine heating/ventilation."""
1791
+ return self.attrs.get('heating', {}).get('climatisationStateReport', {}).get('climatisationState', 'Unknown')
1792
+
1793
+ @property
1794
+ def is_pheater_status_supported(self):
1795
+ """Return true if vehichle has combustion engine heating/ventilation."""
1796
+ if self.attrs.get('heating', {}).get('climatisationStateReport', {}).get('climatisationState', False):
1797
+ return True
1798
+
1799
+ # Windows
1800
+ @property
1801
+ def windows_closed(self):
1802
+ return (self.window_closed_left_front and self.window_closed_left_back and self.window_closed_right_front and self.window_closed_right_back)
1803
+
1804
+ @property
1805
+ def is_windows_closed_supported(self):
1806
+ """Return true if window state is supported"""
1807
+ response = ""
1808
+ if self.attrs.get('status', False):
1809
+ if 'windows' in self.attrs.get('status'):
1810
+ response = self.attrs.get('status')['windows'].get('frontLeft', '')
1811
+ return True if response != '' else False
1812
+
1813
+ @property
1814
+ def window_closed_left_front(self):
1815
+ response = self.attrs.get('status')['windows'].get('frontLeft', '')
1816
+ if response == 'closed':
1817
+ return True
1818
+ else:
1819
+ return False
1820
+
1821
+ @property
1822
+ def is_window_closed_left_front_supported(self):
1823
+ """Return true if window state is supported"""
1824
+ response = ""
1825
+ if self.attrs.get('status', False):
1826
+ if 'windows' in self.attrs.get('status'):
1827
+ response = self.attrs.get('status')['windows'].get('frontLeft', '')
1828
+ return True if response != "" else False
1829
+
1830
+ @property
1831
+ def window_closed_right_front(self):
1832
+ response = self.attrs.get('status')['windows'].get('frontRight', '')
1833
+ if response == 'closed':
1834
+ return True
1835
+ else:
1836
+ return False
1837
+
1838
+ @property
1839
+ def is_window_closed_right_front_supported(self):
1840
+ """Return true if window state is supported"""
1841
+ response = ""
1842
+ if self.attrs.get('status', False):
1843
+ if 'windows' in self.attrs.get('status'):
1844
+ response = self.attrs.get('status')['windows'].get('frontRight', '')
1845
+ return True if response != "" else False
1846
+
1847
+ @property
1848
+ def window_closed_left_back(self):
1849
+ response = self.attrs.get('status')['windows'].get('rearLeft', '')
1850
+ if response == 'closed':
1851
+ return True
1852
+ else:
1853
+ return False
1854
+
1855
+ @property
1856
+ def is_window_closed_left_back_supported(self):
1857
+ """Return true if window state is supported"""
1858
+ response = ""
1859
+ if self.attrs.get('status', False):
1860
+ if 'windows' in self.attrs.get('status'):
1861
+ response = self.attrs.get('status')['windows'].get('rearLeft', '')
1862
+ return True if response != "" else False
1863
+
1864
+ @property
1865
+ def window_closed_right_back(self):
1866
+ response = self.attrs.get('status')['windows'].get('rearRight', '')
1867
+ if response == 'closed':
1868
+ return True
1869
+ else:
1870
+ return False
1871
+
1872
+ @property
1873
+ def is_window_closed_right_back_supported(self):
1874
+ """Return true if window state is supported"""
1875
+ response = ""
1876
+ if self.attrs.get('status', False):
1877
+ if 'windows' in self.attrs.get('status'):
1878
+ response = self.attrs.get('status')['windows'].get('rearRight', '')
1879
+ return True if response != "" else False
1880
+
1881
+ @property
1882
+ def sunroof_closed(self):
1883
+ # 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.
1884
+ response = ""
1885
+ if 'sunRoof' in self.attrs.get('status'):
1886
+ response = self.attrs.get('status').get('sunRoof', '')
1887
+ #else:
1888
+ # response = self.attrs.get('status')['windows'].get('sunRoof', '')
1889
+ if response == 'closed':
1890
+ return True
1891
+ else:
1892
+ return False
1893
+
1894
+ @property
1895
+ def is_sunroof_closed_supported(self):
1896
+ """Return true if sunroof state is supported"""
1897
+ # 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.
1898
+ response = ""
1899
+ if self.attrs.get('status', False):
1900
+ if 'sunRoof' in self.attrs.get('status'):
1901
+ response = self.attrs.get('status').get('sunRoof', '')
1902
+ #elif 'sunRoof' in self.attrs.get('status')['windows']:
1903
+ # response = self.attrs.get('status')['windows'].get('sunRoof', '')
1904
+ return True if response != '' else False
1905
+
1906
+ # Locks
1907
+ @property
1908
+ def door_locked(self):
1909
+ # LEFT FRONT
1910
+ response = self.attrs.get('status')['doors']['frontLeft'].get('locked', 'false')
1911
+ if response != 'true':
1912
+ return False
1913
+ # LEFT REAR
1914
+ response = self.attrs.get('status')['doors']['rearLeft'].get('locked', 'false')
1915
+ if response != 'true':
1916
+ return False
1917
+ # RIGHT FRONT
1918
+ response = self.attrs.get('status')['doors']['frontRight'].get('locked', 'false')
1919
+ if response != 'true':
1920
+ return False
1921
+ # RIGHT REAR
1922
+ response = self.attrs.get('status')['doors']['rearRight'].get('locked', 'false')
1923
+ if response != 'true':
1924
+ return False
1925
+
1926
+ return True
1927
+
1928
+ @property
1929
+ def is_door_locked_supported(self):
1930
+ response = 0
1931
+ if self.attrs.get('status', False):
1932
+ if 'doors' in self.attrs.get('status'):
1933
+ response = self.attrs.get('status')['doors'].get('frontLeft', {}).get('locked', 0)
1934
+ return True if response != 0 else False
1935
+
1936
+ @property
1937
+ def trunk_locked(self):
1938
+ locked=self.attrs.get('status')['trunk'].get('locked', 'false')
1939
+ return True if locked == 'true' else False
1940
+
1941
+ @property
1942
+ def is_trunk_locked_supported(self):
1943
+ if self.attrs.get('status', False):
1944
+ if 'trunk' in self.attrs.get('status'):
1945
+ if 'locked' in self.attrs.get('status').get('trunk'):
1946
+ return True
1947
+ return False
1948
+
1949
+ # Doors, hood and trunk
1950
+ @property
1951
+ def hood_closed(self):
1952
+ """Return true if hood is closed"""
1953
+ open = self.attrs.get('status')['hood'].get('open', 'false')
1954
+ return True if open == 'false' else False
1955
+
1956
+ @property
1957
+ def is_hood_closed_supported(self):
1958
+ """Return true if hood state is supported"""
1959
+ response = 0
1960
+ if self.attrs.get('status', False):
1961
+ if 'hood' in self.attrs.get('status', {}):
1962
+ response = self.attrs.get('status')['hood'].get('open', 0)
1963
+ return True if response != 0 else False
1964
+
1965
+ @property
1966
+ def door_closed_left_front(self):
1967
+ open=self.attrs.get('status')['doors']['frontLeft'].get('open', 'false')
1968
+ return True if open == 'false' else False
1969
+
1970
+ @property
1971
+ def is_door_closed_left_front_supported(self):
1972
+ """Return true if window state is supported"""
1973
+ if self.attrs.get('status', False):
1974
+ if 'doors' in self.attrs.get('status'):
1975
+ if 'frontLeft' in self.attrs.get('status').get('doors', {}):
1976
+ return True
1977
+ return False
1978
+
1979
+ @property
1980
+ def door_closed_right_front(self):
1981
+ open=self.attrs.get('status')['doors']['frontRight'].get('open', 'false')
1982
+ return True if open == 'false' else False
1983
+
1984
+ @property
1985
+ def is_door_closed_right_front_supported(self):
1986
+ """Return true if window state is supported"""
1987
+ if self.attrs.get('status', False):
1988
+ if 'doors' in self.attrs.get('status'):
1989
+ if 'frontRight' in self.attrs.get('status').get('doors', {}):
1990
+ return True
1991
+ return False
1992
+
1993
+ @property
1994
+ def door_closed_left_back(self):
1995
+ open=self.attrs.get('status')['doors']['rearLeft'].get('open', 'false')
1996
+ return True if open == 'false' else False
1997
+
1998
+ @property
1999
+ def is_door_closed_left_back_supported(self):
2000
+ if self.attrs.get('status', False):
2001
+ if 'doors' in self.attrs.get('status'):
2002
+ if 'rearLeft' in self.attrs.get('status').get('doors', {}):
2003
+ return True
2004
+ return False
2005
+
2006
+ @property
2007
+ def door_closed_right_back(self):
2008
+ open=self.attrs.get('status')['doors']['rearRight'].get('open', 'false')
2009
+ return True if open == 'false' else False
2010
+
2011
+ @property
2012
+ def is_door_closed_right_back_supported(self):
2013
+ """Return true if window state is supported"""
2014
+ if self.attrs.get('status', False):
2015
+ if 'doors' in self.attrs.get('status'):
2016
+ if 'rearRight' in self.attrs.get('status').get('doors', {}):
2017
+ return True
2018
+ return False
2019
+
2020
+ @property
2021
+ def trunk_closed(self):
2022
+ open = self.attrs.get('status')['trunk'].get('open', 'false')
2023
+ return True if open == 'false' else False
2024
+
2025
+ @property
2026
+ def is_trunk_closed_supported(self):
2027
+ """Return true if window state is supported"""
2028
+ response = 0
2029
+ if self.attrs.get('status', False):
2030
+ if 'trunk' in self.attrs.get('status', {}):
2031
+ response = self.attrs.get('status')['trunk'].get('open', 0)
2032
+ return True if response != 0 else False
2033
+
2034
+ # Departure timers
2035
+ # Under development
2036
+ @property
2037
+ def departure1(self):
2038
+ """Return timer status and attributes."""
2039
+ if self.attrs.get('departureTimers', False):
2040
+ try:
2041
+ data = {}
2042
+ timerdata = self.attrs.get('departureTimers', {}).get('timers', [])
2043
+ timer = timerdata[0]
2044
+ timer.pop('timestamp', None)
2045
+ timer.pop('timerID', None)
2046
+ timer.pop('profileID', None)
2047
+ data.update(timer)
2048
+ return data
2049
+ except:
2050
+ pass
2051
+ elif self.attrs.get('timers', False):
2052
+ try:
2053
+ response = self.attrs.get('timers', [])
2054
+ if len(self.attrs.get('timers', [])) >= 1:
2055
+ timer = response[0]
2056
+ timer.pop('id', None)
2057
+ else:
2058
+ timer = {}
2059
+ return timer
2060
+ except:
2061
+ pass
2062
+ return None
2063
+
2064
+ @property
2065
+ def is_departure1_supported(self):
2066
+ """Return true if timer 1 is supported."""
2067
+ if len(self.attrs.get('departureTimers', {}).get('timers', [])) >= 1:
2068
+ return True
2069
+ elif len(self.attrs.get('timers', [])) >= 1:
2070
+ return True
2071
+ return False
2072
+
2073
+ @property
2074
+ def departure2(self):
2075
+ """Return timer status and attributes."""
2076
+ if self.attrs.get('departureTimers', False):
2077
+ try:
2078
+ data = {}
2079
+ timerdata = self.attrs.get('departureTimers', {}).get('timers', [])
2080
+ timer = timerdata[1]
2081
+ timer.pop('timestamp', None)
2082
+ timer.pop('timerID', None)
2083
+ timer.pop('profileID', None)
2084
+ data.update(timer)
2085
+ return data
2086
+ except:
2087
+ pass
2088
+ elif self.attrs.get('timers', False):
2089
+ try:
2090
+ response = self.attrs.get('timers', [])
2091
+ if len(self.attrs.get('timers', [])) >= 2:
2092
+ timer = response[1]
2093
+ timer.pop('id', None)
2094
+ else:
2095
+ timer = {}
2096
+ return timer
2097
+ except:
2098
+ pass
2099
+ return None
2100
+
2101
+ @property
2102
+ def is_departure2_supported(self):
2103
+ """Return true if timer 2 is supported."""
2104
+ if len(self.attrs.get('departureTimers', {}).get('timers', [])) >= 2:
2105
+ return True
2106
+ elif len(self.attrs.get('timers', [])) >= 2:
2107
+ return True
2108
+ return False
2109
+
2110
+ @property
2111
+ def departure3(self):
2112
+ """Return timer status and attributes."""
2113
+ if self.attrs.get('departureTimers', False):
2114
+ try:
2115
+ data = {}
2116
+ timerdata = self.attrs.get('departureTimers', {}).get('timers', [])
2117
+ timer = timerdata[2]
2118
+ timer.pop('timestamp', None)
2119
+ timer.pop('timerID', None)
2120
+ timer.pop('profileID', None)
2121
+ data.update(timer)
2122
+ return data
2123
+ except:
2124
+ pass
2125
+ elif self.attrs.get('timers', False):
2126
+ try:
2127
+ response = self.attrs.get('timers', [])
2128
+ if len(self.attrs.get('timers', [])) >= 3:
2129
+ timer = response[2]
2130
+ timer.pop('id', None)
2131
+ else:
2132
+ timer = {}
2133
+ return timer
2134
+ except:
2135
+ pass
2136
+ return None
2137
+
2138
+ @property
2139
+ def is_departure3_supported(self):
2140
+ """Return true if timer 3 is supported."""
2141
+ if len(self.attrs.get('departureTimers', {}).get('timers', [])) >= 3:
2142
+ return True
2143
+ elif len(self.attrs.get('timers', [])) >= 3:
2144
+ return True
2145
+ return False
2146
+
2147
+ # Trip data
2148
+ @property
2149
+ def trip_last_entry(self):
2150
+ return self.attrs.get('tripstatistics', {}).get('short', [{},{}])[-1]
2151
+
2152
+ @property
2153
+ def trip_last_average_speed(self):
2154
+ return self.trip_last_entry.get('averageSpeedKmph')
2155
+
2156
+ @property
2157
+ def is_trip_last_average_speed_supported(self):
2158
+ response = self.trip_last_entry
2159
+ if response and type(response.get('averageSpeedKmph', None)) in (float, int):
2160
+ return True
2161
+
2162
+ @property
2163
+ def trip_last_average_electric_consumption(self):
2164
+ return self.trip_last_entry.get('averageElectricConsumption')
2165
+
2166
+ @property
2167
+ def is_trip_last_average_electric_consumption_supported(self):
2168
+ response = self.trip_last_entry
2169
+ if response and type(response.get('averageElectricConsumption', None)) in (float, int):
2170
+ return True
2171
+
2172
+ @property
2173
+ def trip_last_average_fuel_consumption(self):
2174
+ return self.trip_last_entry.get('averageFuelConsumption')
2175
+
2176
+ @property
2177
+ def is_trip_last_average_fuel_consumption_supported(self):
2178
+ response = self.trip_last_entry
2179
+ if response and type(response.get('averageFuelConsumption', None)) in (float, int):
2180
+ return True
2181
+
2182
+ @property
2183
+ def trip_last_average_auxillary_consumption(self):
2184
+ return self.trip_last_entry.get('averageAuxConsumption')
2185
+
2186
+ @property
2187
+ def is_trip_last_average_auxillary_consumption_supported(self):
2188
+ response = self.trip_last_entry
2189
+ if response and type(response.get('averageAuxConsumption', None)) in (float, int):
2190
+ return True
2191
+
2192
+ @property
2193
+ def trip_last_average_aux_consumer_consumption(self):
2194
+ value = self.trip_last_entry.get('averageAuxConsumerConsumption')
2195
+ return value
2196
+
2197
+ @property
2198
+ def is_trip_last_average_aux_consumer_consumption_supported(self):
2199
+ response = self.trip_last_entry
2200
+ if response and type(response.get('averageAuxConsumerConsumption', None)) in (float, int):
2201
+ return True
2202
+
2203
+ @property
2204
+ def trip_last_duration(self):
2205
+ return self.trip_last_entry.get('travelTime')
2206
+
2207
+ @property
2208
+ def is_trip_last_duration_supported(self):
2209
+ response = self.trip_last_entry
2210
+ if response and type(response.get('travelTime', None)) in (float, int):
2211
+ return True
2212
+
2213
+ @property
2214
+ def trip_last_length(self):
2215
+ return self.trip_last_entry.get('mileageKm')
2216
+
2217
+ @property
2218
+ def is_trip_last_length_supported(self):
2219
+ response = self.trip_last_entry
2220
+ if response and type(response.get('mileageKm', None)) in (float, int):
2221
+ return True
2222
+
2223
+ @property
2224
+ def trip_last_recuperation(self):
2225
+ #Not implemented
2226
+ return self.trip_last_entry.get('recuperation')
2227
+
2228
+ @property
2229
+ def is_trip_last_recuperation_supported(self):
2230
+ #Not implemented
2231
+ response = self.trip_last_entry
2232
+ if response and type(response.get('recuperation', None)) in (float, int):
2233
+ return True
2234
+
2235
+ @property
2236
+ def trip_last_average_recuperation(self):
2237
+ #Not implemented
2238
+ value = self.trip_last_entry.get('averageRecuperation')
2239
+ return value
2240
+
2241
+ @property
2242
+ def is_trip_last_average_recuperation_supported(self):
2243
+ #Not implemented
2244
+ response = self.trip_last_entry
2245
+ if response and type(response.get('averageRecuperation', None)) in (float, int):
2246
+ return True
2247
+
2248
+ @property
2249
+ def trip_last_total_electric_consumption(self):
2250
+ #Not implemented
2251
+ return self.trip_last_entry.get('totalElectricConsumption')
2252
+
2253
+ @property
2254
+ def is_trip_last_total_electric_consumption_supported(self):
2255
+ #Not implemented
2256
+ response = self.trip_last_entry
2257
+ if response and type(response.get('totalElectricConsumption', None)) in (float, int):
2258
+ return True
2259
+
2260
+ @property
2261
+ def trip_last_cycle_entry(self):
2262
+ return self.attrs.get('tripstatistics', {}).get('cyclic', [{},{}])[-1]
2263
+
2264
+ @property
2265
+ def trip_last_cycle_average_speed(self):
2266
+ return self.trip_last_cycle_entry.get('averageSpeedKmph')
2267
+
2268
+ @property
2269
+ def is_trip_last_cycle_average_speed_supported(self):
2270
+ response = self.trip_last_cycle_entry
2271
+ if response and type(response.get('averageSpeedKmph', None)) in (float, int):
2272
+ return True
2273
+
2274
+ @property
2275
+ def trip_last_cycle_average_electric_consumption(self):
2276
+ return self.trip_last_cycle_entry.get('averageElectricConsumption')
2277
+
2278
+ @property
2279
+ def is_trip_last_cycle_average_electric_consumption_supported(self):
2280
+ response = self.trip_last_cycle_entry
2281
+ if response and type(response.get('averageElectricConsumption', None)) in (float, int):
2282
+ return True
2283
+
2284
+ @property
2285
+ def trip_last_cycle_average_fuel_consumption(self):
2286
+ return self.trip_last_cycle_entry.get('averageFuelConsumption')
2287
+
2288
+ @property
2289
+ def is_trip_last_cycle_average_fuel_consumption_supported(self):
2290
+ response = self.trip_last_cycle_entry
2291
+ if response and type(response.get('averageFuelConsumption', None)) in (float, int):
2292
+ return True
2293
+
2294
+ @property
2295
+ def trip_last_cycle_average_auxillary_consumption(self):
2296
+ return self.trip_last_cycle_entry.get('averageAuxConsumption')
2297
+
2298
+ @property
2299
+ def is_trip_last_cycle_average_auxillary_consumption_supported(self):
2300
+ response = self.trip_last_cycle_entry
2301
+ if response and type(response.get('averageAuxConsumption', None)) in (float, int):
2302
+ return True
2303
+
2304
+ @property
2305
+ def trip_last_cycle_average_aux_consumer_consumption(self):
2306
+ value = self.trip_last_cycle_entry.get('averageAuxConsumerConsumption')
2307
+ return value
2308
+
2309
+ @property
2310
+ def is_trip_last_cycle_average_aux_consumer_consumption_supported(self):
2311
+ response = self.trip_last_cycle_entry
2312
+ if response and type(response.get('averageAuxConsumerConsumption', None)) in (float, int):
2313
+ return True
2314
+
2315
+ @property
2316
+ def trip_last_cycle_duration(self):
2317
+ return self.trip_last_cycle_entry.get('travelTime')
2318
+
2319
+ @property
2320
+ def is_trip_last_cycle_duration_supported(self):
2321
+ response = self.trip_last_cycle_entry
2322
+ if response and type(response.get('travelTime', None)) in (float, int):
2323
+ return True
2324
+
2325
+ @property
2326
+ def trip_last_cycle_length(self):
2327
+ return self.trip_last_cycle_entry.get('mileageKm')
2328
+
2329
+ @property
2330
+ def is_trip_last_cycle_length_supported(self):
2331
+ response = self.trip_last_cycle_entry
2332
+ if response and type(response.get('mileageKm', None)) in (float, int):
2333
+ return True
2334
+
2335
+ @property
2336
+ def trip_last_cycle_recuperation(self):
2337
+ #Not implemented
2338
+ return self.trip_last_cycle_entry.get('recuperation')
2339
+
2340
+ @property
2341
+ def is_trip_last_cycle_recuperation_supported(self):
2342
+ #Not implemented
2343
+ response = self.trip_last_cycle_entry
2344
+ if response and type(response.get('recuperation', None)) in (float, int):
2345
+ return True
2346
+
2347
+ @property
2348
+ def trip_last_cycle_average_recuperation(self):
2349
+ #Not implemented
2350
+ value = self.trip_last_cycle_entry.get('averageRecuperation')
2351
+ return value
2352
+
2353
+ @property
2354
+ def is_trip_last_cycle_average_recuperation_supported(self):
2355
+ #Not implemented
2356
+ response = self.trip_last_cycle_entry
2357
+ if response and type(response.get('averageRecuperation', None)) in (float, int):
2358
+ return True
2359
+
2360
+ @property
2361
+ def trip_last_cycle_total_electric_consumption(self):
2362
+ #Not implemented
2363
+ return self.trip_last_cycle_entry.get('totalElectricConsumption')
2364
+
2365
+ @property
2366
+ def is_trip_last_cycle_total_electric_consumption_supported(self):
2367
+ #Not implemented
2368
+ response = self.trip_last_cycle_entry
2369
+ if response and type(response.get('totalElectricConsumption', None)) in (float, int):
2370
+ return True
2371
+
2372
+ # Status of set data requests
2373
+ @property
2374
+ def refresh_action_status(self):
2375
+ """Return latest status of data refresh request."""
2376
+ return self._requests.get('refresh', {}).get('status', 'None')
2377
+
2378
+ @property
2379
+ def refresh_action_timestamp(self):
2380
+ """Return timestamp of latest data refresh request."""
2381
+ timestamp = self._requests.get('refresh', {}).get('timestamp', DATEZERO)
2382
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
2383
+
2384
+ @property
2385
+ def charger_action_status(self):
2386
+ """Return latest status of charger request."""
2387
+ return self._requests.get('batterycharge', {}).get('status', 'None')
2388
+
2389
+ @property
2390
+ def charger_action_timestamp(self):
2391
+ """Return timestamp of latest charger request."""
2392
+ timestamp = self._requests.get('charger', {}).get('timestamp', DATEZERO)
2393
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
2394
+
2395
+ @property
2396
+ def climater_action_status(self):
2397
+ """Return latest status of climater request."""
2398
+ return self._requests.get('climatisation', {}).get('status', 'None')
2399
+
2400
+ @property
2401
+ def climater_action_timestamp(self):
2402
+ """Return timestamp of latest climater request."""
2403
+ timestamp = self._requests.get('climatisation', {}).get('timestamp', DATEZERO)
2404
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
2405
+
2406
+ @property
2407
+ def pheater_action_status(self):
2408
+ """Return latest status of parking heater request."""
2409
+ return self._requests.get('preheater', {}).get('status', 'None')
2410
+
2411
+ @property
2412
+ def pheater_action_timestamp(self):
2413
+ """Return timestamp of latest parking heater request."""
2414
+ timestamp = self._requests.get('preheater', {}).get('timestamp', DATEZERO)
2415
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
2416
+
2417
+ @property
2418
+ def honkandflash_action_status(self):
2419
+ """Return latest status of honk and flash action request."""
2420
+ return self._requests.get('honkandflash', {}).get('status', 'None')
2421
+
2422
+ @property
2423
+ def honkandflash_action_timestamp(self):
2424
+ """Return timestamp of latest honk and flash request."""
2425
+ timestamp = self._requests.get('honkandflash', {}).get('timestamp', DATEZERO)
2426
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
2427
+
2428
+ @property
2429
+ def lock_action_status(self):
2430
+ """Return latest status of lock action request."""
2431
+ return self._requests.get('lock', {}).get('status', 'None')
2432
+
2433
+ @property
2434
+ def lock_action_timestamp(self):
2435
+ """Return timestamp of latest lock action request."""
2436
+ timestamp = self._requests.get('lock', {}).get('timestamp', DATEZERO)
2437
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
2438
+
2439
+ @property
2440
+ def timer_action_status(self):
2441
+ """Return latest status of departure timer request."""
2442
+ return self._requests.get('departuretimer', {}).get('status', 'None')
2443
+
2444
+ @property
2445
+ def timer_action_timestamp(self):
2446
+ """Return timestamp of latest departure timer request."""
2447
+ timestamp = self._requests.get('departuretimer', {}).get('timestamp', DATEZERO)
2448
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
2449
+
2450
+ @property
2451
+ def refresh_data(self):
2452
+ """Get state of data refresh"""
2453
+ if self._requests.get('refresh', {}).get('id', False):
2454
+ timestamp = self._requests.get('refresh', {}).get('timestamp', DATEZERO)
2455
+ expired = datetime.now() - timedelta(minutes=2)
2456
+ if expired < timestamp:
2457
+ return True
2458
+ return False
2459
+
2460
+ @property
2461
+ def is_refresh_data_supported(self):
2462
+ """Data refresh is supported."""
2463
+ if self._connectivities.get('mode', '') == 'online':
2464
+ return True
2465
+
2466
+ # Honk and flash
2467
+ @property
2468
+ def request_honkandflash(self):
2469
+ """State is always False"""
2470
+ return False
2471
+
2472
+ @property
2473
+ def is_request_honkandflash_supported(self):
2474
+ """Honk and flash is supported if service is enabled."""
2475
+ if self._relevantCapabilties.get('honkAndFlash', {}).get('active', False):
2476
+ return True
2477
+
2478
+ @property
2479
+ def request_flash(self):
2480
+ """State is always False"""
2481
+ return False
2482
+
2483
+ @property
2484
+ def is_request_flash_supported(self):
2485
+ """Honk and flash is supported if service is enabled."""
2486
+ if self._relevantCapabilties.get('honkAndFlash', {}).get('active', False):
2487
+ return True
2488
+
2489
+ # Requests data
2490
+ @property
2491
+ def request_in_progress(self):
2492
+ """Request in progress is always supported."""
2493
+ try:
2494
+ for section in self._requests:
2495
+ if self._requests[section].get('id', False):
2496
+ return True
2497
+ except:
2498
+ pass
2499
+ return False
2500
+
2501
+ @property
2502
+ def is_request_in_progress_supported(self):
2503
+ """Request in progress is always supported."""
2504
+ return True
2505
+
2506
+ @property
2507
+ def request_results(self):
2508
+ """Get last request result."""
2509
+ data = {
2510
+ 'latest': self._requests.get('latest', 'N/A'),
2511
+ 'state': self._requests.get('state', 'N/A'),
2512
+ }
2513
+ for section in self._requests:
2514
+ if section in ['departuretimer', 'batterycharge', 'climatisation', 'refresh', 'lock', 'preheater']:
2515
+ timestamp = self._requests.get(section, {}).get('timestamp', DATEZERO)
2516
+ data[section] = self._requests[section].get('status', 'N/A')
2517
+ data[section+'_timestamp'] = timestamp.strftime("%Y-%m-%d %H:%M:%S")
2518
+ return data
2519
+
2520
+ @property
2521
+ def is_request_results_supported(self):
2522
+ """Request results is supported if in progress is supported."""
2523
+ return self.is_request_in_progress_supported
2524
+
2525
+ @property
2526
+ def requests_remaining(self):
2527
+ """Get remaining requests before throttled."""
2528
+ if self.attrs.get('rate_limit_remaining', False):
2529
+ self.requests_remaining = self.attrs.get('rate_limit_remaining')
2530
+ self.attrs.pop('rate_limit_remaining')
2531
+ return self._requests['remaining']
2532
+
2533
+ @requests_remaining.setter
2534
+ def requests_remaining(self, value):
2535
+ self._requests['remaining'] = value
2536
+
2537
+ @property
2538
+ def is_requests_remaining_supported(self):
2539
+ if self.is_request_in_progress_supported:
2540
+ return True if self._requests.get('remaining', False) else False
2541
+
2542
+ #### Helper functions ####
2543
+ def __str__(self):
2544
+ return self.vin
2545
+
2546
+ @property
2547
+ def json(self):
2548
+ def serialize(obj):
2549
+ if isinstance(obj, datetime):
2550
+ return obj.isoformat()
2551
+
2552
+ return to_json(
2553
+ OrderedDict(sorted(self.attrs.items())),
2554
+ indent=4,
2555
+ default=serialize
2556
+ )