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.
@@ -4,7 +4,6 @@
4
4
  import datetime as dt
5
5
  import logging
6
6
  import random
7
- import re
8
7
  import secrets
9
8
  import ssl
10
9
  import string
@@ -12,6 +11,7 @@ import time
12
11
  import typing
13
12
  from datetime import datetime
14
13
 
14
+ import certifi
15
15
  import pytz
16
16
  import requests
17
17
  from requests import RequestException, Response
@@ -29,7 +29,7 @@ from .const import (
29
29
  TEMPERATURE_UNITS,
30
30
  VEHICLE_LOCK_ACTION,
31
31
  )
32
- from .utils import get_child_value
32
+ from .utils import get_child_value, parse_datetime
33
33
 
34
34
  _LOGGER = logging.getLogger(__name__)
35
35
 
@@ -41,7 +41,9 @@ class KiaSSLAdapter(HTTPAdapter):
41
41
  context = create_urllib3_context(
42
42
  ciphers="DEFAULT:@SECLEVEL=1", ssl_version=ssl.PROTOCOL_TLSv1_2
43
43
  )
44
+ context.options |= 0x4
44
45
  kwargs["ssl_context"] = context
46
+ kwargs["ca_certs"] = certifi.where()
45
47
  return super().init_poolmanager(*args, **kwargs)
46
48
 
47
49
 
@@ -104,8 +106,8 @@ def request_with_logging(func):
104
106
  return request_with_logging_wrapper
105
107
 
106
108
 
107
- class KiaUvoAPIUSA(ApiImpl):
108
- """KiaUvoAPIUSA"""
109
+ class KiaUvoApiUSA(ApiImpl):
110
+ """KiaUvoApiUSA"""
109
111
 
110
112
  def __init__(self, region: int, brand: int, language) -> None:
111
113
  self.LANGUAGE: str = language
@@ -122,8 +124,14 @@ class KiaUvoAPIUSA(ApiImpl):
122
124
 
123
125
  self.BASE_URL: str = "api.owners.kia.com"
124
126
  self.API_URL: str = "https://" + self.BASE_URL + "/apigw/v1/"
125
- self.session = requests.Session()
126
- self.session.mount("https://", KiaSSLAdapter())
127
+ self._session = None
128
+
129
+ @property
130
+ def session(self):
131
+ if not self._session:
132
+ self._session = requests.Session()
133
+ self._session.mount("https://", KiaSSLAdapter())
134
+ return self._session
127
135
 
128
136
  def api_headers(self) -> dict:
129
137
  offset = time.localtime().tm_gmtoff / 60 / 60
@@ -278,10 +286,11 @@ class KiaUvoAPIUSA(ApiImpl):
278
286
 
279
287
  def _update_vehicle_properties(self, vehicle: Vehicle, state: dict) -> None:
280
288
  """Get cached vehicle data and update Vehicle instance with it"""
281
- vehicle.last_updated_at = self.get_last_updated_at(
289
+ vehicle.last_updated_at = parse_datetime(
282
290
  get_child_value(
283
291
  state, "lastVehicleInfo.vehicleStatusRpt.vehicleStatus.syncDate.utc"
284
- )
292
+ ),
293
+ self.data_timezone,
285
294
  )
286
295
  vehicle.odometer = (
287
296
  get_child_value(state, "vehicleConfig.vehicleDetail.vehicle.mileage"),
@@ -417,7 +426,7 @@ class KiaUvoAPIUSA(ApiImpl):
417
426
  vehicle.ev_charge_limits_dc = [
418
427
  x["targetSOClevel"] for x in ChargeDict if x["plugType"] == 0
419
428
  ][-1]
420
- except:
429
+ except Exception:
421
430
  _LOGGER.debug(f"{DOMAIN} - SOC Levels couldn't be found. May not be an EV.")
422
431
 
423
432
  vehicle.ev_driving_range = (
@@ -515,8 +524,9 @@ class KiaUvoAPIUSA(ApiImpl):
515
524
  vehicle.location = (
516
525
  get_child_value(state, "lastVehicleInfo.location.coord.lat"),
517
526
  get_child_value(state, "lastVehicleInfo.location.coord.lon"),
518
- self.get_last_updated_at(
519
- get_child_value(state, "lastVehicleInfo.location.syncDate.utc")
527
+ parse_datetime(
528
+ get_child_value(state, "lastVehicleInfo.location.syncDate.utc"),
529
+ self.data_timezone,
520
530
  ),
521
531
  )
522
532
 
@@ -534,25 +544,6 @@ class KiaUvoAPIUSA(ApiImpl):
534
544
 
535
545
  vehicle.data = state
536
546
 
537
- def get_last_updated_at(self, value) -> dt.datetime:
538
- _LOGGER.debug(f"{DOMAIN} - last_updated_at - before {value}")
539
- if value is None:
540
- value = dt.datetime(2000, 1, 1, tzinfo=self.data_timezone)
541
- else:
542
- m = re.match(r"(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})", value)
543
- value = dt.datetime(
544
- year=int(m.group(1)),
545
- month=int(m.group(2)),
546
- day=int(m.group(3)),
547
- hour=int(m.group(4)),
548
- minute=int(m.group(5)),
549
- second=int(m.group(6)),
550
- tzinfo=self.data_timezone,
551
- )
552
-
553
- _LOGGER.debug(f"{DOMAIN} - last_updated_at - after {value}")
554
- return value
555
-
556
547
  def _get_cached_vehicle_state(self, token: Token, vehicle: Vehicle) -> dict:
557
548
  url = self.API_URL + "cmm/gvi"
558
549
 
@@ -629,6 +620,59 @@ class KiaUvoAPIUSA(ApiImpl):
629
620
 
630
621
  return response.headers["Xid"]
631
622
 
623
+ def _seat_settings(self, level) -> dict:
624
+ # See const.SEAT_STATUS for the list and descriptions of levels.
625
+ #
626
+ # The values were determined empirically, see https://github.com/Hyundai-Kia-Connect/kia_uvo/issues/718
627
+ if level == 8: # High heat
628
+ return {
629
+ "heatVentType": 1,
630
+ "heatVentLevel": 4,
631
+ "heatVentStep": 1,
632
+ }
633
+ elif level == 7: # Medium heat
634
+ return {
635
+ "heatVentType": 1,
636
+ "heatVentLevel": 3,
637
+ "heatVentStep": 2,
638
+ }
639
+ elif level == 6: # Low heat
640
+ return {
641
+ "heatVentType": 1,
642
+ "heatVentLevel": 2,
643
+ "heatVentStep": 3,
644
+ }
645
+ elif level == 5: # High cool
646
+ return {
647
+ "heatVentType": 2,
648
+ "heatVentLevel": 4,
649
+ "heatVentStep": 1,
650
+ }
651
+ elif level == 4: # Medium cool
652
+ return {
653
+ "heatVentType": 2,
654
+ "heatVentLevel": 3,
655
+ "heatVentStep": 2,
656
+ }
657
+ elif level == 3: # Low cool
658
+ return {
659
+ "heatVentType": 2,
660
+ "heatVentLevel": 2,
661
+ "heatVentStep": 3,
662
+ }
663
+ elif level == 1: # Generically on, let's assume high heat
664
+ return {
665
+ "heatVentType": 1,
666
+ "heatVentLevel": 4,
667
+ "heatVentStep": 1,
668
+ }
669
+ else: # Off
670
+ return {
671
+ "heatVentType": 0,
672
+ "heatVentLevel": 1,
673
+ "heatVentStep": 0,
674
+ }
675
+
632
676
  def start_climate(
633
677
  self, token: Token, vehicle: Vehicle, options: ClimateRequestOptions
634
678
  ) -> str:
@@ -647,93 +691,45 @@ class KiaUvoAPIUSA(ApiImpl):
647
691
  options.defrost = False
648
692
  if options.duration is None:
649
693
  options.duration = 5
650
- if options.front_left_seat is None:
651
- options.front_left_seat = 0
652
- if options.front_right_seat is None:
653
- options.front_right_seat = 0
654
- if options.rear_left_seat is None:
655
- options.rear_left_seat = 0
656
- if options.rear_right_seat is None:
657
- options.rear_right_seat = 0
658
-
659
- front_left_heatVentType = 0
660
- front_right_heatVentType = 0
661
- rear_left_heatVentType = 0
662
- rear_right_heatVentType = 0
663
- front_left_heatVentLevel = 0
664
- front_right_heatVentLevel = 0
665
- rear_left_heatVentLevel = 0
666
- rear_right_heatVentLevel = 0
667
-
668
- # heated
669
- if options.front_left_seat in (6, 7, 8):
670
- front_left_heatVentType = 1
671
- front_left_heatVentLevel = options.front_left_seat - 4
672
- if options.front_right_seat in (6, 7, 8):
673
- front_right_heatVentType = 1
674
- front_right_heatVentLevel = options.front_right_seat - 4
675
- if options.rear_left_seat in (6, 7, 8):
676
- rear_left_heatVentType = 1
677
- rear_left_heatVentLevel = options.rear_left_seat - 4
678
- if options.rear_right_seat in (6, 7, 8):
679
- rear_right_heatVentType = 1
680
- rear_right_heatVentLevel = options.rear_right_seat - 4
681
-
682
- # ventilated
683
- if options.front_left_seat in (3, 4, 5):
684
- front_left_heatVentType = 2
685
- front_left_heatVentLevel = options.front_left_seat - 1
686
- if options.front_right_seat in (3, 4, 5):
687
- front_right_heatVentType = 2
688
- front_right_heatVentLevel = options.front_right_seat - 1
689
- if options.rear_left_seat in (3, 4, 5):
690
- rear_left_heatVentType = 2
691
- rear_left_heatVentLevel = options.rear_left_seat - 1
692
- if options.rear_right_seat in (3, 4, 5):
693
- rear_right_heatVentType = 2
694
- rear_right_heatVentLevel = options.rear_right_seat - 1
694
+ if options.steering_wheel is None:
695
+ options.steering_wheel = 0
695
696
 
696
697
  body = {
697
698
  "remoteClimate": {
698
- "airCtrl": options.climate,
699
699
  "airTemp": {
700
700
  "unit": 1,
701
701
  "value": str(options.set_temp),
702
702
  },
703
+ "airCtrl": options.climate,
703
704
  "defrost": options.defrost,
704
705
  "heatingAccessory": {
705
- "rearWindow": int(options.heating),
706
- "sideMirror": int(options.heating),
707
- "steeringWheel": int(options.heating),
706
+ "rearWindow": 1 if options.heating in [1, 2, 4] else 0,
707
+ "sideMirror": 1 if options.heating in [1, 4] else 0,
708
+ "steeringWheel": 1 if options.steering_wheel in [1, 2] else 0,
709
+ "steeringWheelStep": options.steering_wheel,
708
710
  },
709
711
  "ignitionOnDuration": {
710
712
  "unit": 4,
711
713
  "value": options.duration,
712
714
  },
713
- "heatVentSeat": {
714
- "driverSeat": {
715
- "heatVentType": front_left_heatVentType,
716
- "heatVentLevel": front_left_heatVentLevel,
717
- "heatVentStep": 1,
718
- },
719
- "passengerSeat": {
720
- "heatVentType": front_right_heatVentType,
721
- "heatVentLevel": front_right_heatVentLevel,
722
- "heatVentStep": 1,
723
- },
724
- "rearLeftSeat": {
725
- "heatVentType": rear_left_heatVentType,
726
- "heatVentLevel": rear_left_heatVentLevel,
727
- "heatVentStep": 1,
728
- },
729
- "rearRightSeat": {
730
- "heatVentType": rear_right_heatVentType,
731
- "heatVentLevel": rear_right_heatVentLevel,
732
- "heatVentStep": 1,
733
- },
734
- },
735
- }
715
+ },
736
716
  }
717
+
718
+ # Kia seems to now be checking if you can set the heated/vented seats at
719
+ # the car level only add to body if the option is not none for any of
720
+ # the seats
721
+ if (
722
+ options.front_left_seat is not None
723
+ or options.front_right_seat is not None
724
+ or options.rear_left_seat is not None
725
+ or options.rear_right_seat is not None
726
+ ):
727
+ body["remoteClimate"]["heatVentSeat"] = {
728
+ "driverSeat": self._seat_settings(options.front_left_seat),
729
+ "passengerSeat": self._seat_settings(options.front_right_seat),
730
+ "rearLeftSeat": self._seat_settings(options.rear_left_seat),
731
+ "rearRightSeat": self._seat_settings(options.rear_right_seat),
732
+ }
737
733
  _LOGGER.debug(f"{DOMAIN} - Planned start_climate payload: {body}")
738
734
  response = self.post_request_with_logging_and_active_session(
739
735
  token=token, url=url, json_body=body, vehicle=vehicle
@@ -1,12 +1,13 @@
1
- # pylint:disable=missing-class-docstring,missing-function-docstring,wildcard-import,unused-wildcard-import,invalid-name
1
+ # pylint:disable=missing-class-docstring,missing-function-docstring,wildcard-import,unused-wildcard-import,invalid-name,logging-fstring-interpolation
2
2
  """Vehicle class"""
3
+
3
4
  import logging
4
5
  import datetime
5
6
  import typing
6
7
  from dataclasses import dataclass, field
7
8
 
8
- from .utils import get_float
9
- from .const import *
9
+ from .utils import get_float, get_safe_local_datetime
10
+ from .const import DISTANCE_UNITS
10
11
 
11
12
  _LOGGER = logging.getLogger(__name__)
12
13
 
@@ -16,9 +17,9 @@ class TripInfo:
16
17
  """Trip Info"""
17
18
 
18
19
  hhmmss: str = None # will not be filled by summary
19
- drive_time: int = None
20
- idle_time: int = None
21
- distance: int = None
20
+ drive_time: int = None # minutes
21
+ idle_time: int = None # minutes
22
+ distance: float = None
22
23
  avg_speed: float = None
23
24
  max_speed: int = None
24
25
 
@@ -59,10 +60,8 @@ class DailyDrivingStats:
59
60
  onboard_electronics_consumption: int = None
60
61
  battery_care_consumption: int = None
61
62
  regenerated_energy: int = None
62
- # distance is expressed in (I assume) whatever unit the vehicle is
63
- # configured in. KMs (rounded) in my case
64
- distance: int = None
65
- distance_unit = DISTANCE_UNITS[1] # set to kms by default
63
+ distance: float = None
64
+ distance_unit: str = DISTANCE_UNITS[1] # set to kms by default
66
65
 
67
66
 
68
67
  @dataclass
@@ -93,8 +92,10 @@ class Vehicle:
93
92
 
94
93
  car_battery_percentage: int = None
95
94
  engine_is_running: bool = None
96
- last_updated_at: datetime.datetime = None
95
+
96
+ _last_updated_at: datetime.datetime = None
97
97
  timezone: datetime.timezone = datetime.timezone.utc # default UTC
98
+
98
99
  dtc_count: typing.Union[int, None] = None
99
100
  dtc_descriptions: typing.Union[dict, None] = None
100
101
 
@@ -155,9 +156,13 @@ class Vehicle:
155
156
  # EV fields (EV/PHEV)
156
157
 
157
158
  ev_charge_port_door_is_open: typing.Union[bool, None] = None
159
+ ev_charging_power: typing.Union[float, None] = None # Charging power in kW
158
160
 
159
161
  ev_charge_limits_dc: typing.Union[int, None] = None
160
162
  ev_charge_limits_ac: typing.Union[int, None] = None
163
+ ev_charging_current: typing.Union[int, None] = (
164
+ None # Europe feature only, ac charging current limit
165
+ )
161
166
  ev_v2l_discharge_limit: typing.Union[int, None] = None
162
167
 
163
168
  # energy consumed and regenerated since the vehicle was paired with the account
@@ -169,11 +174,61 @@ class Vehicle:
169
174
  # expressed in watt-hours (Wh)
170
175
  power_consumption_30d: float = None # Europe feature only
171
176
 
172
- # Europe feature only
173
- daily_stats: list[DailyDrivingStats] = field(default_factory=list)
177
+ # feature only available for some regions (getter/setter for sorting)
178
+ _daily_stats: list[DailyDrivingStats] = field(default_factory=list)
174
179
 
175
- month_trip_info: MonthTripInfo = None # Europe feature only
176
- day_trip_info: DayTripInfo = None # Europe feature only
180
+ @property
181
+ def daily_stats(self):
182
+ return self._daily_stats
183
+
184
+ @daily_stats.setter
185
+ def daily_stats(self, value):
186
+ result = value
187
+ if result is not None and len(result) > 0: # sort on decreasing date
188
+ _LOGGER.debug(f"before daily_stats: {result}")
189
+ result.sort(reverse=True, key=lambda k: k.date)
190
+ _LOGGER.debug(f"after daily_stats: {result}")
191
+ self._daily_stats = result
192
+
193
+ # feature only available for some regions (getter/setter for sorting)
194
+ _month_trip_info: MonthTripInfo = None
195
+
196
+ @property
197
+ def month_trip_info(self):
198
+ return self._month_trip_info
199
+
200
+ @month_trip_info.setter
201
+ def month_trip_info(self, value):
202
+ result = value
203
+ if (
204
+ result is not None
205
+ and hasattr(result, "day_list")
206
+ and len(result.day_list) > 0
207
+ ): # sort on increasing yyyymmdd
208
+ _LOGGER.debug(f"before month_trip_info: {result}")
209
+ result.day_list.sort(key=lambda k: k.yyyymmdd)
210
+ _LOGGER.debug(f"after month_trip_info: {result}")
211
+ self._month_trip_info = result
212
+
213
+ # feature only available for some regions (getter/setter for sorting)
214
+ _day_trip_info: DayTripInfo = None
215
+
216
+ @property
217
+ def day_trip_info(self):
218
+ return self._day_trip_info
219
+
220
+ @day_trip_info.setter
221
+ def day_trip_info(self, value):
222
+ result = value
223
+ if (
224
+ result is not None
225
+ and hasattr(result, "trip_list")
226
+ and len(result.trip_list) > 0
227
+ ): # sort on descending hhmmss
228
+ _LOGGER.debug(f"before day_trip_info: {result}")
229
+ result.trip_list.sort(reverse=True, key=lambda k: k.hhmmss)
230
+ _LOGGER.debug(f"after day_trip_info: {result}")
231
+ self._day_trip_info = result
177
232
 
178
233
  ev_battery_percentage: int = None
179
234
  ev_battery_soh_percentage: int = None
@@ -219,10 +274,26 @@ class Vehicle:
219
274
  ev_first_departure_time: typing.Union[datetime.time, None] = None
220
275
  ev_second_departure_time: typing.Union[datetime.time, None] = None
221
276
 
277
+ ev_first_departure_climate_enabled: typing.Union[bool, None] = None
278
+ ev_second_departure_climate_enabled: typing.Union[bool, None] = None
279
+
280
+ _ev_first_departure_climate_temperature: typing.Union[float, None] = None
281
+ _ev_first_departure_climate_temperature_value: typing.Union[float, None] = None
282
+ _ev_first_departure_climate_temperature_unit: typing.Union[str, None] = None
283
+
284
+ _ev_second_departure_climate_temperature: typing.Union[float, None] = None
285
+ _ev_second_departure_climate_temperature_value: typing.Union[float, None] = None
286
+ _ev_second_departure_climate_temperature_unit: typing.Union[str, None] = None
287
+
288
+ ev_first_departure_climate_defrost: typing.Union[bool, None] = None
289
+ ev_second_departure_climate_defrost: typing.Union[bool, None] = None
290
+
222
291
  ev_off_peak_start_time: typing.Union[datetime.time, None] = None
223
292
  ev_off_peak_end_time: typing.Union[datetime.time, None] = None
224
293
  ev_off_peak_charge_only_enabled: typing.Union[bool, None] = None
225
294
 
295
+ ev_schedule_charge_enabled: typing.Union[bool, None] = None
296
+
226
297
  # IC fields (PHEV/HEV/IC)
227
298
  _fuel_driving_range: float = None
228
299
  _fuel_driving_range_value: float = None
@@ -243,8 +314,12 @@ class Vehicle:
243
314
 
244
315
  @geocode.setter
245
316
  def geocode(self, value):
246
- self._geocode_name = value[0]
247
- self._geocode_address = value[1]
317
+ if value:
318
+ self._geocode_name = value[0]
319
+ self._geocode_address = value[1]
320
+ else:
321
+ self._geocode_name = None
322
+ self._geocode_address = None
248
323
 
249
324
  @property
250
325
  def total_driving_range(self):
@@ -280,6 +355,26 @@ class Vehicle:
280
355
  self._last_service_distance_unit = value[1]
281
356
  self._last_service_distance = value[0]
282
357
 
358
+ @property
359
+ def last_updated_at(self):
360
+ return self._last_updated_at
361
+
362
+ @last_updated_at.setter
363
+ def last_updated_at(self, value):
364
+ # workaround for: Timestamp of "last_updated_at" sensor is wrong #931
365
+ # https://github.com/Hyundai-Kia-Connect/kia_uvo/issues/931#issuecomment-2381569934
366
+ newest_updated_at = get_safe_local_datetime(value)
367
+ previous_updated_at = self._last_updated_at
368
+ if newest_updated_at and previous_updated_at: # both filled
369
+ if newest_updated_at < previous_updated_at:
370
+ utcoffset = newest_updated_at.utcoffset()
371
+ newest_updated_at_corrected = newest_updated_at + utcoffset
372
+ if newest_updated_at_corrected >= previous_updated_at:
373
+ newest_updated_at = newest_updated_at_corrected
374
+ if newest_updated_at < previous_updated_at:
375
+ newest_updated_at = previous_updated_at # keep old because newer
376
+ self._last_updated_at = newest_updated_at
377
+
283
378
  @property
284
379
  def location_latitude(self):
285
380
  return self._location_latitude
@@ -305,7 +400,7 @@ class Vehicle:
305
400
  def location(self, value):
306
401
  self._location_latitude = value[0]
307
402
  self._location_longitude = value[1]
308
- self._location_last_set_time = value[2]
403
+ self._location_last_set_time = get_safe_local_datetime(value[2])
309
404
 
310
405
  @property
311
406
  def odometer(self):
@@ -330,7 +425,7 @@ class Vehicle:
330
425
  def air_temperature(self, value):
331
426
  self._air_temperature_value = value[0]
332
427
  self._air_temperature_unit = value[1]
333
- self._air_temperature = value[0]
428
+ self._air_temperature = value[0] if value[0] != "OFF" else None
334
429
 
335
430
  @property
336
431
  def ev_driving_range(self):
@@ -414,6 +509,34 @@ class Vehicle:
414
509
  self._ev_target_range_charge_DC_unit = value[1]
415
510
  self._ev_target_range_charge_DC = value[0]
416
511
 
512
+ @property
513
+ def ev_first_departure_climate_temperature(self):
514
+ return self._ev_first_departure_climate_temperature
515
+
516
+ @property
517
+ def ev_first_departure_climate_temperature_unit(self):
518
+ return self._ev_first_departure_climate_temperature_unit
519
+
520
+ @ev_first_departure_climate_temperature.setter
521
+ def ev_first_departure_climate_temperature(self, value):
522
+ self._ev_first_departure_climate_temperature_value = value[0]
523
+ self._ev_first_departure_climate_temperature_unit = value[1]
524
+ self._ev_first_departure_climate_temperature = value[0]
525
+
526
+ @property
527
+ def ev_second_departure_climate_temperature(self):
528
+ return self._ev_second_departure_climate_temperature
529
+
530
+ @property
531
+ def ev_second_departure_climate_temperature_unit(self):
532
+ return self._ev_second_departure_climate_temperature_unit
533
+
534
+ @ev_second_departure_climate_temperature.setter
535
+ def ev_second_departure_climate_temperature(self, value):
536
+ self._ev_second_departure_climate_temperature_value = value[0]
537
+ self._ev_second_departure_climate_temperature_unit = value[1]
538
+ self._ev_second_departure_climate_temperature = value[0]
539
+
417
540
  @property
418
541
  def fuel_driving_range(self):
419
542
  return self._fuel_driving_range