pycupra 0.1.7__py3-none-any.whl → 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pycupra/__version__.py CHANGED
@@ -3,4 +3,4 @@ pycupra - A Python 3 library for interacting with the My Cupra/My Seat portal.
3
3
 
4
4
  For more details and documentation, visit the github page at https://github.com/WulfgarW/pycupra
5
5
  """
6
- __version__ = "0.1.7"
6
+ __version__ = "0.1.8"
pycupra/connection.py CHANGED
@@ -109,13 +109,14 @@ TIMEOUT = timedelta(seconds=90)
109
109
  class Connection:
110
110
  """ Connection to Connect services """
111
111
  # Init connection class
112
- def __init__(self, session, brand='cupra', username='', password='', fulldebug=False, nightlyUpdateReduction=False, anonymise=True, **optional):
112
+ def __init__(self, session, brand='cupra', username='', password='', fulldebug=False, nightlyUpdateReduction=False, anonymise=True, tripStatisticsStartYear=None, **optional):
113
113
  """ Initialize """
114
114
  self._session = session
115
115
  self._lock = asyncio.Lock()
116
116
  self._session_fulldebug = fulldebug
117
117
  self._session_nightlyUpdateReduction = nightlyUpdateReduction
118
118
  self._session_anonymise = anonymise
119
+ self._session_tripStatisticsStartYear = tripStatisticsStartYear
119
120
  self._session_headers = HEADERS_SESSION.get(brand).copy()
120
121
  self._session_base = BASE_SESSION
121
122
  self._session_auth_headers = HEADERS_AUTH.copy()
@@ -636,7 +637,12 @@ class Connection:
636
637
  async def _request(self, method, url, **kwargs):
637
638
  """Perform a HTTP query"""
638
639
  if self._session_fulldebug:
639
- _LOGGER.debug(self.anonymise(f'HTTP {method} "{url}"'))
640
+ argsString =''
641
+ if len(kwargs)>0:
642
+ argsString = 'with '
643
+ for k, val in kwargs.items():
644
+ argsString = argsString + f"{k}=\'{val}\' "
645
+ _LOGGER.debug(self.anonymise(f'HTTP {method} "{url}" {argsString}'))
640
646
  try:
641
647
  if datetime.now(tz=None).date() != self._sessionRequestTimestamp.date():
642
648
  # A new day has begun. Store _sessionRequestCounter in history and reset timestamp and counter
@@ -701,6 +707,9 @@ class Connection:
701
707
  if self._session_fulldebug:
702
708
  if 'image/png' in response.headers.get('Content-Type', ''):
703
709
  _LOGGER.debug(self.anonymise(f'Request for "{url}" returned with status code [{response.status}]. Not showing response for Content-Type image/png.'))
710
+ elif method==METH_PUT or method==METH_DELETE:
711
+ # deepcopy() of res can produce errors, if res is the API response on PUT or DELETE
712
+ _LOGGER.debug(f'Request for "{self.anonymise(url)}" returned with status code [{response.status}]. Not showing response for http {method}')
704
713
  else:
705
714
  _LOGGER.debug(self.anonymise(f'Request for "{url}" returned with status code [{response.status}], response: {self.anonymise(deepcopy(res))}'))
706
715
  else:
@@ -1080,6 +1089,12 @@ class Connection:
1080
1089
  async def getTripStatistics(self, vin, baseurl, supportsCyclicTrips):
1081
1090
  """Get short term and cyclic trip statistics."""
1082
1091
  await self.set_token(self._session_auth_brand)
1092
+ if self._session_tripStatisticsStartYear==None:
1093
+ # If connection was not initialised with parameter tripStatisticsStartYear, then the value of the last year is used
1094
+ # (This keeps the statistics shorter in Home Assistant)
1095
+ startYear = datetime.now().year - 1
1096
+ else:
1097
+ startYear = self._session_tripStatisticsStartYear
1083
1098
  try:
1084
1099
  data={'tripstatistics': {}}
1085
1100
  if supportsCyclicTrips:
pycupra/const.py CHANGED
@@ -138,7 +138,7 @@ API_DEPARTURE_TIMERS = '{baseurl}/v1/vehicles/{vin}/departure-timers'
138
138
  API_DEPARTURE_PROFILES = '{baseurl}/v1/vehicles/{vin}/departure/profiles' # Departure profiles
139
139
  API_POSITION = '{baseurl}/v1/vehicles/{vin}/parkingposition' # Position data
140
140
  API_POS_TO_ADDRESS= 'https://maps.googleapis.com/maps/api/directions/json?origin={lat},{lon}&destination={lat},{lon}&traffic_model=best_guess&departure_time=now&language=de&key={apiKeyForGoogle}&mode=driving'
141
- API_TRIP = '{baseurl}/v1/vehicles/{vin}/driving-data/{dataType}?from=1970-01-01T00:00:00Z&to=2099-12-31T09:59:01Z' # Trip statistics (whole history) SHORT/LONG/CYCLIC (WEEK only with from)
141
+ API_TRIP = '{baseurl}/v1/vehicles/{vin}/driving-data/{dataType}?from={startYear}-07-01T00:00:00Z&to=2099-12-31T09:59:01Z' # Trip statistics (whole history) SHORT/LONG/CYCLIC (WEEK only with from)
142
142
  API_MILEAGE = '{baseurl}/v1/vehicles/{vin}/mileage' # Total km etc
143
143
  API_MAINTENANCE = '{baseurl}/v1/vehicles/{vin}/maintenance' # Inspection information
144
144
  API_MEASUREMENTS = '{baseurl}/v1/vehicles/{vin}/measurements/engines' # ???
pycupra/dashboard.py CHANGED
@@ -1002,6 +1002,32 @@ class ChargingState(BinarySensor):
1002
1002
  attr['mode']=mode
1003
1003
  return attr
1004
1004
 
1005
+ class AreaAlarm(BinarySensor):
1006
+ def __init__(self):
1007
+ super().__init__(attr="area_alarm", name="Area alarm", icon="mdi:alarm-light", device_class='safety')
1008
+
1009
+ @property
1010
+ def state(self):
1011
+ return self.vehicle.area_alarm
1012
+
1013
+ @property
1014
+ def assumed_state(self):
1015
+ return False
1016
+
1017
+ @property
1018
+ def attributes(self):
1019
+ attr = {}
1020
+ type = self.vehicle.attrs.get('areaAlarm', {}).get('type', '')
1021
+ zones = self.vehicle.attrs.get('areaAlarm', {}).get('zones', [])
1022
+ timestamp = self.vehicle.attrs.get('areaAlarm', {}).get('timestamp', 0)
1023
+ if type != '':
1024
+ attr['type']=type
1025
+ if len(zones) > 0:
1026
+ attr['zone']=zones[0]
1027
+ if timestamp != 0:
1028
+ attr['timestamp']=timestamp
1029
+ return attr
1030
+
1005
1031
  def create_instruments():
1006
1032
  return [
1007
1033
  Position(),
@@ -1030,6 +1056,7 @@ def create_instruments():
1030
1056
  DepartureProfile2(),
1031
1057
  DepartureProfile3(),
1032
1058
  ChargingState(),
1059
+ AreaAlarm(),
1033
1060
  Sensor(
1034
1061
  attr="distance",
1035
1062
  name="Odometer",
@@ -92,10 +92,10 @@ class FcmPushClientConfig: # pylint:disable=too-many-instance-attributes
92
92
  """Class to provide configuration to
93
93
  :class:`firebase_messaging.FcmPushClientConfig`.FcmPushClient."""
94
94
 
95
- server_heartbeat_interval: int | None = 20 # original value was 10
95
+ server_heartbeat_interval: int | None = 30 # original value was 10
96
96
  """Time in seconds to request the server to send heartbeats"""
97
97
 
98
- client_heartbeat_interval: int | None = 30 # original value was 20
98
+ client_heartbeat_interval: int | None = 40 # original value was 20
99
99
  """Time in seconds to send heartbeats to the server"""
100
100
 
101
101
  send_selective_acknowledgements: bool = True
@@ -218,6 +218,7 @@ class FcmPushClient: # pylint:disable=too-many-instance-attributes
218
218
  or (self.stopping_lock and self.stopping_lock.locked())
219
219
  or not self.do_listen
220
220
  ):
221
+ _logger.debug(f"In _reset. reset_lock={self.reset_lock}, reset_lock.locked={self.reset_lock.locked()}, stopping_lock={self.stopping_lock}, stopping_lock.locked={self.stopping_lock.locked()}, do_listen={self.do_listen}")
221
222
  return
222
223
 
223
224
  async with self.reset_lock: # type: ignore[union-attr]
@@ -725,7 +726,10 @@ class FcmPushClient: # pylint:disable=too-many-instance-attributes
725
726
  else:
726
727
  _logger.exception("Unexpected exception during read\n")
727
728
  if self._try_increment_error_count(ErrorType.CONNECTION):
729
+ _logger.debug("Calling reset()\n")
728
730
  await self._reset()
731
+ else:
732
+ _logger.debug("Not calling reset()\n")
729
733
  except Exception as ex:
730
734
  _logger.error(
731
735
  "Unknown error: %s, shutting down FcmPushClient.\n%s",
@@ -805,3 +809,4 @@ class FcmPushClient: # pylint:disable=too-many-instance-attributes
805
809
  dms.persistent_id = persistent_id
806
810
 
807
811
  # Not supported yet
812
+
pycupra/vehicle.py CHANGED
@@ -4,6 +4,7 @@
4
4
  import re
5
5
  import logging
6
6
  import asyncio
7
+ import json
7
8
 
8
9
  from copy import deepcopy
9
10
  from datetime import datetime, timedelta, timezone
@@ -72,7 +73,6 @@ class Vehicle:
72
73
  self._relevantCapabilties = {
73
74
  'measurements': {'active': False, 'reason': 'not supported', },
74
75
  'climatisation': {'active': False, 'reason': 'not supported'},
75
- #'parkingInformation': {'active': False, 'reason': 'not supported'},
76
76
  'tripStatistics': {'active': False, 'reason': 'not supported', 'supportsCyclicTrips': False},
77
77
  'vehicleHealthInspection': {'active': False, 'reason': 'not supported'},
78
78
  'vehicleHealthWarnings': {'active': False, 'reason': 'not supported'},
@@ -95,6 +95,7 @@ class Vehicle:
95
95
  self._last_get_charger = datetime.now(tz=None) - timedelta(seconds=600)
96
96
  self._last_get_climater = datetime.now(tz=None) - timedelta(seconds=600)
97
97
  self._last_get_mileage = datetime.now(tz=None) - timedelta(seconds=600)
98
+ self._last_get_position = datetime.now(tz=None) - timedelta(seconds=600)
98
99
 
99
100
 
100
101
  #### API get and set functions ####
@@ -131,8 +132,7 @@ class Vehicle:
131
132
  self._relevantCapabilties[id].update(data)
132
133
 
133
134
 
134
- #await self.get_trip_statistic() # in full update
135
-
135
+
136
136
  # Get URLs for model image
137
137
  self._modelimages = await self.get_modelimageurl()
138
138
 
@@ -148,11 +148,16 @@ class Vehicle:
148
148
  hourago = datetime.now() - timedelta(hours = 2)
149
149
  if self._discovered < hourago:
150
150
  await self.discover()
151
- #_LOGGER.debug('Achtung! self.discover() auskommentiert')
152
151
 
153
152
  # Fetch all data if car is not deactivated
154
153
  if not self.deactivated:
155
154
  try:
155
+ if self.attrs.get('areaAlarm', {}) !={}:
156
+ # Delete an area alarm if it is older than 900 seconds
157
+ alarmTimestamp = self.attrs.get('areaAlarm', {}).get('timestamp', 0)
158
+ if alarmTimestamp < datetime.now(tz=None) - timedelta(seconds= 900):
159
+ self.attrs.pop("areaAlarm")
160
+
156
161
  if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
157
162
  # Check, if fcmpushclient still started
158
163
  if not self.firebase._pushClient.is_started():
@@ -170,6 +175,26 @@ class Vehicle:
170
175
  else:
171
176
  fullUpdateExpired = datetime.now(tz=None) - timedelta(seconds= 1100)
172
177
 
178
+ if self.firebaseStatus == FIREBASE_STATUS_ACTIVATION_STOPPED:
179
+ # Trying to activate firebase connection again
180
+ """_LOGGER.debug(f'As firebase status={self.firebaseStatus}, fcmpushclient.start() is called.')
181
+ await self.firebase._pushClient.start()
182
+ #await asyncio.sleep(5)
183
+ if self.firebase._pushClient.is_started():
184
+ self.firebaseStatus = FIREBASE_STATUS_ACTIVATED
185
+ _LOGGER.debug(f'Successfully restarted push client. New firebase status={self.firebaseStatus}')
186
+ else:
187
+ _LOGGER.warning(f'Restart of push client failed. Firebase status={self.firebaseStatus}')"""
188
+ newStatus = await self.stopFirebase()
189
+ if newStatus != FIREBASE_STATUS_NOT_INITIALISED:
190
+ _LOGGER.debug(f'stopFirebase() not successful.')
191
+ newStatus = await self.initialiseFirebase(self._firebaseCredentialsFileName, self.updateCallback)
192
+ if newStatus == FIREBASE_STATUS_ACTIVATED:
193
+ _LOGGER.debug(f'Reinitialisation of firebase successful.New firebase status={self.firebaseStatus}.')
194
+ else:
195
+ self.firebaseStatus = FIREBASE_STATUS_ACTIVATION_STOPPED
196
+ _LOGGER.warning(f'Reinitialisation of firebase failed. New firebase status={self.firebaseStatus}.')
197
+
173
198
  if self._connection._session_nightlyUpdateReduction:
174
199
  # nightlyUpdateReduction is activated
175
200
  if datetime.now(tz=None).hour<5 or datetime.now(tz=None).hour>=22:
@@ -198,16 +223,6 @@ class Vehicle:
198
223
  if self.firebaseStatus != FIREBASE_STATUS_ACTIVATED:
199
224
  await self.get_mileage()
200
225
 
201
- if self.firebaseStatus == FIREBASE_STATUS_ACTIVATION_STOPPED:
202
- # Trying to activate firebase connection again
203
- await self.firebase._pushClient.start()
204
- #await asyncio.sleep(5)
205
- if self.firebase._pushClient.is_started():
206
- self.firebaseStatus = FIREBASE_STATUS_ACTIVATED
207
- _LOGGER.debug(f'Successfully restarted push client. New firebase status ={self.firebaseStatus}')
208
- else:
209
- _LOGGER.debug(f'Restart of push client failed. Firebase status ={self.firebaseStatus}')
210
-
211
226
 
212
227
  await asyncio.gather(
213
228
  #self.get_statusreport(),
@@ -244,6 +259,10 @@ class Vehicle:
244
259
  data = await self._connection.getBasicCarData(self.vin, self._apibase)
245
260
  if data:
246
261
  self._states.update(data)
262
+ return True
263
+ else:
264
+ _LOGGER.debug('Could not fetch basic car data')
265
+ return False
247
266
 
248
267
  async def get_mileage(self):
249
268
  """Fetch basic car data."""
@@ -251,6 +270,10 @@ class Vehicle:
251
270
  if data:
252
271
  self._states.update(data)
253
272
  self._last_get_mileage = datetime.now(tz=None)
273
+ return True
274
+ else:
275
+ _LOGGER.debug('Could not fetch mileage data')
276
+ return False
254
277
 
255
278
  async def get_preheater(self):
256
279
  """Fetch pre-heater data if function is enabled."""
@@ -272,8 +295,10 @@ class Vehicle:
272
295
  if data:
273
296
  self._states.update(data)
274
297
  self._last_get_climater = datetime.now(tz=None)
298
+ return True
275
299
  else:
276
300
  _LOGGER.debug('Could not fetch climater data')
301
+ return False
277
302
  #else:
278
303
  # self._requests.pop('climatisation', None)
279
304
 
@@ -283,8 +308,10 @@ class Vehicle:
283
308
  data = await self._connection.getTripStatistics(self.vin, self._apibase, self._relevantCapabilties['tripStatistics'].get('supportsCyclicTrips', False))
284
309
  if data:
285
310
  self._states.update(data)
311
+ return True
286
312
  else:
287
313
  _LOGGER.debug('Could not fetch trip statistics')
314
+ return False
288
315
 
289
316
  async def get_position(self):
290
317
  """Fetch position data if function is enabled."""
@@ -301,24 +328,21 @@ class Vehicle:
301
328
  except:
302
329
  pass
303
330
  self._states.update(data)
331
+ self._last_get_position = datetime.now(tz=None)
332
+ return True
304
333
  else:
305
334
  _LOGGER.debug('Could not fetch any positional data')
335
+ return False
306
336
 
307
337
  async def get_vehicleHealthWarnings(self):
308
338
  if self._relevantCapabilties.get('vehicleHealthWarnings', {}).get('active', False):
309
339
  data = await self._connection.getVehicleHealthWarnings(self.vin, self._apibase)
310
340
  if data:
311
- #warningsList = data.get('warninglights',{}).get('statuses',[])
312
- #for i in range(len(warningsList)):
313
- # _LOGGER.debug(f'Element {i} in warninglights: {warningsList[i]}')
314
- # if isinstance(warningsList[i], dict):
315
- # if warningsList[i].get('icon',''):
316
- # #Value of icon is very long and can lead to problems
317
- # _LOGGER.debug(f'Substituting value of icon by \'DELETED\'')
318
- # data['warninglights']['statuses'][i]['icon']='DELETED'
319
341
  self._states.update(data)
342
+ return True
320
343
  else:
321
344
  _LOGGER.debug('Could not fetch vehicle health warnings')
345
+ return False
322
346
 
323
347
  async def get_statusreport(self):
324
348
  """Fetch status data if function is enabled."""
@@ -327,8 +351,10 @@ class Vehicle:
327
351
  if data:
328
352
  self._states.update(data)
329
353
  self._last_get_statusreport = datetime.now(tz=None)
354
+ return True
330
355
  else:
331
356
  _LOGGER.debug('Could not fetch status report')
357
+ return False
332
358
 
333
359
  async def get_maintenance(self):
334
360
  """Fetch maintenance data if function is enabled."""
@@ -336,8 +362,10 @@ class Vehicle:
336
362
  data = await self._connection.getMaintenance(self.vin, self._apibase)
337
363
  if data:
338
364
  self._states.update(data)
365
+ return True
339
366
  else:
340
367
  _LOGGER.debug('Could not fetch status report')
368
+ return False
341
369
 
342
370
  async def get_charger(self):
343
371
  """Fetch charger data if function is enabled."""
@@ -346,8 +374,10 @@ class Vehicle:
346
374
  if data:
347
375
  self._states.update(data)
348
376
  self._last_get_charger = datetime.now(tz=None)
377
+ return True
349
378
  else:
350
379
  _LOGGER.debug('Could not fetch charger data')
380
+ return False
351
381
 
352
382
  async def get_departure_timers(self):
353
383
  """Fetch timer data if function is enabled."""
@@ -356,8 +386,10 @@ class Vehicle:
356
386
  if data:
357
387
  self._states.update(data)
358
388
  self._last_get_departure_timers = datetime.now(tz=None)
389
+ return True
359
390
  else:
360
391
  _LOGGER.debug('Could not fetch timers')
392
+ return False
361
393
 
362
394
  async def get_departure_profiles(self):
363
395
  """Fetch timer data if function is enabled."""
@@ -366,8 +398,10 @@ class Vehicle:
366
398
  if data:
367
399
  self._states.update(data)
368
400
  self._last_get_departure_profiles = datetime.now(tz=None)
401
+ return True
369
402
  else:
370
403
  _LOGGER.debug('Could not fetch timers')
404
+ return False
371
405
 
372
406
  #async def wait_for_request(self, section, request, retryCount=36):
373
407
  """Update status of outstanding requests."""
@@ -1000,7 +1034,7 @@ class Vehicle:
1000
1034
  async def set_climatisation_temp(self, temperature=20):
1001
1035
  """Set climatisation target temp."""
1002
1036
  if self.is_electric_climatisation_supported or self.is_auxiliary_climatisation_supported:
1003
- if 16 <= int(temperature) <= 30:
1037
+ if 16 <= float(temperature) <= 30:
1004
1038
  data = {
1005
1039
  'climatisationWithoutExternalPower': self.climatisation_without_external_power,
1006
1040
  'targetTemperature': temperature,
@@ -1051,28 +1085,30 @@ class Vehicle:
1051
1085
  data = {}
1052
1086
  # Validate user input
1053
1087
  if mode.lower() not in ['electric', 'auxiliary', 'start', 'stop', 'on', 'off']:
1088
+ _LOGGER.error(f"Invalid mode for 'set_climatisation': {mode}")
1054
1089
  raise SeatInvalidRequestException(f"Invalid mode for set_climatisation: {mode}")
1055
1090
  elif mode == 'auxiliary' and spin is None:
1056
1091
  raise SeatInvalidRequestException("Starting auxiliary heater requires provided S-PIN")
1057
1092
  if temp is not None:
1058
- if not isinstance(temp, float):
1093
+ if not isinstance(temp, float) and not isinstance(temp, int):
1094
+ _LOGGER.error(f"Invalid type for temp. type={type(temp)}")
1059
1095
  raise SeatInvalidRequestException(f"Invalid type for temp")
1060
1096
  elif not 16 <= float(temp) <=30:
1061
1097
  raise SeatInvalidRequestException(f"Invalid value for temp")
1062
1098
  else:
1063
1099
  temp = self.climatisation_target_temperature
1064
- if hvpower is not None:
1065
- if not isinstance(hvpower, bool):
1066
- raise SeatInvalidRequestException(f"Invalid type for hvpower")
1100
+ #if hvpower is not None:
1101
+ # if not isinstance(hvpower, bool):
1102
+ # raise SeatInvalidRequestException(f"Invalid type for hvpower")
1067
1103
  if self.is_electric_climatisation_supported:
1068
1104
  if self._relevantCapabilties.get('climatisation', {}).get('active', False):
1069
1105
  if mode in ['Start', 'start', 'Electric', 'electric', 'On', 'on']:
1070
1106
  mode = 'start'
1071
1107
  if mode in ['start', 'auxiliary']:
1072
- if hvpower is not None:
1073
- withoutHVPower = hvpower
1074
- else:
1075
- withoutHVPower = self.climatisation_without_external_power
1108
+ #if hvpower is not None:
1109
+ # withoutHVPower = hvpower
1110
+ #else:
1111
+ # withoutHVPower = self.climatisation_without_external_power
1076
1112
  data = {
1077
1113
  'targetTemperature': temp,
1078
1114
  'targetTemperatureUnit': 'celsius',
@@ -1159,7 +1195,7 @@ class Vehicle:
1159
1195
  self._requests['climatisation'] = {'status': 'Exception'}
1160
1196
  raise SeatException('Climatisation action failed')
1161
1197
 
1162
- # Parking heater heating/ventilation (RS)
1198
+ # Parking heater heating/ventilation (RS)
1163
1199
  async def set_pheater(self, mode, spin):
1164
1200
  """Set the mode for the parking heater."""
1165
1201
  if not self.is_pheater_heating_supported:
@@ -1202,7 +1238,7 @@ class Vehicle:
1202
1238
  self._requests['preheater'] = {'status': 'Exception'}
1203
1239
  raise SeatException('Pre-heater action failed')
1204
1240
 
1205
- # Lock
1241
+ # Lock
1206
1242
  async def set_lock(self, action, spin):
1207
1243
  """Remote lock and unlock actions."""
1208
1244
  #if not self._services.get('rlu_v1', False):
@@ -1264,7 +1300,7 @@ class Vehicle:
1264
1300
  self._requests['lock'] = {'status': 'Exception'}
1265
1301
  raise SeatException('Lock action failed')
1266
1302
 
1267
- # Honk and flash (RHF)
1303
+ # Honk and flash (RHF)
1268
1304
  async def set_honkandflash(self, action, lat=None, lng=None):
1269
1305
  """Turn on/off honk and flash."""
1270
1306
  if not self._relevantCapabilties.get('honkAndFlash', {}).get('active', False):
@@ -1321,7 +1357,7 @@ class Vehicle:
1321
1357
  self._requests['honkandflash'] = {'status': 'Exception'}
1322
1358
  raise SeatException('Honk and flash action failed')
1323
1359
 
1324
- # Refresh vehicle data (VSR)
1360
+ # Refresh vehicle data (VSR)
1325
1361
  async def set_refresh(self):
1326
1362
  """Wake up vehicle and update status data."""
1327
1363
  if not self._relevantCapabilties.get('state', {}).get('active', False):
@@ -1357,7 +1393,7 @@ class Vehicle:
1357
1393
  raise SeatException('Data refresh failed')
1358
1394
 
1359
1395
  #### Vehicle class helpers ####
1360
- # Vehicle info
1396
+ # Vehicle info
1361
1397
  @property
1362
1398
  def attrs(self):
1363
1399
  return self._states
@@ -1390,7 +1426,7 @@ class Vehicle:
1390
1426
 
1391
1427
 
1392
1428
  #### Information from vehicle states ####
1393
- # Car information
1429
+ # Car information
1394
1430
  @property
1395
1431
  def nickname(self):
1396
1432
  return self._properties.get('vehicleNickname', '')
@@ -1479,7 +1515,7 @@ class Vehicle:
1479
1515
  if self._modelimages is not None:
1480
1516
  return True
1481
1517
 
1482
- # Lights
1518
+ # Lights
1483
1519
  @property
1484
1520
  def parking_light(self):
1485
1521
  """Return true if parking light is on"""
@@ -1498,7 +1534,7 @@ class Vehicle:
1498
1534
  else:
1499
1535
  return False
1500
1536
 
1501
- # Connection status
1537
+ # Connection status
1502
1538
  @property
1503
1539
  def last_connected(self):
1504
1540
  """Return when vehicle was last connected to connect servers."""
@@ -1515,7 +1551,7 @@ class Vehicle:
1515
1551
  if 'updatedAt' in self.attrs.get('status', {}):
1516
1552
  return True
1517
1553
 
1518
- # Update status
1554
+ # Update status
1519
1555
  @property
1520
1556
  def last_full_update(self):
1521
1557
  """Return when the last full update for the vehicle took place."""
@@ -1527,7 +1563,7 @@ class Vehicle:
1527
1563
  if hasattr(self,'_last_full_update'):
1528
1564
  return True
1529
1565
 
1530
- # Service information
1566
+ # Service information
1531
1567
  @property
1532
1568
  def distance(self):
1533
1569
  """Return vehicle odometer."""
@@ -1615,7 +1651,7 @@ class Vehicle:
1615
1651
  return True
1616
1652
  return False
1617
1653
 
1618
- # Charger related states for EV and PHEV
1654
+ # Charger related states for EV and PHEV
1619
1655
  @property
1620
1656
  def charging(self):
1621
1657
  """Return battery level"""
@@ -1874,7 +1910,7 @@ class Vehicle:
1874
1910
  if self.attrs.get('charging', {}).get('info', {}).get('settings', {}).get('targetSoc', False):
1875
1911
  return True
1876
1912
 
1877
- # Vehicle location states
1913
+ # Vehicle location states
1878
1914
  @property
1879
1915
  def position(self):
1880
1916
  """Return position."""
@@ -1941,7 +1977,7 @@ class Vehicle:
1941
1977
  if 'parkingTimeUTC' in self.attrs.get('findCarResponse', {}):
1942
1978
  return True
1943
1979
 
1944
- # Vehicle fuel level and range
1980
+ # Vehicle fuel level and range
1945
1981
  @property
1946
1982
  def primary_range(self):
1947
1983
  value = -1
@@ -2080,7 +2116,7 @@ class Vehicle:
2080
2116
  return self.is_secondary_range_supported
2081
2117
  return False
2082
2118
 
2083
- # Climatisation settings
2119
+ # Climatisation settings
2084
2120
  @property
2085
2121
  def climatisation_target_temperature(self):
2086
2122
  """Return the target temperature from climater."""
@@ -2154,7 +2190,7 @@ class Vehicle:
2154
2190
  else:
2155
2191
  return False
2156
2192
 
2157
- # Climatisation, electric
2193
+ # Climatisation, electric
2158
2194
  @property
2159
2195
  def electric_climatisation_attributes(self):
2160
2196
  """Return climatisation attributes."""
@@ -2262,7 +2298,7 @@ class Vehicle:
2262
2298
  return True
2263
2299
  return False
2264
2300
 
2265
- # Parking heater, "legacy" auxiliary climatisation
2301
+ # Parking heater, "legacy" auxiliary climatisation
2266
2302
  @property
2267
2303
  def pheater_duration(self):
2268
2304
  return self._climate_duration
@@ -2310,7 +2346,7 @@ class Vehicle:
2310
2346
  if self.attrs.get('heating', {}).get('climatisationStateReport', {}).get('climatisationState', False):
2311
2347
  return True
2312
2348
 
2313
- # Windows
2349
+ # Windows
2314
2350
  @property
2315
2351
  def windows_closed(self):
2316
2352
  return (self.window_closed_left_front and self.window_closed_left_back and self.window_closed_right_front and self.window_closed_right_back)
@@ -2417,7 +2453,7 @@ class Vehicle:
2417
2453
  # response = self.attrs.get('status')['windows'].get('sunRoof', '')
2418
2454
  return True if response != '' else False
2419
2455
 
2420
- # Locks
2456
+ # Locks
2421
2457
  @property
2422
2458
  def door_locked(self):
2423
2459
  # LEFT FRONT
@@ -2460,7 +2496,7 @@ class Vehicle:
2460
2496
  return True
2461
2497
  return False
2462
2498
 
2463
- # Doors, hood and trunk
2499
+ # Doors, hood and trunk
2464
2500
  @property
2465
2501
  def hood_closed(self):
2466
2502
  """Return true if hood is closed"""
@@ -2545,7 +2581,7 @@ class Vehicle:
2545
2581
  response = self.attrs.get('status')['trunk'].get('open', 0)
2546
2582
  return True if response != 0 else False
2547
2583
 
2548
- # Departure timers
2584
+ # Departure timers
2549
2585
  @property
2550
2586
  def departure1(self):
2551
2587
  """Return timer status and attributes."""
@@ -2657,7 +2693,7 @@ class Vehicle:
2657
2693
  return True
2658
2694
  return False
2659
2695
 
2660
- # Departure profiles
2696
+ # Departure profiles
2661
2697
  @property
2662
2698
  def departure_profile1(self):
2663
2699
  """Return profile status and attributes."""
@@ -2730,7 +2766,7 @@ class Vehicle:
2730
2766
  return True
2731
2767
  return False
2732
2768
 
2733
- # Trip data
2769
+ # Trip data
2734
2770
  @property
2735
2771
  def trip_last_entry(self):
2736
2772
  return self.attrs.get('tripstatistics', {}).get('short', [{},{}])[-1]
@@ -2955,7 +2991,26 @@ class Vehicle:
2955
2991
  if response and type(response.get('totalElectricConsumption', None)) in (float, int):
2956
2992
  return True
2957
2993
 
2958
- # Status of set data requests
2994
+ # Area alarm
2995
+ @property
2996
+ def area_alarm(self):
2997
+ """Return True, if attribute areaAlarm is not {}"""
2998
+ alarmPresent = self.attrs.get('areaAlarm', {})
2999
+ if alarmPresent !={}:
3000
+ # Delete an area alarm if it is older than 900 seconds
3001
+ alarmTimestamp = self.attrs.get('areaAlarm', {}).get('timestamp', 0)
3002
+ if alarmTimestamp < datetime.now(tz=None) - timedelta(seconds= 900):
3003
+ self.attrs.pop("areaAlarm")
3004
+ alarmPresent = {}
3005
+ return False if alarmPresent == {} else True
3006
+
3007
+ @property
3008
+ def is_area_alarm_supported(self):
3009
+ """Return True, if vehicle supports area alarm (always True at the moment)"""
3010
+ # Always True at the moment. Have to check, if the geofence capability is a necessary condition
3011
+ return True
3012
+
3013
+ # Status of set data requests
2959
3014
  @property
2960
3015
  def refresh_action_status(self):
2961
3016
  """Return latest status of data refresh request."""
@@ -3060,7 +3115,7 @@ class Vehicle:
3060
3115
  """Data update is supported."""
3061
3116
  return True
3062
3117
 
3063
- # Honk and flash
3118
+ # Honk and flash
3064
3119
  @property
3065
3120
  def request_honkandflash(self):
3066
3121
  """State is always False"""
@@ -3083,7 +3138,7 @@ class Vehicle:
3083
3138
  if self._relevantCapabilties.get('honkAndFlash', {}).get('active', False):
3084
3139
  return True
3085
3140
 
3086
- # Requests data
3141
+ # Requests data
3087
3142
  @property
3088
3143
  def request_in_progress(self):
3089
3144
  """Request in progress is always supported."""
@@ -3138,7 +3193,7 @@ class Vehicle:
3138
3193
  #if self.is_request_in_progress_supported:
3139
3194
  # return True if self._requests.get('remaining', False) else False
3140
3195
 
3141
- #### Helper functions ####
3196
+ #### Helper functions ####
3142
3197
  def __str__(self):
3143
3198
  return self.vin
3144
3199
 
@@ -3157,7 +3212,7 @@ class Vehicle:
3157
3212
 
3158
3213
  async def stopFirebase(self):
3159
3214
  # Check if firebase is activated
3160
- if self.firebaseStatus!= FIREBASE_STATUS_ACTIVATED:
3215
+ if self.firebaseStatus not in (FIREBASE_STATUS_ACTIVATED, FIREBASE_STATUS_ACTIVATION_STOPPED):
3161
3216
  _LOGGER.info(f'No need to stop firebase. Firebase status={self.firebaseStatus}')
3162
3217
  return self.firebaseStatus
3163
3218
 
@@ -3171,7 +3226,7 @@ class Vehicle:
3171
3226
  _LOGGER.warning('Stopping of firebase messaging failed.')
3172
3227
  return self.firebaseStatus
3173
3228
 
3174
- #await asyncio.sleep(5) # Wait to ignore the first notifications
3229
+ #await asyncio.sleep(5)
3175
3230
  self.firebaseStatus = FIREBASE_STATUS_NOT_INITIALISED
3176
3231
  _LOGGER.info('Stopping of firebase messaging was successful.')
3177
3232
  return self.firebaseStatus
@@ -3236,17 +3291,25 @@ class Vehicle:
3236
3291
  _LOGGER.debug(f'Received push notification: notification id={notification}, type={obj.get('data',{}).get('type','')}, requestId={obj.get('data',{}).get('requestId','[None]')}')
3237
3292
  _LOGGER.debug(f' data_message={data_message}, payload={obj.get('data',{}).get('payload','[None]')}')
3238
3293
 
3239
- #temporary output of notifications in a file
3240
- if self.updateCallback == self.update:
3241
- self.storeFirebaseNotifications(obj, notification, data_message)
3294
+ #temporary output of notifications in a file, will be removed in the next release
3295
+ #if self.updateCallback == self.update:
3296
+ # self.storeFirebaseNotifications(obj, notification, data_message)
3242
3297
 
3243
3298
  if self.firebaseStatus != FIREBASE_STATUS_ACTIVATED:
3244
- _LOGGER.info(f'While firebase is not fully activated, received notifications are just acknowledged.')
3245
- # As long as the firebase status is not set to activated, ignore the notifications
3246
- return False
3299
+ if self.firebaseStatus != FIREBASE_STATUS_ACTIVATION_STOPPED:
3300
+ _LOGGER.info(f'While firebase is not fully activated, received notifications are just acknowledged.')
3301
+ # As long as the firebase status is not set to activated, ignore the notifications
3302
+ return False
3303
+ else:
3304
+ # It seems that the firebase connection still works although fcmpushclient.is_started() returned False some time ago
3305
+ _LOGGER.info(f'Firebase status={self.firebaseStatus}, but PyCupra still receives push notifications.')
3306
+ self.firebaseStatus = FIREBASE_STATUS_ACTIVATED
3307
+ _LOGGER.info(f'Set firebase status back to {self.firebaseStatus}.')
3308
+
3247
3309
 
3248
3310
  type = obj.get('data',{}).get('type','')
3249
3311
  requestId = obj.get('data',{}).get('requestId','')
3312
+ payload = obj.get('data',{}).get('payload','')
3250
3313
  openRequest = -1
3251
3314
  if requestId != '':
3252
3315
  _LOGGER.info(f'Received notification of type \'{type}\', request id={requestId} ')
@@ -3317,6 +3380,28 @@ class Vehicle:
3317
3380
  await self.updateCallback(2)
3318
3381
  else:
3319
3382
  _LOGGER.debug(f'It is now {datetime.now(tz=None)}. Last get_climater was at {self._last_get_climater}. So no need to update.')
3383
+ elif type in ('vehicle-area-alarm-vehicle-exits-zone-triggered', 'vehicle-area-alarm-vehicle-enters-zone-triggered'):
3384
+ #if self._last_get_position < datetime.now(tz=None) - timedelta(seconds= 30):
3385
+ # # Update position data only if the last one is older than timedelta
3386
+ # await self.get_position()
3387
+ #else:
3388
+ # _LOGGER.debug(f'It is now {datetime.now(tz=None)}. Last get_position was at {self._last_get_position}. So no need to update.')
3389
+ if payload != '':
3390
+ payloadDict = json.loads(payload) # Convert json string to dict
3391
+ #_LOGGER.debug(f'payloadDict is dict: {isinstance(payloadDict, dict)}')
3392
+ zones = payloadDict.get('description',{}).get('values',[])
3393
+ else:
3394
+ _LOGGER.warning(f'Missing information about areas. Payload ={payload}')
3395
+ zones = []
3396
+ areaAlarm = {'areaAlarm' : {
3397
+ 'type': 'vehicle-exits-zone' if type=='vehicle-area-alarm-vehicle-exits-zone-triggered' else 'vehicle-enters-zone',
3398
+ 'timestamp': datetime.now(tz=None),
3399
+ 'zones': zones
3400
+ }
3401
+ }
3402
+ self._states.update(areaAlarm)
3403
+ if self.updateCallback:
3404
+ await self.updateCallback(2)
3320
3405
  elif type == 'vehicle-wakeup-succeeded':
3321
3406
  if self._requests.get('refresh', {}).get('id', None):
3322
3407
  openRequest= self._requests.get('refresh', {}).get('id', None)
@@ -3327,20 +3412,23 @@ class Vehicle:
3327
3412
  # Do full update only if the last one is older than timedelta or if the notification belongs to an open request initiated by PyCupra
3328
3413
  if self.updateCallback:
3329
3414
  await self.updateCallback(1)
3415
+ elif type in ('vehicle-area-alert-added', 'vehicle-area-alert-updated'):
3416
+ _LOGGER.info(f' Intentionally ignoring a notification of type \'{type}\')')
3330
3417
  else:
3331
3418
  _LOGGER.warning(f' Don\'t know what to do with a notification of type \'{type}\')')
3332
3419
 
3333
3420
 
3334
- def storeFirebaseNotifications(self, obj, notification, data_message):
3335
- _LOGGER.debug(f'In storeFirebaseNotifications. notification={notification}')
3336
- fName = self._firebaseCredentialsFileName
3337
- fName = fName.replace('pycupra_firebase_credentials.json', 'pycupra_firebasenotifications.txt')
3421
+ #temporary output of notifications in a file, will be removed in the next release
3422
+ #def storeFirebaseNotifications(self, obj, notification, data_message):
3423
+ # _LOGGER.debug(f'In storeFirebaseNotifications. notification={notification}')
3424
+ # fName = self._firebaseCredentialsFileName
3425
+ # fName = fName.replace('pycupra_firebase_credentials.json', 'pycupra_firebasenotifications.txt')
3338
3426
 
3339
- with open(fName, "a") as ofile:
3340
- ofile.write(f'{datetime.now()}\n')
3341
- ofile.write(f' notification id={notification}, data_message={data_message}\n')
3342
- ofile.write(f' obj={obj}\n')
3343
- ofile.write("----------------------------------------------------------------\n")
3427
+ # with open(fName, "a") as ofile:
3428
+ # ofile.write(f'{datetime.now()}\n')
3429
+ # ofile.write(f' notification id={notification}, data_message={data_message}\n')
3430
+ # ofile.write(f' obj={obj}\n')
3431
+ # ofile.write("----------------------------------------------------------------\n")
3344
3432
 
3345
3433
 
3346
3434
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycupra
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: A library to read and send vehicle data via Cupra/Seat portal using the same API calls as the MyCupra/MySeat mobile app.
5
5
  Home-page: https://github.com/WulfgarW/pycupra
6
6
  Author: WulfgarW
@@ -1,25 +1,25 @@
1
1
  pycupra/__init__.py,sha256=p0880jPkLqOErX3u3qaLboBLOsEHFpe44axApdaGeqI,231
2
- pycupra/__version__.py,sha256=DKM773a_0KC1VMvD1yaWYuzJUpp3XsxR_HcWB1g_-Ys,207
3
- pycupra/connection.py,sha256=GWup_DcxnHNydb0DRJV79Hd108HJxMrGk89vi5R7meE,91150
4
- pycupra/const.py,sha256=F6y3qt_wS1QxhdSR-NHxB7lpUcf5uKLeoeTAJcAXgsE,10630
5
- pycupra/dashboard.py,sha256=xWggxTyKTBm3ByjRtymVzLJWtlIa52IEJsNx0z7bA9Q,44324
2
+ pycupra/__version__.py,sha256=ahbRianoeRb9v2bqw7g3dKIyoW7jt_rvwY0nas5Bn0c,207
3
+ pycupra/connection.py,sha256=5bUxSUC03O-4kceZn8fPWKyOaxOq13JVMjY-_UqrOwo,92187
4
+ pycupra/const.py,sha256=hCtDfI0gRewmVjXOQRrC4pUphOxGwCA3_Plfp2NwFXU,10637
5
+ pycupra/dashboard.py,sha256=bEmIRvxX08WNa0rftpQ6JrD0GW5FeVTDu5yQr0IUjMM,45178
6
6
  pycupra/exceptions.py,sha256=Nq_F79GP8wjHf5lpvPy9TbSIrRHAJrFMo0T1N9TcgSQ,2917
7
7
  pycupra/firebase.py,sha256=tuN_W3OX3h3-yfdprWzmCn6z_T-BMx-OpL7Z6hOA8Lc,3451
8
8
  pycupra/utilities.py,sha256=6sDxWP13-XtxmqhuBJBGdVbkj48BQ9AxFMrBPxH0J7g,2679
9
- pycupra/vehicle.py,sha256=jOtpn8WUxKBYPIuCuPMfmH19AVfY0Mn1GXtRJM_gosQ,157941
9
+ pycupra/vehicle.py,sha256=1sKJJ4xuBIrx6ZWnH50-b7bzjmqyBgRiETOSzDiNNN8,162609
10
10
  pycupra/firebase_messaging/__init__.py,sha256=oerLHWvEf4qRqu3GxSX6SLY_OYI430ydAiAhKtzyMEM,666
11
11
  pycupra/firebase_messaging/android_checkin_pb2.py,sha256=-U1oGroFt3KRuGDieae3iTcux6mAfx1TFkE1Q35ul2E,2849
12
12
  pycupra/firebase_messaging/android_checkin_pb2.pyi,sha256=7KL-zQIz2Zz7uftcLkv57Podzu-yk6trn50FN4X4A8E,9379
13
13
  pycupra/firebase_messaging/checkin_pb2.py,sha256=lFzCIAkYz9NFUpRbVuW-2kM_EaYKVWHeifHS1PV2eHQ,2795
14
14
  pycupra/firebase_messaging/checkin_pb2.pyi,sha256=mHOqbedt5jZDI20jcyFrTMSnQ0f_tq4zkIlHiaSC3xI,14626
15
15
  pycupra/firebase_messaging/const.py,sha256=XMy8kJ37uBSkTpVpdLeSjxk5UIPuvDuo-rxYdgmo2G8,1191
16
- pycupra/firebase_messaging/fcmpushclient.py,sha256=7ADEYA6Aw4c42bwbIJr_I76wGiLOu99oV7PSNC4UcVs,30157
16
+ pycupra/firebase_messaging/fcmpushclient.py,sha256=5kAp6rnl9EJqVPn9OfHFKUfFDRWA9LAVbfiL-pLRSqo,30550
17
17
  pycupra/firebase_messaging/fcmregister.py,sha256=yZngC-0ZfTygtjfdzg03OW_3xk2n_uSQQ3Lrash5Y_E,18091
18
18
  pycupra/firebase_messaging/mcs_pb2.py,sha256=nwXY7IDgLYPxgpSGs6wyTSyYDdomQsyGqH8R8EgODLg,7733
19
19
  pycupra/firebase_messaging/mcs_pb2.pyi,sha256=HfIhInC3wRg8_caKwUm-V3knE2jTdEQvBy6uXgQ5rHY,33959
20
20
  pycupra/firebase_messaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- pycupra-0.1.7.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
22
- pycupra-0.1.7.dist-info/METADATA,sha256=hQAjfqNAkgF72DsnoCrLL6NtkF5_iIWY8mgc4ssgsCo,3757
23
- pycupra-0.1.7.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
24
- pycupra-0.1.7.dist-info/top_level.txt,sha256=9Lbj_jG4JvpGwt6K3AwhWFc0XieDnuHFOP4x44wSXSQ,8
25
- pycupra-0.1.7.dist-info/RECORD,,
21
+ pycupra-0.1.8.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
22
+ pycupra-0.1.8.dist-info/METADATA,sha256=GC1Av2ilKrcM3hNfIoFPsBRKsSBjzlkIQrH8nmUzijY,3757
23
+ pycupra-0.1.8.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
24
+ pycupra-0.1.8.dist-info/top_level.txt,sha256=9Lbj_jG4JvpGwt6K3AwhWFc0XieDnuHFOP4x44wSXSQ,8
25
+ pycupra-0.1.8.dist-info/RECORD,,