carconnectivity-connector-seatcupra 0.1a3__py3-none-any.whl → 0.1a5__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.1a3
3
+ Version: 0.1a5
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.4a3
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=xpGveoYLp11VothNw4ctnawA60va2R4V5tLdgWtxWZ0,508
2
+ carconnectivity_connectors/seatcupra/_version.py,sha256=3vWvGIvcurJ1u-UTyD0NkVyWEC9cDflcmq5cu0o_AiM,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=U4j5fCJYfVCb_5aBDp9m2F8swDL3Vz63CW-jrtIWiSw,66248
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=7O_sQCfz5doQxcrVGW0_2e9I4HkOhL-WAx87CjQSkg0,90129
8
+ carconnectivity_connectors/seatcupra/vehicle.py,sha256=kiFVbJgq5VQOzf-vSli_2NsMgY0x4pwvJsjPWLGdr1g,3404
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.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
+ carconnectivity_connector_seatcupra-0.1a5.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
18
+ carconnectivity_connector_seatcupra-0.1a5.dist-info/METADATA,sha256=BIc37cr52KvMOs_dhmOQVOpQZZ6tX-BeEaeWxfP311Q,5386
19
+ carconnectivity_connector_seatcupra-0.1a5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
20
+ carconnectivity_connector_seatcupra-0.1a5.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
21
+ carconnectivity_connector_seatcupra-0.1a5.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.1a3'
20
+ __version__ = version = '0.1a5'
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,34 @@ 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()
368
403
  if isinstance(vehicle, SeatCupraVehicle):
369
404
  vehicle = self.fetch_image(vehicle)
370
405
  else:
@@ -388,7 +423,7 @@ class Connector(BaseConnector):
388
423
  vin = vehicle.vin.value
389
424
  if vin is None:
390
425
  raise APIError('VIN is missing')
391
-
426
+
392
427
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/connection'
393
428
  vehicle_connection_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
394
429
  if vehicle_connection_data is not None:
@@ -516,11 +551,10 @@ class Connector(BaseConnector):
516
551
  vin = vehicle.vin.value
517
552
  if vin is None:
518
553
  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)
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}}
524
558
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v5/users/{self.session.user_id}/vehicles/{vin}/mycar'
525
559
  vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
526
560
  if vehicle_status_data:
@@ -592,23 +626,6 @@ class Connector(BaseConnector):
592
626
  if 'services' in vehicle_status_data and vehicle_status_data['services'] is not None:
593
627
  if 'charging' in vehicle_status_data['services'] and vehicle_status_data['services']['charging'] is not None:
594
628
  charging_status: Dict = vehicle_status_data['services']['charging']
595
- if 'status' in charging_status and charging_status['status'] is not None:
596
- if charging_status['status'] in SeatCupraCharging.SeatCupraChargingState:
597
- volkswagen_charging_state = SeatCupraCharging.SeatCupraChargingState(charging_status['status'])
598
- charging_state: Charging.ChargingState = mapping_seatcupra_charging_state[volkswagen_charging_state]
599
- else:
600
- LOG_API.info('Unkown charging state %s not in %s', charging_status['status'],
601
- str(SeatCupraCharging.SeatCupraChargingState))
602
- charging_state = Charging.ChargingState.UNKNOWN
603
- if isinstance(vehicle, ElectricVehicle):
604
- vehicle.charging.state._set_value(value=charging_state) # pylint: disable=protected-access
605
- else:
606
- LOG_API.warning('Vehicle is not an electric or hybrid vehicle, but charging state was fetched')
607
- else:
608
- if isinstance(vehicle, ElectricVehicle):
609
- vehicle.charging.state._set_value(None) # pylint: disable=protected-access
610
- else:
611
- LOG_API.warning('Vehicle is not an electric or hybrid vehicle, but charging state was fetched')
612
629
  if 'targetPct' in charging_status and charging_status['targetPct'] is not None:
613
630
  if isinstance(vehicle, ElectricVehicle):
614
631
  vehicle.charging.settings.target_level._set_value(charging_status['targetPct']) # pylint: disable=protected-access
@@ -638,26 +655,7 @@ class Connector(BaseConnector):
638
655
  vehicle.charging.enabled = False
639
656
  if 'climatisation' in vehicle_status_data['services'] and vehicle_status_data['services']['climatisation'] is not None:
640
657
  climatisation_status: Dict = vehicle_status_data['services']['climatisation']
641
- if 'status' in climatisation_status and climatisation_status['status'] is not None:
642
- if climatisation_status['status'].lower() in [item.value for item in Climatization.ClimatizationState]:
643
- climatization_state: Climatization.ClimatizationState = Climatization.ClimatizationState(climatisation_status['status'].lower())
644
- else:
645
- LOG_API.info('Unknown climatization state %s not in %s', climatisation_status['status'],
646
- str(Climatization.ClimatizationState))
647
- climatization_state = Climatization.ClimatizationState.UNKNOWN
648
- vehicle.climatization.state._set_value(value=climatization_state) # pylint: disable=protected-access
649
- else:
650
- vehicle.climatization.state._set_value(None) # pylint: disable=protected-access
651
- if 'targetTemperatureCelsius' in climatisation_status and climatisation_status['targetTemperatureCelsius'] is not None:
652
- target_temperature: Optional[float] = climatisation_status['targetTemperatureCelsius']
653
- vehicle.climatization.settings.target_temperature._set_value(value=target_temperature, # pylint: disable=protected-access
654
- unit=Temperature.C)
655
- elif 'targetTemperatureFahrenheit' in climatisation_status and climatisation_status['targetTemperatureFahrenheit'] is not None:
656
- target_temperature = climatisation_status['targetTemperatureFahrenheit']
657
- vehicle.climatization.settings.target_temperature._set_value(value=target_temperature, # pylint: disable=protected-access
658
- unit=Temperature.F)
659
- else:
660
- vehicle.climatization.settings.target_temperature._set_value(None) # pylint: disable=protected-access
658
+
661
659
  if 'remainingTime' in climatisation_status and climatisation_status['remainingTime'] is not None:
662
660
  remaining_duration: timedelta = timedelta(minutes=climatisation_status['remainingTime'])
663
661
  estimated_date_reached: datetime = datetime.now(tz=timezone.utc) + remaining_duration
@@ -665,6 +663,7 @@ class Connector(BaseConnector):
665
663
  vehicle.climatization.estimated_date_reached._set_value(value=estimated_date_reached) # pylint: disable=protected-access
666
664
  else:
667
665
  vehicle.climatization.estimated_date_reached._set_value(None) # pylint: disable=protected-access
666
+ # we take status, targetTemperatureCelsius, targetTemperatureFahrenheit, from climatization request
668
667
  log_extra_keys(LOG_API, 'climatisation', climatisation_status, {'status', 'targetTemperatureCelsius', 'targetTemperatureFahrenheit',
669
668
  'remainingTime'})
670
669
  return vehicle
@@ -757,12 +756,68 @@ class Connector(BaseConnector):
757
756
  raise APIError('VIN is missing')
758
757
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/climatisation/status'
759
758
  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)
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'})
762
781
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/climatisation/settings'
763
782
  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)
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'})
766
821
  return vehicle
767
822
 
768
823
  def fetch_charging(self, vehicle: SeatCupraVehicle, no_cache: bool = False) -> SeatCupraVehicle:
@@ -782,24 +837,98 @@ class Connector(BaseConnector):
782
837
  vin = vehicle.vin.value
783
838
  if vin is None:
784
839
  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)
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
797
909
  return vehicle
798
910
 
799
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
+ """
800
929
  if SUPPORT_IMAGES:
801
930
  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)
931
+ data = self._fetch_data(url, session=self.session, allow_http_error=True, no_cache=no_cache)
803
932
  if data is not None: # pylint: disable=too-many-nested-blocks
804
933
  for image_id, image_url in data.items():
805
934
  if image_id == 'isDefault':
@@ -814,7 +943,7 @@ class Connector(BaseConnector):
814
943
  if img is None or self.active_config['max_age'] is None \
815
944
  or (cache_date is not None and cache_date < (datetime.utcnow() - timedelta(seconds=self.active_config['max_age']))):
816
945
  try:
817
- image_download_response = requests.get(image_url, stream=True)
946
+ image_download_response = requests.get(image_url, stream=True, timeout=180)
818
947
  if image_download_response.status_code == requests.codes['ok']:
819
948
  img = Image.open(image_download_response.raw) # pyright: ignore[reportPossiblyUnboundVariable]
820
949
  if self.session.cache is not None:
@@ -921,9 +1050,19 @@ class Connector(BaseConnector):
921
1050
  raise CommandError('VIN in object hierarchy missing')
922
1051
  if 'command' not in command_arguments:
923
1052
  raise CommandError('Command argument missing')
1053
+ command_dict: Dict = {}
924
1054
  if command_arguments['command'] == ChargingStartStopCommand.Command.START:
925
1055
  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)
1056
+ if isinstance(vehicle, SeatCupraElectricVehicle) and vehicle.charging is not None and vehicle.charging.settings is not None \
1057
+ and vehicle.charging.settings.maximum_current is not None and vehicle.charging.settings.maximum_current.enabled \
1058
+ and vehicle.charging.settings.maximum_current.value is not None:
1059
+ if vehicle.charging.settings.maximum_current.value <= 6:
1060
+ command_dict['maxChargeCurrentAC'] = 'reduced'
1061
+ else:
1062
+ command_dict['maxChargeCurrentAC'] = 'maximum'
1063
+ else:
1064
+ command_dict['maxChargeCurrentAC'] = 'maximum'
1065
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
927
1066
  elif command_arguments['command'] == ChargingStartStopCommand.Command.STOP:
928
1067
  url = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/charging/requests/stop'
929
1068
  command_response: requests.Response = self.session.post(url, data='{}', allow_redirects=True)
@@ -949,21 +1088,203 @@ class Connector(BaseConnector):
949
1088
  if 'command' not in command_arguments:
950
1089
  raise CommandError('Command argument missing')
951
1090
  command_dict = {}
952
- command_str: Optional[str] = None
953
1091
  if command_arguments['command'] == ClimatizationStartStopCommand.Command.START:
954
- command_str = 'start'
1092
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/climatisation/start'
1093
+ if vehicle.climatization.settings is None:
1094
+ raise CommandError('Could not control climatisation, there are no climatisation settings for the vehicle available.')
1095
+ if 'target_temperature' in command_arguments:
1096
+ # Round target temperature to nearest 0.5
1097
+ command_dict['targetTemperature'] = round(command_arguments['target_temperature'] * 2) / 2
1098
+ elif vehicle.climatization.settings.target_temperature is not None and vehicle.climatization.settings.target_temperature.enabled \
1099
+ and vehicle.climatization.settings.target_temperature.value is not None:
1100
+ temperature_value = vehicle.climatization.settings.target_temperature.value
1101
+ if vehicle.climatization.settings.target_temperature.unit == Temperature.C:
1102
+ command_dict['targetTemperatureUnit'] = 'celsius'
1103
+ elif vehicle.climatization.settings.target_temperature.unit == Temperature.F:
1104
+ command_dict['targetTemperatureUnit'] = 'farenheit'
1105
+ else:
1106
+ command_dict['targetTemperatureUnit'] = 'celsius'
1107
+ if temperature_value is not None:
1108
+ command_dict['targetTemperature'] = round(temperature_value * 2) / 2
1109
+ if 'target_temperature_unit' in command_arguments:
1110
+ if command_arguments['target_temperature_unit'] == Temperature.C:
1111
+ command_dict['targetTemperatureUnit'] = 'celsius'
1112
+ elif command_arguments['target_temperature_unit'] == Temperature.F:
1113
+ command_dict['targetTemperatureUnit'] = 'farenheit'
1114
+ else:
1115
+ command_dict['targetTemperatureUnit'] = 'celsius'
955
1116
  elif command_arguments['command'] == ClimatizationStartStopCommand.Command.STOP:
956
- command_str = 'stop'
1117
+ url: str = f'https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/{vin}/climatisation/requests/stop'
957
1118
  else:
958
1119
  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
1120
  command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
962
1121
  if command_response.status_code not in [requests.codes['ok'], requests.codes['created']]:
963
1122
  LOG.error('Could not start/stop air conditioning (%s: %s)', command_response.status_code, command_response.text)
964
1123
  raise CommandError(f'Could not start/stop air conditioning ({command_response.status_code}: {command_response.text})')
965
1124
  return command_arguments
966
1125
 
1126
+ def __on_spin(self, spin_command: SpinCommand, command_arguments: Union[str, Dict[str, Any]]) \
1127
+ -> Union[str, Dict[str, Any]]:
1128
+ del spin_command
1129
+ if not isinstance(command_arguments, dict):
1130
+ raise CommandError('Command arguments are not a dictionary')
1131
+ if 'command' not in command_arguments:
1132
+ raise CommandError('Command argument missing')
1133
+ command_dict = {}
1134
+ if self.active_config['spin'] is None:
1135
+ raise CommandError('S-PIN is missing, please add S-PIN to your configuration or .netrc file')
1136
+ if 'spin' in command_arguments:
1137
+ command_dict['currentSpin'] = command_arguments['spin']
1138
+ else:
1139
+ if self.active_config['spin'] is None or self.active_config['spin'] == '':
1140
+ raise CommandError('S-PIN is missing, please add S-PIN to your configuration or .netrc file')
1141
+ command_dict['spin'] = self.active_config['spin']
1142
+ if command_arguments['command'] == SpinCommand.Command.VERIFY:
1143
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/users/{self.session.user_id}/spin/verify'
1144
+ else:
1145
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
1146
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1147
+ if command_response.status_code != requests.codes['ok']:
1148
+ LOG.error('Could not execute spin command (%s: %s)', command_response.status_code, command_response.text)
1149
+ raise CommandError(f'Could not execute spin command ({command_response.status_code}: {command_response.text})')
1150
+ else:
1151
+ LOG.info('Spin verify command executed successfully')
1152
+ return command_arguments
1153
+
1154
+ def __on_wake_sleep(self, wake_sleep_command: WakeSleepCommand, command_arguments: Union[str, Dict[str, Any]]) \
1155
+ -> Union[str, Dict[str, Any]]:
1156
+ if wake_sleep_command.parent is None or wake_sleep_command.parent.parent is None \
1157
+ or not isinstance(wake_sleep_command.parent.parent, GenericVehicle):
1158
+ raise CommandError('Object hierarchy is not as expected')
1159
+ if not isinstance(command_arguments, dict):
1160
+ raise CommandError('Command arguments are not a dictionary')
1161
+ vehicle: GenericVehicle = wake_sleep_command.parent.parent
1162
+ vin: Optional[str] = vehicle.vin.value
1163
+ if vin is None:
1164
+ raise CommandError('VIN in object hierarchy missing')
1165
+ if 'command' not in command_arguments:
1166
+ raise CommandError('Command argument missing')
1167
+ if command_arguments['command'] == WakeSleepCommand.Command.WAKE:
1168
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/vehicle-wakeup/request'
1169
+
1170
+ command_response: requests.Response = self.session.post(url, data='{}', allow_redirects=True)
1171
+ if command_response.status_code not in (requests.codes['ok'], requests.codes['no_content']):
1172
+ LOG.error('Could not execute wake command (%s: %s)', command_response.status_code, command_response.text)
1173
+ raise CommandError(f'Could not execute wake command ({command_response.status_code}: {command_response.text})')
1174
+ elif command_arguments['command'] == WakeSleepCommand.Command.SLEEP:
1175
+ raise CommandError('Sleep command not supported by vehicle. Vehicle will put itself to sleep')
1176
+ else:
1177
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
1178
+ return command_arguments
1179
+
1180
+ def __on_honk_flash(self, honk_flash_command: HonkAndFlashCommand, command_arguments: Union[str, Dict[str, Any]]) \
1181
+ -> Union[str, Dict[str, Any]]:
1182
+ if honk_flash_command.parent is None or honk_flash_command.parent.parent is None \
1183
+ or not isinstance(honk_flash_command.parent.parent, GenericVehicle):
1184
+ raise CommandError('Object hierarchy is not as expected')
1185
+ if not isinstance(command_arguments, dict):
1186
+ raise CommandError('Command arguments are not a dictionary')
1187
+ vehicle: GenericVehicle = honk_flash_command.parent.parent
1188
+ vin: Optional[str] = vehicle.vin.value
1189
+ if vin is None:
1190
+ raise CommandError('VIN in object hierarchy missing')
1191
+ if 'command' not in command_arguments:
1192
+ raise CommandError('Command argument missing')
1193
+ command_dict = {}
1194
+ if command_arguments['command'] in [HonkAndFlashCommand.Command.FLASH, HonkAndFlashCommand.Command.HONK_AND_FLASH]:
1195
+ if 'duration' in command_arguments:
1196
+ command_dict['durationInSeconds'] = command_arguments['duration']
1197
+ else:
1198
+ command_dict['durationInSeconds'] = 10
1199
+ command_dict['mode'] = command_arguments['command'].value
1200
+ command_dict['userPosition'] = {}
1201
+ if vehicle.position is None or vehicle.position.latitude is None or vehicle.position.longitude is None \
1202
+ or vehicle.position.latitude.value is None or vehicle.position.longitude.value is None \
1203
+ or not vehicle.position.latitude.enabled or not vehicle.position.longitude.enabled:
1204
+ raise CommandError('Can only execute honk and flash commands if vehicle position is known')
1205
+ command_dict['userPosition']['latitude'] = vehicle.position.latitude.value
1206
+ command_dict['userPosition']['longitude'] = vehicle.position.longitude.value
1207
+
1208
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/honk-and-flash'
1209
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1210
+ if command_response.status_code not in (requests.codes['ok'], requests.codes['no_content']):
1211
+ LOG.error('Could not execute honk or flash command (%s: %s)', command_response.status_code, command_response.text)
1212
+ raise CommandError(f'Could not execute honk or flash command ({command_response.status_code}: {command_response.text})')
1213
+ else:
1214
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
1215
+ return command_arguments
1216
+
1217
+ def __on_lock_unlock(self, lock_unlock_command: LockUnlockCommand, command_arguments: Union[str, Dict[str, Any]]) \
1218
+ -> Union[str, Dict[str, Any]]:
1219
+ if lock_unlock_command.parent is None or lock_unlock_command.parent.parent is None \
1220
+ or lock_unlock_command.parent.parent.parent is None or not isinstance(lock_unlock_command.parent.parent.parent, GenericVehicle):
1221
+ raise CommandError('Object hierarchy is not as expected')
1222
+ if not isinstance(command_arguments, dict):
1223
+ raise SetterError('Command arguments are not a dictionary')
1224
+ vehicle: GenericVehicle = lock_unlock_command.parent.parent.parent
1225
+ vin: Optional[str] = vehicle.vin.value
1226
+ if vin is None:
1227
+ raise CommandError('VIN in object hierarchy missing')
1228
+ if 'command' not in command_arguments:
1229
+ raise CommandError('Command argument missing')
1230
+ command_dict = {}
1231
+ if 'spin' in command_arguments:
1232
+ command_dict['spin'] = command_arguments['spin']
1233
+ else:
1234
+ if self.active_config['spin'] is None:
1235
+ raise CommandError('S-PIN is missing, please add S-PIN to your configuration or .netrc file')
1236
+ command_dict['spin'] = self.active_config['spin']
1237
+ if command_arguments['command'] == LockUnlockCommand.Command.LOCK:
1238
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/access/lock'
1239
+ elif command_arguments['command'] == LockUnlockCommand.Command.UNLOCK:
1240
+ url = f'https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/{vin}/access/unlock'
1241
+ else:
1242
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
1243
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1244
+ if command_response.status_code != requests.codes['ok']:
1245
+ LOG.error('Could not execute locking command (%s: %s)', command_response.status_code, command_response.text)
1246
+ raise CommandError(f'Could not execute locking command ({command_response.status_code}: {command_response.text})')
1247
+ return command_arguments
1248
+
1249
+ def __on_air_conditioning_settings_change(self, attribute: GenericAttribute, value: Any) -> Any:
1250
+ """
1251
+ Callback for the climatization setting change.
1252
+ """
1253
+ if attribute.parent is None or not isinstance(attribute.parent, SeatCupraClimatization.Settings) \
1254
+ or attribute.parent.parent is None \
1255
+ or attribute.parent.parent.parent is None or not isinstance(attribute.parent.parent.parent, SeatCupraVehicle):
1256
+ raise SetterError('Object hierarchy is not as expected')
1257
+ settings: SeatCupraClimatization.Settings = attribute.parent
1258
+ vehicle: SeatCupraVehicle = attribute.parent.parent.parent
1259
+ vin: Optional[str] = vehicle.vin.value
1260
+ if vin is None:
1261
+ raise SetterError('VIN in object hierarchy missing')
1262
+ setting_dict = {}
1263
+ if settings.target_temperature.enabled and settings.target_temperature.value is not None:
1264
+ # Round target temperature to nearest 0.5
1265
+ # Check if the attribute changed is the target_temperature attribute
1266
+ if isinstance(attribute, TemperatureAttribute) and attribute.id == 'target_temperature':
1267
+ setting_dict['targetTemperature'] = round(value * 2) / 2
1268
+ else:
1269
+ setting_dict['targetTemperature'] = round(settings.target_temperature.value * 2) / 2
1270
+ if settings.target_temperature.unit == Temperature.C:
1271
+ setting_dict['targetTemperatureUnit'] = 'celsius'
1272
+ elif settings.target_temperature.unit == Temperature.F:
1273
+ setting_dict['targetTemperatureUnit'] = 'farenheit'
1274
+ else:
1275
+ setting_dict['targetTemperatureUnit'] = 'celsius'
1276
+ if isinstance(attribute, BooleanAttribute) and attribute.id == 'climatisation_without_external_power':
1277
+ setting_dict['climatisationWithoutExternalPower'] = value
1278
+ elif settings.climatization_without_external_power.enabled and settings.climatization_without_external_power.value is not None:
1279
+ setting_dict['climatisationWithoutExternalPower'] = settings.climatization_without_external_power.value
1280
+
1281
+ url: str = f'https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/{vin}/climatisation/settings'
1282
+ settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
1283
+ if settings_response.status_code not in [requests.codes['ok'], requests.codes['created']]:
1284
+ LOG.error('Could not set climatization settings (%s) %s', settings_response.status_code, settings_response.text)
1285
+ raise SetterError(f'Could not set value ({settings_response.status_code}): {settings_response.text}')
1286
+ return value
1287
+
967
1288
  def get_version(self) -> str:
968
1289
  return __version__
969
1290
 
@@ -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(vehicle=self, 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.