pycupra 0.0.5__tar.gz → 0.0.7__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycupra
3
- Version: 0.0.5
3
+ Version: 0.0.7
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
@@ -59,6 +59,9 @@ RESOURCES = [
59
59
  "departure1",
60
60
  "departure2",
61
61
  "departure3",
62
+ "departure_profile1",
63
+ "departure_profile2",
64
+ "departure_profile3",
62
65
  "distance",
63
66
  "door_closed_left_back",
64
67
  "door_closed_left_front",
@@ -72,6 +75,7 @@ RESOURCES = [
72
75
  "fuel_level",
73
76
  "hood_closed",
74
77
  "last_connected",
78
+ "last_full_update",
75
79
  "lock_action_status",
76
80
  "oil_inspection",
77
81
  "oil_inspection_distance",
@@ -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.0.5"
6
+ __version__ = "0.0.7"
@@ -79,6 +79,7 @@ from .const import (
79
79
  API_CLIMATER_STATUS,
80
80
  API_CLIMATER,
81
81
  API_DEPARTURE_TIMERS,
82
+ API_DEPARTURE_PROFILES,
82
83
  API_MILEAGE,
83
84
  API_CAPABILITIES,
84
85
  #API_CAPABILITIES_MANAGEMENT,
@@ -623,6 +624,8 @@ class Connection:
623
624
  try:
624
625
  if response.status == 204:
625
626
  res = {'status_code': response.status}
627
+ elif response.status == 202 and method==METH_PUT:
628
+ res = response
626
629
  elif response.status >= 200 or response.status <= 300:
627
630
  # If this is a revoke token url, expect Content-Length 0 and return
628
631
  if int(response.headers.get('Content-Length', 0)) == 0 and 'revoke' in url:
@@ -691,7 +694,7 @@ class Connection:
691
694
  for vehicle in self.vehicles:
692
695
  if vehicle.vin not in update_list:
693
696
  _LOGGER.debug(f'Adding {vehicle.vin} for data refresh')
694
- update_list.append(vehicle.update())
697
+ update_list.append(vehicle.update(updateType=1))
695
698
  else:
696
699
  _LOGGER.debug(f'VIN {vehicle.vin} is already queued for data refresh')
697
700
 
@@ -881,16 +884,14 @@ class Connection:
881
884
  _LOGGER.info('Unhandled error while trying to fetch mycar data')
882
885
  except Exception as error:
883
886
  _LOGGER.warning(f'Could not fetch mycar report, error: {error}')
884
- try:
885
- response = await self.get(eval(f"f'{API_WARNINGLIGHTS}'"))
886
- if 'statuses' in response:
887
- data['warninglights'] = response
888
- elif response.get('status_code', {}):
889
- _LOGGER.warning(f'Could not fetch warnlights, HTTP status code: {response.get("status_code")}')
890
- else:
891
- _LOGGER.info('Unhandled error while trying to fetch warnlights')
892
- except Exception as error:
893
- _LOGGER.warning(f'Could not fetch warnlights, error: {error}')
887
+ if data=={}:
888
+ return False
889
+ return data
890
+
891
+ async def getMileage(self, vin, baseurl):
892
+ """Get car information from customer profile, VIN, nickname, etc."""
893
+ await self.set_token(self._session_auth_brand)
894
+ data={}
894
895
  try:
895
896
  response = await self.get(eval(f"f'{API_MILEAGE}'"))
896
897
  if response.get('mileageKm', {}):
@@ -905,6 +906,24 @@ class Connection:
905
906
  return False
906
907
  return data
907
908
 
909
+ async def getVehicleHealthWarnings(self, vin, baseurl):
910
+ """Get car information from customer profile, VIN, nickname, etc."""
911
+ await self.set_token(self._session_auth_brand)
912
+ data={}
913
+ try:
914
+ response = await self.get(eval(f"f'{API_WARNINGLIGHTS}'"))
915
+ if 'statuses' in response:
916
+ data['warninglights'] = response
917
+ elif response.get('status_code', {}):
918
+ _LOGGER.warning(f'Could not fetch warnlights, HTTP status code: {response.get("status_code")}')
919
+ else:
920
+ _LOGGER.info('Unhandled error while trying to fetch warnlights')
921
+ except Exception as error:
922
+ _LOGGER.warning(f'Could not fetch warnlights, error: {error}')
923
+ if data=={}:
924
+ return False
925
+ return data
926
+
908
927
  #async def getOperationList(self, vin, baseurl):
909
928
  """Collect operationlist for VIN, supported/licensed functions."""
910
929
  """try:
@@ -1009,19 +1028,22 @@ class Connection:
1009
1028
  return False
1010
1029
  return data
1011
1030
 
1012
- async def getTripStatistics(self, vin, baseurl):
1031
+ async def getTripStatistics(self, vin, baseurl, supportsCyclicTrips):
1013
1032
  """Get short term and cyclic trip statistics."""
1014
1033
  await self.set_token(self._session_auth_brand)
1015
1034
  try:
1016
1035
  data={'tripstatistics': {}}
1017
- dataType='CYCLIC'
1018
- response = await self.get(eval(f"f'{API_TRIP}'"))
1019
- if response.get('data', []):
1020
- data['tripstatistics']['cyclic']= response.get('data', [])
1021
- elif response.get('status_code', {}):
1022
- _LOGGER.warning(f'Could not fetch trip statistics, HTTP status code: {response.get("status_code")}')
1036
+ if supportsCyclicTrips:
1037
+ dataType='CYCLIC'
1038
+ response = await self.get(eval(f"f'{API_TRIP}'"))
1039
+ if response.get('data', []):
1040
+ data['tripstatistics']['cyclic']= response.get('data', [])
1041
+ elif response.get('status_code', {}):
1042
+ _LOGGER.warning(f'Could not fetch trip statistics, HTTP status code: {response.get("status_code")}')
1043
+ else:
1044
+ _LOGGER.info(f'Unhandled error while trying to fetch trip statistics')
1023
1045
  else:
1024
- _LOGGER.info(f'Unhandled error while trying to fetch trip statistics')
1046
+ _LOGGER.info(f'Vehicle does not support cyclic trips.')
1025
1047
  dataType='SHORT'
1026
1048
  response = await self.get(eval(f"f'{API_TRIP}'"))
1027
1049
  if response.get('data', []):
@@ -1081,11 +1103,33 @@ class Connection:
1081
1103
  data['departureTimers'] = response
1082
1104
  return data
1083
1105
  elif response.get('status_code', {}):
1084
- _LOGGER.warning(f'Could not fetch timers, HTTP status code: {response.get("status_code")}')
1106
+ _LOGGER.warning(f'Could not fetch departure timers, HTTP status code: {response.get("status_code")}')
1085
1107
  else:
1086
1108
  _LOGGER.info('Unknown error while trying to fetch data for departure timers')
1087
1109
  except Exception as error:
1088
- _LOGGER.warning(f'Could not fetch timers, error: {error}')
1110
+ _LOGGER.warning(f'Could not fetch departure timers, error: {error}')
1111
+ return False
1112
+
1113
+ async def getDepartureprofiles(self, vin, baseurl):
1114
+ """Get departure timers."""
1115
+ await self.set_token(self._session_auth_brand)
1116
+ try:
1117
+ response = await self.get(eval(f"f'{API_DEPARTURE_PROFILES}'"))
1118
+ if response.get('timers', {}):
1119
+ for e in range(len(response.get('timers', []))):
1120
+ if response['timers'][e].get('singleTimer','')==None:
1121
+ response['timers'][e].pop('singleTimer')
1122
+ if response['timers'][e].get('recurringTimer','')==None:
1123
+ response['timers'][e].pop('recurringTimer')
1124
+ data={}
1125
+ data['departureProfiles'] = response
1126
+ return data
1127
+ elif response.get('status_code', {}):
1128
+ _LOGGER.warning(f'Could not fetch departure profiles, HTTP status code: {response.get("status_code")}')
1129
+ else:
1130
+ _LOGGER.info('Unknown error while trying to fetch data for departure profiles')
1131
+ except Exception as error:
1132
+ _LOGGER.warning(f'Could not fetch departure profiles, error: {error}')
1089
1133
  return False
1090
1134
 
1091
1135
  async def getClimater(self, vin, baseurl):
@@ -1271,6 +1315,39 @@ class Connection:
1271
1315
  raise
1272
1316
  return False
1273
1317
 
1318
+ async def _setViaPUTtoAPI(self, endpoint, **data):
1319
+ """PUT call to API to set a value or to start an action."""
1320
+ await self.set_token(self._session_auth_brand)
1321
+ try:
1322
+ url = endpoint
1323
+ response = await self._request(METH_PUT,url, **data)
1324
+ if not response:
1325
+ raise SeatException(f'Invalid or no response for endpoint {endpoint}')
1326
+ elif response == 429:
1327
+ raise SeatThrottledException('Action rate limit reached. Start the car to reset the action limit')
1328
+ else:
1329
+ data = {'id': '', 'state' : ''}
1330
+ if 'requestId' in response:
1331
+ data['state'] = 'Request accepted'
1332
+ for key in response:
1333
+ if isinstance(response.get(key), dict):
1334
+ for k in response.get(key):
1335
+ if 'id' in k.lower():
1336
+ data['id'] = str(response.get(key).get(k))
1337
+ if 'state' in k.lower():
1338
+ data['state'] = response.get(key).get(k)
1339
+ else:
1340
+ if 'Id' in key:
1341
+ data['id'] = str(response.get(key))
1342
+ if 'State' in key:
1343
+ data['state'] = response.get(key)
1344
+ if response.get('rate_limit_remaining', False):
1345
+ data['rate_limit_remaining'] = response.get('rate_limit_remaining', None)
1346
+ return data
1347
+ except:
1348
+ raise
1349
+ return False
1350
+
1274
1351
  async def setCharger(self, vin, baseurl, mode, data):
1275
1352
  """Start/Stop charger."""
1276
1353
  if mode in {'start', 'stop'}:
@@ -1321,21 +1398,25 @@ class Connection:
1321
1398
  raise
1322
1399
  return False
1323
1400
 
1401
+ async def setDepartureprofile(self, vin, baseurl, data, spin):
1402
+ """Set departure profiles."""
1403
+ try:
1404
+ url= eval(f"f'{API_DEPARTURE_PROFILES}'")
1405
+ #if data:
1406
+ #if data.get('minSocPercentage',False):
1407
+ # url=url+'/settings'
1408
+ return await self._setViaPUTtoAPI(url, json = data)
1409
+ except:
1410
+ raise
1411
+ return False
1412
+
1324
1413
  async def sendDestination(self, vin, baseurl, data, spin):
1325
1414
  """Send destination to vehicle."""
1326
1415
 
1327
1416
  await self.set_token(self._session_auth_brand)
1328
1417
  try:
1329
1418
  url= eval(f"f'{API_DESTINATION}'")
1330
- response = await self._session.request(
1331
- METH_PUT,
1332
- url,
1333
- headers=self._session_headers,
1334
- timeout=ClientTimeout(total=TIMEOUT.seconds),
1335
- cookies=self._session_cookies,
1336
- raise_for_status=False,
1337
- json=data
1338
- )
1419
+ response = await self._request(METH_PUT, url, json=data)
1339
1420
  if response.status==202: #[202 Accepted]
1340
1421
  _LOGGER.debug(f'Destination {data[0]} successfully sent to API.')
1341
1422
  return response
@@ -133,6 +133,7 @@ API_CHARGING = '{baseurl}/v1/vehicles/{vin}/charging'
133
133
  API_CLIMATER_STATUS = '{baseurl}/v1/vehicles/{vin}/climatisation/status' # Climatisation data
134
134
  API_CLIMATER = '{baseurl}/v2/vehicles/{vin}/climatisation' # Climatisation data
135
135
  API_DEPARTURE_TIMERS = '{baseurl}/v1/vehicles/{vin}/departure-timers' # Departure timers
136
+ API_DEPARTURE_PROFILES = '{baseurl}/v1/vehicles/{vin}/departure/profiles' # Departure profiles
136
137
  API_POSITION = '{baseurl}/v1/vehicles/{vin}/parkingposition' # Position data
137
138
  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'
138
139
  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)
@@ -402,7 +402,7 @@ class RequestHonkAndFlash(Switch):
402
402
 
403
403
  async def turn_on(self):
404
404
  await self.vehicle.set_honkandflash('honkandflash')
405
- await self.vehicle.update()
405
+ #await self.vehicle.update()
406
406
  if self.callback is not None:
407
407
  self.callback()
408
408
 
@@ -428,7 +428,7 @@ class RequestFlash(Switch):
428
428
 
429
429
  async def turn_on(self):
430
430
  await self.vehicle.set_honkandflash('flash')
431
- await self.vehicle.update()
431
+ #await self.vehicle.update()
432
432
  if self.callback is not None:
433
433
  self.callback()
434
434
 
@@ -444,9 +444,9 @@ class RequestFlash(Switch):
444
444
  return dict(last_result = self.vehicle.honkandflash_action_status)
445
445
 
446
446
 
447
- class RequestUpdate(Switch):
447
+ class RequestRefresh(Switch):
448
448
  def __init__(self):
449
- super().__init__(attr="refresh_data", name="Force data refresh", icon="mdi:car-connected")
449
+ super().__init__(attr="refresh_data", name="Request wakeup vehicle", icon="mdi:car-connected")
450
450
 
451
451
  @property
452
452
  def state(self):
@@ -454,7 +454,7 @@ class RequestUpdate(Switch):
454
454
 
455
455
  async def turn_on(self):
456
456
  await self.vehicle.set_refresh()
457
- await self.vehicle.update()
457
+ await self.vehicle.update(updateType=1) #full update after set_refresh
458
458
  if self.callback is not None:
459
459
  self.callback()
460
460
 
@@ -470,6 +470,31 @@ class RequestUpdate(Switch):
470
470
  return dict(last_result = self.vehicle.refresh_action_status)
471
471
 
472
472
 
473
+ class RequestUpdate(Switch):
474
+ def __init__(self):
475
+ super().__init__(attr="update_data", name="Request full update", icon="mdi:timer-refresh")
476
+
477
+ @property
478
+ def state(self):
479
+ return False #self.vehicle.update
480
+
481
+ async def turn_on(self):
482
+ await self.vehicle.update(updateType=1) #full update after set_refresh
483
+ if self.callback is not None:
484
+ self.callback()
485
+
486
+ async def turn_off(self):
487
+ pass
488
+
489
+ @property
490
+ def assumed_state(self):
491
+ return False
492
+
493
+ #@property
494
+ #def attributes(self):
495
+ # return dict()
496
+
497
+
473
498
  class ElectricClimatisation(Switch):
474
499
  def __init__(self):
475
500
  super().__init__(attr="electric_climatisation", name="Electric Climatisation", icon="mdi:radiator")
@@ -480,11 +505,11 @@ class ElectricClimatisation(Switch):
480
505
 
481
506
  async def turn_on(self):
482
507
  await self.vehicle.set_climatisation(mode = 'electric')
483
- await self.vehicle.update()
508
+ #await self.vehicle.update()
484
509
 
485
510
  async def turn_off(self):
486
511
  await self.vehicle.set_climatisation(mode = 'off')
487
- await self.vehicle.update()
512
+ #await self.vehicle.update()
488
513
 
489
514
  @property
490
515
  def assumed_state(self):
@@ -514,11 +539,11 @@ class AuxiliaryClimatisation(Switch):
514
539
 
515
540
  async def turn_on(self):
516
541
  await self.vehicle.set_climatisation(mode = 'auxiliary', spin = self.spin)
517
- await self.vehicle.update()
542
+ #await self.vehicle.update()
518
543
 
519
544
  async def turn_off(self):
520
545
  await self.vehicle.set_climatisation(mode = 'off')
521
- await self.vehicle.update()
546
+ #await self.vehicle.update()
522
547
 
523
548
  @property
524
549
  def assumed_state(self):
@@ -539,11 +564,11 @@ class Charging(Switch):
539
564
 
540
565
  async def turn_on(self):
541
566
  await self.vehicle.set_charger('start')
542
- await self.vehicle.update()
567
+ #await self.vehicle.update()
543
568
 
544
569
  async def turn_off(self):
545
570
  await self.vehicle.set_charger('stop')
546
- await self.vehicle.update()
571
+ #await self.vehicle.update()
547
572
 
548
573
  @property
549
574
  def assumed_state(self):
@@ -564,11 +589,11 @@ class WindowHeater(Switch):
564
589
 
565
590
  async def turn_on(self):
566
591
  await self.vehicle.set_window_heating('start')
567
- await self.vehicle.update()
592
+ #await self.vehicle.update()
568
593
 
569
594
  async def turn_off(self):
570
595
  await self.vehicle.set_window_heating('stop')
571
- await self.vehicle.update()
596
+ #await self.vehicle.update()
572
597
 
573
598
  @property
574
599
  def assumed_state(self):
@@ -617,11 +642,11 @@ class BatteryClimatisation(Switch):
617
642
 
618
643
  async def turn_on(self):
619
644
  await self.vehicle.set_battery_climatisation(True)
620
- await self.vehicle.update()
645
+ #await self.vehicle.update()
621
646
 
622
647
  async def turn_off(self):
623
648
  await self.vehicle.set_battery_climatisation(False)
624
- await self.vehicle.update()
649
+ #await self.vehicle.update()
625
650
 
626
651
  @property
627
652
  def assumed_state(self):
@@ -646,11 +671,11 @@ class PHeaterHeating(Switch):
646
671
 
647
672
  async def turn_on(self):
648
673
  await self.vehicle.set_pheater(mode='heating', spin=self.spin)
649
- await self.vehicle.update()
674
+ #await self.vehicle.update()
650
675
 
651
676
  async def turn_off(self):
652
677
  await self.vehicle.set_pheater(mode='off', spin=self.spin)
653
- await self.vehicle.update()
678
+ #await self.vehicle.update()
654
679
 
655
680
  @property
656
681
  def assumed_state(self):
@@ -675,11 +700,11 @@ class PHeaterVentilation(Switch):
675
700
 
676
701
  async def turn_on(self):
677
702
  await self.vehicle.set_pheater(mode='ventilation', spin=self.spin)
678
- await self.vehicle.update()
703
+ #await self.vehicle.update()
679
704
 
680
705
  async def turn_off(self):
681
706
  await self.vehicle.set_pheater(mode='off', spin=self.spin)
682
- await self.vehicle.update()
707
+ #await self.vehicle.update()
683
708
 
684
709
  @property
685
710
  def assumed_state(self):
@@ -719,11 +744,11 @@ class DepartureTimer1(Switch):
719
744
 
720
745
  async def turn_on(self):
721
746
  await self.vehicle.set_timer_active(id=1, action="on")
722
- await self.vehicle.update()
747
+ #await self.vehicle.update()
723
748
 
724
749
  async def turn_off(self):
725
750
  await self.vehicle.set_timer_active(id=1, action="off")
726
- await self.vehicle.update()
751
+ #await self.vehicle.update()
727
752
 
728
753
  @property
729
754
  def assumed_state(self):
@@ -751,11 +776,11 @@ class DepartureTimer2(Switch):
751
776
 
752
777
  async def turn_on(self):
753
778
  await self.vehicle.set_timer_active(id=2, action="on")
754
- await self.vehicle.update()
779
+ #await self.vehicle.update()
755
780
 
756
781
  async def turn_off(self):
757
782
  await self.vehicle.set_timer_active(id=2, action="off")
758
- await self.vehicle.update()
783
+ #await self.vehicle.update()
759
784
 
760
785
  @property
761
786
  def assumed_state(self):
@@ -782,11 +807,11 @@ class DepartureTimer3(Switch):
782
807
 
783
808
  async def turn_on(self):
784
809
  await self.vehicle.set_timer_active(id=3, action="on")
785
- await self.vehicle.update()
810
+ #await self.vehicle.update()
786
811
 
787
812
  async def turn_off(self):
788
813
  await self.vehicle.set_timer_active(id=3, action="off")
789
- await self.vehicle.update()
814
+ #await self.vehicle.update()
790
815
 
791
816
  @property
792
817
  def assumed_state(self):
@@ -796,6 +821,99 @@ class DepartureTimer3(Switch):
796
821
  def attributes(self):
797
822
  return dict(self.vehicle.departure3)
798
823
 
824
+ class DepartureProfile1(Switch):
825
+ def __init__(self):
826
+ super().__init__(attr="departure_profile1", name="Departure profile 1", icon="mdi:radiator")
827
+
828
+ def configurate(self, **config):
829
+ self.spin = config.get('spin', '')
830
+
831
+ @property
832
+ def state(self):
833
+ status = self.vehicle.departure_profile1.get("enabled", "")
834
+ if status:
835
+ return True
836
+ else:
837
+ return False
838
+
839
+ async def turn_on(self):
840
+ await self.vehicle.set_departure_profile_active(id=1, action="on")
841
+ #await self.vehicle.update()
842
+
843
+ async def turn_off(self):
844
+ await self.vehicle.set_departure_profile_active(id=1, action="off")
845
+ #await self.vehicle.update()
846
+
847
+ @property
848
+ def assumed_state(self):
849
+ return False
850
+
851
+ @property
852
+ def attributes(self):
853
+ return dict(self.vehicle.departure_profile1)
854
+
855
+ class DepartureProfile2(Switch):
856
+ def __init__(self):
857
+ super().__init__(attr="departure_profile2", name="Departure profile 2", icon="mdi:radiator")
858
+
859
+ def configurate(self, **config):
860
+ self.spin = config.get('spin', '')
861
+
862
+ @property
863
+ def state(self):
864
+ status = self.vehicle.departure_profile2.get("enabled", "")
865
+ if status:
866
+ return True
867
+ else:
868
+ return False
869
+
870
+ async def turn_on(self):
871
+ await self.vehicle.set_departure_profile_active(id=2, action="on")
872
+ #await self.vehicle.update()
873
+
874
+ async def turn_off(self):
875
+ await self.vehicle.set_departure_profile_active(id=2, action="off")
876
+ #await self.vehicle.update()
877
+
878
+ @property
879
+ def assumed_state(self):
880
+ return False
881
+
882
+ @property
883
+ def attributes(self):
884
+ return dict(self.vehicle.departure_profile2)
885
+
886
+ class DepartureProfile3(Switch):
887
+ def __init__(self):
888
+ super().__init__(attr="departure_profile3", name="Departure profile 3", icon="mdi:radiator")
889
+
890
+ def configurate(self, **config):
891
+ self.spin = config.get('spin', '')
892
+
893
+ @property
894
+ def state(self):
895
+ status = self.vehicle.departure_profile3.get("enabled", "")
896
+ if status:
897
+ return True
898
+ else:
899
+ return False
900
+
901
+ async def turn_on(self):
902
+ await self.vehicle.set_departure_profile_active(id=3, action="on")
903
+ #await self.vehicle.update()
904
+
905
+ async def turn_off(self):
906
+ await self.vehicle.set_departure_profile_active(id=3, action="off")
907
+ #await self.vehicle.update()
908
+
909
+ @property
910
+ def assumed_state(self):
911
+ return False
912
+
913
+ @property
914
+ def attributes(self):
915
+ return dict(self.vehicle.departure_profile3)
916
+
799
917
 
800
918
  class RequestResults(Sensor):
801
919
  def __init__(self):
@@ -823,6 +941,7 @@ def create_instruments():
823
941
  TrunkLock(),
824
942
  RequestFlash(),
825
943
  RequestHonkAndFlash(),
944
+ RequestRefresh(),
826
945
  RequestUpdate(),
827
946
  WindowHeater(),
828
947
  BatteryClimatisation(),
@@ -838,6 +957,9 @@ def create_instruments():
838
957
  DepartureTimer1(),
839
958
  DepartureTimer2(),
840
959
  DepartureTimer3(),
960
+ DepartureProfile1(),
961
+ DepartureProfile2(),
962
+ DepartureProfile3(),
841
963
  Sensor(
842
964
  attr="distance",
843
965
  name="Odometer",
@@ -904,6 +1026,12 @@ def create_instruments():
904
1026
  icon="mdi:clock",
905
1027
  device_class="timestamp"
906
1028
  ),
1029
+ Sensor(
1030
+ attr="last_full_update",
1031
+ name="Last full update",
1032
+ icon="mdi:clock",
1033
+ device_class="timestamp"
1034
+ ),
907
1035
  Sensor(
908
1036
  attr="parking_time",
909
1037
  name="Parking time",
@@ -5,6 +5,7 @@ import re
5
5
  import logging
6
6
  import asyncio
7
7
 
8
+ from copy import deepcopy
8
9
  from datetime import datetime, timedelta, timezone
9
10
  from json import dumps as to_json
10
11
  from collections import OrderedDict
@@ -43,6 +44,7 @@ class Vehicle:
43
44
 
44
45
  self._requests = {
45
46
  'departuretimer': {'status': '', 'timestamp': DATEZERO},
47
+ 'departureprofile': {'status': '', 'timestamp': DATEZERO},
46
48
  'batterycharge': {'status': '', 'timestamp': DATEZERO},
47
49
  'climatisation': {'status': '', 'timestamp': DATEZERO},
48
50
  'refresh': {'status': '', 'timestamp': DATEZERO},
@@ -56,16 +58,18 @@ class Vehicle:
56
58
  self._climate_duration = 30
57
59
 
58
60
  self._relevantCapabilties = {
59
- 'measurements': {'active': False, 'reason': 'not supported'},
61
+ 'measurements': {'active': False, 'reason': 'not supported', },
60
62
  'climatisation': {'active': False, 'reason': 'not supported'},
61
63
  #'parkingInformation': {'active': False, 'reason': 'not supported'},
62
- 'tripStatistics': {'active': False, 'reason': 'not supported'},
64
+ 'tripStatistics': {'active': False, 'reason': 'not supported', 'supportsCyclicTrips': False},
65
+ 'vehicleHealthInspection': {'active': False, 'reason': 'not supported'},
63
66
  'vehicleHealthWarnings': {'active': False, 'reason': 'not supported'},
64
67
  'state': {'active': False, 'reason': 'not supported'},
65
- 'charging': {'active': False, 'reason': 'not supported'},
68
+ 'charging': {'active': False, 'reason': 'not supported', 'supportsTargetStateOfCharge': False},
66
69
  'honkAndFlash': {'active': False, 'reason': 'not supported'},
67
70
  'parkingPosition': {'active': False, 'reason': 'not supported'},
68
- 'departureTimers': {'active': False, 'reason': 'not supported'},
71
+ 'departureTimers': {'active': False, 'reason': 'not supported', 'supportsSingleTimer': False},
72
+ 'departureProfiles': {'active': False, 'reason': 'not supported', 'supportsSingleTimer': False},
69
73
  'transactionHistoryLockUnlock': {'active': False, 'reason': 'not supported'},
70
74
  'transactionHistoryHonkFlash': {'active': False, 'reason': 'not supported'},
71
75
  }
@@ -74,10 +78,10 @@ class Vehicle:
74
78
  # Init and update vehicle data
75
79
  async def discover(self):
76
80
  """Discover vehicle and initial data."""
77
- await asyncio.gather(
78
- self.get_basiccardata(),
79
- return_exceptions=True
80
- )
81
+ #await asyncio.gather(
82
+ # self.get_basiccardata(),
83
+ # return_exceptions=True
84
+ #)
81
85
  # Extract information of relevant capabilities
82
86
  for capa in self._capabilities:
83
87
  id=capa.get('id', '')
@@ -90,41 +94,67 @@ class Vehicle:
90
94
  data['reason']=capa.get('user-enabled', False)
91
95
  if capa.get('status', False):
92
96
  data['reason']=capa.get('status', '')
97
+ if capa.get('parameters', False):
98
+ if capa['parameters'].get('supportsCyclicTrips',False)==True or capa['parameters'].get('supportsCyclicTrips',False)=='true':
99
+ data['supportsCyclicTrips']=True
100
+ if capa['parameters'].get('supportsTargetStateOfCharge',False)==True or capa['parameters'].get('supportsTargetStateOfCharge',False)=='true':
101
+ data['supportsTargetStateOfCharge']=True
102
+ if capa['parameters'].get('supportsSingleTimer',False)==True or capa['parameters'].get('supportsSingleTimer',False)=='true':
103
+ data['supportsSingleTimer']=True
93
104
  self._relevantCapabilties[id].update(data)
94
105
 
95
106
 
96
- # Get URLs for model image
107
+ await self.get_trip_statistic()
108
+ # Get URLs for model image
97
109
  self._modelimages = await self.get_modelimageurl()
98
110
 
99
111
  self._discovered = datetime.now()
100
112
 
101
- async def update(self):
113
+ async def update(self, updateType=0):
102
114
  """Try to fetch data for all known API endpoints."""
103
115
  # Update vehicle information if not discovered or stale information
104
116
  if not self._discovered:
105
117
  await self.discover()
106
118
  else:
107
- # Rediscover if data is older than 1 hour
108
- hourago = datetime.now() - timedelta(hours = 1)
119
+ # Rediscover if data is older than 2 hours
120
+ hourago = datetime.now() - timedelta(hours = 2)
109
121
  if self._discovered < hourago:
110
- #await self.discover()
111
- _LOGGER.debug('Achtung! self.discover() auskommentiert')
122
+ await self.discover()
123
+ #_LOGGER.debug('Achtung! self.discover() auskommentiert')
112
124
 
113
125
  # Fetch all data if car is not deactivated
114
126
  if not self.deactivated:
115
127
  try:
128
+ # Data to be updated most often
129
+ await asyncio.gather(
130
+ self.get_charger(),
131
+ self.get_basiccardata(),
132
+ return_exceptions=True
133
+ )
134
+
135
+ fullUpdateExpired = datetime.now(timezone.utc) - timedelta(seconds= 1100)
136
+ if hasattr(self, '_last_full_update'):
137
+ _LOGGER.debug(f'last_full_update= {self._last_full_update}, fullUpdateExpired= {fullUpdateExpired}.')
138
+ if updateType!=1 and (hasattr(self, '_last_full_update') and self._last_full_update>fullUpdateExpired):
139
+ _LOGGER.debug(f'Just performed small update for vehicle with VIN {self.vin}.')
140
+ return True
141
+
142
+ # Data to be updated less often
116
143
  await asyncio.gather(
117
144
  self.get_preheater(),
118
145
  self.get_climater(),
119
- self.get_trip_statistic(),
146
+ #self.get_trip_statistic(), # commented out, because getting the trip statistic in discover() should be sufficient
120
147
  self.get_position(),
121
148
  self.get_statusreport(),
122
- self.get_charger(),
123
- self.get_timerprogramming(),
124
- self.get_basiccardata(),
125
- #self.get_modelimageurl(), #commented out because getting the images once in discover() should be sufficient
149
+ self.get_vehicleHealthWarnings(),
150
+ self.get_departure_timers(),
151
+ self.get_departure_profiles(),
152
+ self.get_mileage(),
153
+ #self.get_modelimageurl(), #commented out, because getting the images discover() should be sufficient
126
154
  return_exceptions=True
127
155
  )
156
+ self._last_full_update = datetime.now(timezone.utc)
157
+ _LOGGER.debug(f'Performed full update for vehicle with VIN {self.vin}.')
128
158
  except:
129
159
  raise SeatException("Update failed")
130
160
  return True
@@ -144,6 +174,12 @@ class Vehicle:
144
174
  if data:
145
175
  self._states.update(data)
146
176
 
177
+ async def get_mileage(self):
178
+ """Fetch basic car data."""
179
+ data = await self._connection.getMileage(self.vin, self._apibase)
180
+ if data:
181
+ self._states.update(data)
182
+
147
183
  async def get_preheater(self):
148
184
  """Fetch pre-heater data if function is enabled."""
149
185
  _LOGGER.info('get_preheater() not implemented yet')
@@ -171,7 +207,7 @@ class Vehicle:
171
207
  async def get_trip_statistic(self):
172
208
  """Fetch trip data if function is enabled."""
173
209
  if self._relevantCapabilties.get('tripStatistics', {}).get('active', False):
174
- data = await self._connection.getTripStatistics(self.vin, self._apibase)
210
+ data = await self._connection.getTripStatistics(self.vin, self._apibase, self._relevantCapabilties['tripStatistics'].get('supportsCyclicTrips', False))
175
211
  if data:
176
212
  self._states.update(data)
177
213
  else:
@@ -195,6 +231,14 @@ class Vehicle:
195
231
  else:
196
232
  _LOGGER.debug('Could not fetch any positional data')
197
233
 
234
+ async def get_vehicleHealthWarnings(self):
235
+ if self._relevantCapabilties.get('vehicleHealthWarnings', {}).get('active', False):
236
+ data = await self._connection.getVehicleHealthWarnings(self.vin, self._apibase)
237
+ if data:
238
+ self._states.update(data)
239
+ else:
240
+ _LOGGER.debug('Could not fetch vehicle health warnings')
241
+
198
242
  async def get_statusreport(self):
199
243
  """Fetch status data if function is enabled."""
200
244
  if self._relevantCapabilties.get('state', {}).get('active', False):
@@ -203,7 +247,7 @@ class Vehicle:
203
247
  self._states.update(data)
204
248
  else:
205
249
  _LOGGER.debug('Could not fetch status report')
206
- if self._relevantCapabilties.get('vehicleHealthWarnings', {}).get('active', False):
250
+ if self._relevantCapabilties.get('vehicleHealthInspection', {}).get('active', False):
207
251
  data = await self._connection.getMaintenance(self.vin, self._apibase)
208
252
  if data:
209
253
  self._states.update(data)
@@ -219,7 +263,7 @@ class Vehicle:
219
263
  else:
220
264
  _LOGGER.debug('Could not fetch charger data')
221
265
 
222
- async def get_timerprogramming(self):
266
+ async def get_departure_timers(self):
223
267
  """Fetch timer data if function is enabled."""
224
268
  if self._relevantCapabilties.get('departureTimers', {}).get('active', False):
225
269
  data = await self._connection.getDeparturetimer(self.vin, self._apibase)
@@ -228,6 +272,15 @@ class Vehicle:
228
272
  else:
229
273
  _LOGGER.debug('Could not fetch timers')
230
274
 
275
+ async def get_departure_profiles(self):
276
+ """Fetch timer data if function is enabled."""
277
+ if self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
278
+ data = await self._connection.getDepartureprofiles(self.vin, self._apibase)
279
+ if data:
280
+ self._states.update(data)
281
+ else:
282
+ _LOGGER.debug('Could not fetch timers')
283
+
231
284
  #async def wait_for_request(self, section, request, retryCount=36):
232
285
  """Update status of outstanding requests."""
233
286
  """retryCount -= 1
@@ -289,7 +342,7 @@ class Vehicle:
289
342
  raise SeatInvalidRequestException('Remote start/stop of charger is not supported.')
290
343
  if self._requests['batterycharge'].get('id', False):
291
344
  timestamp = self._requests.get('batterycharge', {}).get('timestamp', datetime.now())
292
- expired = datetime.now() - timedelta(minutes=3)
345
+ expired = datetime.now() - timedelta(minutes=1)
293
346
  if expired > timestamp:
294
347
  self._requests.get('batterycharge', {}).pop('id')
295
348
  else:
@@ -321,8 +374,8 @@ class Vehicle:
321
374
  # Update the charger data and check, if they have changed as expected
322
375
  retry = 0
323
376
  actionSuccessful = False
324
- while not actionSuccessful and retry < 3:
325
- await asyncio.sleep(5)
377
+ while not actionSuccessful and retry < 2:
378
+ await asyncio.sleep(15)
326
379
  await self.get_charger()
327
380
  if mode == 'start':
328
381
  if self.charging:
@@ -338,6 +391,7 @@ class Vehicle:
338
391
  raise
339
392
  retry = retry +1
340
393
  if actionSuccessful:
394
+ _LOGGER.debug('POST request for charger successful. New status as expected.')
341
395
  self._requests.get('batterycharge', {}).pop('id')
342
396
  return True
343
397
  _LOGGER.error('Response to POST request seemed successful but the charging status did not change as expected.')
@@ -351,12 +405,15 @@ class Vehicle:
351
405
 
352
406
  # API endpoint departuretimer
353
407
  async def set_charge_limit(self, limit=50):
354
- """ Set charging limit. """
355
- if not self._relevantCapabilties.get('departureTimers', {}).get('active', False) and not self._relevantCapabilties.get('charging', {}).get('active', False):
408
+ """ Set minimum state of charge limit for departure timers or departure profiles. """
409
+ if (not self._relevantCapabilties.get('departureTimers', {}).get('active', False) and
410
+ not self._relevantCapabilties.get('departureProfiles', {}).get('active', False) and
411
+ not self._relevantCapabilties.get('charging', {}).get('active', False)):
356
412
  _LOGGER.info('Set charging limit is not supported.')
357
413
  raise SeatInvalidRequestException('Set charging limit is not supported.')
358
- data = {}
359
- if self._relevantCapabilties.get('departureTimers', {}).get('active', False):
414
+ if self._relevantCapabilties.get('departureTimers', {}).get('active', False) :
415
+ # Vehicle has departure timers
416
+ data = {}
360
417
  if isinstance(limit, int):
361
418
  if limit in [0, 10, 20, 30, 40, 50]:
362
419
  data['minSocPercentage'] = limit
@@ -365,13 +422,24 @@ class Vehicle:
365
422
  else:
366
423
  raise SeatInvalidRequestException(f'Charge limit "{limit}" is not supported.')
367
424
  return await self._set_timers(data)
425
+ elif self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
426
+ # Vehicle has departure profiles
427
+ data= deepcopy(self.attrs.get('departureProfiles'))
428
+ if isinstance(limit, int):
429
+ if limit in [0, 10, 20, 30, 40, 50]:
430
+ data['minSocPercentage'] = limit
431
+ else:
432
+ raise SeatInvalidRequestException(f'Charge limit must be one of 0, 10, 20, 30, 40 or 50.')
433
+ else:
434
+ raise SeatInvalidRequestException(f'Charge limit "{limit}" is not supported.')
435
+ return await self._set_departure_profiles(data, action='minSocPercentage')
368
436
 
369
437
  async def set_timer_active(self, id=1, action='off'):
370
438
  """ Activate/deactivate departure timers. """
371
439
  data = {}
372
- supported = 'is_departure' + str(id) + "_supported"
440
+ supported = "is_departure" + str(id) + "_supported"
373
441
  if getattr(self, supported) is not True:
374
- raise SeatConfigException(f'This vehicle does not support timer id "{id}".')
442
+ raise SeatConfigException(f'This vehicle does not support timer id {id}.')
375
443
  if self._relevantCapabilties.get('departureTimers', {}).get('active', False):
376
444
  allTimers= self.attrs.get('departureTimers').get('timers', [])
377
445
  for singleTimer in allTimers:
@@ -389,7 +457,7 @@ class Vehicle:
389
457
  else:
390
458
  raise SeatInvalidRequestException(f'Timer action "{action}" is not supported.')
391
459
  return await self._set_timers(data)
392
- raise SeatInvalidRequestException(f'Departure timers id {id} not found.')
460
+ raise SeatInvalidRequestException(f'Departure timer id {id} not found.')
393
461
  else:
394
462
  raise SeatInvalidRequestException('Departure timers are not supported.')
395
463
 
@@ -397,9 +465,9 @@ class Vehicle:
397
465
  """ Set departure schedules. """
398
466
  data = {}
399
467
  # Validate required user inputs
400
- supported = 'is_departure' + str(id) + "_supported"
468
+ supported = "is_departure" + str(id) + "_supported"
401
469
  if getattr(self, supported) is not True:
402
- raise SeatConfigException(f'Timer id "{id}" is not supported for this vehicle.')
470
+ raise SeatConfigException(f'Timer id {id} is not supported for this vehicle.')
403
471
  else:
404
472
  _LOGGER.debug(f'Timer id {id} is supported')
405
473
  if not schedule:
@@ -461,6 +529,8 @@ class Vehicle:
461
529
  else:
462
530
  raise SeatInvalidRequestException('Invalid type for charge max current variable')
463
531
 
532
+ # Prepare data and execute
533
+ data['id'] = id
464
534
  # Converting schedule to data map
465
535
  if schedule.get("enabled",False):
466
536
  data['enabled']=True
@@ -484,7 +554,7 @@ class Vehicle:
484
554
  else:
485
555
  preferedChargingTimes= [{
486
556
  "id" : 1,
487
- "enabled" : True,
557
+ "enabled" : False,
488
558
  "startTimeLocal" : "00:00",
489
559
  "endTimeLocal" : "00:00"
490
560
  }]
@@ -499,7 +569,7 @@ class Vehicle:
499
569
  "fridays":(schedule.get('days',"nnnnnnn")[4]=='y'),
500
570
  "saturdays":(schedule.get('days',"nnnnnnn")[5]=='y'),
501
571
  "sundays":(schedule.get('days',"nnnnnnn")[6]=='y'),
502
- "preferredChargingTimes": preferedChargingTimes
572
+ #"preferredChargingTimes": preferedChargingTimes
503
573
  }
504
574
  }
505
575
  else:
@@ -507,11 +577,10 @@ class Vehicle:
507
577
  _LOGGER.info(f'startDateTime={startDateTime.isoformat()}')
508
578
  data['singleTimer']= {
509
579
  "startDateTimeLocal": startDateTime.isoformat(),
510
- "preferredChargingTimes": preferedChargingTimes
580
+ #"preferredChargingTimes": preferedChargingTimes
511
581
  }
582
+ data["preferredChargingTimes"]= preferedChargingTimes
512
583
 
513
- # Prepare data and execute
514
- data['id'] = id
515
584
  # Now we have to embed the data for the timer 'id' in timers[]
516
585
  data={
517
586
  'timers' : [data]
@@ -527,7 +596,7 @@ class Vehicle:
527
596
  raise SeatInvalidRequestException('Departure timers are not supported.')
528
597
  if self._requests['departuretimer'].get('id', False):
529
598
  timestamp = self._requests.get('departuretimer', {}).get('timestamp', datetime.now())
530
- expired = datetime.now() - timedelta(minutes=3)
599
+ expired = datetime.now() - timedelta(minutes=1)
531
600
  if expired > timestamp:
532
601
  self._requests.get('departuretimer', {}).pop('id')
533
602
  else:
@@ -550,24 +619,33 @@ class Vehicle:
550
619
  # Update the departure timers data and check, if they have changed as expected
551
620
  retry = 0
552
621
  actionSuccessful = False
553
- while not actionSuccessful and retry < 3:
554
- await asyncio.sleep(5)
555
- await self.get_timerprogramming()
622
+ while not actionSuccessful and retry < 2:
623
+ await asyncio.sleep(15)
624
+ await self.get_departure_timers()
556
625
  if data.get('minSocPercentage',False):
557
626
  if data.get('minSocPercentage',-2)==self.attrs.get('departureTimers',{}).get('minSocPercentage',-1):
558
627
  actionSuccessful=True
559
628
  else:
629
+ _LOGGER.debug('Checking if new departure timer is as expected:')
560
630
  timerData = data.get('timers',[])[0]
561
631
  timerDataId = timerData.get('id',False)
632
+ timerDataCopy = deepcopy(timerData)
633
+ timerDataCopy['enabled']=True
562
634
  if timerDataId:
563
635
  newTimers = self.attrs.get('departureTimers',{}).get('timers',[])
564
636
  for newTimer in newTimers:
565
637
  if newTimer.get('id',-1)==timerDataId:
566
- if timerData==newTimer:
638
+ _LOGGER.debug(f'Value of timer sent:{timerData}')
639
+ _LOGGER.debug(f'Value of timer read:{newTimer}')
640
+ if timerData==newTimer:
641
+ actionSuccessful=True
642
+ elif timerDataCopy==newTimer:
643
+ _LOGGER.debug('Data written and data read are the same, but the timer is activated.')
567
644
  actionSuccessful=True
568
645
  break
569
646
  retry = retry +1
570
- if actionSuccessful:
647
+ if True: #actionSuccessful:
648
+ #_LOGGER.debug('POST request for departure timers successful. New status as expected.')
571
649
  self._requests.get('departuretimer', {}).pop('id')
572
650
  return True
573
651
  _LOGGER.error('Response to POST request seemed successful but the departure timers status did not change as expected.')
@@ -579,6 +657,94 @@ class Vehicle:
579
657
  self._requests['departuretimer'] = {'status': 'Exception'}
580
658
  raise SeatException('Failed to set departure timer schedule')
581
659
 
660
+ async def set_departure_profile_active(self, id=1, action='off'):
661
+ """ Activate/deactivate departure profiles. """
662
+ data = {}
663
+ supported = "is_departure_profile" + str(id) + "_supported"
664
+ if getattr(self, supported) is not True:
665
+ raise SeatConfigException(f'This vehicle does not support departure profile id "{id}".')
666
+ if self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
667
+ data= deepcopy(self.attrs.get('departureProfiles'))
668
+ if len(data.get('timers', []))<1:
669
+ raise SeatInvalidRequestException(f'No timers found in departure profile: {data}.')
670
+ idFound=False
671
+ for e in range(len(data.get('timers', []))):
672
+ if data['timers'][e].get('id',-1)==id:
673
+ if action in ['on', 'off']:
674
+ if action=='on':
675
+ enabled=True
676
+ else:
677
+ enabled=False
678
+ data['timers'][e]['enabled'] = enabled
679
+ idFound=True
680
+ _LOGGER.debug(f'Changing departure profile {id} to {action}.')
681
+ else:
682
+ raise SeatInvalidRequestException(f'Profile action "{action}" is not supported.')
683
+ if idFound:
684
+ return await self._set_departure_profiles(data, action=action)
685
+ raise SeatInvalidRequestException(f'Departure profile id {id} not found in {data.get('timers',[])}.')
686
+ else:
687
+ raise SeatInvalidRequestException('Departure profiles are not supported.')
688
+
689
+ async def _set_departure_profiles(self, data=None, action=None):
690
+ """ Set departure profiles. """
691
+ if not self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
692
+ raise SeatInvalidRequestException('Departure profiles are not supported.')
693
+ if self._requests['departureprofile'].get('id', False):
694
+ timestamp = self._requests.get('departureprofile', {}).get('timestamp', datetime.now())
695
+ expired = datetime.now() - timedelta(minutes=1)
696
+ if expired > timestamp:
697
+ self._requests.get('departureprofile', {}).pop('id')
698
+ else:
699
+ raise SeatRequestInProgressException('Scheduling of departure profile is already in progress')
700
+ try:
701
+ self._requests['latest'] = 'Departureprofile'
702
+ response = await self._connection.setDepartureprofile(self.vin, self._apibase, data, spin=False)
703
+ if not response:
704
+ self._requests['departureprofile'] = {'status': 'Failed'}
705
+ _LOGGER.error('Failed to execute departure profile request')
706
+ raise SeatException('Failed to execute departure profile request')
707
+ else:
708
+ self._requests['remaining'] = response.get('rate_limit_remaining', -1)
709
+ self._requests['departureprofile'] = {
710
+ 'timestamp': datetime.now(),
711
+ 'status': response.get('state', 'Unknown'),
712
+ 'id': response.get('id', 0),
713
+ }
714
+ # Update the departure profile data and check, if they have changed as expected
715
+ retry = 0
716
+ actionSuccessful = False
717
+ while not actionSuccessful and retry < 2:
718
+ await asyncio.sleep(15)
719
+ await self.get_departure_profiles()
720
+ if action=='minSocPercentage':
721
+ _LOGGER.debug('Checking if new minSocPercentage is as expected:')
722
+ _LOGGER.debug(f'Value of minSocPercentage sent:{data.get('minSocPercentage',-2)}')
723
+ _LOGGER.debug(f'Value of minSocPercentage read:{self.attrs.get('departureTimers',{}).get('minSocPercentage',-1)}')
724
+ if data.get('minSocPercentage',-2)==self.attrs.get('departureTimers',{}).get('minSocPercentage',-1):
725
+ actionSuccessful=True
726
+ else:
727
+ sendData = data.get('timers',[])
728
+ newData = self.attrs.get('departureProfiles',{}).get('timers',[])
729
+ _LOGGER.debug('Checking if new departure profiles are as expected:')
730
+ _LOGGER.debug(f'Value of data sent:{sendData}')
731
+ _LOGGER.debug(f'Value of data read:{newData}')
732
+ if sendData==newData:
733
+ actionSuccessful=True
734
+ retry = retry +1
735
+ if actionSuccessful:
736
+ self._requests.get('departureprofile', {}).pop('id')
737
+ return True
738
+ _LOGGER.error('Response to PUT request seemed successful but the departure profiles status did not change as expected.')
739
+ return False
740
+ except (SeatInvalidRequestException, SeatException):
741
+ raise
742
+ except Exception as error:
743
+ _LOGGER.warning(f'Failed to execute departure profile request - {error}')
744
+ self._requests['departureprofile'] = {'status': 'Exception'}
745
+ raise SeatException('Failed to set departure profile schedule')
746
+
747
+
582
748
  # Send a destination to vehicle
583
749
  async def send_destination(self, destination=None):
584
750
  """ Send destination to vehicle. """
@@ -704,7 +870,7 @@ class Vehicle:
704
870
  raise SeatInvalidRequestException('Remote control of climatisation functions is not supported.')
705
871
  if self._requests['climatisation'].get('id', False):
706
872
  timestamp = self._requests.get('climatisation', {}).get('timestamp', datetime.now())
707
- expired = datetime.now() - timedelta(minutes=3)
873
+ expired = datetime.now() - timedelta(minutes=1)
708
874
  if expired > timestamp:
709
875
  self._requests.get('climatisation', {}).pop('id')
710
876
  else:
@@ -726,8 +892,8 @@ class Vehicle:
726
892
  # Update the climater data and check, if they have changed as expected
727
893
  retry = 0
728
894
  actionSuccessful = False
729
- while not actionSuccessful and retry < 3:
730
- await asyncio.sleep(5)
895
+ while not actionSuccessful and retry < 2:
896
+ await asyncio.sleep(15)
731
897
  await self.get_climater()
732
898
  if mode == 'start':
733
899
  if self.electric_climatisation:
@@ -749,6 +915,7 @@ class Vehicle:
749
915
  raise
750
916
  retry = retry +1
751
917
  if actionSuccessful:
918
+ _LOGGER.debug('POST request for climater successful. New status as expected.')
752
919
  self._requests.get('climatisation', {}).pop('id')
753
920
  return True
754
921
  _LOGGER.error('Response to POST request seemed successful but the climater status did not change as expected.')
@@ -768,7 +935,7 @@ class Vehicle:
768
935
  raise SeatInvalidRequestException('No parking heater support.')
769
936
  if self._requests['preheater'].get('id', False):
770
937
  timestamp = self._requests.get('preheater', {}).get('timestamp', datetime.now())
771
- expired = datetime.now() - timedelta(minutes=3)
938
+ expired = datetime.now() - timedelta(minutes=1)
772
939
  if expired > timestamp:
773
940
  self._requests.get('preheater', {}).pop('id')
774
941
  else:
@@ -908,7 +1075,7 @@ class Vehicle:
908
1075
  raise SeatInvalidRequestException('Data refresh is not supported.')
909
1076
  if self._requests['refresh'].get('id', False):
910
1077
  timestamp = self._requests.get('refresh', {}).get('timestamp', datetime.now() - timedelta(minutes=5))
911
- expired = datetime.now() - timedelta(minutes=3)
1078
+ expired = datetime.now() - timedelta(minutes=1)
912
1079
  if expired > timestamp:
913
1080
  self._requests.get('refresh', {}).pop('id')
914
1081
  else:
@@ -1052,7 +1219,7 @@ class Vehicle:
1052
1219
  def parking_light(self):
1053
1220
  """Return true if parking light is on"""
1054
1221
  response = self.attrs.get('status').get('lights', 0)
1055
- if response == 'On':
1222
+ if response == 'on':
1056
1223
  return True
1057
1224
  else:
1058
1225
  return False
@@ -1083,6 +1250,17 @@ class Vehicle:
1083
1250
  if 'updatedAt' in self.attrs.get('status', {}):
1084
1251
  return True
1085
1252
 
1253
+ # Update status
1254
+ @property
1255
+ def last_full_update(self):
1256
+ """Return when the last full update for the vehicle took place."""
1257
+ return self._last_full_update.astimezone(tz=None)
1258
+
1259
+ @property
1260
+ def is_last_full_update_supported(self):
1261
+ """Return when last full update for vehicle took place."""
1262
+ return True
1263
+
1086
1264
  # Service information
1087
1265
  @property
1088
1266
  def distance(self):
@@ -2034,7 +2212,6 @@ class Vehicle:
2034
2212
  return True if response != 0 else False
2035
2213
 
2036
2214
  # Departure timers
2037
- # Under development
2038
2215
  @property
2039
2216
  def departure1(self):
2040
2217
  """Return timer status and attributes."""
@@ -2146,6 +2323,79 @@ class Vehicle:
2146
2323
  return True
2147
2324
  return False
2148
2325
 
2326
+ # Departure profiles
2327
+ @property
2328
+ def departure_profile1(self):
2329
+ """Return profile status and attributes."""
2330
+ if self.attrs.get('departureProfiles', False):
2331
+ try:
2332
+ data = {}
2333
+ timerdata = self.attrs.get('departureProfiles', {}).get('timers', [])
2334
+ timer = timerdata[0]
2335
+ #timer.pop('timestamp', None)
2336
+ #timer.pop('timerID', None)
2337
+ #timer.pop('profileID', None)
2338
+ data.update(timer)
2339
+ return data
2340
+ except:
2341
+ pass
2342
+ return None
2343
+
2344
+ @property
2345
+ def is_departure_profile1_supported(self):
2346
+ """Return true if profile 1 is supported."""
2347
+ if len(self.attrs.get('departureProfiles', {}).get('timers', [])) >= 1:
2348
+ return True
2349
+ return False
2350
+
2351
+ @property
2352
+ def departure_profile2(self):
2353
+ """Return profile status and attributes."""
2354
+ if self.attrs.get('departureProfiles', False):
2355
+ try:
2356
+ data = {}
2357
+ timerdata = self.attrs.get('departureProfiles', {}).get('timers', [])
2358
+ timer = timerdata[1]
2359
+ #timer.pop('timestamp', None)
2360
+ #timer.pop('timerID', None)
2361
+ #timer.pop('profileID', None)
2362
+ data.update(timer)
2363
+ return data
2364
+ except:
2365
+ pass
2366
+ return None
2367
+
2368
+ @property
2369
+ def is_departure_profile2_supported(self):
2370
+ """Return true if profile 2 is supported."""
2371
+ if len(self.attrs.get('departureProfiles', {}).get('timers', [])) >= 2:
2372
+ return True
2373
+ return False
2374
+
2375
+ @property
2376
+ def departure_profile3(self):
2377
+ """Return profile status and attributes."""
2378
+ if self.attrs.get('departureProfiles', False):
2379
+ try:
2380
+ data = {}
2381
+ timerdata = self.attrs.get('departureProfiles', {}).get('timers', [])
2382
+ timer = timerdata[2]
2383
+ #timer.pop('timestamp', None)
2384
+ #timer.pop('timerID', None)
2385
+ #timer.pop('profileID', None)
2386
+ data.update(timer)
2387
+ return data
2388
+ except:
2389
+ pass
2390
+ return None
2391
+
2392
+ @property
2393
+ def is_departure_profile3_supported(self):
2394
+ """Return true if profile 3 is supported."""
2395
+ if len(self.attrs.get('departureProfiles', {}).get('timers', [])) >= 3:
2396
+ return True
2397
+ return False
2398
+
2149
2399
  # Trip data
2150
2400
  @property
2151
2401
  def trip_last_entry(self):
@@ -2466,6 +2716,16 @@ class Vehicle:
2466
2716
  if self._connectivities.get('mode', '') == 'online':
2467
2717
  return True
2468
2718
 
2719
+ @property
2720
+ def update_data(self):
2721
+ """Get state of data update"""
2722
+ return False
2723
+
2724
+ @property
2725
+ def is_update_data_supported(self):
2726
+ """Data update is supported."""
2727
+ return True
2728
+
2469
2729
  # Honk and flash
2470
2730
  @property
2471
2731
  def request_honkandflash(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycupra
3
- Version: 0.0.5
3
+ Version: 0.0.7
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes