pycupra 0.0.4__py3-none-any.whl → 0.0.6__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 +124 -29
- pycupra/const.py +1 -0
- pycupra/dashboard.py +118 -22
- pycupra/vehicle.py +278 -60
- {pycupra-0.0.4.dist-info → pycupra-0.0.6.dist-info}/METADATA +1 -1
- pycupra-0.0.6.dist-info/RECORD +13 -0
- pycupra-0.0.4.dist-info/RECORD +0 -13
- {pycupra-0.0.4.dist-info → pycupra-0.0.6.dist-info}/WHEEL +0 -0
- {pycupra-0.0.4.dist-info → pycupra-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {pycupra-0.0.4.dist-info → pycupra-0.0.6.dist-info}/top_level.txt +0 -0
pycupra/__version__.py
CHANGED
pycupra/connection.py
CHANGED
@@ -15,6 +15,8 @@ import string
|
|
15
15
|
import secrets
|
16
16
|
import xmltodict
|
17
17
|
|
18
|
+
from PIL import Image
|
19
|
+
from io import BytesIO
|
18
20
|
from sys import version_info, argv
|
19
21
|
from datetime import timedelta, datetime, timezone
|
20
22
|
from urllib.parse import urljoin, parse_qs, urlparse, urlencode
|
@@ -77,6 +79,7 @@ from .const import (
|
|
77
79
|
API_CLIMATER_STATUS,
|
78
80
|
API_CLIMATER,
|
79
81
|
API_DEPARTURE_TIMERS,
|
82
|
+
API_DEPARTURE_PROFILES,
|
80
83
|
API_MILEAGE,
|
81
84
|
API_CAPABILITIES,
|
82
85
|
#API_CAPABILITIES_MANAGEMENT,
|
@@ -621,6 +624,8 @@ class Connection:
|
|
621
624
|
try:
|
622
625
|
if response.status == 204:
|
623
626
|
res = {'status_code': response.status}
|
627
|
+
elif response.status == 202 and method==METH_PUT:
|
628
|
+
res = response
|
624
629
|
elif response.status >= 200 or response.status <= 300:
|
625
630
|
# If this is a revoke token url, expect Content-Length 0 and return
|
626
631
|
if int(response.headers.get('Content-Length', 0)) == 0 and 'revoke' in url:
|
@@ -879,16 +884,6 @@ class Connection:
|
|
879
884
|
_LOGGER.info('Unhandled error while trying to fetch mycar data')
|
880
885
|
except Exception as error:
|
881
886
|
_LOGGER.warning(f'Could not fetch mycar report, error: {error}')
|
882
|
-
try:
|
883
|
-
response = await self.get(eval(f"f'{API_WARNINGLIGHTS}'"))
|
884
|
-
if 'statuses' in response:
|
885
|
-
data['warninglights'] = response
|
886
|
-
elif response.get('status_code', {}):
|
887
|
-
_LOGGER.warning(f'Could not fetch warnlights, HTTP status code: {response.get("status_code")}')
|
888
|
-
else:
|
889
|
-
_LOGGER.info('Unhandled error while trying to fetch warnlights')
|
890
|
-
except Exception as error:
|
891
|
-
_LOGGER.warning(f'Could not fetch warnlights, error: {error}')
|
892
887
|
try:
|
893
888
|
response = await self.get(eval(f"f'{API_MILEAGE}'"))
|
894
889
|
if response.get('mileageKm', {}):
|
@@ -903,6 +898,24 @@ class Connection:
|
|
903
898
|
return False
|
904
899
|
return data
|
905
900
|
|
901
|
+
async def getVehicleHealthWarnings(self, vin, baseurl):
|
902
|
+
"""Get car information from customer profile, VIN, nickname, etc."""
|
903
|
+
await self.set_token(self._session_auth_brand)
|
904
|
+
data={}
|
905
|
+
try:
|
906
|
+
response = await self.get(eval(f"f'{API_WARNINGLIGHTS}'"))
|
907
|
+
if 'statuses' in response:
|
908
|
+
data['warninglights'] = response
|
909
|
+
elif response.get('status_code', {}):
|
910
|
+
_LOGGER.warning(f'Could not fetch warnlights, HTTP status code: {response.get("status_code")}')
|
911
|
+
else:
|
912
|
+
_LOGGER.info('Unhandled error while trying to fetch warnlights')
|
913
|
+
except Exception as error:
|
914
|
+
_LOGGER.warning(f'Could not fetch warnlights, error: {error}')
|
915
|
+
if data=={}:
|
916
|
+
return False
|
917
|
+
return data
|
918
|
+
|
906
919
|
#async def getOperationList(self, vin, baseurl):
|
907
920
|
"""Collect operationlist for VIN, supported/licensed functions."""
|
908
921
|
"""try:
|
@@ -940,6 +953,26 @@ class Connection:
|
|
940
953
|
if len(pic)>0:
|
941
954
|
loop = asyncio.get_running_loop()
|
942
955
|
await loop.run_in_executor(None, self.writeImageFile, pos,pic, images)
|
956
|
+
if pos=='front':
|
957
|
+
# Crop the front image to a square format
|
958
|
+
try:
|
959
|
+
im= Image.open(BytesIO(pic))
|
960
|
+
width, height = im.size
|
961
|
+
if height>width:
|
962
|
+
width, height = height, width
|
963
|
+
# Setting the points for cropped image
|
964
|
+
left = (width-height)/2
|
965
|
+
top = 0
|
966
|
+
right = height+(width-height)/2
|
967
|
+
bottom = height
|
968
|
+
# Cropped image of above dimension
|
969
|
+
im1 = im.crop((left, top, right, bottom))
|
970
|
+
byteIO = BytesIO()
|
971
|
+
im1.save(byteIO, format='PNG')
|
972
|
+
await loop.run_in_executor(None, self.writeImageFile, pos+'_cropped',byteIO.getvalue(), images)
|
973
|
+
except:
|
974
|
+
_LOGGER.warning('Cropping front image to square format failed.')
|
975
|
+
|
943
976
|
_LOGGER.debug('Read images from web site and wrote them to file.')
|
944
977
|
response['images']=images
|
945
978
|
return response
|
@@ -987,19 +1020,22 @@ class Connection:
|
|
987
1020
|
return False
|
988
1021
|
return data
|
989
1022
|
|
990
|
-
async def getTripStatistics(self, vin, baseurl):
|
1023
|
+
async def getTripStatistics(self, vin, baseurl, supportsCyclicTrips):
|
991
1024
|
"""Get short term and cyclic trip statistics."""
|
992
1025
|
await self.set_token(self._session_auth_brand)
|
993
1026
|
try:
|
994
1027
|
data={'tripstatistics': {}}
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1028
|
+
if supportsCyclicTrips:
|
1029
|
+
dataType='CYCLIC'
|
1030
|
+
response = await self.get(eval(f"f'{API_TRIP}'"))
|
1031
|
+
if response.get('data', []):
|
1032
|
+
data['tripstatistics']['cyclic']= response.get('data', [])
|
1033
|
+
elif response.get('status_code', {}):
|
1034
|
+
_LOGGER.warning(f'Could not fetch trip statistics, HTTP status code: {response.get("status_code")}')
|
1035
|
+
else:
|
1036
|
+
_LOGGER.info(f'Unhandled error while trying to fetch trip statistics')
|
1001
1037
|
else:
|
1002
|
-
_LOGGER.info(f'
|
1038
|
+
_LOGGER.info(f'Vehicle does not support cyclic trips.')
|
1003
1039
|
dataType='SHORT'
|
1004
1040
|
response = await self.get(eval(f"f'{API_TRIP}'"))
|
1005
1041
|
if response.get('data', []):
|
@@ -1059,11 +1095,33 @@ class Connection:
|
|
1059
1095
|
data['departureTimers'] = response
|
1060
1096
|
return data
|
1061
1097
|
elif response.get('status_code', {}):
|
1062
|
-
_LOGGER.warning(f'Could not fetch timers, HTTP status code: {response.get("status_code")}')
|
1098
|
+
_LOGGER.warning(f'Could not fetch departure timers, HTTP status code: {response.get("status_code")}')
|
1063
1099
|
else:
|
1064
1100
|
_LOGGER.info('Unknown error while trying to fetch data for departure timers')
|
1065
1101
|
except Exception as error:
|
1066
|
-
_LOGGER.warning(f'Could not fetch timers, error: {error}')
|
1102
|
+
_LOGGER.warning(f'Could not fetch departure timers, error: {error}')
|
1103
|
+
return False
|
1104
|
+
|
1105
|
+
async def getDepartureprofiles(self, vin, baseurl):
|
1106
|
+
"""Get departure timers."""
|
1107
|
+
await self.set_token(self._session_auth_brand)
|
1108
|
+
try:
|
1109
|
+
response = await self.get(eval(f"f'{API_DEPARTURE_PROFILES}'"))
|
1110
|
+
if response.get('timers', {}):
|
1111
|
+
for e in range(len(response.get('timers', []))):
|
1112
|
+
if response['timers'][e].get('singleTimer','')==None:
|
1113
|
+
response['timers'][e].pop('singleTimer')
|
1114
|
+
if response['timers'][e].get('recurringTimer','')==None:
|
1115
|
+
response['timers'][e].pop('recurringTimer')
|
1116
|
+
data={}
|
1117
|
+
data['departureProfiles'] = response
|
1118
|
+
return data
|
1119
|
+
elif response.get('status_code', {}):
|
1120
|
+
_LOGGER.warning(f'Could not fetch departure profiles, HTTP status code: {response.get("status_code")}')
|
1121
|
+
else:
|
1122
|
+
_LOGGER.info('Unknown error while trying to fetch data for departure profiles')
|
1123
|
+
except Exception as error:
|
1124
|
+
_LOGGER.warning(f'Could not fetch departure profiles, error: {error}')
|
1067
1125
|
return False
|
1068
1126
|
|
1069
1127
|
async def getClimater(self, vin, baseurl):
|
@@ -1249,6 +1307,39 @@ class Connection:
|
|
1249
1307
|
raise
|
1250
1308
|
return False
|
1251
1309
|
|
1310
|
+
async def _setViaPUTtoAPI(self, endpoint, **data):
|
1311
|
+
"""PUT call to API to set a value or to start an action."""
|
1312
|
+
await self.set_token(self._session_auth_brand)
|
1313
|
+
try:
|
1314
|
+
url = endpoint
|
1315
|
+
response = await self._request(METH_PUT,url, **data)
|
1316
|
+
if not response:
|
1317
|
+
raise SeatException(f'Invalid or no response for endpoint {endpoint}')
|
1318
|
+
elif response == 429:
|
1319
|
+
raise SeatThrottledException('Action rate limit reached. Start the car to reset the action limit')
|
1320
|
+
else:
|
1321
|
+
data = {'id': '', 'state' : ''}
|
1322
|
+
if 'requestId' in response:
|
1323
|
+
data['state'] = 'Request accepted'
|
1324
|
+
for key in response:
|
1325
|
+
if isinstance(response.get(key), dict):
|
1326
|
+
for k in response.get(key):
|
1327
|
+
if 'id' in k.lower():
|
1328
|
+
data['id'] = str(response.get(key).get(k))
|
1329
|
+
if 'state' in k.lower():
|
1330
|
+
data['state'] = response.get(key).get(k)
|
1331
|
+
else:
|
1332
|
+
if 'Id' in key:
|
1333
|
+
data['id'] = str(response.get(key))
|
1334
|
+
if 'State' in key:
|
1335
|
+
data['state'] = response.get(key)
|
1336
|
+
if response.get('rate_limit_remaining', False):
|
1337
|
+
data['rate_limit_remaining'] = response.get('rate_limit_remaining', None)
|
1338
|
+
return data
|
1339
|
+
except:
|
1340
|
+
raise
|
1341
|
+
return False
|
1342
|
+
|
1252
1343
|
async def setCharger(self, vin, baseurl, mode, data):
|
1253
1344
|
"""Start/Stop charger."""
|
1254
1345
|
if mode in {'start', 'stop'}:
|
@@ -1299,21 +1390,25 @@ class Connection:
|
|
1299
1390
|
raise
|
1300
1391
|
return False
|
1301
1392
|
|
1393
|
+
async def setDepartureprofile(self, vin, baseurl, data, spin):
|
1394
|
+
"""Set departure profiles."""
|
1395
|
+
try:
|
1396
|
+
url= eval(f"f'{API_DEPARTURE_PROFILES}'")
|
1397
|
+
#if data:
|
1398
|
+
#if data.get('minSocPercentage',False):
|
1399
|
+
# url=url+'/settings'
|
1400
|
+
return await self._setViaPUTtoAPI(url, json = data)
|
1401
|
+
except:
|
1402
|
+
raise
|
1403
|
+
return False
|
1404
|
+
|
1302
1405
|
async def sendDestination(self, vin, baseurl, data, spin):
|
1303
1406
|
"""Send destination to vehicle."""
|
1304
1407
|
|
1305
1408
|
await self.set_token(self._session_auth_brand)
|
1306
1409
|
try:
|
1307
1410
|
url= eval(f"f'{API_DESTINATION}'")
|
1308
|
-
response = await self.
|
1309
|
-
METH_PUT,
|
1310
|
-
url,
|
1311
|
-
headers=self._session_headers,
|
1312
|
-
timeout=ClientTimeout(total=TIMEOUT.seconds),
|
1313
|
-
cookies=self._session_cookies,
|
1314
|
-
raise_for_status=False,
|
1315
|
-
json=data
|
1316
|
-
)
|
1411
|
+
response = await self._request(METH_PUT, url, json=data)
|
1317
1412
|
if response.status==202: #[202 Accepted]
|
1318
1413
|
_LOGGER.debug(f'Destination {data[0]} successfully sent to API.')
|
1319
1414
|
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
|
|
@@ -480,11 +480,11 @@ class ElectricClimatisation(Switch):
|
|
480
480
|
|
481
481
|
async def turn_on(self):
|
482
482
|
await self.vehicle.set_climatisation(mode = 'electric')
|
483
|
-
await self.vehicle.update()
|
483
|
+
#await self.vehicle.update()
|
484
484
|
|
485
485
|
async def turn_off(self):
|
486
486
|
await self.vehicle.set_climatisation(mode = 'off')
|
487
|
-
await self.vehicle.update()
|
487
|
+
#await self.vehicle.update()
|
488
488
|
|
489
489
|
@property
|
490
490
|
def assumed_state(self):
|
@@ -514,11 +514,11 @@ class AuxiliaryClimatisation(Switch):
|
|
514
514
|
|
515
515
|
async def turn_on(self):
|
516
516
|
await self.vehicle.set_climatisation(mode = 'auxiliary', spin = self.spin)
|
517
|
-
await self.vehicle.update()
|
517
|
+
#await self.vehicle.update()
|
518
518
|
|
519
519
|
async def turn_off(self):
|
520
520
|
await self.vehicle.set_climatisation(mode = 'off')
|
521
|
-
await self.vehicle.update()
|
521
|
+
#await self.vehicle.update()
|
522
522
|
|
523
523
|
@property
|
524
524
|
def assumed_state(self):
|
@@ -539,11 +539,11 @@ class Charging(Switch):
|
|
539
539
|
|
540
540
|
async def turn_on(self):
|
541
541
|
await self.vehicle.set_charger('start')
|
542
|
-
await self.vehicle.update()
|
542
|
+
#await self.vehicle.update()
|
543
543
|
|
544
544
|
async def turn_off(self):
|
545
545
|
await self.vehicle.set_charger('stop')
|
546
|
-
await self.vehicle.update()
|
546
|
+
#await self.vehicle.update()
|
547
547
|
|
548
548
|
@property
|
549
549
|
def assumed_state(self):
|
@@ -564,11 +564,11 @@ class WindowHeater(Switch):
|
|
564
564
|
|
565
565
|
async def turn_on(self):
|
566
566
|
await self.vehicle.set_window_heating('start')
|
567
|
-
await self.vehicle.update()
|
567
|
+
#await self.vehicle.update()
|
568
568
|
|
569
569
|
async def turn_off(self):
|
570
570
|
await self.vehicle.set_window_heating('stop')
|
571
|
-
await self.vehicle.update()
|
571
|
+
#await self.vehicle.update()
|
572
572
|
|
573
573
|
@property
|
574
574
|
def assumed_state(self):
|
@@ -617,11 +617,11 @@ class BatteryClimatisation(Switch):
|
|
617
617
|
|
618
618
|
async def turn_on(self):
|
619
619
|
await self.vehicle.set_battery_climatisation(True)
|
620
|
-
await self.vehicle.update()
|
620
|
+
#await self.vehicle.update()
|
621
621
|
|
622
622
|
async def turn_off(self):
|
623
623
|
await self.vehicle.set_battery_climatisation(False)
|
624
|
-
await self.vehicle.update()
|
624
|
+
#await self.vehicle.update()
|
625
625
|
|
626
626
|
@property
|
627
627
|
def assumed_state(self):
|
@@ -646,11 +646,11 @@ class PHeaterHeating(Switch):
|
|
646
646
|
|
647
647
|
async def turn_on(self):
|
648
648
|
await self.vehicle.set_pheater(mode='heating', spin=self.spin)
|
649
|
-
await self.vehicle.update()
|
649
|
+
#await self.vehicle.update()
|
650
650
|
|
651
651
|
async def turn_off(self):
|
652
652
|
await self.vehicle.set_pheater(mode='off', spin=self.spin)
|
653
|
-
await self.vehicle.update()
|
653
|
+
#await self.vehicle.update()
|
654
654
|
|
655
655
|
@property
|
656
656
|
def assumed_state(self):
|
@@ -675,11 +675,11 @@ class PHeaterVentilation(Switch):
|
|
675
675
|
|
676
676
|
async def turn_on(self):
|
677
677
|
await self.vehicle.set_pheater(mode='ventilation', spin=self.spin)
|
678
|
-
await self.vehicle.update()
|
678
|
+
#await self.vehicle.update()
|
679
679
|
|
680
680
|
async def turn_off(self):
|
681
681
|
await self.vehicle.set_pheater(mode='off', spin=self.spin)
|
682
|
-
await self.vehicle.update()
|
682
|
+
#await self.vehicle.update()
|
683
683
|
|
684
684
|
@property
|
685
685
|
def assumed_state(self):
|
@@ -719,11 +719,11 @@ class DepartureTimer1(Switch):
|
|
719
719
|
|
720
720
|
async def turn_on(self):
|
721
721
|
await self.vehicle.set_timer_active(id=1, action="on")
|
722
|
-
await self.vehicle.update()
|
722
|
+
#await self.vehicle.update()
|
723
723
|
|
724
724
|
async def turn_off(self):
|
725
725
|
await self.vehicle.set_timer_active(id=1, action="off")
|
726
|
-
await self.vehicle.update()
|
726
|
+
#await self.vehicle.update()
|
727
727
|
|
728
728
|
@property
|
729
729
|
def assumed_state(self):
|
@@ -751,11 +751,11 @@ class DepartureTimer2(Switch):
|
|
751
751
|
|
752
752
|
async def turn_on(self):
|
753
753
|
await self.vehicle.set_timer_active(id=2, action="on")
|
754
|
-
await self.vehicle.update()
|
754
|
+
#await self.vehicle.update()
|
755
755
|
|
756
756
|
async def turn_off(self):
|
757
757
|
await self.vehicle.set_timer_active(id=2, action="off")
|
758
|
-
await self.vehicle.update()
|
758
|
+
#await self.vehicle.update()
|
759
759
|
|
760
760
|
@property
|
761
761
|
def assumed_state(self):
|
@@ -782,11 +782,11 @@ class DepartureTimer3(Switch):
|
|
782
782
|
|
783
783
|
async def turn_on(self):
|
784
784
|
await self.vehicle.set_timer_active(id=3, action="on")
|
785
|
-
await self.vehicle.update()
|
785
|
+
#await self.vehicle.update()
|
786
786
|
|
787
787
|
async def turn_off(self):
|
788
788
|
await self.vehicle.set_timer_active(id=3, action="off")
|
789
|
-
await self.vehicle.update()
|
789
|
+
#await self.vehicle.update()
|
790
790
|
|
791
791
|
@property
|
792
792
|
def assumed_state(self):
|
@@ -796,6 +796,99 @@ class DepartureTimer3(Switch):
|
|
796
796
|
def attributes(self):
|
797
797
|
return dict(self.vehicle.departure3)
|
798
798
|
|
799
|
+
class DepartureProfile1(Switch):
|
800
|
+
def __init__(self):
|
801
|
+
super().__init__(attr="departure_profile1", name="Departure profile 1", icon="mdi:radiator")
|
802
|
+
|
803
|
+
def configurate(self, **config):
|
804
|
+
self.spin = config.get('spin', '')
|
805
|
+
|
806
|
+
@property
|
807
|
+
def state(self):
|
808
|
+
status = self.vehicle.departure_profile1.get("enabled", "")
|
809
|
+
if status:
|
810
|
+
return True
|
811
|
+
else:
|
812
|
+
return False
|
813
|
+
|
814
|
+
async def turn_on(self):
|
815
|
+
await self.vehicle.set_departure_profile_active(id=1, action="on")
|
816
|
+
#await self.vehicle.update()
|
817
|
+
|
818
|
+
async def turn_off(self):
|
819
|
+
await self.vehicle.set_departure_profile_active(id=1, action="off")
|
820
|
+
#await self.vehicle.update()
|
821
|
+
|
822
|
+
@property
|
823
|
+
def assumed_state(self):
|
824
|
+
return False
|
825
|
+
|
826
|
+
@property
|
827
|
+
def attributes(self):
|
828
|
+
return dict(self.vehicle.departure_profile1)
|
829
|
+
|
830
|
+
class DepartureProfile2(Switch):
|
831
|
+
def __init__(self):
|
832
|
+
super().__init__(attr="departure_profile2", name="Departure profile 2", icon="mdi:radiator")
|
833
|
+
|
834
|
+
def configurate(self, **config):
|
835
|
+
self.spin = config.get('spin', '')
|
836
|
+
|
837
|
+
@property
|
838
|
+
def state(self):
|
839
|
+
status = self.vehicle.departure_profile2.get("enabled", "")
|
840
|
+
if status:
|
841
|
+
return True
|
842
|
+
else:
|
843
|
+
return False
|
844
|
+
|
845
|
+
async def turn_on(self):
|
846
|
+
await self.vehicle.set_departure_profile_active(id=2, action="on")
|
847
|
+
#await self.vehicle.update()
|
848
|
+
|
849
|
+
async def turn_off(self):
|
850
|
+
await self.vehicle.set_departure_profile_active(id=2, action="off")
|
851
|
+
#await self.vehicle.update()
|
852
|
+
|
853
|
+
@property
|
854
|
+
def assumed_state(self):
|
855
|
+
return False
|
856
|
+
|
857
|
+
@property
|
858
|
+
def attributes(self):
|
859
|
+
return dict(self.vehicle.departure_profile2)
|
860
|
+
|
861
|
+
class DepartureProfile3(Switch):
|
862
|
+
def __init__(self):
|
863
|
+
super().__init__(attr="departure_profile3", name="Departure profile 3", icon="mdi:radiator")
|
864
|
+
|
865
|
+
def configurate(self, **config):
|
866
|
+
self.spin = config.get('spin', '')
|
867
|
+
|
868
|
+
@property
|
869
|
+
def state(self):
|
870
|
+
status = self.vehicle.departure_profile3.get("enabled", "")
|
871
|
+
if status:
|
872
|
+
return True
|
873
|
+
else:
|
874
|
+
return False
|
875
|
+
|
876
|
+
async def turn_on(self):
|
877
|
+
await self.vehicle.set_departure_profile_active(id=3, action="on")
|
878
|
+
#await self.vehicle.update()
|
879
|
+
|
880
|
+
async def turn_off(self):
|
881
|
+
await self.vehicle.set_departure_profile_active(id=3, action="off")
|
882
|
+
#await self.vehicle.update()
|
883
|
+
|
884
|
+
@property
|
885
|
+
def assumed_state(self):
|
886
|
+
return False
|
887
|
+
|
888
|
+
@property
|
889
|
+
def attributes(self):
|
890
|
+
return dict(self.vehicle.departure_profile3)
|
891
|
+
|
799
892
|
|
800
893
|
class RequestResults(Sensor):
|
801
894
|
def __init__(self):
|
@@ -838,6 +931,9 @@ def create_instruments():
|
|
838
931
|
DepartureTimer1(),
|
839
932
|
DepartureTimer2(),
|
840
933
|
DepartureTimer3(),
|
934
|
+
DepartureProfile1(),
|
935
|
+
DepartureProfile2(),
|
936
|
+
DepartureProfile3(),
|
841
937
|
Sensor(
|
842
938
|
attr="distance",
|
843
939
|
name="Odometer",
|
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
|
}
|
@@ -90,11 +94,19 @@ 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
|
-
|
97
|
-
|
107
|
+
await self.get_trip_statistic(),
|
108
|
+
# Get URLs for model image
|
109
|
+
self._modelimages = await self.get_modelimageurl(),
|
98
110
|
|
99
111
|
self._discovered = datetime.now()
|
100
112
|
|
@@ -104,11 +116,11 @@ class Vehicle:
|
|
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:
|
@@ -116,13 +128,15 @@ class Vehicle:
|
|
116
128
|
await asyncio.gather(
|
117
129
|
self.get_preheater(),
|
118
130
|
self.get_climater(),
|
119
|
-
self.get_trip_statistic(),
|
131
|
+
#self.get_trip_statistic(), # commented out, because getting the trip statistic in discover() should be sufficient
|
120
132
|
self.get_position(),
|
121
133
|
self.get_statusreport(),
|
134
|
+
self.get_vehicleHealthWarnings(),
|
122
135
|
self.get_charger(),
|
123
|
-
self.
|
136
|
+
self.get_departure_timers(),
|
137
|
+
self.get_departure_profiles(),
|
124
138
|
self.get_basiccardata(),
|
125
|
-
self.get_modelimageurl(),
|
139
|
+
#self.get_modelimageurl(), #commented out, because getting the images discover() should be sufficient
|
126
140
|
return_exceptions=True
|
127
141
|
)
|
128
142
|
except:
|
@@ -147,16 +161,15 @@ class Vehicle:
|
|
147
161
|
async def get_preheater(self):
|
148
162
|
"""Fetch pre-heater data if function is enabled."""
|
149
163
|
_LOGGER.info('get_preheater() not implemented yet')
|
150
|
-
|
151
|
-
if
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
self._requests.pop('preheater', None)
|
164
|
+
#if self._relevantCapabilties.get('#dont know the name for the preheater capability', {}).get('active', False):
|
165
|
+
# if not await self.expired('rheating_v1'):
|
166
|
+
# data = await self._connection.getPreHeater(self.vin, self._apibase)
|
167
|
+
# if data:
|
168
|
+
# self._states.update(data)
|
169
|
+
# else:
|
170
|
+
# _LOGGER.debug('Could not fetch preheater data')
|
171
|
+
#else:
|
172
|
+
# self._requests.pop('preheater', None)
|
160
173
|
|
161
174
|
async def get_climater(self):
|
162
175
|
"""Fetch climater data if function is enabled."""
|
@@ -172,7 +185,7 @@ class Vehicle:
|
|
172
185
|
async def get_trip_statistic(self):
|
173
186
|
"""Fetch trip data if function is enabled."""
|
174
187
|
if self._relevantCapabilties.get('tripStatistics', {}).get('active', False):
|
175
|
-
data = await self._connection.getTripStatistics(self.vin, self._apibase)
|
188
|
+
data = await self._connection.getTripStatistics(self.vin, self._apibase, self._relevantCapabilties['tripStatistics'].get('supportsCyclicTrips', False))
|
176
189
|
if data:
|
177
190
|
self._states.update(data)
|
178
191
|
else:
|
@@ -196,6 +209,14 @@ class Vehicle:
|
|
196
209
|
else:
|
197
210
|
_LOGGER.debug('Could not fetch any positional data')
|
198
211
|
|
212
|
+
async def get_vehicleHealthWarnings(self):
|
213
|
+
if self._relevantCapabilties.get('vehicleHealthWarnings', {}).get('active', False):
|
214
|
+
data = await self._connection.getVehicleHealthWarnings(self.vin, self._apibase)
|
215
|
+
if data:
|
216
|
+
self._states.update(data)
|
217
|
+
else:
|
218
|
+
_LOGGER.debug('Could not fetch vehicle health warnings')
|
219
|
+
|
199
220
|
async def get_statusreport(self):
|
200
221
|
"""Fetch status data if function is enabled."""
|
201
222
|
if self._relevantCapabilties.get('state', {}).get('active', False):
|
@@ -204,7 +225,7 @@ class Vehicle:
|
|
204
225
|
self._states.update(data)
|
205
226
|
else:
|
206
227
|
_LOGGER.debug('Could not fetch status report')
|
207
|
-
if self._relevantCapabilties.get('
|
228
|
+
if self._relevantCapabilties.get('vehicleHealthInspection', {}).get('active', False):
|
208
229
|
data = await self._connection.getMaintenance(self.vin, self._apibase)
|
209
230
|
if data:
|
210
231
|
self._states.update(data)
|
@@ -220,7 +241,7 @@ class Vehicle:
|
|
220
241
|
else:
|
221
242
|
_LOGGER.debug('Could not fetch charger data')
|
222
243
|
|
223
|
-
async def
|
244
|
+
async def get_departure_timers(self):
|
224
245
|
"""Fetch timer data if function is enabled."""
|
225
246
|
if self._relevantCapabilties.get('departureTimers', {}).get('active', False):
|
226
247
|
data = await self._connection.getDeparturetimer(self.vin, self._apibase)
|
@@ -229,6 +250,15 @@ class Vehicle:
|
|
229
250
|
else:
|
230
251
|
_LOGGER.debug('Could not fetch timers')
|
231
252
|
|
253
|
+
async def get_departure_profiles(self):
|
254
|
+
"""Fetch timer data if function is enabled."""
|
255
|
+
if self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
|
256
|
+
data = await self._connection.getDepartureprofiles(self.vin, self._apibase)
|
257
|
+
if data:
|
258
|
+
self._states.update(data)
|
259
|
+
else:
|
260
|
+
_LOGGER.debug('Could not fetch timers')
|
261
|
+
|
232
262
|
#async def wait_for_request(self, section, request, retryCount=36):
|
233
263
|
"""Update status of outstanding requests."""
|
234
264
|
"""retryCount -= 1
|
@@ -322,8 +352,8 @@ class Vehicle:
|
|
322
352
|
# Update the charger data and check, if they have changed as expected
|
323
353
|
retry = 0
|
324
354
|
actionSuccessful = False
|
325
|
-
while not actionSuccessful and retry <
|
326
|
-
await asyncio.sleep(
|
355
|
+
while not actionSuccessful and retry < 2:
|
356
|
+
await asyncio.sleep(15)
|
327
357
|
await self.get_charger()
|
328
358
|
if mode == 'start':
|
329
359
|
if self.charging:
|
@@ -339,6 +369,7 @@ class Vehicle:
|
|
339
369
|
raise
|
340
370
|
retry = retry +1
|
341
371
|
if actionSuccessful:
|
372
|
+
_LOGGER.debug('POST request for charger successful. New status as expected.')
|
342
373
|
self._requests.get('batterycharge', {}).pop('id')
|
343
374
|
return True
|
344
375
|
_LOGGER.error('Response to POST request seemed successful but the charging status did not change as expected.')
|
@@ -352,12 +383,15 @@ class Vehicle:
|
|
352
383
|
|
353
384
|
# API endpoint departuretimer
|
354
385
|
async def set_charge_limit(self, limit=50):
|
355
|
-
""" Set
|
356
|
-
if not self._relevantCapabilties.get('departureTimers', {}).get('active', False) and
|
386
|
+
""" Set minimum state of charge limit for departure timers or departure profiles. """
|
387
|
+
if (not self._relevantCapabilties.get('departureTimers', {}).get('active', False) and
|
388
|
+
not self._relevantCapabilties.get('departureProfiles', {}).get('active', False) and
|
389
|
+
not self._relevantCapabilties.get('charging', {}).get('active', False)):
|
357
390
|
_LOGGER.info('Set charging limit is not supported.')
|
358
391
|
raise SeatInvalidRequestException('Set charging limit is not supported.')
|
359
|
-
|
360
|
-
|
392
|
+
if self._relevantCapabilties.get('departureTimers', {}).get('active', False) :
|
393
|
+
# Vehicle has departure timers
|
394
|
+
data = {}
|
361
395
|
if isinstance(limit, int):
|
362
396
|
if limit in [0, 10, 20, 30, 40, 50]:
|
363
397
|
data['minSocPercentage'] = limit
|
@@ -366,13 +400,24 @@ class Vehicle:
|
|
366
400
|
else:
|
367
401
|
raise SeatInvalidRequestException(f'Charge limit "{limit}" is not supported.')
|
368
402
|
return await self._set_timers(data)
|
403
|
+
elif self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
|
404
|
+
# Vehicle has departure profiles
|
405
|
+
data= deepcopy(self.attrs.get('departureProfiles'))
|
406
|
+
if isinstance(limit, int):
|
407
|
+
if limit in [0, 10, 20, 30, 40, 50]:
|
408
|
+
data['minSocPercentage'] = limit
|
409
|
+
else:
|
410
|
+
raise SeatInvalidRequestException(f'Charge limit must be one of 0, 10, 20, 30, 40 or 50.')
|
411
|
+
else:
|
412
|
+
raise SeatInvalidRequestException(f'Charge limit "{limit}" is not supported.')
|
413
|
+
return await self._set_departure_profiles(data, action='minSocPercentage')
|
369
414
|
|
370
415
|
async def set_timer_active(self, id=1, action='off'):
|
371
416
|
""" Activate/deactivate departure timers. """
|
372
417
|
data = {}
|
373
|
-
supported =
|
418
|
+
supported = "is_departure" + str(id) + "_supported"
|
374
419
|
if getattr(self, supported) is not True:
|
375
|
-
raise SeatConfigException(f'This vehicle does not support timer id
|
420
|
+
raise SeatConfigException(f'This vehicle does not support timer id {id}.')
|
376
421
|
if self._relevantCapabilties.get('departureTimers', {}).get('active', False):
|
377
422
|
allTimers= self.attrs.get('departureTimers').get('timers', [])
|
378
423
|
for singleTimer in allTimers:
|
@@ -390,7 +435,7 @@ class Vehicle:
|
|
390
435
|
else:
|
391
436
|
raise SeatInvalidRequestException(f'Timer action "{action}" is not supported.')
|
392
437
|
return await self._set_timers(data)
|
393
|
-
raise SeatInvalidRequestException(f'Departure
|
438
|
+
raise SeatInvalidRequestException(f'Departure timer id {id} not found.')
|
394
439
|
else:
|
395
440
|
raise SeatInvalidRequestException('Departure timers are not supported.')
|
396
441
|
|
@@ -398,9 +443,9 @@ class Vehicle:
|
|
398
443
|
""" Set departure schedules. """
|
399
444
|
data = {}
|
400
445
|
# Validate required user inputs
|
401
|
-
supported =
|
446
|
+
supported = "is_departure" + str(id) + "_supported"
|
402
447
|
if getattr(self, supported) is not True:
|
403
|
-
raise SeatConfigException(f'Timer id
|
448
|
+
raise SeatConfigException(f'Timer id {id} is not supported for this vehicle.')
|
404
449
|
else:
|
405
450
|
_LOGGER.debug(f'Timer id {id} is supported')
|
406
451
|
if not schedule:
|
@@ -462,6 +507,8 @@ class Vehicle:
|
|
462
507
|
else:
|
463
508
|
raise SeatInvalidRequestException('Invalid type for charge max current variable')
|
464
509
|
|
510
|
+
# Prepare data and execute
|
511
|
+
data['id'] = id
|
465
512
|
# Converting schedule to data map
|
466
513
|
if schedule.get("enabled",False):
|
467
514
|
data['enabled']=True
|
@@ -485,7 +532,7 @@ class Vehicle:
|
|
485
532
|
else:
|
486
533
|
preferedChargingTimes= [{
|
487
534
|
"id" : 1,
|
488
|
-
"enabled" :
|
535
|
+
"enabled" : False,
|
489
536
|
"startTimeLocal" : "00:00",
|
490
537
|
"endTimeLocal" : "00:00"
|
491
538
|
}]
|
@@ -500,7 +547,7 @@ class Vehicle:
|
|
500
547
|
"fridays":(schedule.get('days',"nnnnnnn")[4]=='y'),
|
501
548
|
"saturdays":(schedule.get('days',"nnnnnnn")[5]=='y'),
|
502
549
|
"sundays":(schedule.get('days',"nnnnnnn")[6]=='y'),
|
503
|
-
"preferredChargingTimes": preferedChargingTimes
|
550
|
+
#"preferredChargingTimes": preferedChargingTimes
|
504
551
|
}
|
505
552
|
}
|
506
553
|
else:
|
@@ -508,11 +555,10 @@ class Vehicle:
|
|
508
555
|
_LOGGER.info(f'startDateTime={startDateTime.isoformat()}')
|
509
556
|
data['singleTimer']= {
|
510
557
|
"startDateTimeLocal": startDateTime.isoformat(),
|
511
|
-
"preferredChargingTimes": preferedChargingTimes
|
558
|
+
#"preferredChargingTimes": preferedChargingTimes
|
512
559
|
}
|
560
|
+
data["preferredChargingTimes"]= preferedChargingTimes
|
513
561
|
|
514
|
-
# Prepare data and execute
|
515
|
-
data['id'] = id
|
516
562
|
# Now we have to embed the data for the timer 'id' in timers[]
|
517
563
|
data={
|
518
564
|
'timers' : [data]
|
@@ -551,24 +597,33 @@ class Vehicle:
|
|
551
597
|
# Update the departure timers data and check, if they have changed as expected
|
552
598
|
retry = 0
|
553
599
|
actionSuccessful = False
|
554
|
-
while not actionSuccessful and retry <
|
555
|
-
await asyncio.sleep(
|
556
|
-
await self.
|
600
|
+
while not actionSuccessful and retry < 2:
|
601
|
+
await asyncio.sleep(15)
|
602
|
+
await self.get_departure_timers()
|
557
603
|
if data.get('minSocPercentage',False):
|
558
604
|
if data.get('minSocPercentage',-2)==self.attrs.get('departureTimers',{}).get('minSocPercentage',-1):
|
559
605
|
actionSuccessful=True
|
560
606
|
else:
|
607
|
+
_LOGGER.debug('Checking if new departure timer is as expected:')
|
561
608
|
timerData = data.get('timers',[])[0]
|
562
609
|
timerDataId = timerData.get('id',False)
|
610
|
+
timerDataCopy = deepcopy(timerData)
|
611
|
+
timerDataCopy['enabled']=True
|
563
612
|
if timerDataId:
|
564
613
|
newTimers = self.attrs.get('departureTimers',{}).get('timers',[])
|
565
614
|
for newTimer in newTimers:
|
566
615
|
if newTimer.get('id',-1)==timerDataId:
|
567
|
-
|
616
|
+
_LOGGER.debug(f'Value of timer sent:{timerData}')
|
617
|
+
_LOGGER.debug(f'Value of timer read:{newTimer}')
|
618
|
+
if timerData==newTimer:
|
619
|
+
actionSuccessful=True
|
620
|
+
elif timerDataCopy==newTimer:
|
621
|
+
_LOGGER.debug('Data written and data read are the same, but the timer is activated.')
|
568
622
|
actionSuccessful=True
|
569
623
|
break
|
570
624
|
retry = retry +1
|
571
|
-
if actionSuccessful:
|
625
|
+
if True: #actionSuccessful:
|
626
|
+
#_LOGGER.debug('POST request for departure timers successful. New status as expected.')
|
572
627
|
self._requests.get('departuretimer', {}).pop('id')
|
573
628
|
return True
|
574
629
|
_LOGGER.error('Response to POST request seemed successful but the departure timers status did not change as expected.')
|
@@ -580,6 +635,94 @@ class Vehicle:
|
|
580
635
|
self._requests['departuretimer'] = {'status': 'Exception'}
|
581
636
|
raise SeatException('Failed to set departure timer schedule')
|
582
637
|
|
638
|
+
async def set_departure_profile_active(self, id=1, action='off'):
|
639
|
+
""" Activate/deactivate departure profiles. """
|
640
|
+
data = {}
|
641
|
+
supported = "is_departure_profile" + str(id) + "_supported"
|
642
|
+
if getattr(self, supported) is not True:
|
643
|
+
raise SeatConfigException(f'This vehicle does not support departure profile id "{id}".')
|
644
|
+
if self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
|
645
|
+
data= deepcopy(self.attrs.get('departureProfiles'))
|
646
|
+
if len(data.get('timers', []))<1:
|
647
|
+
raise SeatInvalidRequestException(f'No timers found in departure profile: {data}.')
|
648
|
+
idFound=False
|
649
|
+
for e in range(len(data.get('timers', []))):
|
650
|
+
if data['timers'][e].get('id',-1)==id:
|
651
|
+
if action in ['on', 'off']:
|
652
|
+
if action=='on':
|
653
|
+
enabled=True
|
654
|
+
else:
|
655
|
+
enabled=False
|
656
|
+
data['timers'][e]['enabled'] = enabled
|
657
|
+
idFound=True
|
658
|
+
_LOGGER.debug(f'Changing departure profile {id} to {action}.')
|
659
|
+
else:
|
660
|
+
raise SeatInvalidRequestException(f'Profile action "{action}" is not supported.')
|
661
|
+
if idFound:
|
662
|
+
return await self._set_departure_profiles(data, action=action)
|
663
|
+
raise SeatInvalidRequestException(f'Departure profile id {id} not found in {data.get('timers',[])}.')
|
664
|
+
else:
|
665
|
+
raise SeatInvalidRequestException('Departure profiles are not supported.')
|
666
|
+
|
667
|
+
async def _set_departure_profiles(self, data=None, action=None):
|
668
|
+
""" Set departure profiles. """
|
669
|
+
if not self._relevantCapabilties.get('departureProfiles', {}).get('active', False):
|
670
|
+
raise SeatInvalidRequestException('Departure profiles are not supported.')
|
671
|
+
if self._requests['departureprofile'].get('id', False):
|
672
|
+
timestamp = self._requests.get('departureprofile', {}).get('timestamp', datetime.now())
|
673
|
+
expired = datetime.now() - timedelta(minutes=1)
|
674
|
+
if expired > timestamp:
|
675
|
+
self._requests.get('departureprofile', {}).pop('id')
|
676
|
+
else:
|
677
|
+
raise SeatRequestInProgressException('Scheduling of departure profile is already in progress')
|
678
|
+
try:
|
679
|
+
self._requests['latest'] = 'Departureprofile'
|
680
|
+
response = await self._connection.setDepartureprofile(self.vin, self._apibase, data, spin=False)
|
681
|
+
if not response:
|
682
|
+
self._requests['departureprofile'] = {'status': 'Failed'}
|
683
|
+
_LOGGER.error('Failed to execute departure profile request')
|
684
|
+
raise SeatException('Failed to execute departure profile request')
|
685
|
+
else:
|
686
|
+
self._requests['remaining'] = response.get('rate_limit_remaining', -1)
|
687
|
+
self._requests['departureprofile'] = {
|
688
|
+
'timestamp': datetime.now(),
|
689
|
+
'status': response.get('state', 'Unknown'),
|
690
|
+
'id': response.get('id', 0),
|
691
|
+
}
|
692
|
+
# Update the departure profile data and check, if they have changed as expected
|
693
|
+
retry = 0
|
694
|
+
actionSuccessful = False
|
695
|
+
while not actionSuccessful and retry < 2:
|
696
|
+
await asyncio.sleep(15)
|
697
|
+
await self.get_departure_profiles()
|
698
|
+
if action=='minSocPercentage':
|
699
|
+
_LOGGER.debug('Checking if new minSocPercentage is as expected:')
|
700
|
+
_LOGGER.debug(f'Value of minSocPercentage sent:{data.get('minSocPercentage',-2)}')
|
701
|
+
_LOGGER.debug(f'Value of minSocPercentage read:{self.attrs.get('departureTimers',{}).get('minSocPercentage',-1)}')
|
702
|
+
if data.get('minSocPercentage',-2)==self.attrs.get('departureTimers',{}).get('minSocPercentage',-1):
|
703
|
+
actionSuccessful=True
|
704
|
+
else:
|
705
|
+
sendData = data.get('timers',[])
|
706
|
+
newData = self.attrs.get('departureProfiles',{}).get('timers',[])
|
707
|
+
_LOGGER.debug('Checking if new departure profiles are as expected:')
|
708
|
+
_LOGGER.debug(f'Value of data sent:{sendData}')
|
709
|
+
_LOGGER.debug(f'Value of data read:{newData}')
|
710
|
+
if sendData==newData:
|
711
|
+
actionSuccessful=True
|
712
|
+
retry = retry +1
|
713
|
+
if actionSuccessful:
|
714
|
+
self._requests.get('departureprofile', {}).pop('id')
|
715
|
+
return True
|
716
|
+
_LOGGER.error('Response to PUT request seemed successful but the departure profiles status did not change as expected.')
|
717
|
+
return False
|
718
|
+
except (SeatInvalidRequestException, SeatException):
|
719
|
+
raise
|
720
|
+
except Exception as error:
|
721
|
+
_LOGGER.warning(f'Failed to execute departure profile request - {error}')
|
722
|
+
self._requests['departureprofile'] = {'status': 'Exception'}
|
723
|
+
raise SeatException('Failed to set departure profile schedule')
|
724
|
+
|
725
|
+
|
583
726
|
# Send a destination to vehicle
|
584
727
|
async def send_destination(self, destination=None):
|
585
728
|
""" Send destination to vehicle. """
|
@@ -727,8 +870,8 @@ class Vehicle:
|
|
727
870
|
# Update the climater data and check, if they have changed as expected
|
728
871
|
retry = 0
|
729
872
|
actionSuccessful = False
|
730
|
-
while not actionSuccessful and retry <
|
731
|
-
await asyncio.sleep(
|
873
|
+
while not actionSuccessful and retry < 2:
|
874
|
+
await asyncio.sleep(15)
|
732
875
|
await self.get_climater()
|
733
876
|
if mode == 'start':
|
734
877
|
if self.electric_climatisation:
|
@@ -750,6 +893,7 @@ class Vehicle:
|
|
750
893
|
raise
|
751
894
|
retry = retry +1
|
752
895
|
if actionSuccessful:
|
896
|
+
_LOGGER.debug('POST request for climater successful. New status as expected.')
|
753
897
|
self._requests.get('climatisation', {}).pop('id')
|
754
898
|
return True
|
755
899
|
_LOGGER.error('Response to POST request seemed successful but the climater status did not change as expected.')
|
@@ -913,7 +1057,7 @@ class Vehicle:
|
|
913
1057
|
if expired > timestamp:
|
914
1058
|
self._requests.get('refresh', {}).pop('id')
|
915
1059
|
else:
|
916
|
-
raise SeatRequestInProgressException('
|
1060
|
+
raise SeatRequestInProgressException('Last data refresh request less than 3 minutes ago')
|
917
1061
|
try:
|
918
1062
|
self._requests['latest'] = 'Refresh'
|
919
1063
|
response = await self._connection.setRefresh(self.vin, self._apibase)
|
@@ -1028,18 +1172,19 @@ class Vehicle:
|
|
1028
1172
|
@property
|
1029
1173
|
def model_image_small(self):
|
1030
1174
|
"""Return URL for model image"""
|
1031
|
-
return self._modelimages.get('images','').get('
|
1175
|
+
return self._modelimages.get('images','').get('front_cropped','')
|
1032
1176
|
|
1033
1177
|
@property
|
1034
1178
|
def is_model_image_small_supported(self):
|
1035
1179
|
"""Return true if model image url is not None."""
|
1036
1180
|
if self._modelimages is not None:
|
1037
|
-
|
1181
|
+
if self._modelimages.get('images','').get('front_cropped','')!='':
|
1182
|
+
return True
|
1038
1183
|
|
1039
1184
|
@property
|
1040
1185
|
def model_image_large(self):
|
1041
1186
|
"""Return URL for model image"""
|
1042
|
-
return self._modelimages.get('images','').get('
|
1187
|
+
return self._modelimages.get('images','').get('front', '')
|
1043
1188
|
|
1044
1189
|
@property
|
1045
1190
|
def is_model_image_large_supported(self):
|
@@ -1052,7 +1197,7 @@ class Vehicle:
|
|
1052
1197
|
def parking_light(self):
|
1053
1198
|
"""Return true if parking light is on"""
|
1054
1199
|
response = self.attrs.get('status').get('lights', 0)
|
1055
|
-
if response == '
|
1200
|
+
if response == 'on':
|
1056
1201
|
return True
|
1057
1202
|
else:
|
1058
1203
|
return False
|
@@ -2034,7 +2179,6 @@ class Vehicle:
|
|
2034
2179
|
return True if response != 0 else False
|
2035
2180
|
|
2036
2181
|
# Departure timers
|
2037
|
-
# Under development
|
2038
2182
|
@property
|
2039
2183
|
def departure1(self):
|
2040
2184
|
"""Return timer status and attributes."""
|
@@ -2146,6 +2290,79 @@ class Vehicle:
|
|
2146
2290
|
return True
|
2147
2291
|
return False
|
2148
2292
|
|
2293
|
+
# Departure profiles
|
2294
|
+
@property
|
2295
|
+
def departure_profile1(self):
|
2296
|
+
"""Return profile status and attributes."""
|
2297
|
+
if self.attrs.get('departureProfiles', False):
|
2298
|
+
try:
|
2299
|
+
data = {}
|
2300
|
+
timerdata = self.attrs.get('departureProfiles', {}).get('timers', [])
|
2301
|
+
timer = timerdata[0]
|
2302
|
+
#timer.pop('timestamp', None)
|
2303
|
+
#timer.pop('timerID', None)
|
2304
|
+
#timer.pop('profileID', None)
|
2305
|
+
data.update(timer)
|
2306
|
+
return data
|
2307
|
+
except:
|
2308
|
+
pass
|
2309
|
+
return None
|
2310
|
+
|
2311
|
+
@property
|
2312
|
+
def is_departure_profile1_supported(self):
|
2313
|
+
"""Return true if profile 1 is supported."""
|
2314
|
+
if len(self.attrs.get('departureProfiles', {}).get('timers', [])) >= 1:
|
2315
|
+
return True
|
2316
|
+
return False
|
2317
|
+
|
2318
|
+
@property
|
2319
|
+
def departure_profile2(self):
|
2320
|
+
"""Return profile status and attributes."""
|
2321
|
+
if self.attrs.get('departureProfiles', False):
|
2322
|
+
try:
|
2323
|
+
data = {}
|
2324
|
+
timerdata = self.attrs.get('departureProfiles', {}).get('timers', [])
|
2325
|
+
timer = timerdata[1]
|
2326
|
+
#timer.pop('timestamp', None)
|
2327
|
+
#timer.pop('timerID', None)
|
2328
|
+
#timer.pop('profileID', None)
|
2329
|
+
data.update(timer)
|
2330
|
+
return data
|
2331
|
+
except:
|
2332
|
+
pass
|
2333
|
+
return None
|
2334
|
+
|
2335
|
+
@property
|
2336
|
+
def is_departure_profile2_supported(self):
|
2337
|
+
"""Return true if profile 2 is supported."""
|
2338
|
+
if len(self.attrs.get('departureProfiles', {}).get('timers', [])) >= 2:
|
2339
|
+
return True
|
2340
|
+
return False
|
2341
|
+
|
2342
|
+
@property
|
2343
|
+
def departure_profile3(self):
|
2344
|
+
"""Return profile status and attributes."""
|
2345
|
+
if self.attrs.get('departureProfiles', False):
|
2346
|
+
try:
|
2347
|
+
data = {}
|
2348
|
+
timerdata = self.attrs.get('departureProfiles', {}).get('timers', [])
|
2349
|
+
timer = timerdata[2]
|
2350
|
+
#timer.pop('timestamp', None)
|
2351
|
+
#timer.pop('timerID', None)
|
2352
|
+
#timer.pop('profileID', None)
|
2353
|
+
data.update(timer)
|
2354
|
+
return data
|
2355
|
+
except:
|
2356
|
+
pass
|
2357
|
+
return None
|
2358
|
+
|
2359
|
+
@property
|
2360
|
+
def is_departure_profile3_supported(self):
|
2361
|
+
"""Return true if profile 3 is supported."""
|
2362
|
+
if len(self.attrs.get('departureProfiles', {}).get('timers', [])) >= 3:
|
2363
|
+
return True
|
2364
|
+
return False
|
2365
|
+
|
2149
2366
|
# Trip data
|
2150
2367
|
@property
|
2151
2368
|
def trip_last_entry(self):
|
@@ -2452,11 +2669,12 @@ class Vehicle:
|
|
2452
2669
|
@property
|
2453
2670
|
def refresh_data(self):
|
2454
2671
|
"""Get state of data refresh"""
|
2455
|
-
if self._requests.get('refresh', {}).get('id', False):
|
2456
|
-
|
2457
|
-
|
2458
|
-
|
2459
|
-
|
2672
|
+
#if self._requests.get('refresh', {}).get('id', False):
|
2673
|
+
# timestamp = self._requests.get('refresh', {}).get('timestamp', DATEZERO)
|
2674
|
+
# expired = datetime.now() - timedelta(minutes=2)
|
2675
|
+
# if expired < timestamp:
|
2676
|
+
# return True
|
2677
|
+
#State is always false
|
2460
2678
|
return False
|
2461
2679
|
|
2462
2680
|
@property
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pycupra
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.6
|
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=YbGN0knQ3fpcD8GGO6OA7ZBYa-fQ4unLaw-NTYREVn8,207
|
3
|
+
pycupra/connection.py,sha256=4bcaHn6AwAAY9e3MfVwlPMQzKJZQo_BZttts0lubnro,81120
|
4
|
+
pycupra/const.py,sha256=VEYH8TUsJGJwBwloaajwoElYd0qxE7oesvoagvDdE-4,10161
|
5
|
+
pycupra/dashboard.py,sha256=difLM3R2uXUrVslUjqzH8nN6WPJA-u26rHlGUU6W8Oo,40454
|
6
|
+
pycupra/exceptions.py,sha256=Nq_F79GP8wjHf5lpvPy9TbSIrRHAJrFMo0T1N9TcgSQ,2917
|
7
|
+
pycupra/utilities.py,sha256=cH4MiIzT2WlHgmnl_E7rR0R5LvCXfDNvirJolct50V8,2563
|
8
|
+
pycupra/vehicle.py,sha256=vlSuvrspX_I1RlS8KoZkN09u5cCNNdcIslsA0xPUuqA,120442
|
9
|
+
pycupra-0.0.6.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
10
|
+
pycupra-0.0.6.dist-info/METADATA,sha256=k-diSYUQOySb2FxopmNeWMOXqf_FQG1A5yGwnMpo03M,2578
|
11
|
+
pycupra-0.0.6.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
12
|
+
pycupra-0.0.6.dist-info/top_level.txt,sha256=9Lbj_jG4JvpGwt6K3AwhWFc0XieDnuHFOP4x44wSXSQ,8
|
13
|
+
pycupra-0.0.6.dist-info/RECORD,,
|
pycupra-0.0.4.dist-info/RECORD
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
pycupra/__init__.py,sha256=VPzUfKd5mBFD1UERNV61FbGHih5dQPupLgIfYtmIUi4,230
|
2
|
-
pycupra/__version__.py,sha256=Jg8rvIbOVp1FjzVg0HGzifRrq1AIQh-Wng2duMF9Rns,207
|
3
|
-
pycupra/connection.py,sha256=8kh14krvWZZfALo-waL61Cox79XhfII1GS49Ni-UgqM,76334
|
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=v9U_cBQGXwa8dsmqyfQZm2WGOZ9uKqWW7FoiO4JsAw0,108436
|
9
|
-
pycupra-0.0.4.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
10
|
-
pycupra-0.0.4.dist-info/METADATA,sha256=_YcaDFZEuFj_eJdtsfqg2CW87-dnOKnJbPZQfGASk1E,2578
|
11
|
-
pycupra-0.0.4.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
12
|
-
pycupra-0.0.4.dist-info/top_level.txt,sha256=9Lbj_jG4JvpGwt6K3AwhWFc0XieDnuHFOP4x44wSXSQ,8
|
13
|
-
pycupra-0.0.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|