carconnectivity-connector-seatcupra 0.1a2__py3-none-any.whl → 0.1a4__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
1
  Metadata-Version: 2.2
2
2
  Name: carconnectivity-connector-seatcupra
3
- Version: 0.1a2
3
+ Version: 0.1a4
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.4a1
40
+ Requires-Dist: carconnectivity>=0.4a2
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,9 +1,11 @@
1
1
  carconnectivity_connectors/seatcupra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- carconnectivity_connectors/seatcupra/_version.py,sha256=Ef3bGV58pr38At7Z3T5w6O9Yfnm9fPuFoS1_rJQZJLs,508
2
+ carconnectivity_connectors/seatcupra/_version.py,sha256=XHqmG-GhsB57W0JySzM4ri39xqaz6Dslz1JUg-rSYsA,508
3
3
  carconnectivity_connectors/seatcupra/capability.py,sha256=Oe9tC_u69bj6VmOuNJ21RKoETe2j3QyZCoz-VgcZPQ0,4523
4
- carconnectivity_connectors/seatcupra/charging.py,sha256=kcCJJddZxUXFoayYMpq3lzdnrPp5yexnGBfDB-zQrmE,3336
5
- carconnectivity_connectors/seatcupra/connector.py,sha256=0M_gF9QiKQoiOUwRbFXcoOvVZCS4VrxT65W90l6KZV0,61402
6
- carconnectivity_connectors/seatcupra/vehicle.py,sha256=O6_JNTSwl82EZIix_DNf8eTj4A1rxPwTO1IeN3daAR8,3233
4
+ carconnectivity_connectors/seatcupra/charging.py,sha256=BJe_5GEB0JkP78tpU6kyKpwuwjDZHvm-kt3PTlpQHeU,3336
5
+ carconnectivity_connectors/seatcupra/climatization.py,sha256=0xxWlxrheAPzkVT8WRQtbm6ExZmVdgW7lUdOXyS_qWY,1695
6
+ carconnectivity_connectors/seatcupra/command_impl.py,sha256=mtw8ZwJLmf79fPDZ1N3ImLfB8Gt9JPbzjMuIo2y5v3M,2879
7
+ carconnectivity_connectors/seatcupra/connector.py,sha256=7H5ZszO-UjOeYVVrf9BsK_kbgLwBDvxS2n9V1gnrRN8,87659
8
+ carconnectivity_connectors/seatcupra/vehicle.py,sha256=SxhF4Qh9ABeLyPFLg5AJyvgtYVDFUKfP5KkchsJeAYg,3390
7
9
  carconnectivity_connectors/seatcupra/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
10
  carconnectivity_connectors/seatcupra/auth/auth_util.py,sha256=Y81h8fGOMSMgPtE4wI_TI9WgE_s43uaPjRLBBINhj4g,4433
9
11
  carconnectivity_connectors/seatcupra/auth/my_cupra_session.py,sha256=iK5SlankZqaeneC3SNad8nHHcrP0tTmYxToI_9cqwlo,10744
@@ -12,8 +14,8 @@ carconnectivity_connectors/seatcupra/auth/session_manager.py,sha256=NizIuY-pvkVB
12
14
  carconnectivity_connectors/seatcupra/auth/vw_web_session.py,sha256=hgsCdXugVnSgvLta4hBNtoNgMhAA83paAYO2fUOOFyM,10657
13
15
  carconnectivity_connectors/seatcupra/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
14
16
  carconnectivity_connectors/seatcupra/ui/connector_ui.py,sha256=SNYnlcGJpbWhuLiIHD2l6H9IfSiMz3IgmvXsdossDnE,1412
15
- carconnectivity_connector_seatcupra-0.1a2.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
16
- carconnectivity_connector_seatcupra-0.1a2.dist-info/METADATA,sha256=7gAeWjXp1y-KkPw1JemIM1ncGwsPNuYzaHpWP6WPsDc,5386
17
- carconnectivity_connector_seatcupra-0.1a2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
18
- carconnectivity_connector_seatcupra-0.1a2.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
19
- carconnectivity_connector_seatcupra-0.1a2.dist-info/RECORD,,
17
+ carconnectivity_connector_seatcupra-0.1a4.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
18
+ carconnectivity_connector_seatcupra-0.1a4.dist-info/METADATA,sha256=9Te-Nyk84I3WBL-0wSRphOaPLCSQvBCaWGU4yTG-N9Q,5386
19
+ carconnectivity_connector_seatcupra-0.1a4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
20
+ carconnectivity_connector_seatcupra-0.1a4.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
21
+ carconnectivity_connector_seatcupra-0.1a4.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.1a2'
20
+ __version__ = version = '0.1a4'
21
21
  __version_tuple__ = version_tuple = (0, 1)
@@ -32,7 +32,7 @@ class SeatCupraCharging(Charging): # pylint: disable=too-many-instance-attribut
32
32
  """
33
33
  OFF = 'off'
34
34
  READY_FOR_CHARGING = 'readyForCharging'
35
- NOT_READY_FOR_CHARGING = 'NotReadyForCharging'
35
+ NOT_READY_FOR_CHARGING = 'notReadyForCharging'
36
36
  CONSERVATION = 'conservation'
37
37
  CHARGE_PURPOSE_REACHED_NOT_CONSERVATION_CHARGING = 'chargePurposeReachedAndNotConservationCharging'
38
38
  CHARGE_PURPOSE_REACHED_CONSERVATION = 'chargePurposeReachedAndConservation'
@@ -0,0 +1,39 @@
1
+ """
2
+ Module for charging for Seat/Cupra vehicles.
3
+ """
4
+ from __future__ import annotations
5
+ from typing import TYPE_CHECKING
6
+
7
+ from carconnectivity.climatization import Climatization
8
+ from carconnectivity.objects import GenericObject
9
+ from carconnectivity.vehicle import GenericVehicle
10
+
11
+ if TYPE_CHECKING:
12
+ from typing import Optional
13
+
14
+
15
+ class SeatCupraClimatization(Climatization): # pylint: disable=too-many-instance-attributes
16
+ """
17
+ SeatCupraClimatization class for handling Seat/Cupra vehicle climatization information.
18
+
19
+ This class extends the Climatization class and includes an enumeration of various
20
+ climatization states specific to Volkswagen vehicles.
21
+ """
22
+ def __init__(self, vehicle: GenericVehicle | None = None, origin: Optional[Climatization] = None) -> None:
23
+ if origin is not None:
24
+ super().__init__(vehicle=vehicle, origin=origin)
25
+ if not isinstance(self.settings, SeatCupraClimatization.Settings):
26
+ self.settings: Climatization.Settings = SeatCupraClimatization.Settings(parent=self, origin=origin.settings)
27
+ else:
28
+ super().__init__(vehicle=vehicle)
29
+ self.settings: Climatization.Settings = SeatCupraClimatization.Settings(parent=self, origin=self.settings)
30
+
31
+ class Settings(Climatization.Settings):
32
+ """
33
+ This class represents the settings for a skoda car climatiation.
34
+ """
35
+ def __init__(self, parent: Optional[GenericObject] = None, origin: Optional[Climatization.Settings] = None) -> None:
36
+ if origin is not None:
37
+ super().__init__(parent=parent, origin=origin)
38
+ else:
39
+ super().__init__(parent=parent)
@@ -0,0 +1,72 @@
1
+ """This module defines the classes that represent attributes in the CarConnectivity system."""
2
+ from __future__ import annotations
3
+ from typing import TYPE_CHECKING, Dict, Union
4
+
5
+ from enum import Enum
6
+ import argparse
7
+ import logging
8
+
9
+ from carconnectivity.commands import GenericCommand
10
+ from carconnectivity.objects import GenericObject
11
+ from carconnectivity.errors import SetterError
12
+ from carconnectivity.util import ThrowingArgumentParser
13
+
14
+ if TYPE_CHECKING:
15
+ from carconnectivity.objects import Optional
16
+
17
+ LOG: logging.Logger = logging.getLogger("carconnectivity.connectors.seatcupra")
18
+
19
+
20
+ class SpinCommand(GenericCommand):
21
+ """
22
+ SpinCommand is a command class for verifying the spin
23
+
24
+ """
25
+ def __init__(self, name: str = 'spin', parent: Optional[GenericObject] = None) -> None:
26
+ super().__init__(name=name, parent=parent)
27
+
28
+ @property
29
+ def value(self) -> Optional[Union[str, Dict]]:
30
+ return super().value
31
+
32
+ @value.setter
33
+ def value(self, new_value: Optional[Union[str, Dict]]) -> None:
34
+ if isinstance(new_value, str):
35
+ parser = ThrowingArgumentParser(prog='', add_help=False, exit_on_error=False)
36
+ parser.add_argument('command', help='Command to execute', type=SpinCommand.Command,
37
+ choices=list(SpinCommand.Command))
38
+ parser.add_argument('--spin', dest='spin', help='Spin to be used instead of spin from config or .netrc', type=str, required=False,
39
+ default=None)
40
+ try:
41
+ args = parser.parse_args(new_value.split(sep=' '))
42
+ except argparse.ArgumentError as e:
43
+ raise SetterError(f'Invalid format for SpinCommand: {e.message} {parser.format_usage()}') from e
44
+
45
+ newvalue_dict = {}
46
+ newvalue_dict['command'] = args.command
47
+ if args.spin is not None:
48
+ newvalue_dict['spin'] = args.spin
49
+ new_value = newvalue_dict
50
+ elif isinstance(new_value, dict):
51
+ if 'command' in new_value and isinstance(new_value['command'], str):
52
+ if new_value['command'] in SpinCommand.Command:
53
+ new_value['command'] = SpinCommand.Command(new_value['command'])
54
+ else:
55
+ raise ValueError('Invalid value for SpinCommand. '
56
+ f'Command must be one of {SpinCommand.Command}')
57
+ if self._is_changeable:
58
+ for hook in self._on_set_hooks:
59
+ new_value = hook(self, new_value)
60
+ self._set_value(new_value)
61
+ else:
62
+ raise TypeError('You cannot set this attribute. Attribute is not mutable.')
63
+
64
+ class Command(Enum):
65
+ """
66
+ Enum class representing different commands for SPIN.
67
+
68
+ """
69
+ VERIFY = 'verify'
70
+
71
+ def __str__(self) -> str:
72
+ return self.value
@@ -15,7 +15,7 @@ from carconnectivity.garage import Garage
15
15
  from carconnectivity.errors import AuthenticationError, TooManyRequestsError, RetrievalError, APIError, APICompatibilityError, \
16
16
  TemporaryAuthenticationError, SetterError, CommandError
17
17
  from carconnectivity.util import robust_time_parse, log_extra_keys, config_remove_credentials
18
- from carconnectivity.units import Length, Power, Speed
18
+ from carconnectivity.units import Length, Current
19
19
  from carconnectivity.doors import Doors
20
20
  from carconnectivity.windows import Windows
21
21
  from carconnectivity.lights import Lights
@@ -27,6 +27,7 @@ from carconnectivity.command_impl import ClimatizationStartStopCommand, WakeSlee
27
27
  from carconnectivity.climatization import Climatization
28
28
  from carconnectivity.commands import Commands
29
29
  from carconnectivity.charging import Charging
30
+ from carconnectivity.charging_connector import ChargingConnector
30
31
  from carconnectivity.position import Position
31
32
 
32
33
  from carconnectivity_connectors.base.connector import BaseConnector
@@ -36,6 +37,8 @@ from carconnectivity_connectors.seatcupra._version import __version__
36
37
  from carconnectivity_connectors.seatcupra.capability import Capability
37
38
  from carconnectivity_connectors.seatcupra.vehicle import SeatCupraVehicle, SeatCupraElectricVehicle, SeatCupraCombustionVehicle, SeatCupraHybridVehicle
38
39
  from carconnectivity_connectors.seatcupra.charging import SeatCupraCharging, mapping_seatcupra_charging_state
40
+ from carconnectivity_connectors.seatcupra.climatization import SeatCupraClimatization
41
+ from carconnectivity_connectors.seatcupra.command_impl import SpinCommand
39
42
 
40
43
  SUPPORT_IMAGES = False
41
44
  try:
@@ -222,6 +225,12 @@ class Connector(BaseConnector):
222
225
 
223
226
  This method calls the `fetch_vehicles` method to retrieve vehicle data.
224
227
  """
228
+ # Add spin command
229
+ if self.commands is not None and not self.commands.contains_command('spin'):
230
+ spin_command = SpinCommand(parent=self.commands)
231
+ spin_command._add_on_set_hook(self.__on_spin) # pylint: disable=protected-access
232
+ spin_command.enabled = True
233
+ self.commands.add_command(spin_command)
225
234
  self.fetch_vehicles()
226
235
  self.car_connectivity.transaction_end()
227
236
 
@@ -339,10 +348,10 @@ class Connector(BaseConnector):
339
348
  else:
340
349
  raise APIError('Could not fetch capabilities, capability ID missing')
341
350
  log_extra_keys(LOG_API, 'capability', capability_dict, {'id', 'expirationDate', 'editable', 'parameters'})
342
-
351
+
343
352
  for capability_id in vehicle.capabilities.capabilities.keys() - found_capabilities:
344
353
  vehicle.capabilities.remove_capability(capability_id)
345
-
354
+
346
355
  if vehicle.capabilities.has_capability('charging'):
347
356
  if not isinstance(vehicle, SeatCupraElectricVehicle):
348
357
  LOG.debug('Promoting %s to SeatCupraElectricVehicle object for %s', vehicle.__class__.__name__, vin)
@@ -353,7 +362,7 @@ class Connector(BaseConnector):
353
362
  charging_start_stop_command._add_on_set_hook(self.__on_charging_start_stop) # pylint: disable=protected-access
354
363
  charging_start_stop_command.enabled = True
355
364
  vehicle.charging.commands.add_command(charging_start_stop_command)
356
-
365
+
357
366
  if vehicle.capabilities.has_capability('climatisation'):
358
367
  if vehicle.climatization is not None and vehicle.climatization.commands is not None \
359
368
  and not vehicle.climatization.commands.contains_command('start-stop'):
@@ -363,8 +372,36 @@ class Connector(BaseConnector):
363
372
  climatisation_start_stop_command._add_on_set_hook(self.__on_air_conditioning_start_stop)
364
373
  climatisation_start_stop_command.enabled = True
365
374
  vehicle.climatization.commands.add_command(climatisation_start_stop_command)
366
- else:
367
- vehicle.capabilities.clear_capabilities()
375
+
376
+ if vehicle.capabilities.has_capability('vehicleWakeUpTrigger'):
377
+ if vehicle.commands is not None and vehicle.commands.commands is not None \
378
+ and not vehicle.commands.contains_command('wake-sleep'):
379
+ wake_sleep_command = WakeSleepCommand(parent=vehicle.commands)
380
+ wake_sleep_command._add_on_set_hook(self.__on_wake_sleep) # pylint: disable=protected-access
381
+ wake_sleep_command.enabled = True
382
+ vehicle.commands.add_command(wake_sleep_command)
383
+
384
+ # Add honkAndFlash command if necessary capabilities are available
385
+ if vehicle.capabilities.has_capability('honkAndFlash'):
386
+ if vehicle.commands is not None and vehicle.commands.commands is not None \
387
+ and not vehicle.commands.contains_command('honk-flash'):
388
+ honk_flash_command = HonkAndFlashCommand(parent=vehicle.commands, with_duration=True)
389
+ honk_flash_command._add_on_set_hook(self.__on_honk_flash) # pylint: disable=protected-access
390
+ honk_flash_command.enabled = True
391
+ vehicle.commands.add_command(honk_flash_command)
392
+
393
+ # Add lock and unlock command
394
+ if vehicle.capabilities.has_capability('access'):
395
+ if vehicle.doors is not None and vehicle.doors.commands is not None and vehicle.doors.commands.commands is not None \
396
+ and not vehicle.doors.commands.contains_command('lock-unlock'):
397
+ lock_unlock_command = LockUnlockCommand(parent=vehicle.doors.commands)
398
+ lock_unlock_command._add_on_set_hook(self.__on_lock_unlock) # pylint: disable=protected-access
399
+ lock_unlock_command.enabled = True
400
+ vehicle.doors.commands.add_command(lock_unlock_command)
401
+ else:
402
+ vehicle.capabilities.clear_capabilities()
403
+ if isinstance(vehicle, SeatCupraVehicle):
404
+ vehicle = self.fetch_image(vehicle)
368
405
  else:
369
406
  raise APIError('Could not fetch vehicle data, VIN missing')
370
407
  for vin in set(garage.list_vehicle_vins()) - seen_vehicle_vins:
@@ -386,7 +423,7 @@ class Connector(BaseConnector):
386
423
  vin = vehicle.vin.value
387
424
  if vin is None:
388
425
  raise APIError('VIN is missing')
389
-
426
+
390
427
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/connection'
391
428
  vehicle_connection_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
392
429
  if vehicle_connection_data is not None:
@@ -514,11 +551,10 @@ class Connector(BaseConnector):
514
551
  vin = vehicle.vin.value
515
552
  if vin is None:
516
553
  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)
554
+ # url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/measurements/engines'
555
+ # vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
556
+ # measurements
557
+ # {'primary': {'fuelType': 'gasoline', 'rangeInKm': 120.0}, 'secondary': {'fuelType': 'electric', 'rangeInKm': 40.0}}
522
558
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v5/users/{self.session.user_id}/vehicles/{vin}/mycar'
523
559
  vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
524
560
  if vehicle_status_data:
@@ -590,23 +626,6 @@ class Connector(BaseConnector):
590
626
  if 'services' in vehicle_status_data and vehicle_status_data['services'] is not None:
591
627
  if 'charging' in vehicle_status_data['services'] and vehicle_status_data['services']['charging'] is not None:
592
628
  charging_status: Dict = vehicle_status_data['services']['charging']
593
- if 'status' in charging_status and charging_status['status'] is not None:
594
- if charging_status['status'] in SeatCupraCharging.SeatCupraChargingState:
595
- volkswagen_charging_state = SeatCupraCharging.SeatCupraChargingState(charging_status['status'])
596
- charging_state: Charging.ChargingState = mapping_seatcupra_charging_state[volkswagen_charging_state]
597
- else:
598
- LOG_API.info('Unkown charging state %s not in %s', charging_status['status'],
599
- str(SeatCupraCharging.SeatCupraChargingState))
600
- charging_state = Charging.ChargingState.UNKNOWN
601
- if isinstance(vehicle, ElectricVehicle):
602
- vehicle.charging.state._set_value(value=charging_state) # pylint: disable=protected-access
603
- else:
604
- LOG_API.warning('Vehicle is not an electric or hybrid vehicle, but charging state was fetched')
605
- else:
606
- if isinstance(vehicle, ElectricVehicle):
607
- vehicle.charging.state._set_value(None) # pylint: disable=protected-access
608
- else:
609
- LOG_API.warning('Vehicle is not an electric or hybrid vehicle, but charging state was fetched')
610
629
  if 'targetPct' in charging_status and charging_status['targetPct'] is not None:
611
630
  if isinstance(vehicle, ElectricVehicle):
612
631
  vehicle.charging.settings.target_level._set_value(charging_status['targetPct']) # pylint: disable=protected-access
@@ -636,26 +655,7 @@ class Connector(BaseConnector):
636
655
  vehicle.charging.enabled = False
637
656
  if 'climatisation' in vehicle_status_data['services'] and vehicle_status_data['services']['climatisation'] is not None:
638
657
  climatisation_status: Dict = vehicle_status_data['services']['climatisation']
639
- if 'status' in climatisation_status and climatisation_status['status'] is not None:
640
- if climatisation_status['status'].lower() in [item.value for item in Climatization.ClimatizationState]:
641
- climatization_state: Climatization.ClimatizationState = Climatization.ClimatizationState(climatisation_status['status'].lower())
642
- else:
643
- LOG_API.info('Unknown climatization state %s not in %s', climatisation_status['status'],
644
- str(Climatization.ClimatizationState))
645
- climatization_state = Climatization.ClimatizationState.UNKNOWN
646
- vehicle.climatization.state._set_value(value=climatization_state) # pylint: disable=protected-access
647
- else:
648
- vehicle.climatization.state._set_value(None) # pylint: disable=protected-access
649
- if 'targetTemperatureCelsius' in climatisation_status and climatisation_status['targetTemperatureCelsius'] is not None:
650
- target_temperature: Optional[float] = climatisation_status['targetTemperatureCelsius']
651
- vehicle.climatization.settings.target_temperature._set_value(value=target_temperature, # pylint: disable=protected-access
652
- unit=Temperature.C)
653
- elif 'targetTemperatureFahrenheit' in climatisation_status and climatisation_status['targetTemperatureFahrenheit'] is not None:
654
- target_temperature = climatisation_status['targetTemperatureFahrenheit']
655
- vehicle.climatization.settings.target_temperature._set_value(value=target_temperature, # pylint: disable=protected-access
656
- unit=Temperature.F)
657
- else:
658
- vehicle.climatization.settings.target_temperature._set_value(None) # pylint: disable=protected-access
658
+
659
659
  if 'remainingTime' in climatisation_status and climatisation_status['remainingTime'] is not None:
660
660
  remaining_duration: timedelta = timedelta(minutes=climatisation_status['remainingTime'])
661
661
  estimated_date_reached: datetime = datetime.now(tz=timezone.utc) + remaining_duration
@@ -663,6 +663,7 @@ class Connector(BaseConnector):
663
663
  vehicle.climatization.estimated_date_reached._set_value(value=estimated_date_reached) # pylint: disable=protected-access
664
664
  else:
665
665
  vehicle.climatization.estimated_date_reached._set_value(None) # pylint: disable=protected-access
666
+ # we take status, targetTemperatureCelsius, targetTemperatureFahrenheit, from climatization request
666
667
  log_extra_keys(LOG_API, 'climatisation', climatisation_status, {'status', 'targetTemperatureCelsius', 'targetTemperatureFahrenheit',
667
668
  'remainingTime'})
668
669
  return vehicle
@@ -755,12 +756,68 @@ class Connector(BaseConnector):
755
756
  raise APIError('VIN is missing')
756
757
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/climatisation/status'
757
758
  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)
759
+ # {'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'}]}}
760
+ if data is not None:
761
+ if 'climatisationStatus' in data and data['climatisationStatus'] is not None:
762
+ climatisation_status: Dict = data['climatisationStatus']
763
+ if 'carCapturedTimestamp' not in climatisation_status or climatisation_status['carCapturedTimestamp'] is None:
764
+ raise APIError('Could not fetch vehicle status, carCapturedTimestamp missing')
765
+ captured_at: datetime = robust_time_parse(climatisation_status['carCapturedTimestamp'])
766
+ if 'climatisationState' in climatisation_status and climatisation_status['climatisationState'] is not None:
767
+ if climatisation_status['climatisationState'].lower() in [item.value for item in Climatization.ClimatizationState]:
768
+ climatization_state: Climatization.ClimatizationState = \
769
+ Climatization.ClimatizationState(climatisation_status['climatisationState'].lower())
770
+ else:
771
+ LOG_API.info('Unknown climatization state %s not in %s', climatisation_status['climatisationState'],
772
+ str(Climatization.ClimatizationState))
773
+ climatization_state = Climatization.ClimatizationState.UNKNOWN
774
+ vehicle.climatization.state._set_value(value=climatization_state, measured=captured_at) # pylint: disable=protected-access
775
+ else:
776
+ vehicle.climatization.state._set_value(None) # pylint: disable=protected-access
777
+ log_extra_keys(LOG_API, 'climatisation', data, {'carCapturedTimestamp', 'climatisationState'})
778
+ else:
779
+ vehicle.climatization.state._set_value(None) # pylint: disable=protected-access
780
+ log_extra_keys(LOG_API, 'climatisation', data, {'climatisationStatus'})
760
781
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/climatisation/settings'
761
782
  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)
783
+ if data is not None:
784
+ if not isinstance(vehicle.climatization, SeatCupraClimatization):
785
+ vehicle.climatization = SeatCupraClimatization(vehicle=vehicle, origin=vehicle.climatization)
786
+ if 'carCapturedTimestamp' not in data or data['carCapturedTimestamp'] is None:
787
+ raise APIError('Could not fetch vehicle status, carCapturedTimestamp missing')
788
+ captured_at: datetime = robust_time_parse(data['carCapturedTimestamp'])
789
+ if 'targetTemperatureInCelsius' in data and data['targetTemperatureInCelsius'] is not None:
790
+ # pylint: disable-next=protected-access
791
+ vehicle.climatization.settings.target_temperature._add_on_set_hook(self.__on_air_conditioning_settings_change)
792
+ vehicle.climatization.settings.target_temperature._is_changeable = True # pylint: disable=protected-access
793
+
794
+ target_temperature: Optional[float] = data['targetTemperatureInCelsius']
795
+ vehicle.climatization.settings.target_temperature._set_value(value=target_temperature, # pylint: disable=protected-access
796
+ measured=captured_at,
797
+ unit=Temperature.C)
798
+ elif 'targetTemperatureInFahrenheit' in data and data['targetTemperatureInFahrenheit'] is not None:
799
+ # pylint: disable-next=protected-access
800
+ vehicle.climatization.settings.target_temperature._add_on_set_hook(self.__on_air_conditioning_settings_change)
801
+ vehicle.climatization.settings.target_temperature._is_changeable = True # pylint: disable=protected-access
802
+
803
+ target_temperature = data['targetTemperatureInFahrenheit']
804
+ vehicle.climatization.settings.target_temperature._set_value(value=target_temperature, # pylint: disable=protected-access
805
+ measured=captured_at,
806
+ unit=Temperature.F)
807
+ else:
808
+ vehicle.climatization.settings.target_temperature._set_value(None) # pylint: disable=protected-access
809
+ if 'climatisationWithoutExternalPower' in data and data['climatisationWithoutExternalPower'] is not None:
810
+ # pylint: disable-next=protected-access
811
+ vehicle.climatization.settings.climatization_without_external_power._add_on_set_hook(self.__on_air_conditioning_settings_change)
812
+ vehicle.climatization.settings.climatization_without_external_power._is_changeable = True # pylint: disable=protected-access
813
+
814
+ # pylint: disable-next=protected-access
815
+ vehicle.climatization.settings.climatization_without_external_power._set_value(data['climatisationWithoutExternalPower'],
816
+ measured=captured_at)
817
+ else:
818
+ vehicle.climatization.settings.climatization_without_external_power._set_value(None) # pylint: disable=protected-access
819
+ log_extra_keys(LOG_API, f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/climatisation/settings', data,
820
+ {'carCapturedTimestamp', 'targetTemperatureInCelsius', 'targetTemperatureInFahrenheit', 'climatisationWithoutExternalPower'})
764
821
  return vehicle
765
822
 
766
823
  def fetch_charging(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
@@ -780,18 +837,147 @@ class Connector(BaseConnector):
780
837
  vin = vehicle.vin.value
781
838
  if vin is None:
782
839
  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)
840
+ if isinstance(vehicle, ElectricVehicle):
841
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/charging/status'
842
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
843
+
844
+ if data is not None:
845
+ if 'charging' in data and data['charging'] is not None:
846
+ if 'state' in data['charging'] and data['charging']['state'] is not None:
847
+ if data['charging']['state'] in SeatCupraCharging.SeatCupraChargingState:
848
+ volkswagen_charging_state = SeatCupraCharging.SeatCupraChargingState(data['charging']['state'])
849
+ charging_state: Charging.ChargingState = mapping_seatcupra_charging_state[volkswagen_charging_state]
850
+ else:
851
+ LOG_API.info('Unkown charging state %s not in %s', data['charging']['state'],
852
+ str(SeatCupraCharging.SeatCupraChargingState))
853
+ charging_state = Charging.ChargingState.UNKNOWN
854
+ vehicle.charging.state._set_value(value=charging_state) # pylint: disable=protected-access
855
+ else:
856
+ vehicle.charging.state._set_value(None) # pylint: disable=protected-access
857
+ log_extra_keys(LOG_API, 'charging', data['charging'], {'state'})
858
+ if 'plug' in data and data['plug'] is not None:
859
+ if 'connection' in data['plug'] and data['plug']['connection'] is not None:
860
+ if data['plug']['connection'] in [item.value for item in ChargingConnector.ChargingConnectorConnectionState]:
861
+ plug_state: ChargingConnector.ChargingConnectorConnectionState = \
862
+ ChargingConnector.ChargingConnectorConnectionState(data['plug']['connection'])
863
+ else:
864
+ LOG_API.info('Unknown plug state %s', data['plug']['connection'])
865
+ plug_state = ChargingConnector.ChargingConnectorConnectionState.UNKNOWN
866
+ vehicle.charging.connector.connection_state._set_value(value=plug_state) # pylint: disable=protected-access
867
+ else:
868
+ vehicle.charging.connector.connection_state._set_value(value=None) # pylint: disable=protected-access
869
+ if 'externalPower' in data['plug'] and data['plug']['externalPower'] is not None:
870
+ if data['plug']['externalPower'] in [item.value for item in ChargingConnector.ExternalPower]:
871
+ plug_power_state: ChargingConnector.ExternalPower = \
872
+ ChargingConnector.ExternalPower(data['plug']['externalPower'])
873
+ else:
874
+ LOG_API.info('Unknown plug power state %s', data['plug']['externalPower'])
875
+ plug_power_state = ChargingConnector.ExternalPower.UNKNOWN
876
+ vehicle.charging.connector.external_power._set_value(value=plug_power_state) # pylint: disable=protected-access
877
+ else:
878
+ vehicle.charging.connector.external_power._set_value(None) # pylint: disable=protected-access
879
+ if 'lock' in data['plug'] and data['plug']['lock'] is not None:
880
+ if data['plug']['lock'] in [item.value for item in ChargingConnector.ChargingConnectorLockState]:
881
+ plug_lock_state: ChargingConnector.ChargingConnectorLockState = \
882
+ ChargingConnector.ChargingConnectorLockState(data['plug']['lock'])
883
+ else:
884
+ LOG_API.info('Unknown plug lock state %s', data['plug']['lock'])
885
+ plug_lock_state = ChargingConnector.ChargingConnectorLockState.UNKNOWN
886
+ vehicle.charging.connector.lock_state._set_value(value=plug_lock_state) # pylint: disable=protected-access
887
+ else:
888
+ vehicle.charging.connector.lock_state._set_value(None) # pylint: disable=protected-access
889
+ log_extra_keys(LOG_API, 'plug', data['plug'], {'connection', 'externalPower', 'lock'})
890
+ log_extra_keys(LOG_API, f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/charging/status', data,
891
+ {'state', 'battery', 'charging', 'plug'})
892
+
893
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/charging/settings'
894
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
895
+ if data is not None:
896
+ if 'maxChargeCurrentAc' in data and data['maxChargeCurrentAc'] is not None:
897
+ if data['maxChargeCurrentAc']:
898
+ vehicle.charging.settings.maximum_current._set_value(value=16, # pylint: disable=protected-access
899
+ unit=Current.A)
900
+ else:
901
+ vehicle.charging.settings.maximum_current._set_value(value=6, # pylint: disable=protected-access
902
+ unit=Current.A)
903
+ else:
904
+ vehicle.charging.settings.maximum_current._set_value(None) # pylint: disable=protected-access
905
+ if 'defaultMaxTargetSocPercentage' in data and data['defaultMaxTargetSocPercentage'] is not None:
906
+ vehicle.charging.settings.target_level._set_value(data['defaultMaxTargetSocPercentage']) # pylint: disable=protected-access
907
+ else:
908
+ vehicle.charging.settings.target_level._set_value(None) # pylint: disable=protected-access
909
+ return vehicle
910
+
911
+ def fetch_image(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
912
+ """
913
+ Fetches the image of a given SeatCupraVehicle.
914
+
915
+ This method retrieves the image of the vehicle from a remote server. It supports caching to avoid redundant downloads.
916
+ If caching is enabled and the image is found in the cache and is not expired, it will be loaded from the cache.
917
+ Otherwise, it will be downloaded from the server.
918
+
919
+ Args:
920
+ vehicle (SeatCupraVehicle): The vehicle object for which the image is to be fetched.
921
+ no_cache (bool, optional): If True, bypasses the cache and fetches the image directly from the server. Defaults to False.
922
+
923
+ Returns:
924
+ SeatCupraVehicle: The vehicle object with the fetched image added to its attributes.
925
+
926
+ Raises:
927
+ RetrievalError: If there is a connection error, chunked encoding error, read timeout, or retry error during the image retrieval process.
928
+ """
929
+ if SUPPORT_IMAGES:
930
+ url: str = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vehicle.vin.value}/renders'
931
+ data = self._fetch_data(url, session=self.session, allow_http_error=True, no_cache=no_cache)
932
+ if data is not None: # pylint: disable=too-many-nested-blocks
933
+ for image_id, image_url in data.items():
934
+ if image_id == 'isDefault':
935
+ continue
936
+ img = None
937
+ cache_date = None
938
+ if self.active_config['max_age'] is not None and self.session.cache is not None and image_url in self.session.cache:
939
+ img, cache_date_string = self.session.cache[image_url]
940
+ img = base64.b64decode(img) # pyright: ignore[reportPossiblyUnboundVariable]
941
+ img = Image.open(io.BytesIO(img)) # pyright: ignore[reportPossiblyUnboundVariable]
942
+ cache_date = datetime.fromisoformat(cache_date_string)
943
+ if img is None or self.active_config['max_age'] is None \
944
+ or (cache_date is not None and cache_date < (datetime.utcnow() - timedelta(seconds=self.active_config['max_age']))):
945
+ try:
946
+ image_download_response = requests.get(image_url, stream=True, timeout=180)
947
+ if image_download_response.status_code == requests.codes['ok']:
948
+ img = Image.open(image_download_response.raw) # pyright: ignore[reportPossiblyUnboundVariable]
949
+ if self.session.cache is not None:
950
+ buffered = io.BytesIO() # pyright: ignore[reportPossiblyUnboundVariable]
951
+ img.save(buffered, format="PNG")
952
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8") # pyright: ignore[reportPossiblyUnboundVariable]
953
+ self.session.cache[image_url] = (img_str, str(datetime.utcnow()))
954
+ elif image_download_response.status_code == requests.codes['unauthorized']:
955
+ LOG.info('Server asks for new authorization')
956
+ self.session.login()
957
+ image_download_response = self.session.get(image_url, stream=True)
958
+ if image_download_response.status_code == requests.codes['ok']:
959
+ img = Image.open(image_download_response.raw) # pyright: ignore[reportPossiblyUnboundVariable]
960
+ if self.session.cache is not None:
961
+ buffered = io.BytesIO() # pyright: ignore[reportPossiblyUnboundVariable]
962
+ img.save(buffered, format="PNG")
963
+ img_str = base64.b64encode(buffered.getvalue()).decode("utf-8") # pyright: ignore[reportPossiblyUnboundVariable]
964
+ self.session.cache[image_url] = (img_str, str(datetime.utcnow()))
965
+ except requests.exceptions.ConnectionError as connection_error:
966
+ raise RetrievalError(f'Connection error: {connection_error}') from connection_error
967
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
968
+ raise RetrievalError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
969
+ except requests.exceptions.ReadTimeout as timeout_error:
970
+ raise RetrievalError(f'Timeout during read: {timeout_error}') from timeout_error
971
+ except requests.exceptions.RetryError as retry_error:
972
+ raise RetrievalError(f'Retrying failed: {retry_error}') from retry_error
973
+ if img is not None:
974
+ vehicle._car_images[image_id] = img # pylint: disable=protected-access
975
+ if image_id == 'side':
976
+ if 'car_picture' in vehicle.images.images:
977
+ vehicle.images.images['car_picture']._set_value(img) # pylint: disable=protected-access
978
+ else:
979
+ vehicle.images.images['car_picture'] = ImageAttribute(name="car_picture", parent=vehicle.images,
980
+ value=img, tags={'carconnectivity'})
795
981
  return vehicle
796
982
 
797
983
  def _record_elapsed(self, elapsed: timedelta) -> None:
@@ -907,6 +1093,168 @@ class Connector(BaseConnector):
907
1093
  raise CommandError(f'Could not start/stop air conditioning ({command_response.status_code}: {command_response.text})')
908
1094
  return command_arguments
909
1095
 
1096
+ def __on_spin(self, spin_command: SpinCommand, command_arguments: Union[str, Dict[str, Any]]) \
1097
+ -> Union[str, Dict[str, Any]]:
1098
+ del spin_command
1099
+ if not isinstance(command_arguments, dict):
1100
+ raise CommandError('Command arguments are not a dictionary')
1101
+ if 'command' not in command_arguments:
1102
+ raise CommandError('Command argument missing')
1103
+ command_dict = {}
1104
+ if self.active_config['spin'] is None:
1105
+ raise CommandError('S-PIN is missing, please add S-PIN to your configuration or .netrc file')
1106
+ if 'spin' in command_arguments:
1107
+ command_dict['currentSpin'] = command_arguments['spin']
1108
+ else:
1109
+ if self.active_config['spin'] is None or self.active_config['spin'] == '':
1110
+ raise CommandError('S-PIN is missing, please add S-PIN to your configuration or .netrc file')
1111
+ command_dict['spin'] = self.active_config['spin']
1112
+ if command_arguments['command'] == SpinCommand.Command.VERIFY:
1113
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/users/{self.session.user_id}/spin/verify'
1114
+ else:
1115
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
1116
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1117
+ if command_response.status_code != requests.codes['ok']:
1118
+ LOG.error('Could not execute spin command (%s: %s)', command_response.status_code, command_response.text)
1119
+ raise CommandError(f'Could not execute spin command ({command_response.status_code}: {command_response.text})')
1120
+ else:
1121
+ LOG.info('Spin verify command executed successfully')
1122
+ return command_arguments
1123
+
1124
+ def __on_wake_sleep(self, wake_sleep_command: WakeSleepCommand, command_arguments: Union[str, Dict[str, Any]]) \
1125
+ -> Union[str, Dict[str, Any]]:
1126
+ if wake_sleep_command.parent is None or wake_sleep_command.parent.parent is None \
1127
+ or not isinstance(wake_sleep_command.parent.parent, GenericVehicle):
1128
+ raise CommandError('Object hierarchy is not as expected')
1129
+ if not isinstance(command_arguments, dict):
1130
+ raise CommandError('Command arguments are not a dictionary')
1131
+ vehicle: GenericVehicle = wake_sleep_command.parent.parent
1132
+ vin: Optional[str] = vehicle.vin.value
1133
+ if vin is None:
1134
+ raise CommandError('VIN in object hierarchy missing')
1135
+ if 'command' not in command_arguments:
1136
+ raise CommandError('Command argument missing')
1137
+ if command_arguments['command'] == WakeSleepCommand.Command.WAKE:
1138
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/vehicle-wakeup/request'
1139
+
1140
+ command_response: requests.Response = self.session.post(url, data='{}', allow_redirects=True)
1141
+ if command_response.status_code not in (requests.codes['ok'], requests.codes['no_content']):
1142
+ LOG.error('Could not execute wake command (%s: %s)', command_response.status_code, command_response.text)
1143
+ raise CommandError(f'Could not execute wake command ({command_response.status_code}: {command_response.text})')
1144
+ elif command_arguments['command'] == WakeSleepCommand.Command.SLEEP:
1145
+ raise CommandError('Sleep command not supported by vehicle. Vehicle will put itself to sleep')
1146
+ else:
1147
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
1148
+ return command_arguments
1149
+
1150
+ def __on_honk_flash(self, honk_flash_command: HonkAndFlashCommand, command_arguments: Union[str, Dict[str, Any]]) \
1151
+ -> Union[str, Dict[str, Any]]:
1152
+ if honk_flash_command.parent is None or honk_flash_command.parent.parent is None \
1153
+ or not isinstance(honk_flash_command.parent.parent, GenericVehicle):
1154
+ raise CommandError('Object hierarchy is not as expected')
1155
+ if not isinstance(command_arguments, dict):
1156
+ raise CommandError('Command arguments are not a dictionary')
1157
+ vehicle: GenericVehicle = honk_flash_command.parent.parent
1158
+ vin: Optional[str] = vehicle.vin.value
1159
+ if vin is None:
1160
+ raise CommandError('VIN in object hierarchy missing')
1161
+ if 'command' not in command_arguments:
1162
+ raise CommandError('Command argument missing')
1163
+ command_dict = {}
1164
+ if command_arguments['command'] in [HonkAndFlashCommand.Command.FLASH, HonkAndFlashCommand.Command.HONK_AND_FLASH]:
1165
+ if 'duration' in command_arguments:
1166
+ command_dict['durationInSeconds'] = command_arguments['duration']
1167
+ else:
1168
+ command_dict['durationInSeconds'] = 10
1169
+ command_dict['mode'] = command_arguments['command'].value
1170
+ command_dict['userPosition'] = {}
1171
+ if vehicle.position is None or vehicle.position.latitude is None or vehicle.position.longitude is None \
1172
+ or vehicle.position.latitude.value is None or vehicle.position.longitude.value is None \
1173
+ or not vehicle.position.latitude.enabled or not vehicle.position.longitude.enabled:
1174
+ raise CommandError('Can only execute honk and flash commands if vehicle position is known')
1175
+ command_dict['userPosition']['latitude'] = vehicle.position.latitude.value
1176
+ command_dict['userPosition']['longitude'] = vehicle.position.longitude.value
1177
+
1178
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/honk-and-flash'
1179
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1180
+ if command_response.status_code not in (requests.codes['ok'], requests.codes['no_content']):
1181
+ LOG.error('Could not execute honk or flash command (%s: %s)', command_response.status_code, command_response.text)
1182
+ raise CommandError(f'Could not execute honk or flash command ({command_response.status_code}: {command_response.text})')
1183
+ else:
1184
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
1185
+ return command_arguments
1186
+
1187
+ def __on_lock_unlock(self, lock_unlock_command: LockUnlockCommand, command_arguments: Union[str, Dict[str, Any]]) \
1188
+ -> Union[str, Dict[str, Any]]:
1189
+ if lock_unlock_command.parent is None or lock_unlock_command.parent.parent is None \
1190
+ or lock_unlock_command.parent.parent.parent is None or not isinstance(lock_unlock_command.parent.parent.parent, GenericVehicle):
1191
+ raise CommandError('Object hierarchy is not as expected')
1192
+ if not isinstance(command_arguments, dict):
1193
+ raise SetterError('Command arguments are not a dictionary')
1194
+ vehicle: GenericVehicle = lock_unlock_command.parent.parent.parent
1195
+ vin: Optional[str] = vehicle.vin.value
1196
+ if vin is None:
1197
+ raise CommandError('VIN in object hierarchy missing')
1198
+ if 'command' not in command_arguments:
1199
+ raise CommandError('Command argument missing')
1200
+ command_dict = {}
1201
+ if 'spin' in command_arguments:
1202
+ command_dict['spin'] = command_arguments['spin']
1203
+ else:
1204
+ if self.active_config['spin'] is None:
1205
+ raise CommandError('S-PIN is missing, please add S-PIN to your configuration or .netrc file')
1206
+ command_dict['spin'] = self.active_config['spin']
1207
+ if command_arguments['command'] == LockUnlockCommand.Command.LOCK:
1208
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/access/lock'
1209
+ elif command_arguments['command'] == LockUnlockCommand.Command.UNLOCK:
1210
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/access/unlock'
1211
+ else:
1212
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
1213
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1214
+ if command_response.status_code != requests.codes['ok']:
1215
+ LOG.error('Could not execute locking command (%s: %s)', command_response.status_code, command_response.text)
1216
+ raise CommandError(f'Could not execute locking command ({command_response.status_code}: {command_response.text})')
1217
+ return command_arguments
1218
+
1219
+ def __on_air_conditioning_settings_change(self, attribute: GenericAttribute, value: Any) -> Any:
1220
+ """
1221
+ Callback for the climatization setting change.
1222
+ """
1223
+ if attribute.parent is None or not isinstance(attribute.parent, SeatCupraClimatization.Settings) \
1224
+ or attribute.parent.parent is None \
1225
+ or attribute.parent.parent.parent is None or not isinstance(attribute.parent.parent.parent, SeatCupraVehicle):
1226
+ raise SetterError('Object hierarchy is not as expected')
1227
+ settings: SeatCupraClimatization.Settings = attribute.parent
1228
+ vehicle: SeatCupraVehicle = attribute.parent.parent.parent
1229
+ vin: Optional[str] = vehicle.vin.value
1230
+ if vin is None:
1231
+ raise SetterError('VIN in object hierarchy missing')
1232
+ setting_dict = {}
1233
+ if settings.target_temperature.enabled and settings.target_temperature.value is not None:
1234
+ # Round target temperature to nearest 0.5
1235
+ # Check if the attribute changed is the target_temperature attribute
1236
+ if isinstance(attribute, TemperatureAttribute) and attribute.id == 'target_temperature':
1237
+ setting_dict['targetTemperature'] = round(value * 2) / 2
1238
+ else:
1239
+ setting_dict['targetTemperature'] = round(settings.target_temperature.value * 2) / 2
1240
+ if settings.target_temperature.unit == Temperature.C:
1241
+ setting_dict['targetTemperatureUnit'] = 'celsius'
1242
+ elif settings.target_temperature.unit == Temperature.F:
1243
+ setting_dict['targetTemperatureUnit'] = 'farenheit'
1244
+ else:
1245
+ setting_dict['targetTemperatureUnit'] = 'celsius'
1246
+ if isinstance(attribute, BooleanAttribute) and attribute.id == 'climatisation_without_external_power':
1247
+ setting_dict['climatisationWithoutExternalPower'] = value
1248
+ elif settings.climatization_without_external_power.enabled and settings.climatization_without_external_power.value is not None:
1249
+ setting_dict['climatisationWithoutExternalPower'] = settings.climatization_without_external_power.value
1250
+
1251
+ url: str = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/climatisation/settings'
1252
+ settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
1253
+ if settings_response.status_code not in [requests.codes['ok'], requests.codes['created']]:
1254
+ LOG.error('Could not set climatization settings (%s) %s', settings_response.status_code, settings_response.text)
1255
+ raise SetterError(f'Could not set value ({settings_response.status_code}): {settings_response.text}')
1256
+ return value
1257
+
910
1258
  def get_version(self) -> str:
911
1259
  return __version__
912
1260
 
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING
5
5
  from carconnectivity.vehicle import GenericVehicle, ElectricVehicle, CombustionVehicle, HybridVehicle
6
6
 
7
7
  from carconnectivity_connectors.seatcupra.capability import Capabilities
8
+ from carconnectivity_connectors.seatcupra.climatization import SeatCupraClimatization
8
9
 
9
10
  SUPPORT_IMAGES = False
10
11
  try:
@@ -38,14 +39,15 @@ class SeatCupraVehicle(GenericVehicle): # pylint: disable=too-many-instance-att
38
39
  self.capabilities.parent = self
39
40
  if SUPPORT_IMAGES:
40
41
  self._car_images = origin._car_images
41
-
42
42
  else:
43
43
  super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)
44
+ self.climatization = SeatCupraClimatization(origin=self.climatization)
44
45
  self.capabilities: Capabilities = Capabilities(vehicle=self)
45
46
  if SUPPORT_IMAGES:
46
47
  self._car_images: Dict[str, Image.Image] = {}
47
48
 
48
49
 
50
+
49
51
  class SeatCupraElectricVehicle(ElectricVehicle, SeatCupraVehicle):
50
52
  """
51
53
  Represents a Seat/Cupra electric vehicle.