carconnectivity-connector-seatcupra 0.1a1__tar.gz → 0.1a2__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/PKG-INFO +2 -2
  2. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/pyproject.toml +1 -1
  3. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connector_seatcupra.egg-info/PKG-INFO +2 -2
  4. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connector_seatcupra.egg-info/SOURCES.txt +2 -0
  5. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connector_seatcupra.egg-info/requires.txt +1 -1
  6. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connectors/seatcupra/_version.py +1 -1
  7. carconnectivity_connector_seatcupra-0.1a2/src/carconnectivity_connectors/seatcupra/capability.py +138 -0
  8. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connectors/seatcupra/connector.py +260 -32
  9. carconnectivity_connector_seatcupra-0.1a2/src/carconnectivity_connectors/seatcupra/vehicle.py +82 -0
  10. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/.flake8 +0 -0
  11. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/.github/dependabot.yml +0 -0
  14. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/.github/workflows/build.yml +0 -0
  15. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/.github/workflows/build_and_publish.yml +0 -0
  16. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/.github/workflows/codeql-analysis.yml +0 -0
  17. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/.gitignore +0 -0
  18. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/CHANGELOG.md +0 -0
  19. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/LICENSE +0 -0
  20. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/Makefile +0 -0
  21. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/README.md +0 -0
  22. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/doc/Config.md +0 -0
  23. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/setup.cfg +0 -0
  24. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/setup_requirements.txt +0 -0
  25. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connector_seatcupra.egg-info/dependency_links.txt +0 -0
  26. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connector_seatcupra.egg-info/top_level.txt +0 -0
  27. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connectors/seatcupra/__init__.py +0 -0
  28. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connectors/seatcupra/auth/__init__.py +0 -0
  29. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connectors/seatcupra/auth/auth_util.py +0 -0
  30. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connectors/seatcupra/auth/helpers/blacklist_retry.py +0 -0
  31. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connectors/seatcupra/auth/my_cupra_session.py +0 -0
  32. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connectors/seatcupra/auth/openid_session.py +0 -0
  33. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connectors/seatcupra/auth/session_manager.py +0 -0
  34. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connectors/seatcupra/auth/vw_web_session.py +0 -0
  35. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connectors/seatcupra/charging.py +0 -0
  36. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/src/carconnectivity_connectors/seatcupra/ui/connector_ui.py +0 -0
  37. {carconnectivity_connector_seatcupra-0.1a1 → carconnectivity_connector_seatcupra-0.1a2}/test/integration_test/carConnectivity.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: carconnectivity-connector-seatcupra
3
- Version: 0.1a1
3
+ Version: 0.1a2
4
4
  Summary: CarConnectivity connector for Seat and Cupra services
5
5
  Author: Till Steinbach
6
6
  License: MIT License
@@ -37,7 +37,7 @@ 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.3
40
+ Requires-Dist: carconnectivity>=0.4a1
41
41
  Requires-Dist: oauthlib~=3.2.2
42
42
  Requires-Dist: requests~=2.32.3
43
43
  Requires-Dist: jwt~=1.3.1
@@ -14,7 +14,7 @@ authors = [
14
14
  { name = "Till Steinbach" }
15
15
  ]
16
16
  dependencies = [
17
- "carconnectivity>=0.3",
17
+ "carconnectivity>=0.4a1",
18
18
  "oauthlib~=3.2.2",
19
19
  "requests~=2.32.3",
20
20
  "jwt~=1.3.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: carconnectivity-connector-seatcupra
3
- Version: 0.1a1
3
+ Version: 0.1a2
4
4
  Summary: CarConnectivity connector for Seat and Cupra services
5
5
  Author: Till Steinbach
6
6
  License: MIT License
@@ -37,7 +37,7 @@ 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.3
40
+ Requires-Dist: carconnectivity>=0.4a1
41
41
  Requires-Dist: oauthlib~=3.2.2
42
42
  Requires-Dist: requests~=2.32.3
43
43
  Requires-Dist: jwt~=1.3.1
@@ -20,8 +20,10 @@ src/carconnectivity_connector_seatcupra.egg-info/requires.txt
20
20
  src/carconnectivity_connector_seatcupra.egg-info/top_level.txt
21
21
  src/carconnectivity_connectors/seatcupra/__init__.py
22
22
  src/carconnectivity_connectors/seatcupra/_version.py
23
+ src/carconnectivity_connectors/seatcupra/capability.py
23
24
  src/carconnectivity_connectors/seatcupra/charging.py
24
25
  src/carconnectivity_connectors/seatcupra/connector.py
26
+ src/carconnectivity_connectors/seatcupra/vehicle.py
25
27
  src/carconnectivity_connectors/seatcupra/auth/__init__.py
26
28
  src/carconnectivity_connectors/seatcupra/auth/auth_util.py
27
29
  src/carconnectivity_connectors/seatcupra/auth/my_cupra_session.py
@@ -1,4 +1,4 @@
1
- carconnectivity>=0.3
1
+ carconnectivity>=0.4a1
2
2
  oauthlib~=3.2.2
3
3
  requests~=2.32.3
4
4
  jwt~=1.3.1
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.1a1'
20
+ __version__ = version = '0.1a2'
21
21
  __version_tuple__ = version_tuple = (0, 1)
@@ -0,0 +1,138 @@
1
+ """Module for seat/cupra vehicle capability class."""
2
+ from __future__ import annotations
3
+ from typing import TYPE_CHECKING
4
+
5
+ from enum import IntEnum
6
+
7
+ from carconnectivity.objects import GenericObject
8
+ from carconnectivity.attributes import StringAttribute, BooleanAttribute, DateAttribute
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Dict, Optional, List
12
+ from carconnectivity_connectors.seatcupra.vehicle import SeatCupraVehicle
13
+
14
+
15
+ class Capabilities(GenericObject):
16
+ """
17
+ Represents the capabilities of a Seat/Cupra vehicle.
18
+ """
19
+ def __init__(self, vehicle: SeatCupraVehicle) -> None:
20
+ super().__init__(object_id='capabilities', parent=vehicle)
21
+ self.__capabilities: Dict[str, Capability] = {}
22
+
23
+ @property
24
+ def capabilities(self) -> Dict[str, Capability]:
25
+ """
26
+ Retrieve the capabilities of the vehicle.
27
+
28
+ Returns:
29
+ Dict[str, Capability]: A dictionary of capabilities.
30
+ """
31
+ return self.__capabilities
32
+
33
+ def add_capability(self, capability_id: str, capability: Capability) -> None:
34
+ """
35
+ Adds a capability to the Capabilities of the vehicle.
36
+
37
+ Args:
38
+ capability_id (str): The unique identifier of the capability.
39
+ capability (Capability): The capability object to be added.
40
+
41
+ Returns:
42
+ None
43
+ """
44
+ self.__capabilities[capability_id] = capability
45
+
46
+ def remove_capability(self, capability_id: str) -> None:
47
+ """
48
+ Remove a capability from the Capabilities by its capability ID.
49
+
50
+ Args:
51
+ capability_id (str): The ID of the capability to be removed.
52
+
53
+ Returns:
54
+ None
55
+ """
56
+ if capability_id in self.__capabilities:
57
+ del self.__capabilities[capability_id]
58
+
59
+ def clear_capabilities(self) -> None:
60
+ """
61
+ Remove all capabilities from the Capabilities.
62
+
63
+ Returns:
64
+ None
65
+ """
66
+ self.__capabilities.clear()
67
+
68
+ def get_capability(self, capability_id: str) -> Optional[Capability]:
69
+ """
70
+ Retrieve a capability from the Capabilities by its capability ID.
71
+
72
+ Args:
73
+ capability_id (str): The unique identifier of the capability to retrieve.
74
+
75
+ Returns:
76
+ Capability: The capability object if found, otherwise None.
77
+ """
78
+ return self.__capabilities.get(capability_id)
79
+
80
+ def has_capability(self, capability_id: str) -> bool:
81
+ """
82
+ Check if the Capabilities contains a capability with the specified ID.
83
+
84
+ Args:
85
+ capability_id (str): The unique identifier of the capability to check.
86
+
87
+ Returns:
88
+ bool: True if the capability exists, otherwise False.
89
+ """
90
+ return capability_id in self.__capabilities
91
+
92
+
93
+ class Capability(GenericObject):
94
+ """
95
+ Represents a capability of a SeatCupra vehicle.
96
+ """
97
+
98
+ def __init__(self, capability_id: str, capabilities: Capabilities) -> None:
99
+ if capabilities is None:
100
+ raise ValueError('Cannot create capability without capabilities')
101
+ if id is None:
102
+ raise ValueError('Capability ID cannot be None')
103
+ super().__init__(object_id=capability_id, parent=capabilities)
104
+ self.delay_notifications = True
105
+ self.capability_id = StringAttribute("id", self, capability_id, tags={'connector_custom'})
106
+ self.expiration_date = DateAttribute("expiration_date", self, tags={'connector_custom'})
107
+ self.editable = BooleanAttribute("editable", self, tags={'connector_custom'})
108
+ self.statuses: List[Capability.Status] = []
109
+ self.parameters: Dict[str, bool] = {}
110
+ self.enabled = True
111
+ self.delay_notifications = False
112
+
113
+ class Status(IntEnum):
114
+ """
115
+ Enum for capability status.
116
+ """
117
+ UNKNOWN = 0
118
+ DEACTIVATED = 1001
119
+ INITIALLY_DISABLED = 1003
120
+ DISABLED_BY_USER = 1004
121
+ OFFLINE_MODE = 1005
122
+ WORKSHOP_MODE = 1006
123
+ MISSING_OPERATION = 1007
124
+ MISSING_SERVICE = 1008
125
+ PLAY_PROTECTION = 1009
126
+ POWER_BUDGET_REACHED = 1010
127
+ DEEP_SLEEP = 1011
128
+ LOCATION_DATA_DISABLED = 1013
129
+ LICENSE_INACTIVE = 2001
130
+ LICENSE_EXPIRED = 2002
131
+ MISSING_LICENSE = 2003
132
+ USER_NOT_VERIFIED = 3001
133
+ TERMS_AND_CONDITIONS_NOT_ACCEPTED = 3002
134
+ INSUFFICIENT_RIGHTS = 3003
135
+ CONSENT_MISSING = 3004
136
+ LIMITED_FEATURE = 3005
137
+ AUTH_APP_CERT_ERROR = 3006
138
+ STATUS_UNSUPPORTED = 4001
@@ -16,11 +16,11 @@ from carconnectivity.errors import AuthenticationError, TooManyRequestsError, Re
16
16
  TemporaryAuthenticationError, SetterError, CommandError
17
17
  from carconnectivity.util import robust_time_parse, log_extra_keys, config_remove_credentials
18
18
  from carconnectivity.units import Length, Power, Speed
19
- from carconnectivity.vehicle import GenericVehicle, ElectricVehicle, CombustionVehicle, HybridVehicle
20
19
  from carconnectivity.doors import Doors
21
20
  from carconnectivity.windows import Windows
22
21
  from carconnectivity.lights import Lights
23
22
  from carconnectivity.drive import GenericDrive, ElectricDrive, CombustionDrive
23
+ from carconnectivity.vehicle import GenericVehicle, ElectricVehicle
24
24
  from carconnectivity.attributes import BooleanAttribute, DurationAttribute, GenericAttribute, TemperatureAttribute
25
25
  from carconnectivity.units import Temperature
26
26
  from carconnectivity.command_impl import ClimatizationStartStopCommand, WakeSleepCommand, HonkAndFlashCommand, LockUnlockCommand, ChargingStartStopCommand
@@ -33,6 +33,8 @@ from carconnectivity_connectors.base.connector import BaseConnector
33
33
  from carconnectivity_connectors.seatcupra.auth.session_manager import SessionManager, SessionUser, Service
34
34
  from carconnectivity_connectors.seatcupra.auth.my_cupra_session import MyCupraSession
35
35
  from carconnectivity_connectors.seatcupra._version import __version__
36
+ from carconnectivity_connectors.seatcupra.capability import Capability
37
+ from carconnectivity_connectors.seatcupra.vehicle import SeatCupraVehicle, SeatCupraElectricVehicle, SeatCupraCombustionVehicle, SeatCupraHybridVehicle
36
38
  from carconnectivity_connectors.seatcupra.charging import SeatCupraCharging, mapping_seatcupra_charging_state
37
39
 
38
40
  SUPPORT_IMAGES = False
@@ -129,7 +131,7 @@ class Connector(BaseConnector):
129
131
 
130
132
  self._manager: SessionManager = SessionManager(tokenstore=car_connectivity.get_tokenstore(), cache=car_connectivity.get_cache())
131
133
  session: requests.Session = self._manager.get_session(Service.MY_CUPRA, SessionUser(username=self.active_config['username'],
132
- password=self.active_config['password']))
134
+ password=self.active_config['password']))
133
135
  if not isinstance(session, MyCupraSession):
134
136
  raise AuthenticationError('Could not create session')
135
137
  self.session: MyCupraSession = session
@@ -238,11 +240,16 @@ class Connector(BaseConnector):
238
240
  garage: Garage = self.car_connectivity.garage
239
241
  for vin in set(garage.list_vehicle_vins()):
240
242
  vehicle_to_update: Optional[GenericVehicle] = garage.get_vehicle(vin)
241
- if vehicle_to_update is not None and vehicle_to_update.is_managed_by_connector(self):
243
+ if vehicle_to_update is not None and vehicle_to_update.is_managed_by_connector(self) and isinstance(vehicle_to_update, SeatCupraVehicle):
242
244
  vehicle_to_update = self.fetch_vehicle_status(vehicle_to_update)
243
245
  vehicle_to_update = self.fetch_vehicle_mycar_status(vehicle_to_update)
244
- # TODO check for parking capability
245
- vehicle_to_update = self.fetch_parking_position(vehicle_to_update)
246
+ vehicle_to_update = self.fetch_mileage(vehicle_to_update)
247
+ if vehicle_to_update.capabilities.has_capability('climatisation'):
248
+ vehicle_to_update = self.fetch_climatisation(vehicle_to_update)
249
+ if vehicle_to_update.capabilities.has_capability('charging'):
250
+ vehicle_to_update = self.fetch_charging(vehicle_to_update)
251
+ if vehicle_to_update.capabilities.has_capability('parkingPosition'):
252
+ vehicle_to_update = self.fetch_parking_position(vehicle_to_update)
246
253
 
247
254
  def fetch_vehicles(self) -> None:
248
255
  """
@@ -262,11 +269,12 @@ class Connector(BaseConnector):
262
269
  if 'vehicles' in data and data['vehicles'] is not None:
263
270
  for vehicle_dict in data['vehicles']:
264
271
  if 'vin' in vehicle_dict and vehicle_dict['vin'] is not None:
265
- seen_vehicle_vins.add(vehicle_dict['vin'])
266
- vehicle: Optional[GenericVehicle] = garage.get_vehicle(vehicle_dict['vin']) # pyright: ignore[reportAssignmentType]
272
+ vin: str = vehicle_dict['vin']
273
+ seen_vehicle_vins.add(vin)
274
+ vehicle: Optional[GenericVehicle] = garage.get_vehicle(vin) # pyright: ignore[reportAssignmentType]
267
275
  if vehicle is None:
268
- vehicle = GenericVehicle(vin=vehicle_dict['vin'], garage=garage, managing_connector=self)
269
- garage.add_vehicle(vehicle_dict['vin'], vehicle)
276
+ vehicle = SeatCupraVehicle(vin=vin, garage=garage, managing_connector=self)
277
+ garage.add_vehicle(vin, vehicle)
270
278
 
271
279
  if 'vehicleNickname' in vehicle_dict and vehicle_dict['vehicleNickname'] is not None:
272
280
  vehicle.name._set_value(vehicle_dict['vehicleNickname']) # pylint: disable=protected-access
@@ -299,11 +307,64 @@ class Connector(BaseConnector):
299
307
  vehicle.model_year._set_value(None) # pylint: disable=protected-access
300
308
  log_extra_keys(LOG_API, 'factoryModel', factory_model, {'vehicleBrand', 'vehicleModel', 'modYear'})
301
309
  log_extra_keys(LOG_API, 'specifications', vehicle_dict['specifications'], {'steeringRight', 'factoryModel'})
302
-
303
310
 
304
- #TODO: https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{{VIN}}/connection
305
-
306
- #TODO: https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{{VIN}}/capabilities
311
+ if isinstance(vehicle, SeatCupraVehicle):
312
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/capabilities'
313
+ capabilities_data: Dict[str, Any] | None = self._fetch_data(url, session=self.session)
314
+ if capabilities_data is not None and 'capabilities' in capabilities_data and capabilities_data['capabilities'] is not None:
315
+ found_capabilities = set()
316
+ for capability_dict in capabilities_data['capabilities']:
317
+ if 'id' in capability_dict and capability_dict['id'] is not None:
318
+ capability_id = capability_dict['id']
319
+ found_capabilities.add(capability_id)
320
+ if vehicle.capabilities.has_capability(capability_id):
321
+ capability: Capability = vehicle.capabilities.get_capability(capability_id) # pyright: ignore[reportAssignmentType]
322
+ else:
323
+ capability = Capability(capability_id=capability_id, capabilities=vehicle.capabilities)
324
+ vehicle.capabilities.add_capability(capability_id, capability)
325
+ if 'expirationDate' in capability_dict and capability_dict['expirationDate'] is not None \
326
+ and capability_dict['expirationDate'] != '':
327
+ expiration_date: datetime = robust_time_parse(capability_dict['expirationDate'])
328
+ capability.expiration_date._set_value(expiration_date) # pylint: disable=protected-access
329
+ else:
330
+ capability.expiration_date._set_value(None) # pylint: disable=protected-access
331
+ if 'editable' in capability_dict and capability_dict['editable'] is not None:
332
+ # pylint: disable-next=protected-access
333
+ capability.editable._set_value(capability_dict['editable'])
334
+ else:
335
+ capability.editable._set_value(None) # pylint: disable=protected-access
336
+ if 'parameters' in capability_dict and capability_dict['parameters'] is not None:
337
+ for parameter, value in capability_dict['parameters'].items():
338
+ capability.parameters[parameter] = value
339
+ else:
340
+ raise APIError('Could not fetch capabilities, capability ID missing')
341
+ log_extra_keys(LOG_API, 'capability', capability_dict, {'id', 'expirationDate', 'editable', 'parameters'})
342
+
343
+ for capability_id in vehicle.capabilities.capabilities.keys() - found_capabilities:
344
+ vehicle.capabilities.remove_capability(capability_id)
345
+
346
+ if vehicle.capabilities.has_capability('charging'):
347
+ if not isinstance(vehicle, SeatCupraElectricVehicle):
348
+ LOG.debug('Promoting %s to SeatCupraElectricVehicle object for %s', vehicle.__class__.__name__, vin)
349
+ vehicle = SeatCupraElectricVehicle(origin=vehicle)
350
+ self.car_connectivity.garage.replace_vehicle(vin, vehicle)
351
+ if not vehicle.charging.commands.contains_command('start-stop'):
352
+ charging_start_stop_command: ChargingStartStopCommand = ChargingStartStopCommand(parent=vehicle.charging.commands)
353
+ charging_start_stop_command._add_on_set_hook(self.__on_charging_start_stop) # pylint: disable=protected-access
354
+ charging_start_stop_command.enabled = True
355
+ vehicle.charging.commands.add_command(charging_start_stop_command)
356
+
357
+ if vehicle.capabilities.has_capability('climatisation'):
358
+ if vehicle.climatization is not None and vehicle.climatization.commands is not None \
359
+ and not vehicle.climatization.commands.contains_command('start-stop'):
360
+ climatisation_start_stop_command: ClimatizationStartStopCommand = \
361
+ ClimatizationStartStopCommand(parent=vehicle.climatization.commands)
362
+ # pylint: disable-next=protected-access
363
+ climatisation_start_stop_command._add_on_set_hook(self.__on_air_conditioning_start_stop)
364
+ climatisation_start_stop_command.enabled = True
365
+ vehicle.climatization.commands.add_command(climatisation_start_stop_command)
366
+ else:
367
+ vehicle.capabilities.clear_capabilities()
307
368
  else:
308
369
  raise APIError('Could not fetch vehicle data, VIN missing')
309
370
  for vin in set(garage.list_vehicle_vins()) - seen_vehicle_vins:
@@ -312,7 +373,7 @@ class Connector(BaseConnector):
312
373
  garage.remove_vehicle(vin)
313
374
  self.update_vehicles()
314
375
 
315
- def fetch_vehicle_status(self, vehicle: GenericVehicle, no_cache: bool = False) -> GenericVehicle:
376
+ def fetch_vehicle_status(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
316
377
  """
317
378
  Fetches the status of a vehicle from seat/cupra API.
318
379
 
@@ -325,6 +386,25 @@ class Connector(BaseConnector):
325
386
  vin = vehicle.vin.value
326
387
  if vin is None:
327
388
  raise APIError('VIN is missing')
389
+
390
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/connection'
391
+ vehicle_connection_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
392
+ if vehicle_connection_data is not None:
393
+ if 'connection' in vehicle_connection_data and vehicle_connection_data['connection'] is not None \
394
+ and 'mode' in vehicle_connection_data['connection'] and vehicle_connection_data['connection']['mode'] is not None:
395
+ if vehicle_connection_data['connection']['mode'] in [item.value for item in GenericVehicle.ConnectionState]:
396
+ connection_state: GenericVehicle.ConnectionState = GenericVehicle.ConnectionState(vehicle_connection_data['connection']['mode'])
397
+ vehicle.connection_state._set_value(connection_state) # pylint: disable=protected-access
398
+ else:
399
+ vehicle.connection_state._set_value(GenericVehicle.ConnectionState.UNKNOWN) # pylint: disable=protected-access
400
+ LOG_API.info('Unknown connection state %s', vehicle_connection_data['connection']['mode'])
401
+ log_extra_keys(LOG_API, f'/api/v2/vehicles/{vin}/connection', vehicle_connection_data, {'connection'})
402
+ log_extra_keys(LOG_API, f'/api/v2/vehicles/{vin}/connection', vehicle_connection_data['connection'], {'mode'})
403
+ else:
404
+ vehicle.connection_state._set_value(None) # pylint: disable=protected-access
405
+ else:
406
+ vehicle.connection_state._set_value(None) # pylint: disable=protected-access
407
+
328
408
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/status'
329
409
  vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
330
410
  if vehicle_status_data:
@@ -401,7 +481,7 @@ class Connector(BaseConnector):
401
481
  else:
402
482
  window = Windows.Window(window_id=window_id, windows=vehicle.windows)
403
483
  vehicle.windows.windows[window_id] = window
404
- if window_status in Windows.OpenState:
484
+ if window_status in [item.value for item in Windows.OpenState]:
405
485
  open_state: Windows.OpenState = Windows.OpenState(window_status)
406
486
  if open_state == Windows.OpenState.OPEN:
407
487
  all_windows_closed = False
@@ -420,8 +500,8 @@ class Connector(BaseConnector):
420
500
  log_extra_keys(LOG_API, f'/api/v2/vehicle-status/{vin}', vehicle_status_data, {'updatedAt', 'locked', 'lights', 'hood', 'trunk', 'doors',
421
501
  'windows'})
422
502
  return vehicle
423
-
424
- def fetch_vehicle_mycar_status(self, vehicle: GenericVehicle, no_cache: bool = False) -> GenericVehicle:
503
+
504
+ def fetch_vehicle_mycar_status(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
425
505
  """
426
506
  Fetches the status of a vehicle from seat/cupra API.
427
507
 
@@ -434,6 +514,11 @@ class Connector(BaseConnector):
434
514
  vin = vehicle.vin.value
435
515
  if vin is None:
436
516
  raise APIError('VIN is missing')
517
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/measurements/engines'
518
+ vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
519
+ #measurements
520
+ #{'primary': {'fuelType': 'gasoline', 'rangeInKm': 120.0}, 'secondary': {'fuelType': 'electric', 'rangeInKm': 40.0}}
521
+ print(vehicle_status_data)
437
522
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v5/users/{self.session.user_id}/vehicles/{vin}/mycar'
438
523
  vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
439
524
  if vehicle_status_data:
@@ -490,17 +575,17 @@ class Connector(BaseConnector):
490
575
  has_electric = True
491
576
  elif isinstance(drive, CombustionDrive):
492
577
  has_combustion = True
493
- if has_electric and not has_combustion and not isinstance(vehicle, ElectricVehicle):
494
- LOG.debug('Promoting %s to ElectricVehicle object for %s', vehicle.__class__.__name__, vin)
495
- vehicle = ElectricVehicle(origin=vehicle)
578
+ if has_electric and not has_combustion and not isinstance(vehicle, SeatCupraElectricVehicle):
579
+ LOG.debug('Promoting %s to SeatCupraElectricVehicle object for %s', vehicle.__class__.__name__, vin)
580
+ vehicle = SeatCupraElectricVehicle(origin=vehicle)
496
581
  self.car_connectivity.garage.replace_vehicle(vin, vehicle)
497
- elif has_combustion and not has_electric and not isinstance(vehicle, CombustionVehicle):
498
- LOG.debug('Promoting %s to CombustionVehicle object for %s', vehicle.__class__.__name__, vin)
499
- vehicle = CombustionVehicle(origin=vehicle)
582
+ elif has_combustion and not has_electric and not isinstance(vehicle, SeatCupraCombustionVehicle):
583
+ LOG.debug('Promoting %s to SeatCupraCombustionVehicle object for %s', vehicle.__class__.__name__, vin)
584
+ vehicle = SeatCupraCombustionVehicle(origin=vehicle)
500
585
  self.car_connectivity.garage.replace_vehicle(vin, vehicle)
501
- elif has_combustion and has_electric and not isinstance(vehicle, HybridVehicle):
502
- LOG.debug('Promoting %s to HybridVehicle object for %s', vehicle.__class__.__name__, vin)
503
- vehicle = HybridVehicle(origin=vehicle)
586
+ elif has_combustion and has_electric and not isinstance(vehicle, SeatCupraHybridVehicle):
587
+ LOG.debug('Promoting %s to SeatCupraHybridVehicle object for %s', vehicle.__class__.__name__, vin)
588
+ vehicle = SeatCupraHybridVehicle(origin=vehicle)
504
589
  self.car_connectivity.garage.replace_vehicle(vin, vehicle)
505
590
  if 'services' in vehicle_status_data and vehicle_status_data['services'] is not None:
506
591
  if 'charging' in vehicle_status_data['services'] and vehicle_status_data['services']['charging'] is not None:
@@ -526,7 +611,7 @@ class Connector(BaseConnector):
526
611
  if isinstance(vehicle, ElectricVehicle):
527
612
  vehicle.charging.settings.target_level._set_value(charging_status['targetPct']) # pylint: disable=protected-access
528
613
  if 'chargeMode' in charging_status and charging_status['chargeMode'] is not None:
529
- if charging_status['chargeMode'] in Charging.ChargingType:
614
+ if charging_status['chargeMode'] in [item.value for item in Charging.ChargingType]:
530
615
  if isinstance(vehicle, ElectricVehicle):
531
616
  vehicle.charging.type._set_value(value=Charging.ChargingType(charging_status['chargeMode'])) # pylint: disable=protected-access
532
617
  else:
@@ -552,7 +637,7 @@ class Connector(BaseConnector):
552
637
  if 'climatisation' in vehicle_status_data['services'] and vehicle_status_data['services']['climatisation'] is not None:
553
638
  climatisation_status: Dict = vehicle_status_data['services']['climatisation']
554
639
  if 'status' in climatisation_status and climatisation_status['status'] is not None:
555
- if climatisation_status['status'].lower() in Climatization.ClimatizationState:
640
+ if climatisation_status['status'].lower() in [item.value for item in Climatization.ClimatizationState]:
556
641
  climatization_state: Climatization.ClimatizationState = Climatization.ClimatizationState(climatisation_status['status'].lower())
557
642
  else:
558
643
  LOG_API.info('Unknown climatization state %s not in %s', climatisation_status['status'],
@@ -575,14 +660,14 @@ class Connector(BaseConnector):
575
660
  remaining_duration: timedelta = timedelta(minutes=climatisation_status['remainingTime'])
576
661
  estimated_date_reached: datetime = datetime.now(tz=timezone.utc) + remaining_duration
577
662
  estimated_date_reached = estimated_date_reached.replace(second=0, microsecond=0)
578
- vehicle.charging.estimated_date_reached._set_value(value=estimated_date_reached) # pylint: disable=protected-access
663
+ vehicle.climatization.estimated_date_reached._set_value(value=estimated_date_reached) # pylint: disable=protected-access
579
664
  else:
580
- vehicle.charging.estimated_date_reached._set_value(None) # pylint: disable=protected-access
665
+ vehicle.climatization.estimated_date_reached._set_value(None) # pylint: disable=protected-access
581
666
  log_extra_keys(LOG_API, 'climatisation', climatisation_status, {'status', 'targetTemperatureCelsius', 'targetTemperatureFahrenheit',
582
667
  'remainingTime'})
583
668
  return vehicle
584
669
 
585
- def fetch_parking_position(self, vehicle: GenericVehicle, no_cache: bool = False) -> GenericVehicle:
670
+ def fetch_parking_position(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
586
671
  """
587
672
  Fetches the position of the given vehicle and updates its position attributes.
588
673
 
@@ -600,7 +685,7 @@ class Connector(BaseConnector):
600
685
  if vin is None:
601
686
  raise APIError('VIN is missing')
602
687
  if vehicle.position is None:
603
- raise ValueError('Vehicle has no charging object')
688
+ raise ValueError('Vehicle has no position object')
604
689
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/parkingposition'
605
690
  data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
606
691
  if data is not None:
@@ -622,6 +707,93 @@ class Connector(BaseConnector):
622
707
  vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
623
708
  return vehicle
624
709
 
710
+ def fetch_mileage(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
711
+ """
712
+ Fetches the mileage of the given vehicle and updates its mileage attributes.
713
+
714
+ Args:
715
+ vehicle (SkodaVehicle): The vehicle object containing the VIN and mileage attributes.
716
+
717
+ Returns:
718
+ SkodaVehicle: The updated vehicle object with the fetched mileage data.
719
+
720
+ Raises:
721
+ APIError: If the VIN is missing.
722
+ ValueError: If the vehicle has no position object.
723
+ """
724
+ vin = vehicle.vin.value
725
+ if vin is None:
726
+ raise APIError('VIN is missing')
727
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/mileage'
728
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
729
+ if data is not None:
730
+ if 'mileageKm' in data and data['mileageKm'] is not None:
731
+ vehicle.odometer._set_value(data['mileageKm'], unit=Length.KM) # pylint: disable=protected-access
732
+ else:
733
+ vehicle.odometer._set_value(None) # pylint: disable=protected-access
734
+ log_extra_keys(LOG_API, f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/mileage', data, {'mileageKm'})
735
+ else:
736
+ vehicle.odometer._set_value(None) # pylint: disable=protected-access
737
+ return vehicle
738
+
739
+ def fetch_climatisation(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
740
+ """
741
+ Fetches the mileage of the given vehicle and updates its mileage attributes.
742
+
743
+ Args:
744
+ vehicle (SkodaVehicle): The vehicle object containing the VIN and mileage attributes.
745
+
746
+ Returns:
747
+ SkodaVehicle: The updated vehicle object with the fetched mileage data.
748
+
749
+ Raises:
750
+ APIError: If the VIN is missing.
751
+ ValueError: If the vehicle has no position object.
752
+ """
753
+ vin = vehicle.vin.value
754
+ if vin is None:
755
+ raise APIError('VIN is missing')
756
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/climatisation/status'
757
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
758
+ #{'climatisationStatus': {'carCapturedTimestamp': '2025-02-18T17:24:02Z', 'climatisationState': 'off', 'climatisationTrigger': 'unsupported'}, 'windowHeatingStatus': {'carCapturedTimestamp': '2025-02-18T16:57:51Z', 'windowHeatingStatus': [{'windowLocation': 'front', 'windowHeatingState': 'off'}, {'windowLocation': 'rear', 'windowHeatingState': 'off'}]}}
759
+ print(data)
760
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/climatisation/settings'
761
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
762
+ #{'carCapturedTimestamp': '2025-02-18T17:23:47Z', 'climatisationWithoutExternalPower': True, 'targetTemperatureInCelsius': 22.0, 'targetTemperatureInFahrenheit': 72.0}
763
+ print(data)
764
+ return vehicle
765
+
766
+ def fetch_charging(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
767
+ """
768
+ Fetches the mileage of the given vehicle and updates its mileage attributes.
769
+
770
+ Args:
771
+ vehicle (SkodaVehicle): The vehicle object containing the VIN and mileage attributes.
772
+
773
+ Returns:
774
+ SkodaVehicle: The updated vehicle object with the fetched mileage data.
775
+
776
+ Raises:
777
+ APIError: If the VIN is missing.
778
+ ValueError: If the vehicle has no position object.
779
+ """
780
+ vin = vehicle.vin.value
781
+ if vin is None:
782
+ raise APIError('VIN is missing')
783
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/charging/status'
784
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
785
+ #{'state': 'off', 'battery': {'currentSocPercentage': 86, 'estimatedRangeInKm': 40}, 'charging': {'state': 'notReadyForCharging', 'type': 'off', 'mode': 'invalid'}, 'plug': {'connection': 'disconnected', 'externalPower': 'unavailable', 'lock': 'unlocked'}}
786
+ print(data)
787
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/charging/info'
788
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
789
+ #{'settings': {'maxChargeCurrentAc': 'reduced', 'targetSoc': 100}, 'chargingCareSettings': {}, 'chargingCareStatus': {'batteryCareTargetSoc': 80}}
790
+ print(data)
791
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/charging/settings'
792
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
793
+ #{'maxChargeCurrentAc': False, 'defaultMaxTargetSocPercentage': 100}
794
+ print(data)
795
+ return vehicle
796
+
625
797
  def _record_elapsed(self, elapsed: timedelta) -> None:
626
798
  """
627
799
  Records the elapsed time.
@@ -679,6 +851,62 @@ class Connector(BaseConnector):
679
851
  raise RetrievalError(f'JSON decode error: {json_error}') from json_error
680
852
  return data
681
853
 
854
+ def __on_charging_start_stop(self, start_stop_command: ChargingStartStopCommand, command_arguments: Union[str, Dict[str, Any]]) \
855
+ -> Union[str, Dict[str, Any]]:
856
+ if start_stop_command.parent is None or start_stop_command.parent.parent is None \
857
+ or start_stop_command.parent.parent.parent is None or not isinstance(start_stop_command.parent.parent.parent, SeatCupraVehicle):
858
+ raise CommandError('Object hierarchy is not as expected')
859
+ if not isinstance(command_arguments, dict):
860
+ raise CommandError('Command arguments are not a dictionary')
861
+ vehicle: SeatCupraVehicle = start_stop_command.parent.parent.parent
862
+ vin: Optional[str] = vehicle.vin.value
863
+ if vin is None:
864
+ raise CommandError('VIN in object hierarchy missing')
865
+ if 'command' not in command_arguments:
866
+ raise CommandError('Command argument missing')
867
+ if command_arguments['command'] == ChargingStartStopCommand.Command.START:
868
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/charging/requests/start'
869
+ command_response: requests.Response = self.session.post(url, data='{}', allow_redirects=True)
870
+ elif command_arguments['command'] == ChargingStartStopCommand.Command.STOP:
871
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/charging/requests/stop'
872
+ command_response: requests.Response = self.session.post(url, data='{}', allow_redirects=True)
873
+ else:
874
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
875
+
876
+ if command_response.status_code not in [requests.codes['ok'], requests.codes['created']]:
877
+ LOG.error('Could not start/stop charging (%s: %s)', command_response.status_code, command_response.text)
878
+ raise CommandError(f'Could not start/stop charging ({command_response.status_code}: {command_response.text})')
879
+ return command_arguments
880
+
881
+ def __on_air_conditioning_start_stop(self, start_stop_command: ClimatizationStartStopCommand, command_arguments: Union[str, Dict[str, Any]]) \
882
+ -> Union[str, Dict[str, Any]]:
883
+ if start_stop_command.parent is None or start_stop_command.parent.parent is None \
884
+ or start_stop_command.parent.parent.parent is None or not isinstance(start_stop_command.parent.parent.parent, SeatCupraVehicle):
885
+ raise CommandError('Object hierarchy is not as expected')
886
+ if not isinstance(command_arguments, dict):
887
+ raise CommandError('Command arguments are not a dictionary')
888
+ vehicle: SeatCupraVehicle = start_stop_command.parent.parent.parent
889
+ vin: Optional[str] = vehicle.vin.value
890
+ if vin is None:
891
+ raise CommandError('VIN in object hierarchy missing')
892
+ if 'command' not in command_arguments:
893
+ raise CommandError('Command argument missing')
894
+ command_dict = {}
895
+ command_str: Optional[str] = None
896
+ if command_arguments['command'] == ClimatizationStartStopCommand.Command.START:
897
+ command_str = 'start'
898
+ elif command_arguments['command'] == ClimatizationStartStopCommand.Command.STOP:
899
+ command_str = 'stop'
900
+ else:
901
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
902
+
903
+ url: str = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/climatisation/requests/{command_str}'
904
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
905
+ if command_response.status_code not in [requests.codes['ok'], requests.codes['created']]:
906
+ LOG.error('Could not start/stop air conditioning (%s: %s)', command_response.status_code, command_response.text)
907
+ raise CommandError(f'Could not start/stop air conditioning ({command_response.status_code}: {command_response.text})')
908
+ return command_arguments
909
+
682
910
  def get_version(self) -> str:
683
911
  return __version__
684
912
 
@@ -0,0 +1,82 @@
1
+ """Module for vehicle classes."""
2
+ from __future__ import annotations
3
+ from typing import TYPE_CHECKING
4
+
5
+ from carconnectivity.vehicle import GenericVehicle, ElectricVehicle, CombustionVehicle, HybridVehicle
6
+
7
+ from carconnectivity_connectors.seatcupra.capability import Capabilities
8
+
9
+ SUPPORT_IMAGES = False
10
+ try:
11
+ from PIL import Image
12
+ SUPPORT_IMAGES = True
13
+ except ImportError:
14
+ pass
15
+
16
+ if TYPE_CHECKING:
17
+ from typing import Optional, Dict
18
+ from carconnectivity.garage import Garage
19
+ from carconnectivity_connectors.base.connector import BaseConnector
20
+
21
+
22
+ class SeatCupraVehicle(GenericVehicle): # pylint: disable=too-many-instance-attributes
23
+ """
24
+ A class to represent a generic Seat/Cupra vehicle.
25
+
26
+ Attributes:
27
+ -----------
28
+ vin : StringAttribute
29
+ The vehicle identification number (VIN) of the vehicle.
30
+ license_plate : StringAttribute
31
+ The license plate of the vehicle.
32
+ """
33
+ def __init__(self, vin: Optional[str] = None, garage: Optional[Garage] = None, managing_connector: Optional[BaseConnector] = None,
34
+ origin: Optional[SeatCupraVehicle] = None) -> None:
35
+ if origin is not None:
36
+ super().__init__(origin=origin)
37
+ self.capabilities: Capabilities = origin.capabilities
38
+ self.capabilities.parent = self
39
+ if SUPPORT_IMAGES:
40
+ self._car_images = origin._car_images
41
+
42
+ else:
43
+ super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)
44
+ self.capabilities: Capabilities = Capabilities(vehicle=self)
45
+ if SUPPORT_IMAGES:
46
+ self._car_images: Dict[str, Image.Image] = {}
47
+
48
+
49
+ class SeatCupraElectricVehicle(ElectricVehicle, SeatCupraVehicle):
50
+ """
51
+ Represents a Seat/Cupra electric vehicle.
52
+ """
53
+ def __init__(self, vin: Optional[str] = None, garage: Optional[Garage] = None, managing_connector: Optional[BaseConnector] = None,
54
+ origin: Optional[SeatCupraVehicle] = None) -> None:
55
+ if origin is not None:
56
+ super().__init__(origin=origin)
57
+ else:
58
+ super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)
59
+
60
+
61
+ class SeatCupraCombustionVehicle(CombustionVehicle, SeatCupraVehicle):
62
+ """
63
+ Represents a Seat/Cupra combustion vehicle.
64
+ """
65
+ def __init__(self, vin: Optional[str] = None, garage: Optional[Garage] = None, managing_connector: Optional[BaseConnector] = None,
66
+ origin: Optional[SeatCupraVehicle] = None) -> None:
67
+ if origin is not None:
68
+ super().__init__(origin=origin)
69
+ else:
70
+ super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)
71
+
72
+
73
+ class SeatCupraHybridVehicle(HybridVehicle, SeatCupraVehicle):
74
+ """
75
+ Represents a Seat/Cupra hybrid vehicle.
76
+ """
77
+ def __init__(self, vin: Optional[str] = None, garage: Optional[Garage] = None, managing_connector: Optional[BaseConnector] = None,
78
+ origin: Optional[SeatCupraVehicle] = None) -> None:
79
+ if origin is not None:
80
+ super().__init__(origin=origin)
81
+ else:
82
+ super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)