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/__init__.py +7 -0
- pycupra/__version__.py +6 -0
- pycupra/connection.py +1617 -0
- pycupra/const.py +180 -0
- pycupra/dashboard.py +1258 -0
- pycupra/exceptions.py +87 -0
- pycupra/utilities.py +116 -0
- pycupra/vehicle.py +2556 -0
- pycupra-0.0.1.dist-info/METADATA +57 -0
- pycupra-0.0.1.dist-info/RECORD +13 -0
- pycupra-0.0.1.dist-info/WHEEL +5 -0
- pycupra-0.0.1.dist-info/licenses/LICENSE +201 -0
- pycupra-0.0.1.dist-info/top_level.txt +1 -0
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
|
+
)
|