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