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,13 +4,16 @@
4
4
 
5
5
  import logging
6
6
  import time
7
- import re
8
7
  import datetime as dt
9
8
  import pytz
10
9
  import requests
10
+ import certifi
11
+
11
12
  from requests.adapters import HTTPAdapter
12
13
  from urllib3.util.ssl_ import create_urllib3_context
13
14
 
15
+ from hyundai_kia_connect_api.exceptions import APIError
16
+
14
17
  from .const import (
15
18
  DOMAIN,
16
19
  VEHICLE_LOCK_ACTION,
@@ -19,10 +22,17 @@ from .const import (
19
22
  TEMPERATURE_UNITS,
20
23
  ENGINE_TYPES,
21
24
  )
22
- from .utils import get_child_value, get_float
25
+ from .utils import get_child_value, get_float, parse_datetime
23
26
  from .ApiImpl import ApiImpl, ClimateRequestOptions
24
27
  from .Token import Token
25
- from .Vehicle import DailyDrivingStats, Vehicle
28
+ from .Vehicle import (
29
+ DailyDrivingStats,
30
+ DayTripCounts,
31
+ DayTripInfo,
32
+ MonthTripInfo,
33
+ TripInfo,
34
+ Vehicle,
35
+ )
26
36
 
27
37
 
28
38
  CIPHERS = "DEFAULT@SECLEVEL=1"
@@ -43,7 +53,7 @@ class cipherAdapter(HTTPAdapter):
43
53
 
44
54
  def init_poolmanager(self, *args, **kwargs):
45
55
  kwargs["ssl_context"] = self._setup_ssl_context()
46
-
56
+ kwargs["ca_certs"] = certifi.where()
47
57
  return super().init_poolmanager(*args, **kwargs)
48
58
 
49
59
  def proxy_manager_for(self, *args, **kwargs):
@@ -52,8 +62,8 @@ class cipherAdapter(HTTPAdapter):
52
62
  return super().proxy_manager_for(*args, **kwargs)
53
63
 
54
64
 
55
- class HyundaiBlueLinkAPIUSA(ApiImpl):
56
- """HyundaiBlueLinkAPIUSA"""
65
+ class HyundaiBlueLinkApiUSA(ApiImpl):
66
+ """HyundaiBlueLinkApiUSA"""
57
67
 
58
68
  # initialize with a timestamp which will allow the first fetch to occur
59
69
  last_loc_timestamp = dt.datetime.now(pytz.utc) - dt.timedelta(hours=3)
@@ -235,8 +245,8 @@ class HyundaiBlueLinkAPIUSA(ApiImpl):
235
245
  return None
236
246
 
237
247
  def _update_vehicle_properties(self, vehicle: Vehicle, state: dict) -> None:
238
- vehicle.last_updated_at = self.get_last_updated_at(
239
- get_child_value(state, "vehicleStatus.dateTime")
248
+ vehicle.last_updated_at = parse_datetime(
249
+ get_child_value(state, "vehicleStatus.dateTime"), self.data_timezone
240
250
  )
241
251
  vehicle.total_driving_range = (
242
252
  get_child_value(
@@ -376,6 +386,22 @@ class HyundaiBlueLinkAPIUSA(ApiImpl):
376
386
  vehicle.ev_battery_is_plugged_in = get_child_value(
377
387
  state, "vehicleStatus.evStatus.batteryPlugin"
378
388
  )
389
+ vehicle.ev_charging_power = get_child_value(
390
+ state, "vehicleStatus.evStatus.batteryPower.batteryStndChrgPower"
391
+ )
392
+ ChargeDict = get_child_value(
393
+ state, "vehicleStatus.evStatus.reservChargeInfos.targetSOClist"
394
+ )
395
+ try:
396
+ vehicle.ev_charge_limits_ac = [
397
+ x["targetSOClevel"] for x in ChargeDict if x["plugType"] == 1
398
+ ][-1]
399
+ vehicle.ev_charge_limits_dc = [
400
+ x["targetSOClevel"] for x in ChargeDict if x["plugType"] == 0
401
+ ][-1]
402
+ except Exception:
403
+ _LOGGER.debug(f"{DOMAIN} - SOC Levels couldn't be found. May not be an EV.")
404
+
379
405
  vehicle.ev_driving_range = (
380
406
  get_child_value(
381
407
  state,
@@ -428,14 +454,41 @@ class HyundaiBlueLinkAPIUSA(ApiImpl):
428
454
  vehicle.location = (
429
455
  get_child_value(state, "vehicleStatus.vehicleLocation.coord.lat"),
430
456
  get_child_value(state, "vehicleStatus.vehicleLocation.coord.lon"),
431
- self.get_last_updated_at(
432
- get_child_value(state, "vehicleStatus.vehicleLocation.time")
457
+ parse_datetime(
458
+ get_child_value(state, "vehicleStatus.vehicleLocation.time"),
459
+ self.data_timezone,
433
460
  ),
434
461
  )
435
462
  vehicle.air_control_is_on = get_child_value(state, "vehicleStatus.airCtrlOn")
436
463
 
464
+ # fill vehicle.daily_stats
437
465
  tripStats = []
438
466
  tripDetails = get_child_value(state, "evTripDetails.tripdetails") or {}
467
+
468
+ # compute more digits for distance mileage using odometer and overrule distance
469
+ previous_odometer = None
470
+ for trip in reversed(tripDetails):
471
+ odometer = get_child_value(trip, "odometer.value")
472
+ if previous_odometer and odometer:
473
+ delta_odometer = odometer - previous_odometer
474
+ if delta_odometer >= 0.0:
475
+ trip["distance"] = delta_odometer
476
+ previous_odometer = odometer
477
+
478
+ # overrule odometer with more accuracy from last trip
479
+ if (
480
+ previous_odometer
481
+ and vehicle.odometer
482
+ and previous_odometer > vehicle.odometer
483
+ ):
484
+ _LOGGER.debug(
485
+ f"Overruling odometer: {previous_odometer:.1f} old: {vehicle.odometer:.1f}" # noqa
486
+ )
487
+ vehicle.odometer = (
488
+ previous_odometer,
489
+ DISTANCE_UNITS[3],
490
+ )
491
+
439
492
  for trip in tripDetails:
440
493
  processedTrip = DailyDrivingStats(
441
494
  date=dt.datetime.strptime(trip["startdate"], "%Y-%m-%d %H:%M:%S.%f"),
@@ -446,13 +499,185 @@ class HyundaiBlueLinkAPIUSA(ApiImpl):
446
499
  battery_care_consumption=get_child_value(trip, "batterycare"),
447
500
  regenerated_energy=get_child_value(trip, "regen"),
448
501
  distance=get_child_value(trip, "distance"),
502
+ distance_unit=vehicle.odometer_unit,
449
503
  )
450
504
  tripStats.append(processedTrip)
451
505
 
452
506
  vehicle.daily_stats = tripStats
453
507
 
508
+ # remember trips, store state
509
+ trips = []
510
+ for trip in tripDetails:
511
+ yyyymmdd_hhmmss = trip["startdate"] # remember full date
512
+ drive_time = int(get_child_value(trip["mileagetime"], "value"))
513
+ idle_time = int(get_child_value(trip["duration"], "value")) - drive_time
514
+ processed_trip = TripInfo(
515
+ hhmmss=yyyymmdd_hhmmss,
516
+ drive_time=int(drive_time / 60), # convert seconds to minutes
517
+ idle_time=int(idle_time / 60), # convert seconds to minutes
518
+ distance=float(trip["distance"]),
519
+ avg_speed=get_child_value(trip["avgspeed"], "value"),
520
+ max_speed=int(get_child_value(trip["maxspeed"], "value")),
521
+ )
522
+ trips.append(processed_trip)
523
+
524
+ _LOGGER.debug(f"_update_vehicle_properties filled_trips: {trips}")
525
+ if len(trips) > 0:
526
+ state["filled_trips"] = trips
527
+
454
528
  vehicle.data = state
455
529
 
530
+ def update_month_trip_info(
531
+ self,
532
+ token,
533
+ vehicle,
534
+ yyyymm_string,
535
+ ) -> None:
536
+ """
537
+ feature only available for some regions.
538
+ Updates the vehicle.month_trip_info for the specified month.
539
+
540
+ Default this information is None:
541
+
542
+ month_trip_info: MonthTripInfo = None
543
+ """
544
+ _LOGGER.debug(f"update_month_trip_info: {yyyymm_string}")
545
+ vehicle.month_trip_info = None
546
+
547
+ if vehicle.data is None or "filled_trips" not in vehicle.data:
548
+ _LOGGER.debug(f"filled_trips is empty: {vehicle.data}")
549
+ return # nothing to fill
550
+
551
+ trips = vehicle.data["filled_trips"]
552
+
553
+ month_trip_info: MonthTripInfo = None
554
+ month_trip_info_count = 0
555
+
556
+ for trip in trips:
557
+ date_str = trip.hhmmss
558
+ yyyymm = date_str[0:4] + date_str[5:7]
559
+ if yyyymm == yyyymm_string:
560
+ if month_trip_info_count == 0:
561
+ month_trip_info = MonthTripInfo(
562
+ yyyymm=yyyymm_string,
563
+ summary=TripInfo(
564
+ drive_time=trip.drive_time,
565
+ idle_time=trip.idle_time,
566
+ distance=trip.distance,
567
+ avg_speed=trip.avg_speed,
568
+ max_speed=trip.max_speed,
569
+ ),
570
+ day_list=[],
571
+ )
572
+ month_trip_info_count = 1
573
+ else:
574
+ # increment totals for month (for the few trips available)
575
+ month_trip_info_count += 1
576
+ summary = month_trip_info.summary
577
+ summary.drive_time += trip.drive_time
578
+ summary.idle_time += trip.idle_time
579
+ summary.distance += trip.distance
580
+ summary.avg_speed += trip.avg_speed
581
+ summary.max_speed = max(trip.max_speed, summary.max_speed)
582
+
583
+ month_trip_info.summary.avg_speed /= month_trip_info_count
584
+ month_trip_info.summary.avg_speed = round(
585
+ month_trip_info.summary.avg_speed, 1
586
+ )
587
+
588
+ # also fill DayTripCount
589
+ yyyymmdd = yyyymm + date_str[8:10]
590
+ day_trip_found = False
591
+ for day in month_trip_info.day_list:
592
+ if day.yyyymmdd == yyyymmdd:
593
+ day.trip_count += 1
594
+ day_trip_found = True
595
+
596
+ if not day_trip_found:
597
+ month_trip_info.day_list.append(
598
+ DayTripCounts(yyyymmdd=yyyymmdd, trip_count=1)
599
+ )
600
+
601
+ vehicle.month_trip_info = month_trip_info
602
+
603
+ def update_day_trip_info(
604
+ self,
605
+ token,
606
+ vehicle,
607
+ yyyymmdd_string,
608
+ ) -> None:
609
+ """
610
+ feature only available for some regions.
611
+ Updates the vehicle.day_trip_info information for the specified day.
612
+
613
+ Default this information is None:
614
+
615
+ day_trip_info: DayTripInfo = None
616
+ """
617
+ _LOGGER.debug(f"update_day_trip_info: {yyyymmdd_string}")
618
+ vehicle.day_trip_info = None
619
+
620
+ if vehicle.data is None or "filled_trips" not in vehicle.data:
621
+ _LOGGER.debug(f"filled_trips is empty: {vehicle.data}")
622
+ return # nothing to fill
623
+
624
+ trips = vehicle.data["filled_trips"]
625
+ _LOGGER.debug(f"filled_trips: {trips}")
626
+
627
+ day_trip_info: DayTripInfo = None
628
+ day_trip_info_count = 0
629
+
630
+ for trip in trips:
631
+ date_str = trip.hhmmss
632
+ yyyymmdd = date_str[0:4] + date_str[5:7] + date_str[8:10]
633
+ _LOGGER.debug(f"update_day_trip_info: {yyyymmdd} trip: {trip}")
634
+ if yyyymmdd == yyyymmdd_string:
635
+ if day_trip_info_count == 0:
636
+ day_trip_info = DayTripInfo(
637
+ yyyymmdd=yyyymmdd_string,
638
+ summary=TripInfo(
639
+ drive_time=trip.drive_time,
640
+ idle_time=trip.idle_time,
641
+ distance=trip.distance,
642
+ avg_speed=trip.avg_speed,
643
+ max_speed=trip.max_speed,
644
+ ),
645
+ trip_list=[],
646
+ )
647
+ day_trip_info_count = 1
648
+ else:
649
+ # increment totals for month (for the few trips available)
650
+ day_trip_info_count += 1
651
+ summary = day_trip_info.summary
652
+ summary.drive_time += trip.drive_time
653
+ summary.idle_time += trip.idle_time
654
+ summary.distance += trip.distance
655
+ summary.avg_speed += trip.avg_speed
656
+ summary.max_speed = max(trip.max_speed, summary.max_speed)
657
+
658
+ day_trip_info.summary.avg_speed /= day_trip_info_count
659
+ day_trip_info.summary.avg_speed = round(
660
+ day_trip_info.summary.avg_speed, 1
661
+ )
662
+
663
+ # also fill TripInfo
664
+ hhmmss = date_str[11:13] + date_str[14:16] + date_str[17:19]
665
+ day_trip_info.trip_list.append(
666
+ TripInfo(
667
+ hhmmss=hhmmss,
668
+ drive_time=trip.drive_time,
669
+ idle_time=trip.idle_time,
670
+ distance=trip.distance,
671
+ avg_speed=trip.avg_speed,
672
+ max_speed=trip.max_speed,
673
+ )
674
+ )
675
+ _LOGGER.debug(
676
+ f"update_day_trip_info: trip_list result: {day_trip_info.trip_list}"
677
+ )
678
+
679
+ vehicle.day_trip_info = day_trip_info
680
+
456
681
  def update_vehicle_with_cached_state(self, token: Token, vehicle: Vehicle) -> None:
457
682
  state = {}
458
683
  state["vehicleDetails"] = self._get_vehicle_details(token, vehicle)
@@ -538,6 +763,8 @@ class HyundaiBlueLinkAPIUSA(ApiImpl):
538
763
  elif action == VEHICLE_LOCK_ACTION.UNLOCK:
539
764
  url = self.API_URL + "rcs/rdo/on"
540
765
  _LOGGER.debug(f"{DOMAIN} - Calling unlock")
766
+ else:
767
+ raise APIError(f"Invalid action value: {action}")
541
768
 
542
769
  headers = self._get_vehicle_headers(token, vehicle)
543
770
  headers["APPCLOUD-VIN"] = vehicle.VIN
@@ -636,32 +863,65 @@ class HyundaiBlueLinkAPIUSA(ApiImpl):
636
863
  _LOGGER.debug(f"{DOMAIN} - Stop engine response: {response.text}")
637
864
 
638
865
  def start_charge(self, token: Token, vehicle: Vehicle) -> None:
639
- pass
866
+ if vehicle.engine_type != ENGINE_TYPES.EV:
867
+ return {}
868
+
869
+ _LOGGER.debug(f"{DOMAIN} - Start charging..")
870
+
871
+ url = self.API_URL + "evc/charge/start"
872
+ headers = self._get_vehicle_headers(token, vehicle)
873
+ _LOGGER.debug(f"{DOMAIN} - Start charging headers: {headers}")
874
+
875
+ response = self.sessions.post(url, headers=headers)
876
+ _LOGGER.debug(
877
+ f"{DOMAIN} - Start charge response status code: {response.status_code}"
878
+ )
879
+ _LOGGER.debug(f"{DOMAIN} - Start charge response: {response.text}")
640
880
 
641
881
  def stop_charge(self, token: Token, vehicle: Vehicle) -> None:
642
- pass
882
+ if vehicle.engine_type != ENGINE_TYPES.EV:
883
+ return {}
643
884
 
644
- def get_last_updated_at(self, value) -> dt.datetime:
645
- _LOGGER.debug(f"{DOMAIN} - last_updated_at - before {value}")
646
- if value is None:
647
- value = dt.datetime(2000, 1, 1, tzinfo=self.data_timezone)
648
- else:
649
- value = (
650
- value.replace("-", "")
651
- .replace("T", "")
652
- .replace(":", "")
653
- .replace("Z", "")
654
- )
655
- m = re.match(r"(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})", value)
656
- value = dt.datetime(
657
- year=int(m.group(1)),
658
- month=int(m.group(2)),
659
- day=int(m.group(3)),
660
- hour=int(m.group(4)),
661
- minute=int(m.group(5)),
662
- second=int(m.group(6)),
663
- tzinfo=self.data_timezone,
664
- )
885
+ _LOGGER.debug(f"{DOMAIN} - Stop charging..")
886
+
887
+ url = self.API_URL + "evc/charge/stop"
888
+ headers = self._get_vehicle_headers(token, vehicle)
889
+ _LOGGER.debug(f"{DOMAIN} - Stop charging headers: {headers}")
890
+
891
+ response = self.sessions.post(url, headers=headers)
892
+ _LOGGER.debug(
893
+ f"{DOMAIN} - Stop charge response status code: {response.status_code}"
894
+ )
895
+ _LOGGER.debug(f"{DOMAIN} - Stop charge response: {response.text}")
665
896
 
666
- _LOGGER.debug(f"{DOMAIN} - last_updated_at - after {value}")
667
- return value
897
+ def set_charge_limits(
898
+ self, token: Token, vehicle: Vehicle, ac: int, dc: int
899
+ ) -> str:
900
+ if vehicle.engine_type != ENGINE_TYPES.EV:
901
+ return {}
902
+
903
+ _LOGGER.debug(f"{DOMAIN} - Setting charge limits..")
904
+ url = self.API_URL + "evc/charge/targetsoc/set"
905
+ headers = self._get_vehicle_headers(token, vehicle)
906
+ _LOGGER.debug(f"{DOMAIN} - Setting charge limits: {headers}")
907
+
908
+ data = {
909
+ "targetSOClist": [
910
+ {
911
+ "plugType": 0,
912
+ "targetSOClevel": int(dc),
913
+ },
914
+ {
915
+ "plugType": 1,
916
+ "targetSOClevel": int(ac),
917
+ },
918
+ ]
919
+ }
920
+
921
+ _LOGGER.debug(f"{DOMAIN} - Setting charge limits body: {data}")
922
+
923
+ response = self.sessions.post(url, json=data, headers=headers)
924
+ _LOGGER.debug(
925
+ f"{DOMAIN} - Setting charge limits response status code: {response.status_code}" # noqa
926
+ )
927
+ _LOGGER.debug(f"{DOMAIN} - Setting charge limits: {response.text}")