pycupra 0.0.5__py3-none-any.whl → 0.0.7__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 +1 -1
- pycupra/connection.py +111 -30
- pycupra/const.py +1 -0
- pycupra/dashboard.py +153 -25
- pycupra/vehicle.py +312 -52
- {pycupra-0.0.5.dist-info → pycupra-0.0.7.dist-info}/METADATA +1 -1
- pycupra-0.0.7.dist-info/RECORD +13 -0
- pycupra-0.0.5.dist-info/RECORD +0 -13
- {pycupra-0.0.5.dist-info → pycupra-0.0.7.dist-info}/WHEEL +0 -0
- {pycupra-0.0.5.dist-info → pycupra-0.0.7.dist-info}/licenses/LICENSE +0 -0
- {pycupra-0.0.5.dist-info → pycupra-0.0.7.dist-info}/top_level.txt +0 -0
pycupra/__version__.py
CHANGED
pycupra/connection.py
CHANGED
@@ -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
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
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
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
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'
|
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.
|
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
|
pycupra/const.py
CHANGED
@@ -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)
|
pycupra/dashboard.py
CHANGED
@@ -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
|
447
|
+
class RequestRefresh(Switch):
|
448
448
|
def __init__(self):
|
449
|
-
super().__init__(attr="refresh_data", name="
|
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",
|
pycupra/vehicle.py
CHANGED
@@ -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
|
-
|
79
|
-
|
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
|
-
|
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
|
108
|
-
hourago = datetime.now() - timedelta(hours =
|
119
|
+
# Rediscover if data is older than 2 hours
|
120
|
+
hourago = datetime.now() - timedelta(hours = 2)
|
109
121
|
if self._discovered < hourago:
|
110
|
-
|
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.
|
123
|
-
self.
|
124
|
-
self.
|
125
|
-
|
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('
|
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
|
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=
|
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 <
|
325
|
-
await asyncio.sleep(
|
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
|
355
|
-
if not self._relevantCapabilties.get('departureTimers', {}).get('active', False) and
|
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
|
-
|
359
|
-
|
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 =
|
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
|
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
|
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 =
|
468
|
+
supported = "is_departure" + str(id) + "_supported"
|
401
469
|
if getattr(self, supported) is not True:
|
402
|
-
raise SeatConfigException(f'Timer id
|
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" :
|
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=
|
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 <
|
554
|
-
await asyncio.sleep(
|
555
|
-
await self.
|
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
|
-
|
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=
|
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 <
|
730
|
-
await asyncio.sleep(
|
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=
|
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=
|
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 == '
|
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.
|
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
|
@@ -0,0 +1,13 @@
|
|
1
|
+
pycupra/__init__.py,sha256=VPzUfKd5mBFD1UERNV61FbGHih5dQPupLgIfYtmIUi4,230
|
2
|
+
pycupra/__version__.py,sha256=sE47EQF8GjjvZy9jhhygnhK2JLO76vFrSBB6HfkoIW4,207
|
3
|
+
pycupra/connection.py,sha256=LK1v3Eb37lbVKIbQWIjn4xAIAFJqxXwlguX5iq2Ylm8,81393
|
4
|
+
pycupra/const.py,sha256=VEYH8TUsJGJwBwloaajwoElYd0qxE7oesvoagvDdE-4,10161
|
5
|
+
pycupra/dashboard.py,sha256=Dqs4qDF-rEoYXoS7NrNsvRMsCGzoAjVUEbfTSOdDAXo,41310
|
6
|
+
pycupra/exceptions.py,sha256=Nq_F79GP8wjHf5lpvPy9TbSIrRHAJrFMo0T1N9TcgSQ,2917
|
7
|
+
pycupra/utilities.py,sha256=cH4MiIzT2WlHgmnl_E7rR0R5LvCXfDNvirJolct50V8,2563
|
8
|
+
pycupra/vehicle.py,sha256=cEg7wG4WX6p4MfShRGZZ955cqvYERI6CmfWSN5NanSQ,122147
|
9
|
+
pycupra-0.0.7.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
10
|
+
pycupra-0.0.7.dist-info/METADATA,sha256=0W-3PCD7jSDrUtYauZKSzFfSe_dzbTqREQjNxlv28Qo,2578
|
11
|
+
pycupra-0.0.7.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
12
|
+
pycupra-0.0.7.dist-info/top_level.txt,sha256=9Lbj_jG4JvpGwt6K3AwhWFc0XieDnuHFOP4x44wSXSQ,8
|
13
|
+
pycupra-0.0.7.dist-info/RECORD,,
|
pycupra-0.0.5.dist-info/RECORD
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
pycupra/__init__.py,sha256=VPzUfKd5mBFD1UERNV61FbGHih5dQPupLgIfYtmIUi4,230
|
2
|
-
pycupra/__version__.py,sha256=pbFZ_GKa38mHgdQKZpGKVzHcmF4OGZEi3EgIrJ-6wRo,207
|
3
|
-
pycupra/connection.py,sha256=9Ha_zlAi6Cw6gNI1OA8roCoIJOEa1ZYgnRVO_Uk5c8s,77628
|
4
|
-
pycupra/const.py,sha256=Mx9pPZifQBpn9lTsLH8R7xkUHrXRvul8w_b6LLLD7gE,10038
|
5
|
-
pycupra/dashboard.py,sha256=7sVQI10lMspAOfVOlMEvMlndiNlUxjpWoNobUU9CZrw,37636
|
6
|
-
pycupra/exceptions.py,sha256=Nq_F79GP8wjHf5lpvPy9TbSIrRHAJrFMo0T1N9TcgSQ,2917
|
7
|
-
pycupra/utilities.py,sha256=cH4MiIzT2WlHgmnl_E7rR0R5LvCXfDNvirJolct50V8,2563
|
8
|
-
pycupra/vehicle.py,sha256=ea2InLdtWV1NNKG6V3VgYj-VJPe4IDVapB7aB14IKDQ,108646
|
9
|
-
pycupra-0.0.5.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
10
|
-
pycupra-0.0.5.dist-info/METADATA,sha256=xjH9t2ZtKIuMSPUTt4y_a9pD_jKRP25UZsEccpIoY4s,2578
|
11
|
-
pycupra-0.0.5.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
12
|
-
pycupra-0.0.5.dist-info/top_level.txt,sha256=9Lbj_jG4JvpGwt6K3AwhWFc0XieDnuHFOP4x44wSXSQ,8
|
13
|
-
pycupra-0.0.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|