pycupra 0.1.6__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.6"
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()
@@ -151,6 +152,7 @@ class Connection:
151
152
  self.addToAnonymisationKeys('family_name')
152
153
  self.addToAnonymisationKeys('birthdate')
153
154
  self.addToAnonymisationKeys('vin')
155
+ self._error401 = False
154
156
 
155
157
 
156
158
  def _clear_cookies(self):
@@ -596,22 +598,31 @@ class Connection:
596
598
  'request_info': error.request_info
597
599
  }
598
600
  if error.status == 401:
599
- _LOGGER.warning('Received "Unauthorized" while fetching data.\nThis can occur if tokens expired or refresh service is unavailable.')
601
+ _LOGGER.warning('Received "Unauthorized" while fetching data. This can occur if tokens expired or refresh service is unavailable.')
602
+ if self._error401 != True:
603
+ self._error401 = True
604
+ rc=await self.refresh_token(self._session_auth_brand)
605
+ if rc:
606
+ _LOGGER.info('Successfully refreshed tokens after error 401.')
607
+ self._error401 = False
608
+ #return True
609
+ else:
610
+ _LOGGER.info('Refresh of tokens after error 401 not successful.')
600
611
  elif error.status == 400:
601
- _LOGGER.error('Received "Bad Request" from server.\nThe request might be malformed or not implemented correctly for this vehicle.')
612
+ _LOGGER.error('Received "Bad Request" from server. The request might be malformed or not implemented correctly for this vehicle.')
602
613
  elif error.status == 412:
603
- _LOGGER.debug('Received "Pre-condition failed".\nService might be temporarily unavailable.')
614
+ _LOGGER.debug('Received "Pre-condition failed". Service might be temporarily unavailable.')
604
615
  elif error.status == 500:
605
- _LOGGER.info('Received "Internal server error".\nThe service is temporarily unavailable.')
616
+ _LOGGER.info('Received "Internal server error". The service is temporarily unavailable.')
606
617
  elif error.status == 502:
607
- _LOGGER.info('Received "Bad gateway".\nEither the endpoint is temporarily unavailable or not supported for this vehicle.')
618
+ _LOGGER.info('Received "Bad gateway". Either the endpoint is temporarily unavailable or not supported for this vehicle.')
608
619
  elif 400 <= error.status <= 499:
609
620
  _LOGGER.error('Received unhandled error indicating client-side problem.\nRestart or try again later.')
610
621
  elif 500 <= error.status <= 599:
611
622
  _LOGGER.error('Received unhandled error indicating server-side problem.\nThe service might be temporarily unavailable.')
612
623
  else:
613
624
  _LOGGER.error('Received unhandled error while requesting API endpoint.')
614
- _LOGGER.debug(f'HTTP request information: {data}')
625
+ _LOGGER.debug(self.anonymise(f'HTTP request information: {data}'))
615
626
  return data
616
627
  except Exception as e:
617
628
  _LOGGER.debug(f'Got non HTTP related error: {e}')
@@ -626,7 +637,12 @@ class Connection:
626
637
  async def _request(self, method, url, **kwargs):
627
638
  """Perform a HTTP query"""
628
639
  if self._session_fulldebug:
629
- _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}'))
630
646
  try:
631
647
  if datetime.now(tz=None).date() != self._sessionRequestTimestamp.date():
632
648
  # A new day has begun. Store _sessionRequestCounter in history and reset timestamp and counter
@@ -691,6 +707,9 @@ class Connection:
691
707
  if self._session_fulldebug:
692
708
  if 'image/png' in response.headers.get('Content-Type', ''):
693
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}')
694
713
  else:
695
714
  _LOGGER.debug(self.anonymise(f'Request for "{url}" returned with status code [{response.status}], response: {self.anonymise(deepcopy(res))}'))
696
715
  else:
@@ -704,7 +723,7 @@ class Connection:
704
723
  _LOGGER.debug(self.anonymise(f'Data call returned: {response}'))
705
724
  return response
706
725
  except aiohttp.client_exceptions.ClientResponseError as error:
707
- _LOGGER.debug(f'Request failed. Data: {data}, HTTP request headers: {self._session_headers}')
726
+ _LOGGER.debug(self.anonymise(f'Request failed. Data: {data}, HTTP request headers: {self._session_headers}'))
708
727
  if error.status == 401:
709
728
  _LOGGER.error('Unauthorized')
710
729
  elif error.status == 400:
@@ -1070,6 +1089,12 @@ class Connection:
1070
1089
  async def getTripStatistics(self, vin, baseurl, supportsCyclicTrips):
1071
1090
  """Get short term and cyclic trip statistics."""
1072
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
1073
1098
  try:
1074
1099
  data={'tripstatistics': {}}
1075
1100
  if supportsCyclicTrips:
@@ -1625,7 +1650,7 @@ class Connection:
1625
1650
  if expires > now:
1626
1651
  return expires
1627
1652
  else:
1628
- _LOGGER.debug(f'Token expired at {expires.strftime("%Y-%m-%d %H:%M:%S")})')
1653
+ _LOGGER.debug(f'Token expired at {expires.strftime("%Y-%m-%d %H:%M:%S")}')
1629
1654
  return False
1630
1655
  except Exception as e:
1631
1656
  _LOGGER.info(f'Token validation failed, {e}')
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' # ???
@@ -192,3 +192,4 @@ FIREBASE_STATUS_NOT_INITIALISED= 0
192
192
  FIREBASE_STATUS_ACTIVATED= 1
193
193
  FIREBASE_STATUS_NOT_WANTED= -2
194
194
  FIREBASE_STATUS_ACTIVATION_FAILED= -1
195
+ FIREBASE_STATUS_ACTIVATION_STOPPED= -3
pycupra/dashboard.py CHANGED
@@ -755,6 +755,18 @@ class Warnings(Sensor):
755
755
  def assumed_state(self):
756
756
  return False
757
757
 
758
+ @property
759
+ def attributes(self):
760
+ attrs = {'warnings': 'No warnings'}
761
+ if self.vehicle.attrs.get('warninglights', {}).get('statuses',[]):
762
+ warningTextList = []
763
+ for elem in self.vehicle.attrs['warninglights']['statuses']:
764
+ if isinstance(elem, dict):
765
+ if elem.get('text',''):
766
+ warningTextList.append(elem.get('text',''))
767
+ attrs['warnings'] = warningTextList
768
+ return attrs
769
+
758
770
  class DepartureTimer1(Switch):
759
771
  def __init__(self):
760
772
  super().__init__(attr="departure1", name="Departure timer 1", icon="mdi:radiator")
@@ -990,6 +1002,32 @@ class ChargingState(BinarySensor):
990
1002
  attr['mode']=mode
991
1003
  return attr
992
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
+
993
1031
  def create_instruments():
994
1032
  return [
995
1033
  Position(),
@@ -1018,6 +1056,7 @@ def create_instruments():
1018
1056
  DepartureProfile2(),
1019
1057
  DepartureProfile3(),
1020
1058
  ChargingState(),
1059
+ AreaAlarm(),
1021
1060
  Sensor(
1022
1061
  attr="distance",
1023
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]
@@ -599,10 +600,21 @@ class FcmPushClient: # pylint:disable=too-many-instance-attributes
599
600
  return
600
601
 
601
602
  if isinstance(msg, DataMessageStanza):
602
- await self._handle_data_message(msg)
603
- self.persistent_ids.append(msg.persistent_id)
603
+ #await self._handle_data_message(msg)
604
+ #self.persistent_ids.append(msg.persistent_id)
605
+ #if self.config.send_selective_acknowledgements:
606
+ # await self._send_selective_ack(msg.persistent_id)
604
607
  if self.config.send_selective_acknowledgements:
605
- await self._send_selective_ack(msg.persistent_id)
608
+ # As handle_data_message with the callback of onNotification can take some time, send_selective_ack is called in parallel
609
+ await asyncio.gather(
610
+ self._handle_data_message(msg),
611
+ self._send_selective_ack(msg.persistent_id),
612
+ return_exceptions=True
613
+ )
614
+ self.persistent_ids.append(msg.persistent_id),
615
+ else:
616
+ await self._handle_data_message(msg)
617
+ self.persistent_ids.append(msg.persistent_id)
606
618
  elif isinstance(msg, HeartbeatPing):
607
619
  await self._handle_ping(msg)
608
620
  elif isinstance(msg, HeartbeatAck):
@@ -714,7 +726,10 @@ class FcmPushClient: # pylint:disable=too-many-instance-attributes
714
726
  else:
715
727
  _logger.exception("Unexpected exception during read\n")
716
728
  if self._try_increment_error_count(ErrorType.CONNECTION):
729
+ _logger.debug("Calling reset()\n")
717
730
  await self._reset()
731
+ else:
732
+ _logger.debug("Not calling reset()\n")
718
733
  except Exception as ex:
719
734
  _logger.error(
720
735
  "Unknown error: %s, shutting down FcmPushClient.\n%s",
@@ -794,3 +809,4 @@ class FcmPushClient: # pylint:disable=too-many-instance-attributes
794
809
  dms.persistent_id = persistent_id
795
810
 
796
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
@@ -24,6 +25,7 @@ from .const import (
24
25
  FIREBASE_STATUS_NOT_INITIALISED,
25
26
  FIREBASE_STATUS_ACTIVATED,
26
27
  FIREBASE_STATUS_ACTIVATION_FAILED,
28
+ FIREBASE_STATUS_ACTIVATION_STOPPED,
27
29
  FIREBASE_STATUS_NOT_WANTED,
28
30
  )
29
31
 
@@ -71,7 +73,6 @@ class Vehicle:
71
73
  self._relevantCapabilties = {
72
74
  'measurements': {'active': False, 'reason': 'not supported', },
73
75
  'climatisation': {'active': False, 'reason': 'not supported'},
74
- #'parkingInformation': {'active': False, 'reason': 'not supported'},
75
76
  'tripStatistics': {'active': False, 'reason': 'not supported', 'supportsCyclicTrips': False},
76
77
  'vehicleHealthInspection': {'active': False, 'reason': 'not supported'},
77
78
  'vehicleHealthWarnings': {'active': False, 'reason': 'not supported'},
@@ -94,6 +95,7 @@ class Vehicle:
94
95
  self._last_get_charger = datetime.now(tz=None) - timedelta(seconds=600)
95
96
  self._last_get_climater = datetime.now(tz=None) - timedelta(seconds=600)
96
97
  self._last_get_mileage = datetime.now(tz=None) - timedelta(seconds=600)
98
+ self._last_get_position = datetime.now(tz=None) - timedelta(seconds=600)
97
99
 
98
100
 
99
101
  #### API get and set functions ####
@@ -130,8 +132,7 @@ class Vehicle:
130
132
  self._relevantCapabilties[id].update(data)
131
133
 
132
134
 
133
- #await self.get_trip_statistic() # in full update
134
-
135
+
135
136
  # Get URLs for model image
136
137
  self._modelimages = await self.get_modelimageurl()
137
138
 
@@ -147,12 +148,22 @@ class Vehicle:
147
148
  hourago = datetime.now() - timedelta(hours = 2)
148
149
  if self._discovered < hourago:
149
150
  await self.discover()
150
- #_LOGGER.debug('Achtung! self.discover() auskommentiert')
151
151
 
152
152
  # Fetch all data if car is not deactivated
153
153
  if not self.deactivated:
154
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
+
155
161
  if self.firebaseStatus == FIREBASE_STATUS_ACTIVATED:
162
+ # Check, if fcmpushclient still started
163
+ if not self.firebase._pushClient.is_started():
164
+ _LOGGER.warning(f'firebaseStatus={self.firebaseStatus}, but state of push client is not started. Changing firebaseStatus to {FIREBASE_STATUS_ACTIVATION_STOPPED}')
165
+ self.firebaseStatus = FIREBASE_STATUS_ACTIVATION_STOPPED
166
+
156
167
  fullUpdateExpired = datetime.now(tz=None) - timedelta(seconds= 1700)
157
168
  oldMileage = self.distance
158
169
  if self._last_get_mileage < datetime.now(tz=None) - timedelta(seconds= 300):
@@ -164,6 +175,26 @@ class Vehicle:
164
175
  else:
165
176
  fullUpdateExpired = datetime.now(tz=None) - timedelta(seconds= 1100)
166
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
+
167
198
  if self._connection._session_nightlyUpdateReduction:
168
199
  # nightlyUpdateReduction is activated
169
200
  if datetime.now(tz=None).hour<5 or datetime.now(tz=None).hour>=22:
@@ -192,6 +223,7 @@ class Vehicle:
192
223
  if self.firebaseStatus != FIREBASE_STATUS_ACTIVATED:
193
224
  await self.get_mileage()
194
225
 
226
+
195
227
  await asyncio.gather(
196
228
  #self.get_statusreport(),
197
229
  self.get_charger(),
@@ -227,6 +259,10 @@ class Vehicle:
227
259
  data = await self._connection.getBasicCarData(self.vin, self._apibase)
228
260
  if data:
229
261
  self._states.update(data)
262
+ return True
263
+ else:
264
+ _LOGGER.debug('Could not fetch basic car data')
265
+ return False
230
266
 
231
267
  async def get_mileage(self):
232
268
  """Fetch basic car data."""
@@ -234,6 +270,10 @@ class Vehicle:
234
270
  if data:
235
271
  self._states.update(data)
236
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
237
277
 
238
278
  async def get_preheater(self):
239
279
  """Fetch pre-heater data if function is enabled."""
@@ -255,8 +295,10 @@ class Vehicle:
255
295
  if data:
256
296
  self._states.update(data)
257
297
  self._last_get_climater = datetime.now(tz=None)
298
+ return True
258
299
  else:
259
300
  _LOGGER.debug('Could not fetch climater data')
301
+ return False
260
302
  #else:
261
303
  # self._requests.pop('climatisation', None)
262
304
 
@@ -266,8 +308,10 @@ class Vehicle:
266
308
  data = await self._connection.getTripStatistics(self.vin, self._apibase, self._relevantCapabilties['tripStatistics'].get('supportsCyclicTrips', False))
267
309
  if data:
268
310
  self._states.update(data)
311
+ return True
269
312
  else:
270
313
  _LOGGER.debug('Could not fetch trip statistics')
314
+ return False
271
315
 
272
316
  async def get_position(self):
273
317
  """Fetch position data if function is enabled."""
@@ -284,24 +328,21 @@ class Vehicle:
284
328
  except:
285
329
  pass
286
330
  self._states.update(data)
331
+ self._last_get_position = datetime.now(tz=None)
332
+ return True
287
333
  else:
288
334
  _LOGGER.debug('Could not fetch any positional data')
335
+ return False
289
336
 
290
337
  async def get_vehicleHealthWarnings(self):
291
338
  if self._relevantCapabilties.get('vehicleHealthWarnings', {}).get('active', False):
292
339
  data = await self._connection.getVehicleHealthWarnings(self.vin, self._apibase)
293
340
  if data:
294
- #warningsList = data.get('warninglights',{}).get('statuses',[])
295
- #for i in range(len(warningsList)):
296
- # _LOGGER.debug(f'Element {i} in warninglights: {warningsList[i]}')
297
- # if isinstance(warningsList[i], dict):
298
- # if warningsList[i].get('icon',''):
299
- # #Value of icon is very long and can lead to problems
300
- # _LOGGER.debug(f'Substituting value of icon by \'DELETED\'')
301
- # data['warninglights']['statuses'][i]['icon']='DELETED'
302
341
  self._states.update(data)
342
+ return True
303
343
  else:
304
344
  _LOGGER.debug('Could not fetch vehicle health warnings')
345
+ return False
305
346
 
306
347
  async def get_statusreport(self):
307
348
  """Fetch status data if function is enabled."""
@@ -310,8 +351,10 @@ class Vehicle:
310
351
  if data:
311
352
  self._states.update(data)
312
353
  self._last_get_statusreport = datetime.now(tz=None)
354
+ return True
313
355
  else:
314
356
  _LOGGER.debug('Could not fetch status report')
357
+ return False
315
358
 
316
359
  async def get_maintenance(self):
317
360
  """Fetch maintenance data if function is enabled."""
@@ -319,8 +362,10 @@ class Vehicle:
319
362
  data = await self._connection.getMaintenance(self.vin, self._apibase)
320
363
  if data:
321
364
  self._states.update(data)
365
+ return True
322
366
  else:
323
367
  _LOGGER.debug('Could not fetch status report')
368
+ return False
324
369
 
325
370
  async def get_charger(self):
326
371
  """Fetch charger data if function is enabled."""
@@ -329,8 +374,10 @@ class Vehicle:
329
374
  if data:
330
375
  self._states.update(data)
331
376
  self._last_get_charger = datetime.now(tz=None)
377
+ return True
332
378
  else:
333
379
  _LOGGER.debug('Could not fetch charger data')
380
+ return False
334
381
 
335
382
  async def get_departure_timers(self):
336
383
  """Fetch timer data if function is enabled."""
@@ -339,8 +386,10 @@ class Vehicle:
339
386
  if data:
340
387
  self._states.update(data)
341
388
  self._last_get_departure_timers = datetime.now(tz=None)
389
+ return True
342
390
  else:
343
391
  _LOGGER.debug('Could not fetch timers')
392
+ return False
344
393
 
345
394
  async def get_departure_profiles(self):
346
395
  """Fetch timer data if function is enabled."""
@@ -349,8 +398,10 @@ class Vehicle:
349
398
  if data:
350
399
  self._states.update(data)
351
400
  self._last_get_departure_profiles = datetime.now(tz=None)
401
+ return True
352
402
  else:
353
403
  _LOGGER.debug('Could not fetch timers')
404
+ return False
354
405
 
355
406
  #async def wait_for_request(self, section, request, retryCount=36):
356
407
  """Update status of outstanding requests."""
@@ -983,7 +1034,7 @@ class Vehicle:
983
1034
  async def set_climatisation_temp(self, temperature=20):
984
1035
  """Set climatisation target temp."""
985
1036
  if self.is_electric_climatisation_supported or self.is_auxiliary_climatisation_supported:
986
- if 16 <= int(temperature) <= 30:
1037
+ if 16 <= float(temperature) <= 30:
987
1038
  data = {
988
1039
  'climatisationWithoutExternalPower': self.climatisation_without_external_power,
989
1040
  'targetTemperature': temperature,
@@ -1034,28 +1085,30 @@ class Vehicle:
1034
1085
  data = {}
1035
1086
  # Validate user input
1036
1087
  if mode.lower() not in ['electric', 'auxiliary', 'start', 'stop', 'on', 'off']:
1088
+ _LOGGER.error(f"Invalid mode for 'set_climatisation': {mode}")
1037
1089
  raise SeatInvalidRequestException(f"Invalid mode for set_climatisation: {mode}")
1038
1090
  elif mode == 'auxiliary' and spin is None:
1039
1091
  raise SeatInvalidRequestException("Starting auxiliary heater requires provided S-PIN")
1040
1092
  if temp is not None:
1041
- 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)}")
1042
1095
  raise SeatInvalidRequestException(f"Invalid type for temp")
1043
1096
  elif not 16 <= float(temp) <=30:
1044
1097
  raise SeatInvalidRequestException(f"Invalid value for temp")
1045
1098
  else:
1046
1099
  temp = self.climatisation_target_temperature
1047
- if hvpower is not None:
1048
- if not isinstance(hvpower, bool):
1049
- 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")
1050
1103
  if self.is_electric_climatisation_supported:
1051
1104
  if self._relevantCapabilties.get('climatisation', {}).get('active', False):
1052
1105
  if mode in ['Start', 'start', 'Electric', 'electric', 'On', 'on']:
1053
1106
  mode = 'start'
1054
1107
  if mode in ['start', 'auxiliary']:
1055
- if hvpower is not None:
1056
- withoutHVPower = hvpower
1057
- else:
1058
- withoutHVPower = self.climatisation_without_external_power
1108
+ #if hvpower is not None:
1109
+ # withoutHVPower = hvpower
1110
+ #else:
1111
+ # withoutHVPower = self.climatisation_without_external_power
1059
1112
  data = {
1060
1113
  'targetTemperature': temp,
1061
1114
  'targetTemperatureUnit': 'celsius',
@@ -1142,7 +1195,7 @@ class Vehicle:
1142
1195
  self._requests['climatisation'] = {'status': 'Exception'}
1143
1196
  raise SeatException('Climatisation action failed')
1144
1197
 
1145
- # Parking heater heating/ventilation (RS)
1198
+ # Parking heater heating/ventilation (RS)
1146
1199
  async def set_pheater(self, mode, spin):
1147
1200
  """Set the mode for the parking heater."""
1148
1201
  if not self.is_pheater_heating_supported:
@@ -1185,7 +1238,7 @@ class Vehicle:
1185
1238
  self._requests['preheater'] = {'status': 'Exception'}
1186
1239
  raise SeatException('Pre-heater action failed')
1187
1240
 
1188
- # Lock
1241
+ # Lock
1189
1242
  async def set_lock(self, action, spin):
1190
1243
  """Remote lock and unlock actions."""
1191
1244
  #if not self._services.get('rlu_v1', False):
@@ -1247,7 +1300,7 @@ class Vehicle:
1247
1300
  self._requests['lock'] = {'status': 'Exception'}
1248
1301
  raise SeatException('Lock action failed')
1249
1302
 
1250
- # Honk and flash (RHF)
1303
+ # Honk and flash (RHF)
1251
1304
  async def set_honkandflash(self, action, lat=None, lng=None):
1252
1305
  """Turn on/off honk and flash."""
1253
1306
  if not self._relevantCapabilties.get('honkAndFlash', {}).get('active', False):
@@ -1304,7 +1357,7 @@ class Vehicle:
1304
1357
  self._requests['honkandflash'] = {'status': 'Exception'}
1305
1358
  raise SeatException('Honk and flash action failed')
1306
1359
 
1307
- # Refresh vehicle data (VSR)
1360
+ # Refresh vehicle data (VSR)
1308
1361
  async def set_refresh(self):
1309
1362
  """Wake up vehicle and update status data."""
1310
1363
  if not self._relevantCapabilties.get('state', {}).get('active', False):
@@ -1340,7 +1393,7 @@ class Vehicle:
1340
1393
  raise SeatException('Data refresh failed')
1341
1394
 
1342
1395
  #### Vehicle class helpers ####
1343
- # Vehicle info
1396
+ # Vehicle info
1344
1397
  @property
1345
1398
  def attrs(self):
1346
1399
  return self._states
@@ -1373,7 +1426,7 @@ class Vehicle:
1373
1426
 
1374
1427
 
1375
1428
  #### Information from vehicle states ####
1376
- # Car information
1429
+ # Car information
1377
1430
  @property
1378
1431
  def nickname(self):
1379
1432
  return self._properties.get('vehicleNickname', '')
@@ -1462,7 +1515,7 @@ class Vehicle:
1462
1515
  if self._modelimages is not None:
1463
1516
  return True
1464
1517
 
1465
- # Lights
1518
+ # Lights
1466
1519
  @property
1467
1520
  def parking_light(self):
1468
1521
  """Return true if parking light is on"""
@@ -1481,7 +1534,7 @@ class Vehicle:
1481
1534
  else:
1482
1535
  return False
1483
1536
 
1484
- # Connection status
1537
+ # Connection status
1485
1538
  @property
1486
1539
  def last_connected(self):
1487
1540
  """Return when vehicle was last connected to connect servers."""
@@ -1498,7 +1551,7 @@ class Vehicle:
1498
1551
  if 'updatedAt' in self.attrs.get('status', {}):
1499
1552
  return True
1500
1553
 
1501
- # Update status
1554
+ # Update status
1502
1555
  @property
1503
1556
  def last_full_update(self):
1504
1557
  """Return when the last full update for the vehicle took place."""
@@ -1510,7 +1563,7 @@ class Vehicle:
1510
1563
  if hasattr(self,'_last_full_update'):
1511
1564
  return True
1512
1565
 
1513
- # Service information
1566
+ # Service information
1514
1567
  @property
1515
1568
  def distance(self):
1516
1569
  """Return vehicle odometer."""
@@ -1598,7 +1651,7 @@ class Vehicle:
1598
1651
  return True
1599
1652
  return False
1600
1653
 
1601
- # Charger related states for EV and PHEV
1654
+ # Charger related states for EV and PHEV
1602
1655
  @property
1603
1656
  def charging(self):
1604
1657
  """Return battery level"""
@@ -1857,7 +1910,7 @@ class Vehicle:
1857
1910
  if self.attrs.get('charging', {}).get('info', {}).get('settings', {}).get('targetSoc', False):
1858
1911
  return True
1859
1912
 
1860
- # Vehicle location states
1913
+ # Vehicle location states
1861
1914
  @property
1862
1915
  def position(self):
1863
1916
  """Return position."""
@@ -1924,7 +1977,7 @@ class Vehicle:
1924
1977
  if 'parkingTimeUTC' in self.attrs.get('findCarResponse', {}):
1925
1978
  return True
1926
1979
 
1927
- # Vehicle fuel level and range
1980
+ # Vehicle fuel level and range
1928
1981
  @property
1929
1982
  def primary_range(self):
1930
1983
  value = -1
@@ -2063,7 +2116,7 @@ class Vehicle:
2063
2116
  return self.is_secondary_range_supported
2064
2117
  return False
2065
2118
 
2066
- # Climatisation settings
2119
+ # Climatisation settings
2067
2120
  @property
2068
2121
  def climatisation_target_temperature(self):
2069
2122
  """Return the target temperature from climater."""
@@ -2137,7 +2190,7 @@ class Vehicle:
2137
2190
  else:
2138
2191
  return False
2139
2192
 
2140
- # Climatisation, electric
2193
+ # Climatisation, electric
2141
2194
  @property
2142
2195
  def electric_climatisation_attributes(self):
2143
2196
  """Return climatisation attributes."""
@@ -2236,14 +2289,7 @@ class Vehicle:
2236
2289
  @property
2237
2290
  def warnings(self):
2238
2291
  """Return warnings."""
2239
- if self.attrs.get('warninglights', {}).get('statuses',[]):
2240
- warningTextList = []
2241
- for elem in self.attrs['warninglights']['statuses']:
2242
- if isinstance(elem, dict):
2243
- if elem.get('text',''):
2244
- warningTextList.append(elem.get('text',''))
2245
- return warningTextList
2246
- return 'No warnings'
2292
+ return len(self.attrs.get('warninglights', {}).get('statuses',[]))
2247
2293
 
2248
2294
  @property
2249
2295
  def is_warnings_supported(self):
@@ -2252,7 +2298,7 @@ class Vehicle:
2252
2298
  return True
2253
2299
  return False
2254
2300
 
2255
- # Parking heater, "legacy" auxiliary climatisation
2301
+ # Parking heater, "legacy" auxiliary climatisation
2256
2302
  @property
2257
2303
  def pheater_duration(self):
2258
2304
  return self._climate_duration
@@ -2300,7 +2346,7 @@ class Vehicle:
2300
2346
  if self.attrs.get('heating', {}).get('climatisationStateReport', {}).get('climatisationState', False):
2301
2347
  return True
2302
2348
 
2303
- # Windows
2349
+ # Windows
2304
2350
  @property
2305
2351
  def windows_closed(self):
2306
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)
@@ -2407,7 +2453,7 @@ class Vehicle:
2407
2453
  # response = self.attrs.get('status')['windows'].get('sunRoof', '')
2408
2454
  return True if response != '' else False
2409
2455
 
2410
- # Locks
2456
+ # Locks
2411
2457
  @property
2412
2458
  def door_locked(self):
2413
2459
  # LEFT FRONT
@@ -2450,7 +2496,7 @@ class Vehicle:
2450
2496
  return True
2451
2497
  return False
2452
2498
 
2453
- # Doors, hood and trunk
2499
+ # Doors, hood and trunk
2454
2500
  @property
2455
2501
  def hood_closed(self):
2456
2502
  """Return true if hood is closed"""
@@ -2535,7 +2581,7 @@ class Vehicle:
2535
2581
  response = self.attrs.get('status')['trunk'].get('open', 0)
2536
2582
  return True if response != 0 else False
2537
2583
 
2538
- # Departure timers
2584
+ # Departure timers
2539
2585
  @property
2540
2586
  def departure1(self):
2541
2587
  """Return timer status and attributes."""
@@ -2647,7 +2693,7 @@ class Vehicle:
2647
2693
  return True
2648
2694
  return False
2649
2695
 
2650
- # Departure profiles
2696
+ # Departure profiles
2651
2697
  @property
2652
2698
  def departure_profile1(self):
2653
2699
  """Return profile status and attributes."""
@@ -2720,7 +2766,7 @@ class Vehicle:
2720
2766
  return True
2721
2767
  return False
2722
2768
 
2723
- # Trip data
2769
+ # Trip data
2724
2770
  @property
2725
2771
  def trip_last_entry(self):
2726
2772
  return self.attrs.get('tripstatistics', {}).get('short', [{},{}])[-1]
@@ -2945,7 +2991,26 @@ class Vehicle:
2945
2991
  if response and type(response.get('totalElectricConsumption', None)) in (float, int):
2946
2992
  return True
2947
2993
 
2948
- # 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
2949
3014
  @property
2950
3015
  def refresh_action_status(self):
2951
3016
  """Return latest status of data refresh request."""
@@ -3050,7 +3115,7 @@ class Vehicle:
3050
3115
  """Data update is supported."""
3051
3116
  return True
3052
3117
 
3053
- # Honk and flash
3118
+ # Honk and flash
3054
3119
  @property
3055
3120
  def request_honkandflash(self):
3056
3121
  """State is always False"""
@@ -3073,7 +3138,7 @@ class Vehicle:
3073
3138
  if self._relevantCapabilties.get('honkAndFlash', {}).get('active', False):
3074
3139
  return True
3075
3140
 
3076
- # Requests data
3141
+ # Requests data
3077
3142
  @property
3078
3143
  def request_in_progress(self):
3079
3144
  """Request in progress is always supported."""
@@ -3128,7 +3193,7 @@ class Vehicle:
3128
3193
  #if self.is_request_in_progress_supported:
3129
3194
  # return True if self._requests.get('remaining', False) else False
3130
3195
 
3131
- #### Helper functions ####
3196
+ #### Helper functions ####
3132
3197
  def __str__(self):
3133
3198
  return self.vin
3134
3199
 
@@ -3147,7 +3212,7 @@ class Vehicle:
3147
3212
 
3148
3213
  async def stopFirebase(self):
3149
3214
  # Check if firebase is activated
3150
- if self.firebaseStatus!= FIREBASE_STATUS_ACTIVATED:
3215
+ if self.firebaseStatus not in (FIREBASE_STATUS_ACTIVATED, FIREBASE_STATUS_ACTIVATION_STOPPED):
3151
3216
  _LOGGER.info(f'No need to stop firebase. Firebase status={self.firebaseStatus}')
3152
3217
  return self.firebaseStatus
3153
3218
 
@@ -3161,7 +3226,7 @@ class Vehicle:
3161
3226
  _LOGGER.warning('Stopping of firebase messaging failed.')
3162
3227
  return self.firebaseStatus
3163
3228
 
3164
- #await asyncio.sleep(5) # Wait to ignore the first notifications
3229
+ #await asyncio.sleep(5)
3165
3230
  self.firebaseStatus = FIREBASE_STATUS_NOT_INITIALISED
3166
3231
  _LOGGER.info('Stopping of firebase messaging was successful.')
3167
3232
  return self.firebaseStatus
@@ -3226,17 +3291,25 @@ class Vehicle:
3226
3291
  _LOGGER.debug(f'Received push notification: notification id={notification}, type={obj.get('data',{}).get('type','')}, requestId={obj.get('data',{}).get('requestId','[None]')}')
3227
3292
  _LOGGER.debug(f' data_message={data_message}, payload={obj.get('data',{}).get('payload','[None]')}')
3228
3293
 
3229
- #temporary output of notifications in a file
3230
- if self.updateCallback == self.update:
3231
- 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)
3232
3297
 
3233
3298
  if self.firebaseStatus != FIREBASE_STATUS_ACTIVATED:
3234
- _LOGGER.info(f'While firebase is not fully activated, received notifications are just acknowledged.')
3235
- # As long as the firebase status is not set to activated, ignore the notifications
3236
- 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
+
3237
3309
 
3238
3310
  type = obj.get('data',{}).get('type','')
3239
3311
  requestId = obj.get('data',{}).get('requestId','')
3312
+ payload = obj.get('data',{}).get('payload','')
3240
3313
  openRequest = -1
3241
3314
  if requestId != '':
3242
3315
  _LOGGER.info(f'Received notification of type \'{type}\', request id={requestId} ')
@@ -3307,6 +3380,28 @@ class Vehicle:
3307
3380
  await self.updateCallback(2)
3308
3381
  else:
3309
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)
3310
3405
  elif type == 'vehicle-wakeup-succeeded':
3311
3406
  if self._requests.get('refresh', {}).get('id', None):
3312
3407
  openRequest= self._requests.get('refresh', {}).get('id', None)
@@ -3317,22 +3412,23 @@ class Vehicle:
3317
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
3318
3413
  if self.updateCallback:
3319
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}\')')
3320
3417
  else:
3321
3418
  _LOGGER.warning(f' Don\'t know what to do with a notification of type \'{type}\')')
3322
3419
 
3323
-
3324
-
3325
3420
 
3326
- def storeFirebaseNotifications(self, obj, notification, data_message):
3327
- _LOGGER.debug(f'In storeFirebaseNotifications. notification={notification}')
3328
- fName = self._firebaseCredentialsFileName
3329
- 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')
3330
3426
 
3331
- with open(fName, "a") as ofile:
3332
- ofile.write(f'{datetime.now()}\n')
3333
- ofile.write(f' notification id={notification}, data_message={data_message}\n')
3334
- ofile.write(f' obj={obj}\n')
3335
- 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")
3336
3432
 
3337
3433
 
3338
3434
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycupra
3
- Version: 0.1.6
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=orABFX8h4AFdsffPt4uVXqXeC0Wz3ZJOeNAzyeNw7O8,207
3
- pycupra/connection.py,sha256=cZIqsjEwA1rTkhVyjowrTdbWnviOXFSso-WR8FnSrmg,90610
4
- pycupra/const.py,sha256=cJJ9xrof6HZ7ZE7nXnB6tU4qMbSadlN8mgs43Igy7Mo,10591
5
- pycupra/dashboard.py,sha256=RlJPdTvJV7Urog5gCz_4pYWNd5_ApQIVJUCwJIi_7L8,43822
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=stxeyn2vd2j2yQ3dWEmT9XX1aZNZ7FdYvxbIraF6M0o,157175
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=nO3WJUMQ7G2M9CE3aUlEdD75tFbC04gDsd4UsksQqKM,29543
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.6.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
22
- pycupra-0.1.6.dist-info/METADATA,sha256=u9ZdIDY607S5-5APn3VwI1nJgvWHPngzptNHedmY_k4,3757
23
- pycupra-0.1.6.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
24
- pycupra-0.1.6.dist-info/top_level.txt,sha256=9Lbj_jG4JvpGwt6K3AwhWFc0XieDnuHFOP4x44wSXSQ,8
25
- pycupra-0.1.6.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,,