hyundai-kia-connect-api 3.17.6__py2.py3-none-any.whl → 3.32.0__py2.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.
@@ -6,8 +6,8 @@ import base64
6
6
  import random
7
7
  import datetime as dt
8
8
  import logging
9
- import re
10
9
  import uuid
10
+ import math
11
11
  from time import sleep
12
12
  from urllib.parse import parse_qs, urlparse
13
13
 
@@ -17,9 +17,11 @@ from bs4 import BeautifulSoup
17
17
  from dateutil import tz
18
18
 
19
19
  from .ApiImpl import (
20
- ApiImpl,
21
20
  ClimateRequestOptions,
21
+ ScheduleChargingClimateRequestOptions,
22
22
  )
23
+ from .ApiImplType1 import ApiImplType1
24
+
23
25
  from .Token import Token
24
26
  from .Vehicle import (
25
27
  Vehicle,
@@ -43,23 +45,31 @@ from .const import (
43
45
  SEAT_STATUS,
44
46
  TEMPERATURE_UNITS,
45
47
  VEHICLE_LOCK_ACTION,
48
+ VALET_MODE_ACTION,
49
+ )
50
+ from .exceptions import (
51
+ AuthenticationError,
52
+ DuplicateRequestError,
53
+ RequestTimeoutError,
54
+ ServiceTemporaryUnavailable,
55
+ NoDataFound,
56
+ InvalidAPIResponseError,
57
+ APIError,
58
+ RateLimitingError,
59
+ DeviceIDError,
46
60
  )
47
- from .exceptions import *
48
61
  from .utils import (
49
62
  get_child_value,
50
63
  get_index_into_hex_temp,
51
64
  get_hex_temp_into_index,
65
+ parse_datetime,
52
66
  )
53
67
 
54
68
  _LOGGER = logging.getLogger(__name__)
55
69
 
56
70
  USER_AGENT_OK_HTTP: str = "okhttp/3.12.0"
57
- USER_AGENT_MOZILLA: str = (
58
- "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19" # noqa
59
- )
60
- ACCEPT_HEADER_ALL: str = (
61
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" # noqa
62
- )
71
+ USER_AGENT_MOZILLA: str = "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19" # noqa
72
+ ACCEPT_HEADER_ALL: str = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" # noqa
63
73
 
64
74
  SUPPORTED_LANGUAGES_LIST = [
65
75
  "en", # English
@@ -122,12 +132,11 @@ def _check_response_for_errors(response: dict) -> None:
122
132
  )
123
133
 
124
134
 
125
- class KiaUvoApiEU(ApiImpl):
135
+ class KiaUvoApiEU(ApiImplType1):
126
136
  data_timezone = tz.gettz("Europe/Berlin")
127
137
  temperature_range = [x * 0.5 for x in range(28, 60)]
128
138
 
129
139
  def __init__(self, region: int, brand: int, language: str) -> None:
130
- self.ccu_ccs2_protocol_support = None
131
140
  language = language.lower()
132
141
  # Strip language variants (e.g. en-Gb)
133
142
  if len(language) > 2:
@@ -159,9 +168,7 @@ class KiaUvoApiEU(ApiImpl):
159
168
  self.CFB: str = base64.b64decode(
160
169
  "RFtoRq/vDXJmRndoZaZQyfOot7OrIqGVFj96iY2WL3yyH5Z/pUvlUhqmCxD2t+D65SQ="
161
170
  )
162
- self.BASIC_AUTHORIZATION: str = (
163
- "Basic NmQ0NzdjMzgtM2NhNC00Y2YzLTk1NTctMmExOTI5YTk0NjU0OktVeTQ5WHhQekxwTHVvSzB4aEJDNzdXNlZYaG10UVI5aVFobUlGampvWTRJcHhzVg==" # noqa
164
- )
171
+ self.BASIC_AUTHORIZATION: str = "Basic NmQ0NzdjMzgtM2NhNC00Y2YzLTk1NTctMmExOTI5YTk0NjU0OktVeTQ5WHhQekxwTHVvSzB4aEJDNzdXNlZYaG10UVI5aVFobUlGampvWTRJcHhzVg==" # noqa
165
172
  self.LOGIN_FORM_HOST = "eu-account.hyundai.com"
166
173
  self.PUSH_TYPE = "GCM"
167
174
  elif BRANDS[self.brand] == BRAND_GENESIS:
@@ -172,9 +179,7 @@ class KiaUvoApiEU(ApiImpl):
172
179
  self.CFB: str = base64.b64decode(
173
180
  "RFtoRq/vDXJmRndoZaZQyYo3/qFLtVReW8P7utRPcc0ZxOzOELm9mexvviBk/qqIp4A="
174
181
  )
175
- self.BASIC_AUTHORIZATION: str = (
176
- "Basic NmQ0NzdjMzgtM2NhNC00Y2YzLTk1NTctMmExOTI5YTk0NjU0OktVeTQ5WHhQekxwTHVvSzB4aEJDNzdXNlZYaG10UVI5aVFobUlGampvWTRJcHhzVg==" # noqa
177
- )
182
+ self.BASIC_AUTHORIZATION: str = "Basic NmQ0NzdjMzgtM2NhNC00Y2YzLTk1NTctMmExOTI5YTk0NjU0OktVeTQ5WHhQekxwTHVvSzB4aEJDNzdXNlZYaG10UVI5aVFobUlGampvWTRJcHhzVg==" # noqa
178
183
  self.LOGIN_FORM_HOST = "accounts-eu.genesis.com"
179
184
  self.PUSH_TYPE = "GCM"
180
185
 
@@ -226,18 +231,14 @@ class KiaUvoApiEU(ApiImpl):
226
231
  + "&state=$service_id:$user_id"
227
232
  )
228
233
 
229
- def _get_authenticated_headers(self, token: Token) -> dict:
230
- return {
231
- "Authorization": token.access_token,
232
- "ccsp-service-id": self.CCSP_SERVICE_ID,
233
- "ccsp-application-id": self.APP_ID,
234
- "Stamp": self._get_stamp(),
235
- "ccsp-device-id": token.device_id,
236
- "Host": self.BASE_URL,
237
- "Connection": "Keep-Alive",
238
- "Accept-Encoding": "gzip",
239
- "Ccuccs2protocolsupport": self.ccu_ccs2_protocol_support,
240
- "User-Agent": USER_AGENT_OK_HTTP,
234
+ def _get_control_headers(self, token: Token, vehicle: Vehicle) -> dict:
235
+ control_token, _ = self._get_control_token(token)
236
+ authenticated_headers = self._get_authenticated_headers(
237
+ token, vehicle.ccu_ccs2_protocol_support
238
+ )
239
+ return authenticated_headers | {
240
+ "Authorization": control_token,
241
+ "AuthorizationCCSP": control_token,
241
242
  }
242
243
 
243
244
  def login(self, username: str, password: str) -> Token:
@@ -277,7 +278,8 @@ class KiaUvoApiEU(ApiImpl):
277
278
  def get_vehicles(self, token: Token) -> list[Vehicle]:
278
279
  url = self.SPA_API_URL + "vehicles"
279
280
  response = requests.get(
280
- url, headers=self._get_authenticated_headers(token)
281
+ url,
282
+ headers=self._get_authenticated_headers(token),
281
283
  ).json()
282
284
  _LOGGER.debug(f"{DOMAIN} - Get Vehicles Response: {response}")
283
285
  _check_response_for_errors(response)
@@ -307,25 +309,6 @@ class KiaUvoApiEU(ApiImpl):
307
309
  result.append(vehicle)
308
310
  return result
309
311
 
310
- def get_last_updated_at(self, value) -> dt.datetime:
311
- _LOGGER.debug(f"{DOMAIN} - last_updated_at - before {value}")
312
- if value is None:
313
- value = dt.datetime(2000, 1, 1, tzinfo=self.data_timezone)
314
- else:
315
- m = re.match(r"(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})", value)
316
- value = dt.datetime(
317
- year=int(m.group(1)),
318
- month=int(m.group(2)),
319
- day=int(m.group(3)),
320
- hour=int(m.group(4)),
321
- minute=int(m.group(5)),
322
- second=int(m.group(6)),
323
- tzinfo=self.data_timezone,
324
- )
325
-
326
- _LOGGER.debug(f"{DOMAIN} - last_updated_at - after {value}")
327
- return value
328
-
329
312
  def _get_time_from_string(self, value, timesection) -> dt.datetime.time:
330
313
  if value is not None:
331
314
  lastTwo = int(value[-2:])
@@ -341,12 +324,29 @@ class KiaUvoApiEU(ApiImpl):
341
324
  return value
342
325
 
343
326
  def update_vehicle_with_cached_state(self, token: Token, vehicle: Vehicle) -> None:
344
- state = self._get_cached_vehicle_state(token, vehicle)
345
- self.ccu_ccs2_protocol_support = str(vehicle.ccu_ccs2_protocol_support)
327
+ url = self.SPA_API_URL + "vehicles/" + vehicle.id
328
+ is_ccs2 = vehicle.ccu_ccs2_protocol_support != 0
329
+ if is_ccs2:
330
+ url += "/ccs2/carstatus/latest"
331
+ else:
332
+ url += "/status/latest"
333
+
334
+ response = requests.get(
335
+ url,
336
+ headers=self._get_authenticated_headers(
337
+ token, vehicle.ccu_ccs2_protocol_support
338
+ ),
339
+ ).json()
340
+
341
+ _LOGGER.debug(f"{DOMAIN} - get_cached_vehicle_status response: {response}")
342
+ _check_response_for_errors(response)
346
343
 
347
344
  if vehicle.ccu_ccs2_protocol_support == 0:
348
- self._update_vehicle_properties(vehicle, state)
345
+ self._update_vehicle_properties(
346
+ vehicle, response["resMsg"]["vehicleStatusInfo"]
347
+ )
349
348
  else:
349
+ state = response["resMsg"]["state"]["Vehicle"]
350
350
  self._update_vehicle_properties_ccs2(vehicle, state)
351
351
 
352
352
  if (
@@ -396,294 +396,23 @@ class KiaUvoApiEU(ApiImpl):
396
396
  else:
397
397
  self._update_vehicle_drive_info(vehicle, state)
398
398
 
399
- def _update_vehicle_properties_ccs2(self, vehicle: Vehicle, state: dict) -> None:
400
- if get_child_value(state, "Date"):
401
- vehicle.last_updated_at = self.get_last_updated_at(
402
- get_child_value(state, "Date")
403
- )
404
- else:
405
- vehicle.last_updated_at = dt.datetime.now(self.data_timezone)
406
-
407
- vehicle.odometer = (
408
- get_child_value(state, "Drivetrain.Odometer"),
409
- DISTANCE_UNITS[1],
410
- )
411
- vehicle.car_battery_percentage = get_child_value(
412
- state, "Electronics.Battery.Level"
413
- )
414
-
415
- vehicle.engine_is_running = get_child_value(state, "DrivingReady")
416
-
417
- air_temp = get_child_value(
418
- state,
419
- "Cabin.HVAC.Row1.Driver.Temperature.Value",
420
- )
421
-
422
- if air_temp != "OFF":
423
- vehicle.air_temperature = (air_temp, TEMPERATURE_UNITS[1])
424
-
425
- defrost_is_on = get_child_value(state, "Body.Windshield.Front.Defog.State")
426
- if defrost_is_on in [0, 2]:
427
- vehicle.defrost_is_on = False
428
- elif defrost_is_on == 1:
429
- vehicle.defrost_is_on = True
430
-
431
- steer_wheel_heat = get_child_value(state, "Cabin.SteeringWheel.Heat.State")
432
- if steer_wheel_heat in [0, 2]:
433
- vehicle.steering_wheel_heater_is_on = False
434
- elif steer_wheel_heat == 1:
435
- vehicle.steering_wheel_heater_is_on = True
436
-
437
- defrost_rear_is_on = get_child_value(state, "Body.Windshield.Rear.Defog.State")
438
- if defrost_rear_is_on in [0, 2]:
439
- vehicle.back_window_heater_is_on = False
440
- elif defrost_rear_is_on == 1:
441
- vehicle.back_window_heater_is_on = True
442
-
443
- # TODO: status.sideMirrorHeat
444
-
445
- vehicle.front_left_seat_status = SEAT_STATUS[
446
- get_child_value(state, "Cabin.Seat.Row1.Driver.Climate.State")
447
- ]
448
-
449
- vehicle.front_right_seat_status = SEAT_STATUS[
450
- get_child_value(state, "Cabin.Seat.Row1.Passenger.Climate.State")
451
- ]
452
-
453
- vehicle.rear_left_seat_status = SEAT_STATUS[
454
- get_child_value(state, "Cabin.Seat.Row2.Left.Climate.State")
455
- ]
456
-
457
- vehicle.rear_right_seat_status = SEAT_STATUS[
458
- get_child_value(state, "Cabin.Seat.Row2.Right.Climate.State")
459
- ]
460
-
461
- # TODO: status.doorLock
462
-
463
- vehicle.front_left_door_is_open = get_child_value(
464
- state, "Cabin.Door.Row1.Driver.Open"
465
- )
466
- vehicle.front_right_door_is_open = get_child_value(
467
- state, "Cabin.Door.Row1.Passenger.Open"
468
- )
469
- vehicle.back_left_door_is_open = get_child_value(
470
- state, "Cabin.Door.Row2.Left.Open"
471
- )
472
- vehicle.back_right_door_is_open = get_child_value(
473
- state, "Cabin.Door.Row2.Right.Open"
474
- )
475
-
476
- # TODO: should the windows and trunc also be checked?
477
- vehicle.is_locked = not (
478
- vehicle.front_left_door_is_open
479
- or vehicle.front_right_door_is_open
480
- or vehicle.back_left_door_is_open
481
- or vehicle.back_right_door_is_open
482
- )
483
-
484
- vehicle.hood_is_open = get_child_value(state, "Body.Hood.Open")
485
- vehicle.front_left_window_is_open = get_child_value(
486
- state, "Cabin.Window.Row1.Driver.Open"
487
- )
488
- vehicle.front_right_window_is_open = get_child_value(
489
- state, "Cabin.Window.Row1.Passenger.Open"
490
- )
491
- vehicle.back_left_window_is_open = get_child_value(
492
- state, "Cabin.Window.Row2.Left.Open"
493
- )
494
- vehicle.back_right_window_is_open = get_child_value(
495
- state, "Cabin.Window.Row2.Right.Open"
496
- )
497
- vehicle.tire_pressure_rear_left_warning_is_on = bool(
498
- get_child_value(state, "Chassis.Axle.Row2.Left.Tire.PressureLow")
499
- )
500
- vehicle.tire_pressure_front_left_warning_is_on = bool(
501
- get_child_value(state, "Chassis.Axle.Row1.Left.Tire.PressureLow")
502
- )
503
- vehicle.tire_pressure_front_right_warning_is_on = bool(
504
- get_child_value(state, "Chassis.Axle.Row1.Right.Tire.PressureLow")
505
- )
506
- vehicle.tire_pressure_rear_right_warning_is_on = bool(
507
- get_child_value(state, "Chassis.Axle.Row2.Right.Tire.PressureLow")
508
- )
509
- vehicle.tire_pressure_all_warning_is_on = bool(
510
- get_child_value(state, "Chassis.Axle.Tire.PressureLow")
511
- )
512
- vehicle.trunk_is_open = get_child_value(state, "Body.Trunk.Open")
513
-
514
- vehicle.ev_battery_percentage = get_child_value(
515
- state, "Green.BatteryManagement.BatteryRemain.Ratio"
516
- )
517
- vehicle.ev_battery_remain = get_child_value(
518
- state, "Green.BatteryManagement.BatteryRemain.Value"
519
- )
520
- vehicle.ev_battery_capacity = get_child_value(
521
- state, "Green.BatteryManagement.BatteryCapacity.Value"
522
- )
523
- vehicle.ev_battery_soh_percentage = get_child_value(
524
- state, "Green.BatteryManagement.SoH.Ratio"
525
- )
526
- vehicle.ev_battery_is_plugged_in = get_child_value(
527
- state, "Green.ChargingInformation.ElectricCurrentLevel.State"
528
- )
529
- vehicle.ev_battery_is_plugged_in = get_child_value(
530
- state, "Green.ChargingInformation.ConnectorFastening.State"
531
- )
532
- charging_door_state = get_child_value(state, "Green.ChargingDoor.State")
533
- if charging_door_state in [0, 2]:
534
- vehicle.ev_charge_port_door_is_open = False
535
- elif charging_door_state == 1:
536
- vehicle.ev_charge_port_door_is_open = True
537
-
538
- vehicle.total_driving_range = (
539
- float(
540
- get_child_value(
541
- state,
542
- "Drivetrain.FuelSystem.DTE.Total", # noqa
543
- )
544
- ),
545
- DISTANCE_UNITS[
546
- get_child_value(
547
- state,
548
- "Drivetrain.FuelSystem.DTE.Unit", # noqa
549
- )
550
- ],
551
- )
552
-
553
- if vehicle.engine_type == ENGINE_TYPES.EV:
554
- # ev_driving_range is the same as total_driving_range for pure EV
555
- vehicle.ev_driving_range = (
556
- vehicle.total_driving_range,
557
- vehicle.total_driving_range_unit,
558
- )
559
- # TODO: vehicle.ev_driving_range for non EV
560
-
561
- vehicle.washer_fluid_warning_is_on = get_child_value(
562
- state, "Body.Windshield.Front.WasherFluid.LevelLow"
563
- )
564
-
565
- vehicle.ev_estimated_current_charge_duration = (
566
- get_child_value(state, "Green.ChargingInformation.Charging.RemainTime"),
567
- "m",
568
- )
569
- vehicle.ev_estimated_fast_charge_duration = (
570
- get_child_value(state, "Green.ChargingInformation.EstimatedTime.Standard"),
571
- "m",
572
- )
573
- vehicle.ev_estimated_portable_charge_duration = (
574
- get_child_value(state, "Green.ChargingInformation.EstimatedTime.ICCB"),
575
- "m",
576
- )
577
- vehicle.ev_estimated_station_charge_duration = (
578
- get_child_value(state, "Green.ChargingInformation.EstimatedTime.Quick"),
579
- "m",
580
- )
581
- vehicle.ev_charge_limits_ac = get_child_value(
582
- state, "Green.ChargingInformation.TargetSoC.Standard"
583
- )
584
- vehicle.ev_charge_limits_dc = get_child_value(
585
- state, "Green.ChargingInformation.TargetSoC.Quick"
586
- )
587
- vehicle.ev_v2l_discharge_limit = get_child_value(
588
- state, "Green.Electric.SmartGrid.VehicleToLoad.DischargeLimitation.SoC"
589
- )
590
- vehicle.ev_target_range_charge_AC = (
591
- get_child_value(
592
- state,
593
- "Green.ChargingInformation.DTE.TargetSoC.Standard", # noqa
594
- ),
595
- DISTANCE_UNITS[
596
- get_child_value(
597
- state,
598
- "Drivetrain.FuelSystem.DTE.Unit", # noqa
599
- )
600
- ],
601
- )
602
- vehicle.ev_target_range_charge_DC = (
603
- get_child_value(
604
- state,
605
- "Green.ChargingInformation.DTE.TargetSoC.Quick", # noqa
606
- ),
607
- DISTANCE_UNITS[
608
- get_child_value(
609
- state,
610
- "Drivetrain.FuelSystem.DTE.Unit", # noqa
611
- )
612
- ],
613
- )
614
- vehicle.ev_first_departure_enabled = bool(
615
- get_child_value(state, "Green.Reservation.Departure.Schedule1.Enable")
616
- )
617
-
618
- vehicle.ev_second_departure_enabled = bool(
619
- get_child_value(state, "Green.Reservation.Departure.Schedule2.Enable")
620
- )
621
-
622
- # TODO: vehicle.ev_first_departure_days --> Green.Reservation.Departure.Schedule1.(Mon,Tue,Wed,Thu,Fri,Sat,Sun) # noqa
623
- # TODO: vehicle.ev_second_departure_days --> Green.Reservation.Departure.Schedule2.(Mon,Tue,Wed,Thu,Fri,Sat,Sun) # noqa
624
- # TODO: vehicle.ev_first_departure_time --> Green.Reservation.Departure.Schedule1.(Min,Hour) # noqa
625
- # TODO: vehicle.ev_second_departure_time --> Green.Reservation.Departure.Schedule2.(Min,Hour) # noqa
626
- # TODO: vehicle.ev_off_peak_charge_only_enabled --> unknown settings are in --> Green.Reservation.OffPeakTime and OffPeakTime2 # noqa
627
-
628
- vehicle.washer_fluid_warning_is_on = get_child_value(
629
- state, "Body.Windshield.Front.WasherFluid.LevelLow"
630
- )
631
- vehicle.brake_fluid_warning_is_on = get_child_value(
632
- state, "Chassis.Brake.Fluid.Warning"
633
- )
634
-
635
- vehicle.fuel_level = get_child_value(state, "Drivetrain.FuelSystem.FuelLevel")
636
- vehicle.fuel_level_is_low = get_child_value(
637
- state, "Drivetrain.FuelSystem.LowFuelWarning"
638
- )
639
- vehicle.air_control_is_on = get_child_value(
640
- state, "Cabin.HVAC.Row1.Driver.Blower.SpeedLevel"
641
- )
642
- vehicle.smart_key_battery_warning_is_on = bool(
643
- get_child_value(state, "Electronics.FOB.LowBattery")
644
- )
645
-
646
- if get_child_value(state, "Location.GeoCoord.Latitude"):
647
- location_last_updated_at = dt.datetime(
648
- 2000, 1, 1, tzinfo=self.data_timezone
649
- )
650
- timestamp = get_child_value(state, "Location.TimeStamp")
651
- if timestamp is not None:
652
- location_last_updated_at = dt.datetime(
653
- year=int(get_child_value(timestamp, "Year")),
654
- month=int(get_child_value(timestamp, "Mon")),
655
- day=int(get_child_value(timestamp, "Day")),
656
- hour=int(get_child_value(timestamp, "Hour")),
657
- minute=int(get_child_value(timestamp, "Min")),
658
- second=int(get_child_value(timestamp, "Sec")),
659
- tzinfo=self.data_timezone,
660
- )
661
-
662
- vehicle.location = (
663
- get_child_value(state, "Location.GeoCoord.Latitude"),
664
- get_child_value(state, "Location.GeoCoord.Longitude"),
665
- location_last_updated_at,
666
- )
667
-
668
- vehicle.data = state
669
-
670
399
  def _update_vehicle_properties(self, vehicle: Vehicle, state: dict) -> None:
671
400
  if get_child_value(state, "vehicleStatus.time"):
672
- vehicle.last_updated_at = self.get_last_updated_at(
673
- get_child_value(state, "vehicleStatus.time")
401
+ vehicle.last_updated_at = parse_datetime(
402
+ get_child_value(state, "vehicleStatus.time"), self.data_timezone
674
403
  )
675
404
  else:
676
405
  vehicle.last_updated_at = dt.datetime.now(self.data_timezone)
677
-
678
- vehicle.odometer = (
679
- get_child_value(state, "odometer.value"),
680
- DISTANCE_UNITS[
681
- get_child_value(
682
- state,
683
- "odometer.unit",
684
- )
685
- ],
686
- )
406
+ if get_child_value(state, "odometer.value"):
407
+ vehicle.odometer = (
408
+ get_child_value(state, "odometer.value"),
409
+ DISTANCE_UNITS[
410
+ get_child_value(
411
+ state,
412
+ "odometer.unit",
413
+ )
414
+ ],
415
+ )
687
416
  vehicle.car_battery_percentage = get_child_value(
688
417
  state, "vehicleStatus.battery.batSoc"
689
418
  )
@@ -791,6 +520,11 @@ class KiaUvoApiEU(ApiImpl):
791
520
  vehicle.ev_charge_port_door_is_open = True
792
521
  elif ev_charge_port_door_is_open == 2:
793
522
  vehicle.ev_charge_port_door_is_open = False
523
+
524
+ vehicle.ev_charging_power = get_child_value(
525
+ state, "vehicleStatus.evStatus.batteryPower.batteryStndChrgPower"
526
+ )
527
+
794
528
  if (
795
529
  get_child_value(
796
530
  state,
@@ -866,7 +600,7 @@ class KiaUvoApiEU(ApiImpl):
866
600
  vehicle.ev_charge_limits_dc = [
867
601
  x["targetSOClevel"] for x in target_soc_list if x["plugType"] == 0
868
602
  ][-1]
869
- except:
603
+ except Exception:
870
604
  _LOGGER.debug(f"{DOMAIN} - SOC Levels couldn't be found. May not be an EV.")
871
605
  if (
872
606
  get_child_value(
@@ -962,6 +696,72 @@ class KiaUvoApiEU(ApiImpl):
962
696
  ),
963
697
  )
964
698
 
699
+ vehicle.ev_first_departure_climate_enabled = bool(
700
+ get_child_value(
701
+ state,
702
+ "vehicleStatus.evStatus.reservChargeInfos.reservChargeInfo.reservChargeInfoDetail.reservFatcSet.airCtrl", # noqa
703
+ )
704
+ )
705
+
706
+ vehicle.ev_second_departure_climate_enabled = bool(
707
+ get_child_value(
708
+ state,
709
+ "vehicleStatus.evStatus.reservChargeInfos.reserveChargeInfo2.reservChargeInfoDetail.reservFatcSet.airCtrl", # noqa
710
+ )
711
+ )
712
+
713
+ if get_child_value(
714
+ state,
715
+ "vehicleStatus.evStatus.reservChargeInfos.reservChargeInfo.reservChargeInfoDetail.reservFatcSet.airTemp.value", # noqa
716
+ ):
717
+ temp_index = get_hex_temp_into_index(
718
+ get_child_value(
719
+ state,
720
+ "vehicleStatus.evStatus.reservChargeInfos.reservChargeInfo.reservChargeInfoDetail.reservFatcSet.airTemp.value", # noqa
721
+ )
722
+ )
723
+
724
+ vehicle.ev_first_departure_climate_temperature = (
725
+ self.temperature_range[temp_index],
726
+ TEMPERATURE_UNITS[
727
+ get_child_value(
728
+ state,
729
+ "vehicleStatus.evStatus.reservChargeInfos.reservChargeInfo.reservChargeInfoDetail.reservFatcSet.airTemp.unit", # noqa
730
+ )
731
+ ],
732
+ )
733
+
734
+ if get_child_value(
735
+ state,
736
+ "vehicleStatus.evStatus.reservChargeInfos.reserveChargeInfo2.reservChargeInfoDetail.reservFatcSet.airTemp.value", # noqa
737
+ ):
738
+ temp_index = get_hex_temp_into_index(
739
+ get_child_value(
740
+ state,
741
+ "vehicleStatus.evStatus.reservChargeInfos.reserveChargeInfo2.reservChargeInfoDetail.reservFatcSet.airTemp.value", # noqa
742
+ )
743
+ )
744
+
745
+ vehicle.ev_second_departure_climate_temperature = (
746
+ self.temperature_range[temp_index],
747
+ TEMPERATURE_UNITS[
748
+ get_child_value(
749
+ state,
750
+ "vehicleStatus.evStatus.reservChargeInfos.reserveChargeInfo2.reservChargeInfoDetail.reservFatcSet.airTemp.unit", # noqa
751
+ )
752
+ ],
753
+ )
754
+
755
+ vehicle.ev_first_departure_climate_defrost = get_child_value(
756
+ state,
757
+ "vehicleStatus.evStatus.reservChargeInfos.reservChargeInfo.reservChargeInfoDetail.reservFatcSet.defrost", # noqa
758
+ )
759
+
760
+ vehicle.ev_second_departure_climate_defrost = get_child_value(
761
+ state,
762
+ "vehicleStatus.evStatus.reservChargeInfos.reserveChargeInfo2.reservChargeInfoDetail.reservFatcSet.defrost", # noqa
763
+ )
764
+
965
765
  vehicle.ev_off_peak_start_time = self._get_time_from_string(
966
766
  get_child_value(
967
767
  state,
@@ -1005,6 +805,23 @@ class KiaUvoApiEU(ApiImpl):
1005
805
  ):
1006
806
  vehicle.ev_off_peak_charge_only_enabled = False
1007
807
 
808
+ if (
809
+ get_child_value(
810
+ state,
811
+ "vehicleStatus.evStatus.reservChargeInfos.reservFlag", # noqa
812
+ )
813
+ == 1
814
+ ):
815
+ vehicle.ev_schedule_charge_enabled = True
816
+ elif (
817
+ get_child_value(
818
+ state,
819
+ "vehicleStatus.evStatus.reservChargeInfos.reservFlag", # noqa
820
+ )
821
+ == 0
822
+ ):
823
+ vehicle.ev_schedule_charge_enabled = False
824
+
1008
825
  vehicle.washer_fluid_warning_is_on = get_child_value(
1009
826
  state, "vehicleStatus.washerFluidStatus"
1010
827
  )
@@ -1022,8 +839,8 @@ class KiaUvoApiEU(ApiImpl):
1022
839
  vehicle.location = (
1023
840
  get_child_value(state, "vehicleLocation.coord.lat"),
1024
841
  get_child_value(state, "vehicleLocation.coord.lon"),
1025
- self.get_last_updated_at(
1026
- get_child_value(state, "vehicleLocation.time")
842
+ parse_datetime(
843
+ get_child_value(state, "vehicleLocation.time"), self.data_timezone
1027
844
  ),
1028
845
  )
1029
846
  vehicle.data = state
@@ -1041,7 +858,10 @@ class KiaUvoApiEU(ApiImpl):
1041
858
  else:
1042
859
  url = url + "/ccs2/carstatus/latest"
1043
860
  response = requests.get(
1044
- url, headers=self._get_authenticated_headers(token)
861
+ url,
862
+ headers=self._get_authenticated_headers(
863
+ token, vehicle.ccu_ccs2_protocol_support
864
+ ),
1045
865
  ).json()
1046
866
  _LOGGER.debug(f"{DOMAIN} - get_cached_vehicle_status response: {response}")
1047
867
  _check_response_for_errors(response)
@@ -1056,19 +876,25 @@ class KiaUvoApiEU(ApiImpl):
1056
876
 
1057
877
  try:
1058
878
  response = requests.get(
1059
- url, headers=self._get_authenticated_headers(token)
879
+ url,
880
+ headers=self._get_authenticated_headers(
881
+ token, vehicle.ccu_ccs2_protocol_support
882
+ ),
1060
883
  ).json()
1061
884
  _LOGGER.debug(f"{DOMAIN} - _get_location response: {response}")
1062
885
  _check_response_for_errors(response)
1063
886
  return response["resMsg"]["gpsDetail"]
1064
- except:
887
+ except Exception:
1065
888
  _LOGGER.warning(f"{DOMAIN} - _get_location failed")
1066
889
  return None
1067
890
 
1068
891
  def _get_forced_vehicle_state(self, token: Token, vehicle: Vehicle) -> dict:
1069
892
  url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/status"
1070
893
  response = requests.get(
1071
- url, headers=self._get_authenticated_headers(token)
894
+ url,
895
+ headers=self._get_authenticated_headers(
896
+ token, vehicle.ccu_ccs2_protocol_support
897
+ ),
1072
898
  ).json()
1073
899
  _LOGGER.debug(f"{DOMAIN} - Received forced vehicle data: {response}")
1074
900
  _check_response_for_errors(response)
@@ -1079,12 +905,28 @@ class KiaUvoApiEU(ApiImpl):
1079
905
  def lock_action(
1080
906
  self, token: Token, vehicle: Vehicle, action: VEHICLE_LOCK_ACTION
1081
907
  ) -> str:
1082
- url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/control/door"
908
+ if not vehicle.ccu_ccs2_protocol_support:
909
+ url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/control/door"
910
+
911
+ payload = {"action": action.value, "deviceId": token.device_id}
912
+ headers = self._get_authenticated_headers(
913
+ token, vehicle.ccu_ccs2_protocol_support
914
+ )
915
+
916
+ else:
917
+ url = self.SPA_API_URL_V2 + "vehicles/" + vehicle.id + "/ccs2/control/door"
918
+
919
+ payload = {"command": action.value}
920
+ headers = self._get_control_headers(
921
+ token, vehicle
922
+ )
1083
923
 
1084
- payload = {"action": action.value, "deviceId": token.device_id}
1085
924
  _LOGGER.debug(f"{DOMAIN} - Lock Action Request: {payload}")
925
+
1086
926
  response = requests.post(
1087
- url, json=payload, headers=self._get_authenticated_headers(token)
927
+ url,
928
+ json=payload,
929
+ headers=headers
1088
930
  ).json()
1089
931
  _LOGGER.debug(f"{DOMAIN} - Lock Action Response: {response}")
1090
932
  _check_response_for_errors(response)
@@ -1099,7 +941,7 @@ class KiaUvoApiEU(ApiImpl):
1099
941
  payload = {"action": action.value}
1100
942
  _LOGGER.debug(f"{DOMAIN} - Charge Port Action Request: {payload}")
1101
943
  response = requests.post(
1102
- url, json=payload, headers=self._get_authenticated_headers(token)
944
+ url, json=payload, headers=self._get_control_headers(token, vehicle)
1103
945
  ).json()
1104
946
  _LOGGER.debug(f"{DOMAIN} - Charge Port Action Response: {response}")
1105
947
  _check_response_for_errors(response)
@@ -1140,7 +982,11 @@ class KiaUvoApiEU(ApiImpl):
1140
982
  }
1141
983
  _LOGGER.debug(f"{DOMAIN} - Start Climate Action Request: {payload}")
1142
984
  response = requests.post(
1143
- url, json=payload, headers=self._get_authenticated_headers(token)
985
+ url,
986
+ json=payload,
987
+ headers=self._get_authenticated_headers(
988
+ token, vehicle.ccu_ccs2_protocol_support
989
+ ),
1144
990
  ).json()
1145
991
  _LOGGER.debug(f"{DOMAIN} - Start Climate Action Response: {response}")
1146
992
  _check_response_for_errors(response)
@@ -1162,7 +1008,11 @@ class KiaUvoApiEU(ApiImpl):
1162
1008
  }
1163
1009
  _LOGGER.debug(f"{DOMAIN} - Stop Climate Action Request: {payload}")
1164
1010
  response = requests.post(
1165
- url, json=payload, headers=self._get_authenticated_headers(token)
1011
+ url,
1012
+ json=payload,
1013
+ headers=self._get_authenticated_headers(
1014
+ token, vehicle.ccu_ccs2_protocol_support
1015
+ ),
1166
1016
  ).json()
1167
1017
  _LOGGER.debug(f"{DOMAIN} - Stop Climate Action Response: {response}")
1168
1018
  _check_response_for_errors(response)
@@ -1170,12 +1020,27 @@ class KiaUvoApiEU(ApiImpl):
1170
1020
  return response["msgId"]
1171
1021
 
1172
1022
  def start_charge(self, token: Token, vehicle: Vehicle) -> str:
1173
- url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/control/charge"
1023
+ if not vehicle.ccu_ccs2_protocol_support:
1024
+ url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/control/charge"
1025
+
1026
+ payload = {"action": "start", "deviceId": token.device_id}
1027
+ headers = self._get_authenticated_headers(
1028
+ token, vehicle.ccu_ccs2_protocol_support
1029
+ )
1030
+
1031
+ else:
1032
+ url = self.SPA_API_URL_V2 + "vehicles/" + vehicle.id + "/ccs2/control/charge"
1033
+
1034
+ payload = {"command": "start"}
1035
+ headers = self._get_control_headers(
1036
+ token, vehicle
1037
+ )
1174
1038
 
1175
- payload = {"action": "start", "deviceId": token.device_id}
1176
1039
  _LOGGER.debug(f"{DOMAIN} - Start Charge Action Request: {payload}")
1177
1040
  response = requests.post(
1178
- url, json=payload, headers=self._get_authenticated_headers(token)
1041
+ url,
1042
+ json=payload,
1043
+ headers=headers
1179
1044
  ).json()
1180
1045
  _LOGGER.debug(f"{DOMAIN} - Start Charge Action Response: {response}")
1181
1046
  _check_response_for_errors(response)
@@ -1183,18 +1048,63 @@ class KiaUvoApiEU(ApiImpl):
1183
1048
  return response["msgId"]
1184
1049
 
1185
1050
  def stop_charge(self, token: Token, vehicle: Vehicle) -> str:
1186
- url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/control/charge"
1051
+ if not vehicle.ccu_ccs2_protocol_support:
1052
+ url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/control/charge"
1053
+
1054
+ payload = {"action": "stop", "deviceId": token.device_id}
1055
+ headers = self._get_authenticated_headers(
1056
+ token, vehicle.ccu_ccs2_protocol_support
1057
+ )
1058
+
1059
+ else:
1060
+ url = self.SPA_API_URL_V2 + "vehicles/" + vehicle.id + "/ccs2/control/charge"
1061
+
1062
+ payload = {"command": "stop"}
1063
+ headers = self._get_control_headers(
1064
+ token, vehicle
1065
+ )
1187
1066
 
1188
- payload = {"action": "stop", "deviceId": token.device_id}
1189
- _LOGGER.debug(f"{DOMAIN} - Stop Charge Action Request {payload}")
1067
+ _LOGGER.debug(f"{DOMAIN} - Stop Charge Action Request: {payload}")
1190
1068
  response = requests.post(
1191
- url, json=payload, headers=self._get_authenticated_headers(token)
1069
+ url,
1070
+ json=payload,
1071
+ headers=headers
1192
1072
  ).json()
1193
1073
  _LOGGER.debug(f"{DOMAIN} - Stop Charge Action Response: {response}")
1194
1074
  _check_response_for_errors(response)
1195
1075
  token.device_id = self._get_device_id(self._get_stamp())
1196
1076
  return response["msgId"]
1197
1077
 
1078
+ def start_hazard_lights(self, token: Token, vehicle: Vehicle) -> str:
1079
+ url = self.SPA_API_URL_V2 + "vehicles/" + vehicle.id + "/ccs2/control/light"
1080
+
1081
+ payload = {"command": "on"}
1082
+ _LOGGER.debug(f"{DOMAIN} - Start Hazard Lights Request: {payload}")
1083
+ response = requests.post(
1084
+ url,
1085
+ json=payload,
1086
+ headers=self._get_control_headers(token, vehicle),
1087
+ ).json()
1088
+ _LOGGER.debug(f"{DOMAIN} - Start Hazard Lights Response: {response}")
1089
+ _check_response_for_errors(response)
1090
+ token.device_id = self._get_device_id(self._get_stamp())
1091
+ return response["msgId"]
1092
+
1093
+ def start_hazard_lights_and_horn(self, token: Token, vehicle: Vehicle) -> str:
1094
+ url = self.SPA_API_URL_V2 + "vehicles/" + vehicle.id + "/ccs2/control/hornlight"
1095
+
1096
+ payload = {"command": "on"}
1097
+ _LOGGER.debug(f"{DOMAIN} - Start Hazard Lights and Horn Request: {payload}")
1098
+ response = requests.post(
1099
+ url,
1100
+ json=payload,
1101
+ headers=self._get_control_headers(token, vehicle),
1102
+ ).json()
1103
+ _LOGGER.debug(f"{DOMAIN} - Start Hazard Lights and Horn Response: {response}")
1104
+ _check_response_for_errors(response)
1105
+ token.device_id = self._get_device_id(self._get_stamp())
1106
+ return response["msgId"]
1107
+
1198
1108
  def _get_charge_limits(self, token: Token, vehicle: Vehicle) -> dict:
1199
1109
  # Not currently used as value is in the general get.
1200
1110
  # Most likely this forces the car the update it.
@@ -1202,7 +1112,10 @@ class KiaUvoApiEU(ApiImpl):
1202
1112
 
1203
1113
  _LOGGER.debug(f"{DOMAIN} - Get Charging Limits Request")
1204
1114
  response = requests.get(
1205
- url, headers=self._get_authenticated_headers(token)
1115
+ url,
1116
+ headers=self._get_authenticated_headers(
1117
+ token, vehicle.ccu_ccs2_protocol_support
1118
+ ),
1206
1119
  ).json()
1207
1120
  _LOGGER.debug(f"{DOMAIN} - Get Charging Limits Response: {response}")
1208
1121
  _check_response_for_errors(response)
@@ -1229,7 +1142,9 @@ class KiaUvoApiEU(ApiImpl):
1229
1142
  response = requests.post(
1230
1143
  url,
1231
1144
  json=payload,
1232
- headers=self._get_authenticated_headers(token),
1145
+ headers=self._get_authenticated_headers(
1146
+ token, vehicle.ccu_ccs2_protocol_support
1147
+ ),
1233
1148
  )
1234
1149
  response = response.json()
1235
1150
  _LOGGER.debug(f"{DOMAIN} - get_trip_info response {response}")
@@ -1243,7 +1158,7 @@ class KiaUvoApiEU(ApiImpl):
1243
1158
  yyyymm_string,
1244
1159
  ) -> None:
1245
1160
  """
1246
- Europe feature only.
1161
+ feature only available for some regions.
1247
1162
  Updates the vehicle.month_trip_info for the specified month.
1248
1163
 
1249
1164
  Default this information is None:
@@ -1278,9 +1193,6 @@ class KiaUvoApiEU(ApiImpl):
1278
1193
  )
1279
1194
  result.day_list.append(processed_day)
1280
1195
 
1281
- if len(result.day_list) > 0: # sort on increasing yyyymmdd
1282
- result.day_list.sort(key=lambda k: k.yyyymmdd)
1283
-
1284
1196
  vehicle.month_trip_info = result
1285
1197
 
1286
1198
  def update_day_trip_info(
@@ -1290,7 +1202,7 @@ class KiaUvoApiEU(ApiImpl):
1290
1202
  yyyymmdd_string,
1291
1203
  ) -> None:
1292
1204
  """
1293
- Europe feature only.
1205
+ feature only available for some regions.
1294
1206
  Updates the vehicle.day_trip_info information for the specified day.
1295
1207
 
1296
1208
  Default this information is None:
@@ -1329,9 +1241,6 @@ class KiaUvoApiEU(ApiImpl):
1329
1241
  )
1330
1242
  result.trip_list.append(processed_trip)
1331
1243
 
1332
- if len(result.trip_list) > 0: # sort on descending hhmmss
1333
- result.trip_list.sort(reverse=True, key=lambda k: k.hhmmss)
1334
-
1335
1244
  vehicle.day_trip_info = result
1336
1245
 
1337
1246
  def _get_driving_info(self, token: Token, vehicle: Vehicle) -> dict:
@@ -1340,7 +1249,9 @@ class KiaUvoApiEU(ApiImpl):
1340
1249
  responseAlltime = requests.post(
1341
1250
  url,
1342
1251
  json={"periodTarget": 1},
1343
- headers=self._get_authenticated_headers(token),
1252
+ headers=self._get_authenticated_headers(
1253
+ token, vehicle.ccu_ccs2_protocol_support
1254
+ ),
1344
1255
  )
1345
1256
  responseAlltime = responseAlltime.json()
1346
1257
  _LOGGER.debug(f"{DOMAIN} - get_driving_info responseAlltime {responseAlltime}")
@@ -1349,7 +1260,9 @@ class KiaUvoApiEU(ApiImpl):
1349
1260
  response30d = requests.post(
1350
1261
  url,
1351
1262
  json={"periodTarget": 0},
1352
- headers=self._get_authenticated_headers(token),
1263
+ headers=self._get_authenticated_headers(
1264
+ token, vehicle.ccu_ccs2_protocol_support
1265
+ ),
1353
1266
  )
1354
1267
  response30d = response30d.json()
1355
1268
  _LOGGER.debug(f"{DOMAIN} - get_driving_info response30d {response30d}")
@@ -1373,13 +1286,22 @@ class KiaUvoApiEU(ApiImpl):
1373
1286
  ),
1374
1287
  regenerated_energy=get_child_value(day, "regenPwr"),
1375
1288
  distance=get_child_value(day, "calculativeOdo"),
1289
+ distance_unit=vehicle.odometer_unit,
1376
1290
  )
1377
1291
  drivingInfo["dailyStats"].append(processedDay)
1378
1292
 
1379
1293
  for drivingInfoItem in response30d["resMsg"]["drivingInfo"]:
1380
1294
  if (
1381
1295
  drivingInfoItem["drivingPeriod"] == 0
1382
- and drivingInfoItem["calculativeOdo"] > 0
1296
+ and next(
1297
+ (
1298
+ v
1299
+ for k, v in drivingInfoItem.items()
1300
+ if k.lower() == "calculativeodo"
1301
+ ),
1302
+ 0,
1303
+ )
1304
+ > 0
1383
1305
  ):
1384
1306
  drivingInfo["consumption30d"] = round(
1385
1307
  drivingInfoItem["totalPwrCsp"]
@@ -1387,12 +1309,6 @@ class KiaUvoApiEU(ApiImpl):
1387
1309
  )
1388
1310
  break
1389
1311
 
1390
- daily_stats = drivingInfo["dailyStats"]
1391
- _LOGGER.debug(f"KiaUvoApiEU: before daily_stats: {daily_stats}") # noqa
1392
- if len(daily_stats) > 0: # sort on decreasing date
1393
- daily_stats.sort(reverse=True, key=lambda k: k.date)
1394
- drivingInfo["dailyStats"] = daily_stats
1395
- _LOGGER.debug(f"KiaUvoApiEU: after daily_stats: {daily_stats}") # noqa
1396
1312
  return drivingInfo
1397
1313
  else:
1398
1314
  _LOGGER.debug(
@@ -1418,12 +1334,162 @@ class KiaUvoApiEU(ApiImpl):
1418
1334
  ]
1419
1335
  }
1420
1336
  response = requests.post(
1421
- url, json=body, headers=self._get_authenticated_headers(token)
1337
+ url,
1338
+ json=body,
1339
+ headers=self._get_authenticated_headers(
1340
+ token, vehicle.ccu_ccs2_protocol_support
1341
+ ),
1422
1342
  ).json()
1423
1343
  _LOGGER.debug(f"{DOMAIN} - Set Charge Limits Response: {response}")
1424
1344
  _check_response_for_errors(response)
1425
1345
  return response["msgId"]
1426
1346
 
1347
+ def set_charging_current(self, token: Token, vehicle: Vehicle, level: int) -> str:
1348
+ url = (
1349
+ self.SPA_API_URL + "vehicles/" + vehicle.id + "/ccs2/charge/chargingcurrent"
1350
+ )
1351
+
1352
+ body = {"chargingCurrent": level}
1353
+ response = requests.post(
1354
+ url,
1355
+ json=body,
1356
+ headers=self._get_authenticated_headers(
1357
+ token, vehicle.ccu_ccs2_protocol_support
1358
+ ),
1359
+ ).json()
1360
+ _LOGGER.debug(f"{DOMAIN} - Set Charging Current Response: {response}")
1361
+ _check_response_for_errors(response)
1362
+ return response["msgId"]
1363
+
1364
+ def schedule_charging_and_climate(
1365
+ self,
1366
+ token: Token,
1367
+ vehicle: Vehicle,
1368
+ options: ScheduleChargingClimateRequestOptions,
1369
+ ) -> str:
1370
+ url = self.SPA_API_URL_V2 + "vehicles/" + vehicle.id
1371
+ url = url + "/ccs2" # does not depend on vehicle.ccu_ccs2_protocol_support
1372
+ url = url + "/reservation/chargehvac"
1373
+
1374
+ def set_default_departure_options(
1375
+ departure_options: ScheduleChargingClimateRequestOptions.DepartureOptions,
1376
+ ) -> None:
1377
+ if departure_options.enabled is None:
1378
+ departure_options.enabled = False
1379
+ if departure_options.days is None:
1380
+ departure_options.days = [0]
1381
+ if departure_options.time is None:
1382
+ departure_options.time = dt.time()
1383
+
1384
+ if options.first_departure is None:
1385
+ options.first_departure = (
1386
+ ScheduleChargingClimateRequestOptions.DepartureOptions()
1387
+ )
1388
+ if options.second_departure is None:
1389
+ options.second_departure = (
1390
+ ScheduleChargingClimateRequestOptions.DepartureOptions()
1391
+ )
1392
+
1393
+ set_default_departure_options(options.first_departure)
1394
+ set_default_departure_options(options.second_departure)
1395
+ departures = [options.first_departure, options.second_departure]
1396
+
1397
+ if options.charging_enabled is None:
1398
+ options.charging_enabled = False
1399
+ if options.off_peak_start_time is None:
1400
+ options.off_peak_start_time = dt.time()
1401
+ if options.off_peak_end_time is None:
1402
+ options.off_peak_end_time = options.off_peak_start_time
1403
+ if options.off_peak_charge_only_enabled is None:
1404
+ options.off_peak_charge_only_enabled = False
1405
+ if options.climate_enabled is None:
1406
+ options.climate_enabled = False
1407
+ if options.temperature is None:
1408
+ options.temperature = 21.0
1409
+ if options.temperature_unit is None:
1410
+ options.temperature_unit = 0
1411
+ if options.defrost is None:
1412
+ options.defrost = False
1413
+
1414
+ temperature: float = options.temperature
1415
+ if options.temperature_unit == 0:
1416
+ # Round to nearest 0.5
1417
+ temperature = round(temperature * 2.0) / 2.0
1418
+ # Cap at 27, floor at 17
1419
+ if temperature > 27.0:
1420
+ temperature = 27.0
1421
+ elif temperature < 17.0:
1422
+ temperature = 17.0
1423
+
1424
+ payload = {
1425
+ "reservChargeInfo" + str(i + 1): {
1426
+ "reservChargeSet": departures[i].enabled,
1427
+ "reservInfo": {
1428
+ "day": departures[i].days,
1429
+ "time": {
1430
+ "time": departures[i].time.strftime("%I%M"),
1431
+ "timeSection": 1 if departures[i].time >= dt.time(12, 0) else 0,
1432
+ },
1433
+ },
1434
+ "reservFatcSet": {
1435
+ "airCtrl": 1 if options.climate_enabled else 0,
1436
+ "airTemp": {
1437
+ "value": f"{temperature:.1f}",
1438
+ "hvacTempType": 1,
1439
+ "unit": options.temperature_unit,
1440
+ },
1441
+ "heating1": 0,
1442
+ "defrost": options.defrost,
1443
+ },
1444
+ }
1445
+ for i in range(2)
1446
+ }
1447
+
1448
+ payload = payload | {
1449
+ "offPeakPowerInfo": {
1450
+ "offPeakPowerTime1": {
1451
+ "endtime": {
1452
+ "timeSection": (
1453
+ 1 if options.off_peak_end_time >= dt.time(12, 0) else 0
1454
+ ),
1455
+ "time": options.off_peak_end_time.strftime("%I%M"),
1456
+ },
1457
+ "starttime": {
1458
+ "timeSection": (
1459
+ 1 if options.off_peak_start_time >= dt.time(12, 0) else 0
1460
+ ),
1461
+ "time": options.off_peak_start_time.strftime("%I%M"),
1462
+ },
1463
+ },
1464
+ "offPeakPowerFlag": 2 if options.off_peak_charge_only_enabled else 1,
1465
+ },
1466
+ "reservFlag": 1 if options.charging_enabled else 0,
1467
+ }
1468
+
1469
+ _LOGGER.debug(f"{DOMAIN} - Schedule Charging and Climate Request: {payload}")
1470
+ response = requests.post(
1471
+ url, json=payload, headers=self._get_control_headers(token, vehicle)
1472
+ ).json()
1473
+ _LOGGER.debug(f"{DOMAIN} - Schedule Charging and Climate Response: {response}")
1474
+ _check_response_for_errors(response)
1475
+ token.device_id = self._get_device_id(self._get_stamp())
1476
+ return response["msgId"]
1477
+
1478
+ def valet_mode_action(
1479
+ self, token: Token, vehicle: Vehicle, action: VALET_MODE_ACTION
1480
+ ) -> str:
1481
+ url = self.SPA_API_URL_V2 + "vehicles/" + vehicle.id + "/control/valet"
1482
+
1483
+ payload = {"action": action.value}
1484
+ _LOGGER.debug(f"{DOMAIN} - Valet Mode Action Request: {payload}")
1485
+ response = requests.post(
1486
+ url, json=payload, headers=self._get_control_headers(token, vehicle)
1487
+ ).json()
1488
+ _LOGGER.debug(f"{DOMAIN} - Valet Mode Action Response: {response}")
1489
+ _check_response_for_errors(response)
1490
+ token.device_id = self._get_device_id(self._get_stamp())
1491
+ return response["msgId"]
1492
+
1427
1493
  def _get_stamp(self) -> str:
1428
1494
  raw_data = f"{self.APP_ID}:{int(dt.datetime.now().timestamp())}".encode()
1429
1495
  result = bytes(b1 ^ b2 for b1, b2 in zip(self.CFB, raw_data))
@@ -1654,6 +1720,27 @@ class KiaUvoApiEU(ApiImpl):
1654
1720
  refresh_token = token_type + " " + response["access_token"]
1655
1721
  return token_type, refresh_token
1656
1722
 
1723
+ def _get_control_token(self, token: Token) -> Token:
1724
+ url = self.USER_API_URL + "pin?token="
1725
+ headers = {
1726
+ "Authorization": token.access_token,
1727
+ "Content-type": "application/json",
1728
+ "Host": self.BASE_URL,
1729
+ "Accept-Encoding": "gzip",
1730
+ "User-Agent": USER_AGENT_OK_HTTP,
1731
+ }
1732
+
1733
+ data = {"deviceId": token.device_id, "pin": token.pin}
1734
+ _LOGGER.debug(f"{DOMAIN} - Get Control Token Data: {data}")
1735
+ response = requests.put(url, json=data, headers=headers)
1736
+ response = response.json()
1737
+ _LOGGER.debug(f"{DOMAIN} - Get Control Token Response {response}")
1738
+ control_token = "Bearer " + response["controlToken"]
1739
+ control_token_expire_at = math.floor(
1740
+ dt.datetime.now().timestamp() + response["expiresTime"]
1741
+ )
1742
+ return control_token, control_token_expire_at
1743
+
1657
1744
  def check_action_status(
1658
1745
  self,
1659
1746
  token: Token,
@@ -1687,7 +1774,10 @@ class KiaUvoApiEU(ApiImpl):
1687
1774
 
1688
1775
  else:
1689
1776
  response = requests.get(
1690
- url, headers=self._get_authenticated_headers(token)
1777
+ url,
1778
+ headers=self._get_authenticated_headers(
1779
+ token, vehicle.ccu_ccs2_protocol_support
1780
+ ),
1691
1781
  ).json()
1692
1782
  _LOGGER.debug(f"{DOMAIN} - Check last action status Response: {response}")
1693
1783
  _check_response_for_errors(response)