carconnectivity-connector-seatcupra 0.1.2a1__py3-none-any.whl → 0.2__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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: carconnectivity-connector-seatcupra
3
- Version: 0.1.2a1
3
+ Version: 0.2
4
4
  Summary: CarConnectivity connector for Seat and Cupra services
5
5
  Author: Till Steinbach
6
6
  License: MIT License
@@ -37,10 +37,11 @@ Classifier: Topic :: Software Development :: Libraries
37
37
  Requires-Python: >=3.9
38
38
  Description-Content-Type: text/markdown
39
39
  License-File: LICENSE
40
- Requires-Dist: carconnectivity>=0.4
40
+ Requires-Dist: carconnectivity>=0.5
41
41
  Requires-Dist: oauthlib~=3.2.2
42
42
  Requires-Dist: requests~=2.32.3
43
43
  Requires-Dist: jwt~=1.3.1
44
+ Dynamic: license-file
44
45
 
45
46
 
46
47
 
@@ -1,21 +1,21 @@
1
+ carconnectivity_connector_seatcupra-0.2.dist-info/licenses/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
1
2
  carconnectivity_connectors/seatcupra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- carconnectivity_connectors/seatcupra/_version.py,sha256=VzTjVbsUC2B6Y5R_ncX3UCI-1y_cf0-27tYAHy1JsEY,513
3
+ carconnectivity_connectors/seatcupra/_version.py,sha256=3yop_Zw31tLW5V7T20Ar9d_ywZdoSjaF0mFPUD-T2xY,506
3
4
  carconnectivity_connectors/seatcupra/capability.py,sha256=936V06hOX8AuAMxL_S9wVyVa36Xw1bo9081X0xf5f94,5064
4
- carconnectivity_connectors/seatcupra/charging.py,sha256=BJe_5GEB0JkP78tpU6kyKpwuwjDZHvm-kt3PTlpQHeU,3336
5
+ carconnectivity_connectors/seatcupra/charging.py,sha256=mayvseay5x2r2qjWqol0ijlgoBL2L2A0A96T44FOiHg,4076
5
6
  carconnectivity_connectors/seatcupra/climatization.py,sha256=0xxWlxrheAPzkVT8WRQtbm6ExZmVdgW7lUdOXyS_qWY,1695
6
7
  carconnectivity_connectors/seatcupra/command_impl.py,sha256=LmBOCWGZPfJCG_4-5449xvO6NAvnPDsAWEBKlsG4WoI,3051
7
- carconnectivity_connectors/seatcupra/connector.py,sha256=llxFhVdpnL5MxmHbOej1wMio5tareY3zha0qaFhnPgs,107680
8
- carconnectivity_connectors/seatcupra/vehicle.py,sha256=s0G-HqG5qcwStDxD3649KgLMa3lKPZ4TOGWRJEuQzsQ,3403
8
+ carconnectivity_connectors/seatcupra/connector.py,sha256=_8Lvg1yqHHbH4mkhThwq0KZZL64rzKoFYurQCQurqY4,130482
9
+ carconnectivity_connectors/seatcupra/vehicle.py,sha256=LHkAlVD_C8xOX81wCGFZbZqyhctpKx-CN0T3NZJ2jFk,3946
9
10
  carconnectivity_connectors/seatcupra/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
11
  carconnectivity_connectors/seatcupra/auth/auth_util.py,sha256=Y81h8fGOMSMgPtE4wI_TI9WgE_s43uaPjRLBBINhj4g,4433
11
- carconnectivity_connectors/seatcupra/auth/my_cupra_session.py,sha256=SXO_77diYSmHN3KNSccDASk95Ko8uK5WWKSjIJNHrUA,13558
12
+ carconnectivity_connectors/seatcupra/auth/my_cupra_session.py,sha256=VF_9U8fESLkndVaPn2W1ZxZwNr9-ndeaegeTVT5FyYk,13904
12
13
  carconnectivity_connectors/seatcupra/auth/openid_session.py,sha256=pGdTSt2zMtPWD4EY8MoZTj8lT6_krfa1Xt3Fyh877FA,16972
13
14
  carconnectivity_connectors/seatcupra/auth/session_manager.py,sha256=ZIDvC848T3fy6PgGqCl8A2SzaNhu2YG19Xam5kgp7SA,5635
14
15
  carconnectivity_connectors/seatcupra/auth/vw_web_session.py,sha256=CcI6m68IyRs6WsMDu-IsW3Dj85vyGiMmxvFqNETMHO0,10929
15
16
  carconnectivity_connectors/seatcupra/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
16
17
  carconnectivity_connectors/seatcupra/ui/connector_ui.py,sha256=SNYnlcGJpbWhuLiIHD2l6H9IfSiMz3IgmvXsdossDnE,1412
17
- carconnectivity_connector_seatcupra-0.1.2a1.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
18
- carconnectivity_connector_seatcupra-0.1.2a1.dist-info/METADATA,sha256=ii6XPx0SHu1N2SEXwPeDC-Pei__53Le9BlV08yKzRUw,5473
19
- carconnectivity_connector_seatcupra-0.1.2a1.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
20
- carconnectivity_connector_seatcupra-0.1.2a1.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
21
- carconnectivity_connector_seatcupra-0.1.2a1.dist-info/RECORD,,
18
+ carconnectivity_connector_seatcupra-0.2.dist-info/METADATA,sha256=3RrvEwAnY_qIK95IUNMpVuc42qYt9LU9aZ-At4Qvf-g,5491
19
+ carconnectivity_connector_seatcupra-0.2.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
20
+ carconnectivity_connector_seatcupra-0.2.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
21
+ carconnectivity_connector_seatcupra-0.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (77.0.3)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.1.2a1'
21
- __version_tuple__ = version_tuple = (0, 1, 2)
20
+ __version__ = version = '0.2'
21
+ __version_tuple__ = version_tuple = (0, 2)
@@ -46,6 +46,7 @@ class MyCupraSession(VWWebSession):
46
46
 
47
47
  self.headers = CaseInsensitiveDict({
48
48
  'accept': '*/*',
49
+ 'connection': 'keep-alive',
49
50
  'content-type': 'application/json',
50
51
  'user-agent': 'SEATApp/2.5.0 (com.seat.myseat.ola; build:202410171614; iOS 15.8.3) Alamofire/5.7.0 Mobile',
51
52
  'accept-language': 'de-de',
@@ -62,6 +63,7 @@ class MyCupraSession(VWWebSession):
62
63
 
63
64
  self.headers = CaseInsensitiveDict({
64
65
  'accept': '*/*',
66
+ 'connection': 'keep-alive',
65
67
  'content-type': 'application/json',
66
68
  'user-agent': 'CUPRAApp%20-%20Store/20220503 CFNetwork/1333.0.4 Darwin/21.5.0',
67
69
  'accept-language': 'de-de',
@@ -220,12 +222,19 @@ class MyCupraSession(VWWebSession):
220
222
  if headers is None:
221
223
  headers = dict(self.headers)
222
224
 
223
- body: Dict[str, str] = {
224
- 'client_id': self.client_id,
225
- 'client_secret': 'eb8814e641c81a2640ad62eeccec11c98effc9bccd4269ab7af338b50a94b3a2',
226
- 'grant_type': 'refresh_token',
227
- 'refresh_token': self.refresh_token
228
- }
225
+ if self.is_seat:
226
+ body: Dict[str, str] = {
227
+ 'client_id': self.client_id,
228
+ 'grant_type': 'refresh_token',
229
+ 'refresh_token': self.refresh_token
230
+ }
231
+ else:
232
+ body: Dict[str, str] = {
233
+ 'client_id': self.client_id,
234
+ 'client_secret': 'eb8814e641c81a2640ad62eeccec11c98effc9bccd4269ab7af338b50a94b3a2',
235
+ 'grant_type': 'refresh_token',
236
+ 'refresh_token': self.refresh_token
237
+ }
229
238
 
230
239
  headers['content-type'] = 'application/x-www-form-urlencoded; charset=utf-8'
231
240
 
@@ -12,6 +12,8 @@ from carconnectivity.vehicle import ElectricVehicle
12
12
  if TYPE_CHECKING:
13
13
  from typing import Optional, Dict
14
14
 
15
+ from carconnectivity.objects import GenericObject
16
+
15
17
 
16
18
  class SeatCupraCharging(Charging): # pylint: disable=too-many-instance-attributes
17
19
  """
@@ -22,9 +24,22 @@ class SeatCupraCharging(Charging): # pylint: disable=too-many-instance-attribut
22
24
  """
23
25
  def __init__(self, vehicle: ElectricVehicle | None = None, origin: Optional[Charging] = None) -> None:
24
26
  if origin is not None:
25
- super().__init__(origin=origin)
27
+ super().__init__(vehicle=vehicle, origin=origin)
28
+ self.settings = SeatCupraCharging.Settings(parent=self, origin=origin.settings)
26
29
  else:
27
30
  super().__init__(vehicle=vehicle)
31
+ self.settings = SeatCupraCharging.Settings(parent=self, origin=self.settings)
32
+
33
+ class Settings(Charging.Settings):
34
+ """
35
+ This class represents the settings for car volkswagen car charging.
36
+ """
37
+ def __init__(self, parent: Optional[GenericObject] = None, origin: Optional[Charging.Settings] = None) -> None:
38
+ if origin is not None:
39
+ super().__init__(parent=parent, origin=origin)
40
+ else:
41
+ super().__init__(parent=parent)
42
+ self.max_current_in_ampere: Optional[bool] = None
28
43
 
29
44
  class SeatCupraChargingState(Enum,):
30
45
  """
@@ -20,17 +20,20 @@ from carconnectivity.units import Length, Current
20
20
  from carconnectivity.doors import Doors
21
21
  from carconnectivity.windows import Windows
22
22
  from carconnectivity.lights import Lights
23
- from carconnectivity.drive import GenericDrive, ElectricDrive, CombustionDrive
23
+ from carconnectivity.drive import GenericDrive, ElectricDrive, CombustionDrive, DieselDrive
24
24
  from carconnectivity.vehicle import GenericVehicle, ElectricVehicle
25
- from carconnectivity.attributes import BooleanAttribute, DurationAttribute, GenericAttribute, TemperatureAttribute, EnumAttribute
25
+ from carconnectivity.attributes import BooleanAttribute, DurationAttribute, GenericAttribute, TemperatureAttribute, EnumAttribute, CurrentAttribute, \
26
+ LevelAttribute
26
27
  from carconnectivity.units import Temperature
27
- from carconnectivity.command_impl import ClimatizationStartStopCommand, WakeSleepCommand, HonkAndFlashCommand, LockUnlockCommand, ChargingStartStopCommand
28
+ from carconnectivity.command_impl import ClimatizationStartStopCommand, WakeSleepCommand, HonkAndFlashCommand, LockUnlockCommand, ChargingStartStopCommand, \
29
+ WindowHeatingStartStopCommand
28
30
  from carconnectivity.climatization import Climatization
29
31
  from carconnectivity.commands import Commands
30
32
  from carconnectivity.charging import Charging
31
33
  from carconnectivity.charging_connector import ChargingConnector
32
34
  from carconnectivity.position import Position
33
35
  from carconnectivity.enums import ConnectionState
36
+ from carconnectivity.window_heating import WindowHeatings
34
37
 
35
38
  from carconnectivity_connectors.base.connector import BaseConnector
36
39
  from carconnectivity_connectors.seatcupra.auth.session_manager import SessionManager, SessionUser, Service
@@ -288,6 +291,7 @@ class Connector(BaseConnector):
288
291
  vehicle_to_update = self.fetch_vehicle_status(vehicle_to_update)
289
292
  vehicle_to_update = self.fetch_vehicle_mycar_status(vehicle_to_update)
290
293
  vehicle_to_update = self.fetch_mileage(vehicle_to_update)
294
+ vehicle_to_update = self.fetch_ranges(vehicle_to_update)
291
295
  if vehicle_to_update.capabilities.has_capability('climatisation', check_status_ok=True):
292
296
  vehicle_to_update = self.fetch_climatisation(vehicle_to_update)
293
297
  if vehicle_to_update.capabilities.has_capability('charging', check_status_ok=True):
@@ -414,7 +418,7 @@ class Connector(BaseConnector):
414
418
  capability.parameters[parameter] = value
415
419
  else:
416
420
  raise APIError('Could not fetch capabilities, capability ID missing')
417
- log_extra_keys(LOG_API, 'capability', capability_dict, {'id', 'expirationDate', 'editable', 'parameters'})
421
+ log_extra_keys(LOG_API, 'capability', capability_dict, {'id', 'expirationDate', 'editable', 'parameters', 'status'})
418
422
 
419
423
  for capability_id in vehicle.capabilities.capabilities.keys() - found_capabilities:
420
424
  vehicle.capabilities.remove_capability(capability_id)
@@ -422,7 +426,7 @@ class Connector(BaseConnector):
422
426
  if vehicle.capabilities.has_capability('charging', check_status_ok=True):
423
427
  if not isinstance(vehicle, SeatCupraElectricVehicle):
424
428
  LOG.debug('Promoting %s to SeatCupraElectricVehicle object for %s', vehicle.__class__.__name__, vin)
425
- vehicle = SeatCupraElectricVehicle(origin=vehicle)
429
+ vehicle = SeatCupraElectricVehicle(garage=self.car_connectivity.garage, origin=vehicle)
426
430
  self.car_connectivity.garage.replace_vehicle(vin, vehicle)
427
431
  if not vehicle.charging.commands.contains_command('start-stop'):
428
432
  charging_start_stop_command: ChargingStartStopCommand = ChargingStartStopCommand(parent=vehicle.charging.commands)
@@ -646,6 +650,8 @@ class Connector(BaseConnector):
646
650
  else:
647
651
  if engine_type == GenericDrive.Type.ELECTRIC:
648
652
  drive = ElectricDrive(drive_id=drive_id, drives=vehicle.drives)
653
+ elif engine_type == GenericDrive.Type.DIESEL:
654
+ drive = DieselDrive(drive_id=drive_id, drives=vehicle.drives)
649
655
  elif engine_type in [GenericDrive.Type.FUEL,
650
656
  GenericDrive.Type.GASOLINE,
651
657
  GenericDrive.Type.PETROL,
@@ -684,15 +690,15 @@ class Connector(BaseConnector):
684
690
  has_combustion = True
685
691
  if has_electric and not has_combustion and not isinstance(vehicle, SeatCupraElectricVehicle):
686
692
  LOG.debug('Promoting %s to SeatCupraElectricVehicle object for %s', vehicle.__class__.__name__, vin)
687
- vehicle = SeatCupraElectricVehicle(origin=vehicle)
693
+ vehicle = SeatCupraElectricVehicle(garage=self.car_connectivity.garage, origin=vehicle)
688
694
  self.car_connectivity.garage.replace_vehicle(vin, vehicle)
689
695
  elif has_combustion and not has_electric and not isinstance(vehicle, SeatCupraCombustionVehicle):
690
696
  LOG.debug('Promoting %s to SeatCupraCombustionVehicle object for %s', vehicle.__class__.__name__, vin)
691
- vehicle = SeatCupraCombustionVehicle(origin=vehicle)
697
+ vehicle = SeatCupraCombustionVehicle(garage=self.car_connectivity.garage, origin=vehicle)
692
698
  self.car_connectivity.garage.replace_vehicle(vin, vehicle)
693
699
  elif has_combustion and has_electric and not isinstance(vehicle, SeatCupraHybridVehicle):
694
700
  LOG.debug('Promoting %s to SeatCupraHybridVehicle object for %s', vehicle.__class__.__name__, vin)
695
- vehicle = SeatCupraHybridVehicle(origin=vehicle)
701
+ vehicle = SeatCupraHybridVehicle(garage=self.car_connectivity.garage, origin=vehicle)
696
702
  self.car_connectivity.garage.replace_vehicle(vin, vehicle)
697
703
  if 'services' in vehicle_status_data and vehicle_status_data['services'] is not None:
698
704
  if 'charging' in vehicle_status_data['services'] and vehicle_status_data['services']['charging'] is not None:
@@ -845,6 +851,40 @@ class Connector(BaseConnector):
845
851
  vehicle.odometer._set_value(None) # pylint: disable=protected-access
846
852
  return vehicle
847
853
 
854
+ def fetch_ranges(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
855
+ vin = vehicle.vin.value
856
+ if vin is None:
857
+ raise APIError('VIN is missing')
858
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/ranges'
859
+ # {'ranges': [{'rangeName': 'gasolineRangeKm', 'value': 100.0}, {'rangeName': 'electricRangeKm', 'value': 28.0}]}
860
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
861
+ if data is not None:
862
+ if 'ranges' in data and data['ranges'] is not None:
863
+ for drive in vehicle.drives.drives.values():
864
+ if drive.type.enabled and drive.type.value == GenericDrive.Type.ELECTRIC:
865
+ for range_dict in data['ranges']:
866
+ if 'rangeName' in range_dict and range_dict['rangeName'] is not None and range_dict['rangeName'] == 'electricRangeKm' \
867
+ and 'value' in range_dict and range_dict['value'] is not None:
868
+ drive.range._set_value(range_dict['value'], unit=Length.KM) # pylint: disable=protected-access
869
+ break
870
+ elif drive.type.enabled and drive.type.value == GenericDrive.Type.GASOLINE:
871
+ for range_dict in data['ranges']:
872
+ if 'rangeName' in range_dict and range_dict['rangeName'] is not None and range_dict['rangeName'] == 'gasolineRangeKm' \
873
+ and 'value' in range_dict and range_dict['value'] is not None:
874
+ drive.range._set_value(range_dict['value'], unit=Length.KM) # pylint: disable=protected-access
875
+ break
876
+ elif drive.type.enabled and drive.type.value == GenericDrive.Type.DIESEL:
877
+ for range_dict in data['ranges']:
878
+ if 'rangeName' in range_dict and range_dict['rangeName'] is not None and range_dict['rangeName'] == 'dieselRangeKm' \
879
+ and 'value' in range_dict and range_dict['value'] is not None:
880
+ drive.range._set_value(range_dict['value'], unit=Length.KM) # pylint: disable=protected-access
881
+ elif 'rangeName' in range_dict and range_dict['rangeName'] is not None and range_dict['rangeName'] == 'adBlueKm' \
882
+ and 'value' in range_dict and range_dict['value'] is not None:
883
+ if isinstance(drive, DieselDrive):
884
+ drive.adblue_range._set_value(range_dict['value'], unit=Length.KM) # pylint: disable=protected-access
885
+ log_extra_keys(LOG_API, f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/ranges', data, {'ranges'})
886
+ return vehicle
887
+
848
888
  def fetch_maintenance(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
849
889
  vin = vehicle.vin.value
850
890
  if vin is None:
@@ -921,7 +961,57 @@ class Connector(BaseConnector):
921
961
  log_extra_keys(LOG_API, 'climatisationStatus', data['climatisationStatus'], {'carCapturedTimestamp', 'climatisationState'})
922
962
  else:
923
963
  vehicle.climatization.state._set_value(None) # pylint: disable=protected-access
924
- log_extra_keys(LOG_API, 'climatisation', data, {'climatisationStatus'})
964
+ if 'windowHeatingStatus' in data and data['windowHeatingStatus'] is not None:
965
+ window_heating_status: Dict = data['windowHeatingStatus']
966
+ if 'carCapturedTimestamp' not in window_heating_status or window_heating_status['carCapturedTimestamp'] is None:
967
+ raise APIError('Could not fetch vehicle status, carCapturedTimestamp missing')
968
+ captured_at: datetime = robust_time_parse(window_heating_status['carCapturedTimestamp'])
969
+ if 'windowHeatingStatus' in window_heating_status and window_heating_status['windowHeatingStatus'] is not None:
970
+ heating_on: bool = False
971
+ all_heating_invalid: bool = True
972
+ for window_heating in window_heating_status['windowHeatingStatus']:
973
+ if 'windowLocation' in window_heating and window_heating['windowLocation'] is not None:
974
+ window_id = window_heating['windowLocation']
975
+ if window_id in vehicle.window_heatings.windows:
976
+ window: WindowHeatings.WindowHeating = vehicle.window_heatings.windows[window_id]
977
+ else:
978
+ window = WindowHeatings.WindowHeating(window_id=window_id, window_heatings=vehicle.window_heatings)
979
+ vehicle.window_heatings.windows[window_id] = window
980
+ if 'windowHeatingState' in window_heating and window_heating['windowHeatingState'] is not None:
981
+ if window_heating['windowHeatingState'] in [item.value for item in WindowHeatings.HeatingState]:
982
+ window_heating_state: WindowHeatings.HeatingState = WindowHeatings.HeatingState(window_heating['windowHeatingState'])
983
+ if window_heating_state == WindowHeatings.HeatingState.ON:
984
+ heating_on = True
985
+ if window_heating_state in [WindowHeatings.HeatingState.ON,
986
+ WindowHeatings.HeatingState.OFF]:
987
+ all_heating_invalid = False
988
+ window.heating_state._set_value(window_heating_state, measured=captured_at) # pylint: disable=protected-access
989
+ else:
990
+ LOG_API.info('Unknown window heating state %s not in %s', window_heating['windowHeatingState'],
991
+ str(WindowHeatings.HeatingState))
992
+ # pylint: disable-next=protected-access
993
+ window.heating_state._set_value(WindowHeatings.HeatingState.UNKNOWN, measured=captured_at)
994
+ else:
995
+ window.heating_state._set_value(None, measured=captured_at) # pylint: disable=protected-access
996
+ log_extra_keys(LOG_API, 'windowHeatingStatus', window_heating, {'windowLocation', 'windowHeatingState'})
997
+ if all_heating_invalid:
998
+ # pylint: disable-next=protected-access
999
+ vehicle.window_heatings.heating_state._set_value(WindowHeatings.HeatingState.INVALID, measured=captured_at)
1000
+ else:
1001
+ if heating_on:
1002
+ # pylint: disable-next=protected-access
1003
+ vehicle.window_heatings.heating_state._set_value(WindowHeatings.HeatingState.ON, measured=captured_at)
1004
+ else:
1005
+ # pylint: disable-next=protected-access
1006
+ vehicle.window_heatings.heating_state._set_value(WindowHeatings.HeatingState.OFF, measured=captured_at)
1007
+ if vehicle.window_heatings is not None and vehicle.window_heatings.commands is not None \
1008
+ and not vehicle.window_heatings.commands.contains_command('start-stop'):
1009
+ start_stop_command = WindowHeatingStartStopCommand(parent=vehicle.window_heatings.commands)
1010
+ start_stop_command._add_on_set_hook(self.__on_window_heating_start_stop) # pylint: disable=protected-access
1011
+ start_stop_command.enabled = True
1012
+ vehicle.window_heatings.commands.add_command(start_stop_command)
1013
+ log_extra_keys(LOG_API, 'windowHeatingStatus', window_heating_status, {'carCapturedTimestamp', 'windowHeatingStatus'})
1014
+ log_extra_keys(LOG_API, 'climatisation', data, {'climatisationStatus', 'windowHeatingStatus'})
925
1015
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/climatisation/settings'
926
1016
  data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
927
1017
  if data is not None:
@@ -1043,22 +1133,84 @@ class Connector(BaseConnector):
1043
1133
  log_extra_keys(LOG_API, f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/charging/status', data,
1044
1134
  {'state', 'battery', 'charging', 'plug'})
1045
1135
 
1046
- url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/charging/settings'
1136
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/charging/settings'
1047
1137
  data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
1048
1138
  if data is not None:
1049
- if 'maxChargeCurrentAc' in data and data['maxChargeCurrentAc'] is not None:
1050
- if data['maxChargeCurrentAc']:
1051
- vehicle.charging.settings.maximum_current._set_value(value=16, # pylint: disable=protected-access
1052
- unit=Current.A)
1139
+ # {'settings': {'maxChargeCurrentAC': 'reduced', 'carCapturedTimestamp': '2025-03-18T16:50:33Z', 'autoUnlockPlugWhenCharged': None, 'targetSoc_pct': 100, 'batteryCareTargetSocPercentage': 80}}
1140
+ if 'settings' in data and data['settings'] is not None:
1141
+ if 'carCapturedTimestamp' not in data['settings'] or data['settings']['carCapturedTimestamp'] is None:
1142
+ raise APIError('Could not fetch vehicle status, carCapturedTimestamp missing')
1143
+ captured_at: datetime = robust_time_parse(data['settings']['carCapturedTimestamp'])
1144
+ if 'maxChargeCurrentAC_A' in data['settings'] and data['settings']['maxChargeCurrentAC_A'] is not None:
1145
+ if isinstance(vehicle.charging.settings, SeatCupraCharging.Settings):
1146
+ vehicle.charging.settings.max_current_in_ampere = True
1147
+ else:
1148
+ raise ValueError('Charging settings not of type VolkswagenCharging.Settings')
1149
+ vehicle.charging.settings.maximum_current.minimum = 6.0
1150
+ vehicle.charging.settings.maximum_current.maximum = 16.0
1151
+ vehicle.charging.settings.maximum_current.precision = 1.0
1152
+ # pylint: disable-next=protected-access
1153
+ vehicle.charging.settings.maximum_current._add_on_set_hook(self.__on_charging_settings_change)
1154
+ vehicle.charging.settings.maximum_current._is_changeable = True # pylint: disable=protected-access
1155
+ vehicle.charging.settings.maximum_current._set_value(data['settings']['maxChargeCurrentAC_A'], # pylint: disable=protected-access
1156
+ measured=captured_at)
1157
+ elif 'maxChargeCurrentAC' in data['settings'] and data['settings']['maxChargeCurrentAC'] is not None:
1158
+ if isinstance(vehicle.charging.settings, SeatCupraCharging.Settings):
1159
+ vehicle.charging.settings.max_current_in_ampere = False
1160
+ else:
1161
+ raise ValueError('Charging settings not of type VolkswagenCharging.Settings')
1162
+ vehicle.charging.settings.maximum_current.minimum = 6.0
1163
+ vehicle.charging.settings.maximum_current.maximum = 16.0
1164
+ vehicle.charging.settings.maximum_current.precision = 1.0
1165
+ # pylint: disable-next=protected-access
1166
+ vehicle.charging.settings.maximum_current._add_on_set_hook(self.__on_charging_settings_change)
1167
+ vehicle.charging.settings.maximum_current._is_changeable = True # pylint: disable=protected-access
1168
+ if data['settings']['maxChargeCurrentAC'] == 'maximum':
1169
+ vehicle.charging.settings.maximum_current._set_value(16.0, # pylint: disable=protected-access
1170
+ measured=captured_at)
1171
+ elif data['settings']['maxChargeCurrentAC'] == 'reduced':
1172
+ vehicle.charging.settings.maximum_current._set_value(6.0, # pylint: disable=protected-access
1173
+ measured=captured_at)
1174
+ else:
1175
+ LOG_API.info('Unknown max charge current %s', data['settings']['maxChargeCurrentAC'])
1176
+ vehicle.charging.settings.maximum_current._set_value(None, measured=captured_at) # pylint: disable=protected-access
1053
1177
  else:
1054
- vehicle.charging.settings.maximum_current._set_value(value=6, # pylint: disable=protected-access
1055
- unit=Current.A)
1056
- else:
1057
- vehicle.charging.settings.maximum_current._set_value(None) # pylint: disable=protected-access
1058
- if 'defaultMaxTargetSocPercentage' in data and data['defaultMaxTargetSocPercentage'] is not None:
1059
- vehicle.charging.settings.target_level._set_value(data['defaultMaxTargetSocPercentage']) # pylint: disable=protected-access
1060
- else:
1061
- vehicle.charging.settings.target_level._set_value(None) # pylint: disable=protected-access
1178
+ vehicle.charging.settings.maximum_current._set_value(None, measured=captured_at) # pylint: disable=protected-access
1179
+ if 'autoUnlockPlugWhenCharged' in data['settings'] and data['settings']['autoUnlockPlugWhenCharged'] is not None:
1180
+ # pylint: disable-next=protected-access
1181
+ vehicle.charging.settings.auto_unlock._add_on_set_hook(self.__on_charging_settings_change)
1182
+ vehicle.charging.settings.auto_unlock._is_changeable = True # pylint: disable=protected-access
1183
+ if data['settings']['autoUnlockPlugWhenCharged'] == 'on':
1184
+ vehicle.charging.settings.auto_unlock._set_value(True, # pylint: disable=protected-access
1185
+ measured=captured_at)
1186
+ elif data['settings']['autoUnlockPlugWhenCharged'] == 'off':
1187
+ vehicle.charging.settings.auto_unlock._set_value(False, # pylint: disable=protected-access
1188
+ measured=captured_at)
1189
+ else:
1190
+ LOG_API.info('Unknown auto unlock plug when charged %s', data['settings']['autoUnlockPlugWhenCharged'])
1191
+ vehicle.charging.settings.auto_unlock._set_value(None, measured=captured_at) # pylint: disable=protected-access
1192
+ else:
1193
+ vehicle.charging.settings.auto_unlock._set_value(None, measured=captured_at) # pylint: disable=protected-access
1194
+ if 'targetSoc_pct' in data['settings'] and data['settings']['targetSoc_pct'] is not None:
1195
+ charging_capability: Optional[Capability] = vehicle.capabilities.get_capability('charging')
1196
+ if charging_capability is not None and ('supportsTargetStateOfCharge' not in charging_capability.parameters
1197
+ or charging_capability.parameters['supportsTargetStateOfCharge'] != 'false'):
1198
+ vehicle.charging.settings.target_level.minimum = 50.0
1199
+ vehicle.charging.settings.target_level.maximum = 100.0
1200
+ vehicle.charging.settings.target_level.precision = 10.0
1201
+ # pylint: disable-next=protected-access
1202
+ vehicle.charging.settings.target_level._add_on_set_hook(self.__on_charging_settings_change)
1203
+ vehicle.charging.settings.target_level._is_changeable = True # pylint: disable=protected-access
1204
+ vehicle.charging.settings.target_level._set_value(data['settings']['targetSoc_pct'], # pylint: disable=protected-access
1205
+ measured=captured_at)
1206
+ else:
1207
+ vehicle.charging.settings.target_level._set_value(None, measured=captured_at) # pylint: disable=protected-access
1208
+ log_extra_keys(LOG_API, 'chargingSettings', data['settings'], {'carCapturedTimestamp', 'maxChargeCurrentAC_A', 'maxChargeCurrentAC',
1209
+ 'autoUnlockPlugWhenCharged', 'targetSoc_pct'})
1210
+ else:
1211
+ vehicle.charging.settings.maximum_current._set_value(None) # pylint: disable=protected-access
1212
+ vehicle.charging.settings.auto_unlock._set_value(None) # pylint: disable=protected-access
1213
+ vehicle.charging.settings.target_level._set_value(None) # pylint: disable=protected-access
1062
1214
  return vehicle
1063
1215
 
1064
1216
  def fetch_image(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
@@ -1205,7 +1357,6 @@ class Connector(BaseConnector):
1205
1357
  raise CommandError('VIN in object hierarchy missing')
1206
1358
  if 'command' not in command_arguments:
1207
1359
  raise CommandError('Command argument missing')
1208
- command_dict: Dict = {}
1209
1360
  try:
1210
1361
  if command_arguments['command'] == ChargingStartStopCommand.Command.START:
1211
1362
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/charging/requests/start'
@@ -1494,20 +1645,25 @@ class Connector(BaseConnector):
1494
1645
  if settings.target_temperature.enabled and settings.target_temperature.value is not None:
1495
1646
  # Round target temperature to nearest 0.5
1496
1647
  # Check if the attribute changed is the target_temperature attribute
1648
+ precision: float = settings.target_temperature.precision if settings.target_temperature.precision is not None else 0.5
1497
1649
  if isinstance(attribute, TemperatureAttribute) and attribute.id == 'target_temperature':
1498
- setting_dict['targetTemperature'] = round(value * 2) / 2
1650
+ value = round(value / settings.target_temperature.precision) * settings.target_temperature.precision
1651
+ setting_dict['targetTemperature'] = value
1499
1652
  else:
1500
- setting_dict['targetTemperature'] = round(settings.target_temperature.value * 2) / 2
1653
+ setting_dict['targetTemperature'] = round(settings.target_temperature.value / precision) * precision
1501
1654
  if settings.target_temperature.unit == Temperature.C:
1502
1655
  setting_dict['targetTemperatureUnit'] = 'celsius'
1503
1656
  elif settings.target_temperature.unit == Temperature.F:
1504
1657
  setting_dict['targetTemperatureUnit'] = 'farenheit'
1505
1658
  else:
1506
1659
  setting_dict['targetTemperatureUnit'] = 'celsius'
1507
- if isinstance(attribute, BooleanAttribute) and attribute.id == 'climatisation_without_external_power':
1508
- setting_dict['climatisationWithoutExternalPower'] = value
1509
- elif settings.climatization_without_external_power.enabled and settings.climatization_without_external_power.value is not None:
1510
- setting_dict['climatisationWithoutExternalPower'] = settings.climatization_without_external_power.value
1660
+ climatization_capability: Optional[Capability] = vehicle.capabilities.get_capability('climatisation')
1661
+ if climatization_capability is not None and ('supportsOffGridClimatisation' not in climatization_capability.parameters
1662
+ or climatization_capability.parameters['supportsOffGridClimatisation'] != 'false'):
1663
+ if isinstance(attribute, BooleanAttribute) and attribute.id == 'climatisation_without_external_power':
1664
+ setting_dict['climatisationWithoutExternalPower'] = value
1665
+ elif settings.climatization_without_external_power.enabled and settings.climatization_without_external_power.value is not None:
1666
+ setting_dict['climatisationWithoutExternalPower'] = settings.climatization_without_external_power.value
1511
1667
 
1512
1668
  url: str = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/climatisation/settings'
1513
1669
  try:
@@ -1526,6 +1682,114 @@ class Connector(BaseConnector):
1526
1682
  raise SetterError(f'Retrying failed: {retry_error}') from retry_error
1527
1683
  return value
1528
1684
 
1685
+ def __on_window_heating_start_stop(self, start_stop_command: WindowHeatingStartStopCommand, command_arguments: Union[str, Dict[str, Any]]) \
1686
+ -> Union[str, Dict[str, Any]]:
1687
+ if start_stop_command.parent is None or start_stop_command.parent.parent is None \
1688
+ or start_stop_command.parent.parent.parent is None or not isinstance(start_stop_command.parent.parent.parent, SeatCupraVehicle):
1689
+ raise CommandError('Object hierarchy is not as expected')
1690
+ if not isinstance(command_arguments, dict):
1691
+ raise CommandError('Command arguments are not a dictionary')
1692
+ vehicle: SeatCupraVehicle = start_stop_command.parent.parent.parent
1693
+ vin: Optional[str] = vehicle.vin.value
1694
+ if vin is None:
1695
+ raise CommandError('VIN in object hierarchy missing')
1696
+ if 'command' not in command_arguments:
1697
+ raise CommandError('Command argument missing')
1698
+ try:
1699
+ if command_arguments['command'] == WindowHeatingStartStopCommand.Command.START:
1700
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/windowheating/requests/start'
1701
+ command_response: requests.Response = self.session.post(url, allow_redirects=True)
1702
+ elif command_arguments['command'] == WindowHeatingStartStopCommand.Command.STOP:
1703
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/windowheating/requests/stop'
1704
+ command_response: requests.Response = self.session.post(url, allow_redirects=True)
1705
+ else:
1706
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
1707
+
1708
+ if command_response.status_code not in [requests.codes['ok'], requests.codes['created']]:
1709
+ LOG.error('Could not start/stop window heating (%s: %s)', command_response.status_code, command_response.text)
1710
+ raise CommandError(f'Could not start/stop window heating ({command_response.status_code}: {command_response.text})')
1711
+ except requests.exceptions.ConnectionError as connection_error:
1712
+ raise CommandError(f'Connection error: {connection_error}.'
1713
+ ' If this happens frequently, please check if other applications communicate with the Seat/Cupra server.') from connection_error
1714
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
1715
+ raise CommandError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
1716
+ except requests.exceptions.ReadTimeout as timeout_error:
1717
+ raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
1718
+ except requests.exceptions.RetryError as retry_error:
1719
+ raise CommandError(f'Retrying failed: {retry_error}') from retry_error
1720
+ return command_arguments
1721
+
1722
+ def __on_charging_settings_change(self, attribute: GenericAttribute, value: Any) -> Any:
1723
+ """
1724
+ Callback for the charging setting change.
1725
+ """
1726
+ if attribute.parent is None or not isinstance(attribute.parent, SeatCupraCharging.Settings) \
1727
+ or attribute.parent.parent is None \
1728
+ or attribute.parent.parent.parent is None or not isinstance(attribute.parent.parent.parent, SeatCupraVehicle):
1729
+ raise SetterError('Object hierarchy is not as expected')
1730
+ settings: SeatCupraCharging.Settings = attribute.parent
1731
+ vehicle: SeatCupraVehicle = attribute.parent.parent.parent
1732
+ vin: Optional[str] = vehicle.vin.value
1733
+ if vin is None:
1734
+ raise SetterError('VIN in object hierarchy missing')
1735
+ setting_dict = {}
1736
+ precision: float = settings.maximum_current.precision if settings.maximum_current.precision is not None else 1.0
1737
+ if isinstance(attribute, CurrentAttribute) and attribute.id == 'maximum_current':
1738
+ value = round(value / precision) * precision
1739
+ if settings.max_current_in_ampere:
1740
+ setting_dict['maxChargeCurrentAcInAmperes'] = value
1741
+ else:
1742
+ if value < 6:
1743
+ raise SetterError('Maximum current must be greater than 6 amps')
1744
+ if value < 16:
1745
+ setting_dict['maxChargeCurrentAc'] = 'reduced'
1746
+ value = 6.0
1747
+ else:
1748
+ setting_dict['maxChargeCurrentAc'] = 'maximum'
1749
+ value = 16.0
1750
+ elif settings.maximum_current.enabled and settings.maximum_current.value is not None:
1751
+ if settings.max_current_in_ampere:
1752
+ setting_dict['maxChargeCurrentAc_A'] = round(settings.maximum_current.value / precision) * precision
1753
+ else:
1754
+ if settings.maximum_current.value < 6:
1755
+ raise SetterError('Maximum current must be greater than 6 amps')
1756
+ if settings.maximum_current.value < 16:
1757
+ setting_dict['maxChargeCurrentAc'] = 'reduced'
1758
+ settings.maximum_current.value = 6.0
1759
+ else:
1760
+ setting_dict['maxChargeCurrentAc'] = 'maximum'
1761
+ settings.maximum_current.value = 16.0
1762
+ if isinstance(attribute, BooleanAttribute) and attribute.id == 'auto_unlock':
1763
+ setting_dict['autoUnlockPlugWhenChargedAc'] = 'on' if value else 'off'
1764
+ elif settings.auto_unlock.enabled and settings.auto_unlock.value is not None:
1765
+ setting_dict['autoUnlockPlugWhenChargedAc'] = 'on' if settings.auto_unlock.value else 'off'
1766
+ charging_capability: Optional[Capability] = vehicle.capabilities.get_capability('charging')
1767
+ if charging_capability is not None and ('supportsTargetStateOfCharge' not in charging_capability.parameters
1768
+ or charging_capability.parameters['supportsTargetStateOfCharge'] != 'false'):
1769
+ precision: float = settings.target_level.precision if settings.target_level.precision is not None else 10.0
1770
+ if isinstance(attribute, LevelAttribute) and attribute.id == 'target_level':
1771
+ value = round(value / precision) * precision
1772
+ setting_dict['targetSoc'] = value
1773
+ elif settings.target_level.enabled and settings.target_level.value is not None:
1774
+ setting_dict['targetSoc'] = round(settings.target_level.value / precision) * precision
1775
+
1776
+ url: str = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/charging/settings'
1777
+ try:
1778
+ settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
1779
+ if settings_response.status_code not in [requests.codes['ok'], requests.codes['created']]:
1780
+ LOG.error('Could not set charging settings (%s)', settings_response.status_code)
1781
+ raise SetterError(f'Could not set value ({settings_response.status_code})')
1782
+ except requests.exceptions.ConnectionError as connection_error:
1783
+ raise SetterError(f'Connection error: {connection_error}.'
1784
+ ' If this happens frequently, please check if other applications communicate with the Volkswagen server.') from connection_error
1785
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
1786
+ raise SetterError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
1787
+ except requests.exceptions.ReadTimeout as timeout_error:
1788
+ raise SetterError(f'Timeout during read: {timeout_error}') from timeout_error
1789
+ except requests.exceptions.RetryError as retry_error:
1790
+ raise SetterError(f'Retrying failed: {retry_error}') from retry_error
1791
+ return value
1792
+
1529
1793
  def get_version(self) -> str:
1530
1794
  return __version__
1531
1795
 
@@ -6,6 +6,7 @@ from carconnectivity.vehicle import GenericVehicle, ElectricVehicle, CombustionV
6
6
 
7
7
  from carconnectivity_connectors.seatcupra.capability import Capabilities
8
8
  from carconnectivity_connectors.seatcupra.climatization import SeatCupraClimatization
9
+ from carconnectivity_connectors.seatcupra.charging import SeatCupraCharging
9
10
 
10
11
  SUPPORT_IMAGES = False
11
12
  try:
@@ -17,6 +18,8 @@ except ImportError:
17
18
  if TYPE_CHECKING:
18
19
  from typing import Optional, Dict
19
20
  from carconnectivity.garage import Garage
21
+ from carconnectivity.charging import Charging
22
+
20
23
  from carconnectivity_connectors.base.connector import BaseConnector
21
24
 
22
25
 
@@ -34,7 +37,7 @@ class SeatCupraVehicle(GenericVehicle): # pylint: disable=too-many-instance-att
34
37
  def __init__(self, vin: Optional[str] = None, garage: Optional[Garage] = None, managing_connector: Optional[BaseConnector] = None,
35
38
  origin: Optional[SeatCupraVehicle] = None) -> None:
36
39
  if origin is not None:
37
- super().__init__(origin=origin)
40
+ super().__init__(garage=garage, origin=origin)
38
41
  self.capabilities: Capabilities = origin.capabilities
39
42
  self.capabilities.parent = self
40
43
  if SUPPORT_IMAGES:
@@ -54,9 +57,14 @@ class SeatCupraElectricVehicle(ElectricVehicle, SeatCupraVehicle):
54
57
  def __init__(self, vin: Optional[str] = None, garage: Optional[Garage] = None, managing_connector: Optional[BaseConnector] = None,
55
58
  origin: Optional[SeatCupraVehicle] = None) -> None:
56
59
  if origin is not None:
57
- super().__init__(origin=origin)
60
+ super().__init__(garage=garage, origin=origin)
61
+ if isinstance(origin, ElectricVehicle):
62
+ self.charging: Charging = SeatCupraCharging(vehicle=self, origin=origin.charging)
63
+ else:
64
+ self.charging: Charging = SeatCupraCharging(vehicle=self, origin=self.charging)
58
65
  else:
59
66
  super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)
67
+ self.charging: Charging = SeatCupraCharging(vehicle=self, origin=self.charging)
60
68
 
61
69
 
62
70
  class SeatCupraCombustionVehicle(CombustionVehicle, SeatCupraVehicle):
@@ -66,7 +74,7 @@ class SeatCupraCombustionVehicle(CombustionVehicle, SeatCupraVehicle):
66
74
  def __init__(self, vin: Optional[str] = None, garage: Optional[Garage] = None, managing_connector: Optional[BaseConnector] = None,
67
75
  origin: Optional[SeatCupraVehicle] = None) -> None:
68
76
  if origin is not None:
69
- super().__init__(origin=origin)
77
+ super().__init__(garage=garage, origin=origin)
70
78
  else:
71
79
  super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)
72
80
 
@@ -78,6 +86,6 @@ class SeatCupraHybridVehicle(HybridVehicle, SeatCupraVehicle):
78
86
  def __init__(self, vin: Optional[str] = None, garage: Optional[Garage] = None, managing_connector: Optional[BaseConnector] = None,
79
87
  origin: Optional[SeatCupraVehicle] = None) -> None:
80
88
  if origin is not None:
81
- super().__init__(origin=origin)
89
+ super().__init__(garage=garage, origin=origin)
82
90
  else:
83
91
  super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)