carconnectivity-connector-seatcupra 0.1a1__py3-none-any.whl → 0.1a3__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: carconnectivity-connector-seatcupra
3
- Version: 0.1a1
3
+ Version: 0.1a3
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
@@ -1,7 +1,9 @@
1
1
  carconnectivity_connectors/seatcupra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- carconnectivity_connectors/seatcupra/_version.py,sha256=_BIT6nGqXqfHgeHw9NwtWXgZnV8T_7fSo6lEqHbnf88,508
2
+ carconnectivity_connectors/seatcupra/_version.py,sha256=xpGveoYLp11VothNw4ctnawA60va2R4V5tLdgWtxWZ0,508
3
+ carconnectivity_connectors/seatcupra/capability.py,sha256=Oe9tC_u69bj6VmOuNJ21RKoETe2j3QyZCoz-VgcZPQ0,4523
3
4
  carconnectivity_connectors/seatcupra/charging.py,sha256=kcCJJddZxUXFoayYMpq3lzdnrPp5yexnGBfDB-zQrmE,3336
4
- carconnectivity_connectors/seatcupra/connector.py,sha256=nyfIjft7pXPfQojU93BfNdXQ-fSmU7DFh9Xqw4ujfd4,44278
5
+ carconnectivity_connectors/seatcupra/connector.py,sha256=U4j5fCJYfVCb_5aBDp9m2F8swDL3Vz63CW-jrtIWiSw,66248
6
+ carconnectivity_connectors/seatcupra/vehicle.py,sha256=O6_JNTSwl82EZIix_DNf8eTj4A1rxPwTO1IeN3daAR8,3233
5
7
  carconnectivity_connectors/seatcupra/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
8
  carconnectivity_connectors/seatcupra/auth/auth_util.py,sha256=Y81h8fGOMSMgPtE4wI_TI9WgE_s43uaPjRLBBINhj4g,4433
7
9
  carconnectivity_connectors/seatcupra/auth/my_cupra_session.py,sha256=iK5SlankZqaeneC3SNad8nHHcrP0tTmYxToI_9cqwlo,10744
@@ -10,8 +12,8 @@ carconnectivity_connectors/seatcupra/auth/session_manager.py,sha256=NizIuY-pvkVB
10
12
  carconnectivity_connectors/seatcupra/auth/vw_web_session.py,sha256=hgsCdXugVnSgvLta4hBNtoNgMhAA83paAYO2fUOOFyM,10657
11
13
  carconnectivity_connectors/seatcupra/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
12
14
  carconnectivity_connectors/seatcupra/ui/connector_ui.py,sha256=SNYnlcGJpbWhuLiIHD2l6H9IfSiMz3IgmvXsdossDnE,1412
13
- carconnectivity_connector_seatcupra-0.1a1.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
14
- carconnectivity_connector_seatcupra-0.1a1.dist-info/METADATA,sha256=eG-9qfRLgK8bseYbWLxo_brIPtu02IrgXjW5ZiimCc0,5384
15
- carconnectivity_connector_seatcupra-0.1a1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
16
- carconnectivity_connector_seatcupra-0.1a1.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
17
- carconnectivity_connector_seatcupra-0.1a1.dist-info/RECORD,,
15
+ carconnectivity_connector_seatcupra-0.1a3.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
16
+ carconnectivity_connector_seatcupra-0.1a3.dist-info/METADATA,sha256=JuFFjlEF1M3633J-O80Mi-anX0iHHznBaBLuZTBWkbo,5386
17
+ carconnectivity_connector_seatcupra-0.1a3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
18
+ carconnectivity_connector_seatcupra-0.1a3.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
19
+ carconnectivity_connector_seatcupra-0.1a3.dist-info/RECORD,,
@@ -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.1a3'
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,66 @@ 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()
368
+ if isinstance(vehicle, SeatCupraVehicle):
369
+ vehicle = self.fetch_image(vehicle)
307
370
  else:
308
371
  raise APIError('Could not fetch vehicle data, VIN missing')
309
372
  for vin in set(garage.list_vehicle_vins()) - seen_vehicle_vins:
@@ -312,7 +375,7 @@ class Connector(BaseConnector):
312
375
  garage.remove_vehicle(vin)
313
376
  self.update_vehicles()
314
377
 
315
- def fetch_vehicle_status(self, vehicle: GenericVehicle, no_cache: bool = False) -> GenericVehicle:
378
+ def fetch_vehicle_status(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
316
379
  """
317
380
  Fetches the status of a vehicle from seat/cupra API.
318
381
 
@@ -325,6 +388,25 @@ class Connector(BaseConnector):
325
388
  vin = vehicle.vin.value
326
389
  if vin is None:
327
390
  raise APIError('VIN is missing')
391
+
392
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/connection'
393
+ vehicle_connection_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
394
+ if vehicle_connection_data is not None:
395
+ if 'connection' in vehicle_connection_data and vehicle_connection_data['connection'] is not None \
396
+ and 'mode' in vehicle_connection_data['connection'] and vehicle_connection_data['connection']['mode'] is not None:
397
+ if vehicle_connection_data['connection']['mode'] in [item.value for item in GenericVehicle.ConnectionState]:
398
+ connection_state: GenericVehicle.ConnectionState = GenericVehicle.ConnectionState(vehicle_connection_data['connection']['mode'])
399
+ vehicle.connection_state._set_value(connection_state) # pylint: disable=protected-access
400
+ else:
401
+ vehicle.connection_state._set_value(GenericVehicle.ConnectionState.UNKNOWN) # pylint: disable=protected-access
402
+ LOG_API.info('Unknown connection state %s', vehicle_connection_data['connection']['mode'])
403
+ log_extra_keys(LOG_API, f'/api/v2/vehicles/{vin}/connection', vehicle_connection_data, {'connection'})
404
+ log_extra_keys(LOG_API, f'/api/v2/vehicles/{vin}/connection', vehicle_connection_data['connection'], {'mode'})
405
+ else:
406
+ vehicle.connection_state._set_value(None) # pylint: disable=protected-access
407
+ else:
408
+ vehicle.connection_state._set_value(None) # pylint: disable=protected-access
409
+
328
410
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/status'
329
411
  vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
330
412
  if vehicle_status_data:
@@ -401,7 +483,7 @@ class Connector(BaseConnector):
401
483
  else:
402
484
  window = Windows.Window(window_id=window_id, windows=vehicle.windows)
403
485
  vehicle.windows.windows[window_id] = window
404
- if window_status in Windows.OpenState:
486
+ if window_status in [item.value for item in Windows.OpenState]:
405
487
  open_state: Windows.OpenState = Windows.OpenState(window_status)
406
488
  if open_state == Windows.OpenState.OPEN:
407
489
  all_windows_closed = False
@@ -420,8 +502,8 @@ class Connector(BaseConnector):
420
502
  log_extra_keys(LOG_API, f'/api/v2/vehicle-status/{vin}', vehicle_status_data, {'updatedAt', 'locked', 'lights', 'hood', 'trunk', 'doors',
421
503
  'windows'})
422
504
  return vehicle
423
-
424
- def fetch_vehicle_mycar_status(self, vehicle: GenericVehicle, no_cache: bool = False) -> GenericVehicle:
505
+
506
+ def fetch_vehicle_mycar_status(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
425
507
  """
426
508
  Fetches the status of a vehicle from seat/cupra API.
427
509
 
@@ -434,6 +516,11 @@ class Connector(BaseConnector):
434
516
  vin = vehicle.vin.value
435
517
  if vin is None:
436
518
  raise APIError('VIN is missing')
519
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/measurements/engines'
520
+ vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
521
+ #measurements
522
+ #{'primary': {'fuelType': 'gasoline', 'rangeInKm': 120.0}, 'secondary': {'fuelType': 'electric', 'rangeInKm': 40.0}}
523
+ print(vehicle_status_data)
437
524
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v5/users/{self.session.user_id}/vehicles/{vin}/mycar'
438
525
  vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
439
526
  if vehicle_status_data:
@@ -490,17 +577,17 @@ class Connector(BaseConnector):
490
577
  has_electric = True
491
578
  elif isinstance(drive, CombustionDrive):
492
579
  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)
580
+ if has_electric and not has_combustion and not isinstance(vehicle, SeatCupraElectricVehicle):
581
+ LOG.debug('Promoting %s to SeatCupraElectricVehicle object for %s', vehicle.__class__.__name__, vin)
582
+ vehicle = SeatCupraElectricVehicle(origin=vehicle)
496
583
  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)
584
+ elif has_combustion and not has_electric and not isinstance(vehicle, SeatCupraCombustionVehicle):
585
+ LOG.debug('Promoting %s to SeatCupraCombustionVehicle object for %s', vehicle.__class__.__name__, vin)
586
+ vehicle = SeatCupraCombustionVehicle(origin=vehicle)
500
587
  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)
588
+ elif has_combustion and has_electric and not isinstance(vehicle, SeatCupraHybridVehicle):
589
+ LOG.debug('Promoting %s to SeatCupraHybridVehicle object for %s', vehicle.__class__.__name__, vin)
590
+ vehicle = SeatCupraHybridVehicle(origin=vehicle)
504
591
  self.car_connectivity.garage.replace_vehicle(vin, vehicle)
505
592
  if 'services' in vehicle_status_data and vehicle_status_data['services'] is not None:
506
593
  if 'charging' in vehicle_status_data['services'] and vehicle_status_data['services']['charging'] is not None:
@@ -526,7 +613,7 @@ class Connector(BaseConnector):
526
613
  if isinstance(vehicle, ElectricVehicle):
527
614
  vehicle.charging.settings.target_level._set_value(charging_status['targetPct']) # pylint: disable=protected-access
528
615
  if 'chargeMode' in charging_status and charging_status['chargeMode'] is not None:
529
- if charging_status['chargeMode'] in Charging.ChargingType:
616
+ if charging_status['chargeMode'] in [item.value for item in Charging.ChargingType]:
530
617
  if isinstance(vehicle, ElectricVehicle):
531
618
  vehicle.charging.type._set_value(value=Charging.ChargingType(charging_status['chargeMode'])) # pylint: disable=protected-access
532
619
  else:
@@ -552,7 +639,7 @@ class Connector(BaseConnector):
552
639
  if 'climatisation' in vehicle_status_data['services'] and vehicle_status_data['services']['climatisation'] is not None:
553
640
  climatisation_status: Dict = vehicle_status_data['services']['climatisation']
554
641
  if 'status' in climatisation_status and climatisation_status['status'] is not None:
555
- if climatisation_status['status'].lower() in Climatization.ClimatizationState:
642
+ if climatisation_status['status'].lower() in [item.value for item in Climatization.ClimatizationState]:
556
643
  climatization_state: Climatization.ClimatizationState = Climatization.ClimatizationState(climatisation_status['status'].lower())
557
644
  else:
558
645
  LOG_API.info('Unknown climatization state %s not in %s', climatisation_status['status'],
@@ -575,14 +662,14 @@ class Connector(BaseConnector):
575
662
  remaining_duration: timedelta = timedelta(minutes=climatisation_status['remainingTime'])
576
663
  estimated_date_reached: datetime = datetime.now(tz=timezone.utc) + remaining_duration
577
664
  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
665
+ vehicle.climatization.estimated_date_reached._set_value(value=estimated_date_reached) # pylint: disable=protected-access
579
666
  else:
580
- vehicle.charging.estimated_date_reached._set_value(None) # pylint: disable=protected-access
667
+ vehicle.climatization.estimated_date_reached._set_value(None) # pylint: disable=protected-access
581
668
  log_extra_keys(LOG_API, 'climatisation', climatisation_status, {'status', 'targetTemperatureCelsius', 'targetTemperatureFahrenheit',
582
669
  'remainingTime'})
583
670
  return vehicle
584
671
 
585
- def fetch_parking_position(self, vehicle: GenericVehicle, no_cache: bool = False) -> GenericVehicle:
672
+ def fetch_parking_position(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
586
673
  """
587
674
  Fetches the position of the given vehicle and updates its position attributes.
588
675
 
@@ -600,7 +687,7 @@ class Connector(BaseConnector):
600
687
  if vin is None:
601
688
  raise APIError('VIN is missing')
602
689
  if vehicle.position is None:
603
- raise ValueError('Vehicle has no charging object')
690
+ raise ValueError('Vehicle has no position object')
604
691
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/parkingposition'
605
692
  data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
606
693
  if data is not None:
@@ -622,6 +709,148 @@ class Connector(BaseConnector):
622
709
  vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
623
710
  return vehicle
624
711
 
712
+ def fetch_mileage(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
713
+ """
714
+ Fetches the mileage of the given vehicle and updates its mileage attributes.
715
+
716
+ Args:
717
+ vehicle (SkodaVehicle): The vehicle object containing the VIN and mileage attributes.
718
+
719
+ Returns:
720
+ SkodaVehicle: The updated vehicle object with the fetched mileage data.
721
+
722
+ Raises:
723
+ APIError: If the VIN is missing.
724
+ ValueError: If the vehicle has no position object.
725
+ """
726
+ vin = vehicle.vin.value
727
+ if vin is None:
728
+ raise APIError('VIN is missing')
729
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/mileage'
730
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
731
+ if data is not None:
732
+ if 'mileageKm' in data and data['mileageKm'] is not None:
733
+ vehicle.odometer._set_value(data['mileageKm'], unit=Length.KM) # pylint: disable=protected-access
734
+ else:
735
+ vehicle.odometer._set_value(None) # pylint: disable=protected-access
736
+ log_extra_keys(LOG_API, f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/mileage', data, {'mileageKm'})
737
+ else:
738
+ vehicle.odometer._set_value(None) # pylint: disable=protected-access
739
+ return vehicle
740
+
741
+ def fetch_climatisation(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
742
+ """
743
+ Fetches the mileage of the given vehicle and updates its mileage attributes.
744
+
745
+ Args:
746
+ vehicle (SkodaVehicle): The vehicle object containing the VIN and mileage attributes.
747
+
748
+ Returns:
749
+ SkodaVehicle: The updated vehicle object with the fetched mileage data.
750
+
751
+ Raises:
752
+ APIError: If the VIN is missing.
753
+ ValueError: If the vehicle has no position object.
754
+ """
755
+ vin = vehicle.vin.value
756
+ if vin is None:
757
+ raise APIError('VIN is missing')
758
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/climatisation/status'
759
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
760
+ #{'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'}]}}
761
+ print(data)
762
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/climatisation/settings'
763
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
764
+ #{'carCapturedTimestamp': '2025-02-18T17:23:47Z', 'climatisationWithoutExternalPower': True, 'targetTemperatureInCelsius': 22.0, 'targetTemperatureInFahrenheit': 72.0}
765
+ print(data)
766
+ return vehicle
767
+
768
+ def fetch_charging(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
769
+ """
770
+ Fetches the mileage of the given vehicle and updates its mileage attributes.
771
+
772
+ Args:
773
+ vehicle (SkodaVehicle): The vehicle object containing the VIN and mileage attributes.
774
+
775
+ Returns:
776
+ SkodaVehicle: The updated vehicle object with the fetched mileage data.
777
+
778
+ Raises:
779
+ APIError: If the VIN is missing.
780
+ ValueError: If the vehicle has no position object.
781
+ """
782
+ vin = vehicle.vin.value
783
+ if vin is None:
784
+ raise APIError('VIN is missing')
785
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/charging/status'
786
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
787
+ #{'state': 'off', 'battery': {'currentSocPercentage': 86, 'estimatedRangeInKm': 40}, 'charging': {'state': 'notReadyForCharging', 'type': 'off', 'mode': 'invalid'}, 'plug': {'connection': 'disconnected', 'externalPower': 'unavailable', 'lock': 'unlocked'}}
788
+ print(data)
789
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/charging/info'
790
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
791
+ #{'settings': {'maxChargeCurrentAc': 'reduced', 'targetSoc': 100}, 'chargingCareSettings': {}, 'chargingCareStatus': {'batteryCareTargetSoc': 80}}
792
+ print(data)
793
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/charging/settings'
794
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
795
+ #{'maxChargeCurrentAc': False, 'defaultMaxTargetSocPercentage': 100}
796
+ print(data)
797
+ return vehicle
798
+
799
+ def fetch_image(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
800
+ if SUPPORT_IMAGES:
801
+ url: str = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vehicle.vin.value}/renders'
802
+ data = self._fetch_data(url, session=self.session, allow_http_error=True)
803
+ if data is not None: # pylint: disable=too-many-nested-blocks
804
+ for image_id, image_url in data.items():
805
+ if image_id == 'isDefault':
806
+ continue
807
+ img = None
808
+ cache_date = None
809
+ if self.active_config['max_age'] is not None and self.session.cache is not None and image_url in self.session.cache:
810
+ img, cache_date_string = self.session.cache[image_url]
811
+ img = base64.b64decode(img) # pyright: ignore[reportPossiblyUnboundVariable]
812
+ img = Image.open(io.BytesIO(img)) # pyright: ignore[reportPossiblyUnboundVariable]
813
+ cache_date = datetime.fromisoformat(cache_date_string)
814
+ if img is None or self.active_config['max_age'] is None \
815
+ or (cache_date is not None and cache_date < (datetime.utcnow() - timedelta(seconds=self.active_config['max_age']))):
816
+ try:
817
+ image_download_response = requests.get(image_url, stream=True)
818
+ if image_download_response.status_code == requests.codes['ok']:
819
+ img = Image.open(image_download_response.raw) # pyright: ignore[reportPossiblyUnboundVariable]
820
+ if self.session.cache is not None:
821
+ buffered = io.BytesIO() # pyright: ignore[reportPossiblyUnboundVariable]
822
+ img.save(buffered, format="PNG")
823
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8") # pyright: ignore[reportPossiblyUnboundVariable]
824
+ self.session.cache[image_url] = (img_str, str(datetime.utcnow()))
825
+ elif image_download_response.status_code == requests.codes['unauthorized']:
826
+ LOG.info('Server asks for new authorization')
827
+ self.session.login()
828
+ image_download_response = self.session.get(image_url, stream=True)
829
+ if image_download_response.status_code == requests.codes['ok']:
830
+ img = Image.open(image_download_response.raw) # pyright: ignore[reportPossiblyUnboundVariable]
831
+ if self.session.cache is not None:
832
+ buffered = io.BytesIO() # pyright: ignore[reportPossiblyUnboundVariable]
833
+ img.save(buffered, format="PNG")
834
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8") # pyright: ignore[reportPossiblyUnboundVariable]
835
+ self.session.cache[image_url] = (img_str, str(datetime.utcnow()))
836
+ except requests.exceptions.ConnectionError as connection_error:
837
+ raise RetrievalError(f'Connection error: {connection_error}') from connection_error
838
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
839
+ raise RetrievalError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
840
+ except requests.exceptions.ReadTimeout as timeout_error:
841
+ raise RetrievalError(f'Timeout during read: {timeout_error}') from timeout_error
842
+ except requests.exceptions.RetryError as retry_error:
843
+ raise RetrievalError(f'Retrying failed: {retry_error}') from retry_error
844
+ if img is not None:
845
+ vehicle._car_images[image_id] = img # pylint: disable=protected-access
846
+ if image_id == 'side':
847
+ if 'car_picture' in vehicle.images.images:
848
+ vehicle.images.images['car_picture']._set_value(img) # pylint: disable=protected-access
849
+ else:
850
+ vehicle.images.images['car_picture'] = ImageAttribute(name="car_picture", parent=vehicle.images,
851
+ value=img, tags={'carconnectivity'})
852
+ return vehicle
853
+
625
854
  def _record_elapsed(self, elapsed: timedelta) -> None:
626
855
  """
627
856
  Records the elapsed time.
@@ -679,6 +908,62 @@ class Connector(BaseConnector):
679
908
  raise RetrievalError(f'JSON decode error: {json_error}') from json_error
680
909
  return data
681
910
 
911
+ def __on_charging_start_stop(self, start_stop_command: ChargingStartStopCommand, command_arguments: Union[str, Dict[str, Any]]) \
912
+ -> Union[str, Dict[str, Any]]:
913
+ if start_stop_command.parent is None or start_stop_command.parent.parent is None \
914
+ or start_stop_command.parent.parent.parent is None or not isinstance(start_stop_command.parent.parent.parent, SeatCupraVehicle):
915
+ raise CommandError('Object hierarchy is not as expected')
916
+ if not isinstance(command_arguments, dict):
917
+ raise CommandError('Command arguments are not a dictionary')
918
+ vehicle: SeatCupraVehicle = start_stop_command.parent.parent.parent
919
+ vin: Optional[str] = vehicle.vin.value
920
+ if vin is None:
921
+ raise CommandError('VIN in object hierarchy missing')
922
+ if 'command' not in command_arguments:
923
+ raise CommandError('Command argument missing')
924
+ if command_arguments['command'] == ChargingStartStopCommand.Command.START:
925
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/charging/requests/start'
926
+ command_response: requests.Response = self.session.post(url, data='{}', allow_redirects=True)
927
+ elif command_arguments['command'] == ChargingStartStopCommand.Command.STOP:
928
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/charging/requests/stop'
929
+ command_response: requests.Response = self.session.post(url, data='{}', allow_redirects=True)
930
+ else:
931
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
932
+
933
+ if command_response.status_code not in [requests.codes['ok'], requests.codes['created']]:
934
+ LOG.error('Could not start/stop charging (%s: %s)', command_response.status_code, command_response.text)
935
+ raise CommandError(f'Could not start/stop charging ({command_response.status_code}: {command_response.text})')
936
+ return command_arguments
937
+
938
+ def __on_air_conditioning_start_stop(self, start_stop_command: ClimatizationStartStopCommand, command_arguments: Union[str, Dict[str, Any]]) \
939
+ -> Union[str, Dict[str, Any]]:
940
+ if start_stop_command.parent is None or start_stop_command.parent.parent is None \
941
+ or start_stop_command.parent.parent.parent is None or not isinstance(start_stop_command.parent.parent.parent, SeatCupraVehicle):
942
+ raise CommandError('Object hierarchy is not as expected')
943
+ if not isinstance(command_arguments, dict):
944
+ raise CommandError('Command arguments are not a dictionary')
945
+ vehicle: SeatCupraVehicle = start_stop_command.parent.parent.parent
946
+ vin: Optional[str] = vehicle.vin.value
947
+ if vin is None:
948
+ raise CommandError('VIN in object hierarchy missing')
949
+ if 'command' not in command_arguments:
950
+ raise CommandError('Command argument missing')
951
+ command_dict = {}
952
+ command_str: Optional[str] = None
953
+ if command_arguments['command'] == ClimatizationStartStopCommand.Command.START:
954
+ command_str = 'start'
955
+ elif command_arguments['command'] == ClimatizationStartStopCommand.Command.STOP:
956
+ command_str = 'stop'
957
+ else:
958
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
959
+
960
+ url: str = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/climatisation/requests/{command_str}'
961
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
962
+ if command_response.status_code not in [requests.codes['ok'], requests.codes['created']]:
963
+ LOG.error('Could not start/stop air conditioning (%s: %s)', command_response.status_code, command_response.text)
964
+ raise CommandError(f'Could not start/stop air conditioning ({command_response.status_code}: {command_response.text})')
965
+ return command_arguments
966
+
682
967
  def get_version(self) -> str:
683
968
  return __version__
684
969
 
@@ -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)