hyundai-kia-connect-api 3.36.0__py2.py3-none-any.whl → 3.37.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.
@@ -0,0 +1,861 @@
1
+ """KiaUvoApiIN.py"""
2
+
3
+ # pylint:disable=missing-timeout,missing-class-docstring,missing-function-docstring,wildcard-import,unused-wildcard-import,invalid-name,logging-fstring-interpolation,broad-except,bare-except,super-init-not-called,unused-argument,line-too-long,too-many-lines
4
+
5
+ import base64
6
+ import random
7
+ import datetime as dt
8
+ import logging
9
+ import uuid
10
+ import re
11
+ import math
12
+ from urllib.parse import parse_qs, urlparse
13
+ from typing import Optional
14
+ import pytz
15
+ import requests
16
+ from dateutil import tz
17
+
18
+
19
+ from .ApiImplType1 import ApiImplType1
20
+ from .ApiImplType1 import _check_response_for_errors
21
+
22
+ from .Token import Token
23
+ from .Vehicle import (
24
+ Vehicle,
25
+ DailyDrivingStats,
26
+ MonthTripInfo,
27
+ DayTripInfo,
28
+ TripInfo,
29
+ DayTripCounts,
30
+ )
31
+ from .const import (
32
+ BRAND_HYUNDAI,
33
+ BRAND_KIA,
34
+ BRANDS,
35
+ CHARGE_PORT_ACTION,
36
+ DISTANCE_UNITS,
37
+ DOMAIN,
38
+ ENGINE_TYPES,
39
+ SEAT_STATUS,
40
+ TEMPERATURE_UNITS,
41
+ VEHICLE_LOCK_ACTION,
42
+ VALET_MODE_ACTION,
43
+ )
44
+ from .exceptions import (
45
+ AuthenticationError,
46
+ )
47
+ from .utils import (
48
+ get_child_value,
49
+ get_hex_temp_into_index,
50
+ )
51
+
52
+ _LOGGER = logging.getLogger(__name__)
53
+
54
+ USER_AGENT_OK_HTTP: str = "okhttp/3.12.0"
55
+ 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
56
+ 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
57
+
58
+ SUPPORTED_LANGUAGES_LIST = [
59
+ "en", # English
60
+ "de", # German
61
+ "fr", # French
62
+ "it", # Italian
63
+ "es", # Spanish
64
+ "sv", # Swedish
65
+ "nl", # Dutch
66
+ "no", # Norwegian
67
+ "cs", # Czech
68
+ "sk", # Slovak
69
+ "hu", # Hungarian
70
+ "da", # Danish
71
+ "pl", # Polish
72
+ "fi", # Finnish
73
+ "pt", # Portuguese
74
+ ]
75
+
76
+
77
+ class KiaUvoApiIN(ApiImplType1):
78
+ data_timezone = tz.gettz("Asia/Kolkata")
79
+ temperature_range = [x * 0.5 for x in range(28, 60)]
80
+
81
+ def __init__(self, brand: int) -> None:
82
+ # Strip language variants (e.g. en-Gb)
83
+ super().__init__()
84
+ self.brand = brand
85
+
86
+ if BRANDS[brand] == BRAND_HYUNDAI:
87
+ self.BASE_DOMAIN: str = "prd.in-ccapi.hyundai.connected-car.io"
88
+ self.PORT: int = 8080
89
+ self.CCSP_SERVICE_ID: str = "e5b3f6d0-7f83-43c9-aff3-a254db7af368"
90
+ self.APP_ID: str = "5a27df80-4ca1-4154-8c09-6f4029d91cf7"
91
+ self.CFB: str = base64.b64decode(
92
+ "RFtoRq/vDXJmRndoZaZQyfOot7OrIqGVFj96iY2WL3yyH5Z/pUvlUhqmCxD2t+D65SQ="
93
+ )
94
+ self.BASIC_AUTHORIZATION: str = "Basic ZTViM2Y2ZDAtN2Y4My00M2M5LWFmZjMtYTI1NGRiN2FmMzY4OjVKRk9DcjZDMjRPZk96bERxWnA3RXdxcmtMMFd3MDRVYXhjRGlFNlVkM3FJNVNFNA==" # noqa
95
+ self.LOGIN_FORM_HOST = "prd.in-ccapi.hyundai.connected-car.io"
96
+ self.PUSH_TYPE = "GCM"
97
+ self.GCM_SENDER_ID = 974204007939
98
+ elif BRANDS[brand] == BRAND_KIA:
99
+ raise NotImplementedError()
100
+
101
+ self.BASE_URL: str = self.BASE_DOMAIN + ":" + str(self.PORT)
102
+ self.USER_API_URL: str = "https://" + self.BASE_URL + "/api/v1/user/"
103
+ self.SPA_API_URL: str = "https://" + self.BASE_URL + "/api/v1/spa/"
104
+ self.SPA_API_URL_V2: str = "https://" + self.BASE_URL + "/api/v2/spa/"
105
+
106
+ self.CLIENT_ID: str = self.CCSP_SERVICE_ID
107
+
108
+ def _get_authenticated_headers(
109
+ self, token: Token, ccs2_support: Optional[int] = None
110
+ ) -> dict:
111
+ return {
112
+ "Authorization": token.access_token,
113
+ "ccsp-service-id": self.CCSP_SERVICE_ID,
114
+ "ccsp-application-id": self.APP_ID,
115
+ "ccsp-device-id": token.device_id,
116
+ "Host": self.BASE_URL,
117
+ "Connection": "Keep-Alive",
118
+ "Accept-Encoding": "gzip",
119
+ "User-Agent": USER_AGENT_OK_HTTP,
120
+ }
121
+
122
+ def login(self, username: str, password: str) -> Token:
123
+ stamp = self._get_stamp()
124
+ device_id = self._get_device_id(stamp)
125
+ cookies = self._get_cookies()
126
+ authorization_code = None
127
+ try:
128
+ authorization_code = self._get_authorization_code_with_redirect_url(
129
+ username, password, cookies
130
+ )
131
+ except Exception:
132
+ _LOGGER.debug(f"{DOMAIN} - get_authorization_code_with_redirect_url failed")
133
+
134
+ if authorization_code is None:
135
+ raise AuthenticationError("Login Failed")
136
+
137
+ _, access_token, authorization_code = self._get_access_token(
138
+ stamp, authorization_code
139
+ )
140
+ _, refresh_token = self._get_refresh_token(stamp, authorization_code)
141
+ valid_until = dt.datetime.now(pytz.utc) + dt.timedelta(hours=23)
142
+
143
+ return Token(
144
+ username=username,
145
+ password=password,
146
+ access_token=access_token,
147
+ refresh_token=refresh_token,
148
+ device_id=device_id,
149
+ valid_until=valid_until,
150
+ )
151
+
152
+ def get_vehicles(self, token: Token) -> list[Vehicle]:
153
+ url = self.SPA_API_URL + "vehicles"
154
+ response = requests.get(
155
+ url,
156
+ headers=self._get_authenticated_headers(token),
157
+ ).json()
158
+ _LOGGER.debug(f"{DOMAIN} - Get Vehicles Response: {response}")
159
+ _check_response_for_errors(response)
160
+ result = []
161
+ for entry in response["resMsg"]["vehicles"]:
162
+ entry_engine_type = None
163
+ if entry["type"] == "GN":
164
+ entry_engine_type = ENGINE_TYPES.ICE
165
+ elif entry["type"] == "EV":
166
+ entry_engine_type = ENGINE_TYPES.EV
167
+ elif entry["type"] == "PHEV":
168
+ entry_engine_type = ENGINE_TYPES.PHEV
169
+ elif entry["type"] == "HV":
170
+ entry_engine_type = ENGINE_TYPES.HEV
171
+ elif entry["type"] == "PE":
172
+ entry_engine_type = ENGINE_TYPES.PHEV
173
+ vehicle: Vehicle = Vehicle(
174
+ id=entry["vehicleId"],
175
+ name=entry["nickname"],
176
+ model=entry["vehicleName"],
177
+ registration_date=entry["regDate"],
178
+ VIN=entry["vin"],
179
+ timezone=self.data_timezone,
180
+ engine_type=entry_engine_type,
181
+ ccu_ccs2_protocol_support=entry["ccuCCS2ProtocolSupport"],
182
+ )
183
+ result.append(vehicle)
184
+ return result
185
+
186
+ def _get_time_from_string(self, value, timesection) -> dt.datetime.time:
187
+ if value is not None:
188
+ lastTwo = int(value[-2:])
189
+ if lastTwo > 60:
190
+ value = int(value) + 40
191
+ if int(value) > 1260:
192
+ value = dt.datetime.strptime(str(value), "%H%M").time()
193
+ else:
194
+ d = dt.datetime.strptime(str(value), "%I%M")
195
+ if timesection > 0:
196
+ d += dt.timedelta(hours=12)
197
+ value = d.time()
198
+ return value
199
+
200
+ def update_vehicle_with_cached_state(self, token: Token, vehicle: Vehicle) -> None:
201
+ state = self._get_cached_vehicle_state(token, vehicle)
202
+
203
+ self._update_vehicle_properties(vehicle, state)
204
+
205
+ state = self._get_maintenance_alert(token, vehicle)
206
+
207
+ self._update_vehicle_maintenance_alert(vehicle, state)
208
+
209
+ state = self._get_location(token, vehicle)
210
+
211
+ self._update_vehicle_location(vehicle, state)
212
+
213
+ def _update_vehicle_maintenance_alert(self, vehicle: Vehicle, state: dict) -> None:
214
+ if get_child_value(state, "odometer"):
215
+ vehicle.odometer = (get_child_value(state, "odometer"), DISTANCE_UNITS[1])
216
+
217
+ def _get_maintenance_alert(self, token: Token, vehicle: Vehicle) -> dict:
218
+ url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/setting/alert/maintenance"
219
+ _LOGGER.error(f"Getting maintenance alert from {url}")
220
+ response = requests.get(
221
+ url, headers=self._get_authenticated_headers(token)
222
+ ).json()
223
+ _LOGGER.error(response)
224
+ _check_response_for_errors(response)
225
+ return response["resMsg"]
226
+
227
+ def _update_vehicle_location(self, vehicle: Vehicle, state: dict) -> None:
228
+ if get_child_value(state, "coord.lat"):
229
+ vehicle.location = (
230
+ get_child_value(state, "coord.lat"),
231
+ get_child_value(state, "coord.lon"),
232
+ self.get_last_updated_at(get_child_value(state, "time")),
233
+ )
234
+
235
+ def force_refresh_vehicle_state(self, token: Token, vehicle: Vehicle) -> None:
236
+ state = self._get_forced_vehicle_state(token, vehicle)
237
+ state["vehicleLocation"] = self._get_location(token, vehicle)
238
+ self._update_vehicle_properties(vehicle, state)
239
+ # Only call for driving info on cars we know have a chance of supporting it.
240
+ # Could be expanded if other types do support it.
241
+ if (
242
+ vehicle.engine_type == ENGINE_TYPES.EV
243
+ or vehicle.engine_type == ENGINE_TYPES.PHEV
244
+ ):
245
+ try:
246
+ state = self._get_driving_info(token, vehicle)
247
+ except Exception as e:
248
+ # we don't know if all car types provide this information.
249
+ # we also don't know what the API returns if the info is unavailable.
250
+ # so, catch any exception and move on.
251
+ _LOGGER.exception(
252
+ """Failed to parse driving info. Possible reasons:
253
+ - new API format
254
+ - API outage
255
+ """,
256
+ exc_info=e,
257
+ )
258
+ else:
259
+ self._update_vehicle_drive_info(vehicle, state)
260
+
261
+ def _update_vehicle_properties(self, vehicle: Vehicle, state: dict) -> None:
262
+ if get_child_value(state, "time"):
263
+ vehicle.last_updated_at = self.get_last_updated_at(
264
+ get_child_value(state, "time")
265
+ )
266
+ else:
267
+ vehicle.last_updated_at = dt.datetime.now(self.data_timezone)
268
+
269
+ vehicle.engine_is_running = get_child_value(state, "engine")
270
+
271
+ # Converts temp to usable number. Currently only support celsius.
272
+ # Future to do is check unit in case the care itself is set to F.
273
+ if get_child_value(state, "airTemp.value"):
274
+ tempIndex = get_hex_temp_into_index(get_child_value(state, "airTemp.value"))
275
+
276
+ vehicle.air_temperature = (
277
+ self.temperature_range[tempIndex],
278
+ TEMPERATURE_UNITS[
279
+ get_child_value(
280
+ state,
281
+ "airTemp.unit",
282
+ )
283
+ ],
284
+ )
285
+ vehicle.defrost_is_on = get_child_value(state, "defrost")
286
+ steer_wheel_heat = get_child_value(state, "steerWheelHeat")
287
+ if steer_wheel_heat in [0, 2]:
288
+ vehicle.steering_wheel_heater_is_on = False
289
+ elif steer_wheel_heat == 1:
290
+ vehicle.steering_wheel_heater_is_on = True
291
+
292
+ vehicle.back_window_heater_is_on = get_child_value(state, "sideBackWindowHeat")
293
+ vehicle.front_left_seat_status = SEAT_STATUS[
294
+ get_child_value(state, "seatHeaterVentState.astSeatHeatState")
295
+ ]
296
+ vehicle.front_right_seat_status = SEAT_STATUS[
297
+ get_child_value(state, "seatHeaterVentState.drvSeatHeatState")
298
+ ]
299
+ vehicle.rear_left_seat_status = SEAT_STATUS[
300
+ get_child_value(state, "seatHeaterVentState.rlSeatHeatState")
301
+ ]
302
+ vehicle.rear_right_seat_status = SEAT_STATUS[
303
+ get_child_value(state, "seatHeaterVentState.rrSeatHeatState")
304
+ ]
305
+ vehicle.is_locked = get_child_value(state, "doorLock")
306
+ vehicle.front_left_door_is_open = get_child_value(state, "doorOpen.frontLeft")
307
+ vehicle.front_right_door_is_open = get_child_value(state, "doorOpen.frontRight")
308
+ vehicle.back_left_door_is_open = get_child_value(state, "doorOpen.backLeft")
309
+ vehicle.back_right_door_is_open = get_child_value(state, "doorOpen.backRight")
310
+ vehicle.hood_is_open = get_child_value(state, "hoodOpen")
311
+ vehicle.front_left_window_is_open = get_child_value(
312
+ state, "windowOpen.frontLeft"
313
+ )
314
+ vehicle.front_right_window_is_open = get_child_value(
315
+ state, "windowOpen.frontRight"
316
+ )
317
+ vehicle.back_left_window_is_open = get_child_value(state, "windowOpen.backLeft")
318
+ vehicle.back_right_window_is_open = get_child_value(
319
+ state, "windowOpen.backRight"
320
+ )
321
+ vehicle.tire_pressure_rear_left_warning_is_on = bool(
322
+ get_child_value(state, "tirePressureLamp.tirePressureLampRL")
323
+ )
324
+ vehicle.tire_pressure_front_left_warning_is_on = bool(
325
+ get_child_value(state, "tirePressureLamp.tirePressureLampFL")
326
+ )
327
+ vehicle.tire_pressure_front_right_warning_is_on = bool(
328
+ get_child_value(state, "tirePressureLamp.tirePressureLampFR")
329
+ )
330
+ vehicle.tire_pressure_rear_right_warning_is_on = bool(
331
+ get_child_value(state, "tirePressureLamp.tirePressureLampRR")
332
+ )
333
+ vehicle.tire_pressure_all_warning_is_on = bool(
334
+ get_child_value(state, "tirePressureLamp.tirePressureLampAll")
335
+ )
336
+ vehicle.trunk_is_open = get_child_value(state, "trunkOpen")
337
+ if get_child_value(
338
+ state,
339
+ "dte.value",
340
+ ):
341
+ vehicle.fuel_driving_range = (
342
+ get_child_value(
343
+ state,
344
+ "dte.value",
345
+ ),
346
+ DISTANCE_UNITS[get_child_value(state, "dte.unit")],
347
+ )
348
+
349
+ vehicle.brake_fluid_warning_is_on = get_child_value(state, "breakOilStatus")
350
+ vehicle.fuel_level = get_child_value(state, "fuelLevel")
351
+ vehicle.fuel_level_is_low = get_child_value(state, "lowFuelLight")
352
+ vehicle.air_control_is_on = get_child_value(state, "airCtrlOn")
353
+ vehicle.smart_key_battery_warning_is_on = get_child_value(
354
+ state, "smartKeyBatteryWarning"
355
+ )
356
+
357
+ vehicle.data = state
358
+
359
+ def _update_vehicle_drive_info(self, vehicle: Vehicle, state: dict) -> None:
360
+ vehicle.total_power_consumed = get_child_value(state, "totalPwrCsp")
361
+ vehicle.total_power_regenerated = get_child_value(state, "regenPwr")
362
+ vehicle.power_consumption_30d = get_child_value(state, "consumption30d")
363
+ vehicle.daily_stats = get_child_value(state, "dailyStats")
364
+
365
+ def _get_cached_vehicle_state(self, token: Token, vehicle: Vehicle) -> dict:
366
+ url = self.SPA_API_URL + "vehicles/" + vehicle.id
367
+ if vehicle.ccu_ccs2_protocol_support == 0:
368
+ url = url + "/status/latest"
369
+ else:
370
+ url = url + "/ccs2/carstatus/latest"
371
+ response = requests.get(
372
+ url,
373
+ headers=self._get_authenticated_headers(
374
+ token, vehicle.ccu_ccs2_protocol_support
375
+ ),
376
+ ).json()
377
+ _LOGGER.debug(f"{DOMAIN} - get_cached_vehicle_status response: {response}")
378
+ _check_response_for_errors(response)
379
+ response = response["resMsg"]
380
+ return response
381
+
382
+ def _get_location(self, token: Token, vehicle: Vehicle) -> dict:
383
+ url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/location/park"
384
+ _LOGGER.error(f"Getting location from {url}")
385
+
386
+ try:
387
+ response = requests.get(
388
+ url,
389
+ headers=self._get_authenticated_headers(
390
+ token, vehicle.ccu_ccs2_protocol_support
391
+ ),
392
+ ).json()
393
+ _LOGGER.error(f"{DOMAIN} - _get_location response: {response}")
394
+ _check_response_for_errors(response)
395
+ return response["resMsg"]
396
+ except Exception:
397
+ _LOGGER.warning(f"{DOMAIN} - _get_location failed")
398
+ return None
399
+
400
+ def lock_action(
401
+ self, token: Token, vehicle: Vehicle, action: VEHICLE_LOCK_ACTION
402
+ ) -> str:
403
+ url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/control/door"
404
+
405
+ payload = {"action": action.value, "deviceId": token.device_id}
406
+ _LOGGER.debug(f"{DOMAIN} - Lock Action Request: {payload}")
407
+ response = requests.post(
408
+ url, json=payload, headers=self._get_authenticated_headers(token)
409
+ ).json()
410
+ _LOGGER.debug(f"{DOMAIN} - Lock Action Response: {response}")
411
+ _check_response_for_errors(response)
412
+ return response["msgId"]
413
+
414
+ def _get_forced_vehicle_state(self, token: Token, vehicle: Vehicle) -> dict:
415
+ url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/status"
416
+ response = requests.get(
417
+ url,
418
+ headers=self._get_authenticated_headers(
419
+ token, vehicle.ccu_ccs2_protocol_support
420
+ ),
421
+ ).json()
422
+ _LOGGER.debug(f"{DOMAIN} - Received forced vehicle data: {response}")
423
+ _check_response_for_errors(response)
424
+ mapped_response = {}
425
+ mapped_response["vehicleStatus"] = response["resMsg"]
426
+ return mapped_response
427
+
428
+ def charge_port_action(
429
+ self, token: Token, vehicle: Vehicle, action: CHARGE_PORT_ACTION
430
+ ) -> str:
431
+ url = self.SPA_API_URL_V2 + "vehicles/" + vehicle.id + "/control/portdoor"
432
+
433
+ payload = {"action": action.value, "deviceID": token.device_id}
434
+ _LOGGER.debug(f"{DOMAIN} - Charge Port Action Request: {payload}")
435
+ response = requests.post(
436
+ url, json=payload, headers=self._get_control_headers(token, vehicle)
437
+ ).json()
438
+ _LOGGER.debug(f"{DOMAIN} - Charge Port Action Response: {response}")
439
+ _check_response_for_errors(response)
440
+ token.device_id = self._get_device_id(self._get_stamp())
441
+ return response["msgId"]
442
+
443
+ def start_hazard_lights(self, token: Token, vehicle: Vehicle) -> str:
444
+ url = self.SPA_API_URL_V2 + "vehicles/" + vehicle.id + "/ccs2/control/light"
445
+
446
+ payload = {"command": "on"}
447
+ _LOGGER.debug(f"{DOMAIN} - Start Hazard Lights Request: {payload}")
448
+ response = requests.post(
449
+ url,
450
+ json=payload,
451
+ headers=self._get_control_headers(token, vehicle),
452
+ ).json()
453
+ _LOGGER.debug(f"{DOMAIN} - Start Hazard Lights Response: {response}")
454
+ _check_response_for_errors(response)
455
+ token.device_id = self._get_device_id(self._get_stamp())
456
+ return response["msgId"]
457
+
458
+ def start_hazard_lights_and_horn(self, token: Token, vehicle: Vehicle) -> str:
459
+ url = self.SPA_API_URL_V2 + "vehicles/" + vehicle.id + "/ccs2/control/hornlight"
460
+
461
+ payload = {"command": "on"}
462
+ _LOGGER.debug(f"{DOMAIN} - Start Hazard Lights and Horn Request: {payload}")
463
+ response = requests.post(
464
+ url,
465
+ json=payload,
466
+ headers=self._get_control_headers(token, vehicle),
467
+ ).json()
468
+ _LOGGER.debug(f"{DOMAIN} - Start Hazard Lights and Horn Response: {response}")
469
+ _check_response_for_errors(response)
470
+ token.device_id = self._get_device_id(self._get_stamp())
471
+ return response["msgId"]
472
+
473
+ def _get_charge_limits(self, token: Token, vehicle: Vehicle) -> dict:
474
+ # Not currently used as value is in the general get.
475
+ # Most likely this forces the car the update it.
476
+ url = f"{self.SPA_API_URL}vehicles/{vehicle.id}/charge/target"
477
+
478
+ _LOGGER.debug(f"{DOMAIN} - Get Charging Limits Request")
479
+ response = requests.get(
480
+ url,
481
+ headers=self._get_authenticated_headers(
482
+ token, vehicle.ccu_ccs2_protocol_support
483
+ ),
484
+ ).json()
485
+ _LOGGER.debug(f"{DOMAIN} - Get Charging Limits Response: {response}")
486
+ _check_response_for_errors(response)
487
+ # API sometimes returns multiple entries per plug type and they conflict.
488
+ # The car itself says the last entry per plug type is the truth when tested
489
+ # (EU Ioniq Electric Facelift MY 2019)
490
+ if response["resMsg"] is not None:
491
+ return response["resMsg"]
492
+
493
+ def _get_trip_info(
494
+ self,
495
+ token: Token,
496
+ vehicle: Vehicle,
497
+ date_string: str,
498
+ trip_period_type: int,
499
+ ) -> dict:
500
+ url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/tripinfo"
501
+ if trip_period_type == 0: # month
502
+ payload = {"tripPeriodType": 0, "setTripMonth": date_string}
503
+ else:
504
+ payload = {"tripPeriodType": 1, "setTripDay": date_string}
505
+
506
+ _LOGGER.debug(f"{DOMAIN} - get_trip_info Request {payload}")
507
+ response = requests.post(
508
+ url,
509
+ json=payload,
510
+ headers=self._get_authenticated_headers(
511
+ token, vehicle.ccu_ccs2_protocol_support
512
+ ),
513
+ )
514
+ response = response.json()
515
+ _LOGGER.debug(f"{DOMAIN} - get_trip_info response {response}")
516
+ _check_response_for_errors(response)
517
+ return response
518
+
519
+ def update_month_trip_info(
520
+ self,
521
+ token,
522
+ vehicle,
523
+ yyyymm_string,
524
+ ) -> None:
525
+ """
526
+ feature only available for some regions.
527
+ Updates the vehicle.month_trip_info for the specified month.
528
+
529
+ Default this information is None:
530
+
531
+ month_trip_info: MonthTripInfo = None
532
+ """
533
+ vehicle.month_trip_info = None
534
+ json_result = self._get_trip_info(
535
+ token,
536
+ vehicle,
537
+ yyyymm_string,
538
+ 0, # month trip info
539
+ )
540
+ msg = json_result["resMsg"]
541
+ if msg["monthTripDayCnt"] > 0:
542
+ result = MonthTripInfo(
543
+ yyyymm=yyyymm_string,
544
+ day_list=[],
545
+ summary=TripInfo(
546
+ drive_time=msg["tripDrvTime"],
547
+ idle_time=msg["tripIdleTime"],
548
+ distance=msg["tripDist"],
549
+ avg_speed=msg["tripAvgSpeed"],
550
+ max_speed=msg["tripMaxSpeed"],
551
+ ),
552
+ )
553
+
554
+ for day in msg["tripDayList"]:
555
+ processed_day = DayTripCounts(
556
+ yyyymmdd=day["tripDayInMonth"],
557
+ trip_count=day["tripCntDay"],
558
+ )
559
+ result.day_list.append(processed_day)
560
+
561
+ vehicle.month_trip_info = result
562
+
563
+ def update_day_trip_info(
564
+ self,
565
+ token,
566
+ vehicle,
567
+ yyyymmdd_string,
568
+ ) -> None:
569
+ """
570
+ feature only available for some regions.
571
+ Updates the vehicle.day_trip_info information for the specified day.
572
+
573
+ Default this information is None:
574
+
575
+ day_trip_info: DayTripInfo = None
576
+ """
577
+ vehicle.day_trip_info = None
578
+ json_result = self._get_trip_info(
579
+ token,
580
+ vehicle,
581
+ yyyymmdd_string,
582
+ 1, # day trip info
583
+ )
584
+ day_trip_list = json_result["resMsg"]["dayTripList"]
585
+ if len(day_trip_list) > 0:
586
+ msg = day_trip_list[0]
587
+ result = DayTripInfo(
588
+ yyyymmdd=yyyymmdd_string,
589
+ trip_list=[],
590
+ summary=TripInfo(
591
+ drive_time=msg["tripDrvTime"],
592
+ idle_time=msg["tripIdleTime"],
593
+ distance=msg["tripDist"],
594
+ avg_speed=msg["tripAvgSpeed"],
595
+ max_speed=msg["tripMaxSpeed"],
596
+ ),
597
+ )
598
+ for trip in msg["tripList"]:
599
+ processed_trip = TripInfo(
600
+ hhmmss=trip["tripTime"],
601
+ drive_time=trip["tripDrvTime"],
602
+ idle_time=trip["tripIdleTime"],
603
+ distance=trip["tripDist"],
604
+ avg_speed=trip["tripAvgSpeed"],
605
+ max_speed=trip["tripMaxSpeed"],
606
+ )
607
+ result.trip_list.append(processed_trip)
608
+
609
+ vehicle.day_trip_info = result
610
+
611
+ def _get_driving_info(self, token: Token, vehicle: Vehicle) -> dict:
612
+ url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/drvhistory"
613
+
614
+ responseAlltime = requests.post(
615
+ url,
616
+ json={"periodTarget": 1},
617
+ headers=self._get_authenticated_headers(
618
+ token, vehicle.ccu_ccs2_protocol_support
619
+ ),
620
+ )
621
+ responseAlltime = responseAlltime.json()
622
+ _LOGGER.debug(f"{DOMAIN} - get_driving_info responseAlltime {responseAlltime}")
623
+ _check_response_for_errors(responseAlltime)
624
+
625
+ response30d = requests.post(
626
+ url,
627
+ json={"periodTarget": 0},
628
+ headers=self._get_authenticated_headers(
629
+ token, vehicle.ccu_ccs2_protocol_support
630
+ ),
631
+ )
632
+ response30d = response30d.json()
633
+ _LOGGER.debug(f"{DOMAIN} - get_driving_info response30d {response30d}")
634
+ _check_response_for_errors(response30d)
635
+ if get_child_value(responseAlltime, "resMsg.drivingInfo.0"):
636
+ drivingInfo = responseAlltime["resMsg"]["drivingInfo"][0]
637
+
638
+ drivingInfo["dailyStats"] = []
639
+ if get_child_value(response30d, "resMsg.drivingInfoDetail.0"):
640
+ for day in response30d["resMsg"]["drivingInfoDetail"]:
641
+ processedDay = DailyDrivingStats(
642
+ date=dt.datetime.strptime(day["drivingDate"], "%Y%m%d"),
643
+ total_consumed=get_child_value(day, "totalPwrCsp"),
644
+ engine_consumption=get_child_value(day, "motorPwrCsp"),
645
+ climate_consumption=get_child_value(day, "climatePwrCsp"),
646
+ onboard_electronics_consumption=get_child_value(
647
+ day, "eDPwrCsp"
648
+ ),
649
+ battery_care_consumption=get_child_value(
650
+ day, "batteryMgPwrCsp"
651
+ ),
652
+ regenerated_energy=get_child_value(day, "regenPwr"),
653
+ distance=get_child_value(day, "calculativeOdo"),
654
+ distance_unit=vehicle.odometer_unit,
655
+ )
656
+ drivingInfo["dailyStats"].append(processedDay)
657
+
658
+ for drivingInfoItem in response30d["resMsg"]["drivingInfo"]:
659
+ if (
660
+ drivingInfoItem["drivingPeriod"] == 0
661
+ and next(
662
+ (
663
+ v
664
+ for k, v in drivingInfoItem.items()
665
+ if k.lower() == "calculativeodo"
666
+ ),
667
+ 0,
668
+ )
669
+ > 0
670
+ ):
671
+ drivingInfo["consumption30d"] = round(
672
+ drivingInfoItem["totalPwrCsp"]
673
+ / drivingInfoItem["calculativeOdo"]
674
+ )
675
+ break
676
+
677
+ return drivingInfo
678
+ else:
679
+ _LOGGER.debug(
680
+ f"{DOMAIN} - Driving info didn't return valid data. This may be normal if the car doesn't support it." # noqa
681
+ )
682
+ return None
683
+
684
+ def valet_mode_action(
685
+ self, token: Token, vehicle: Vehicle, action: VALET_MODE_ACTION
686
+ ) -> str:
687
+ url = self.SPA_API_URL_V2 + "vehicles/" + vehicle.id + "/control/valet"
688
+
689
+ payload = {"action": action.value}
690
+ _LOGGER.debug(f"{DOMAIN} - Valet Mode Action Request: {payload}")
691
+ response = requests.post(
692
+ url, json=payload, headers=self._get_control_headers(token, vehicle)
693
+ ).json()
694
+ _LOGGER.debug(f"{DOMAIN} - Valet Mode Action Response: {response}")
695
+ _check_response_for_errors(response)
696
+ token.device_id = self._get_device_id(self._get_stamp())
697
+ return response["msgId"]
698
+
699
+ def _get_stamp(self) -> str:
700
+ raw_data = f"{self.APP_ID}:{int(dt.datetime.now().timestamp())}".encode()
701
+ result = bytes(b1 ^ b2 for b1, b2 in zip(self.CFB, raw_data))
702
+ return base64.b64encode(result).decode("utf-8")
703
+
704
+ def _get_device_id(self, stamp: str):
705
+ my_hex = "%064x" % random.randrange( # pylint: disable=consider-using-f-string
706
+ 10**80
707
+ )
708
+ registration_id = my_hex[:64]
709
+ url = self.SPA_API_URL + "notifications/register"
710
+ payload = {
711
+ "pushRegId": registration_id,
712
+ "pushType": self.PUSH_TYPE,
713
+ "uuid": str(uuid.uuid4()),
714
+ }
715
+
716
+ headers = {
717
+ "ccsp-service-id": self.CCSP_SERVICE_ID,
718
+ "ccsp-application-id": self.APP_ID,
719
+ "Stamp": stamp,
720
+ "Content-Type": "application/json;charset=UTF-8",
721
+ "Host": self.BASE_URL,
722
+ "Connection": "Keep-Alive",
723
+ "Accept-Encoding": "gzip",
724
+ "User-Agent": USER_AGENT_OK_HTTP,
725
+ }
726
+
727
+ _LOGGER.debug(f"{DOMAIN} - Get Device ID request: {url} {headers} {payload}")
728
+ response = requests.post(url, headers=headers, json=payload)
729
+ response = response.json()
730
+ _check_response_for_errors(response)
731
+ _LOGGER.debug(f"{DOMAIN} - Get Device ID response: {response}")
732
+
733
+ device_id = response["resMsg"]["deviceId"]
734
+ return device_id
735
+
736
+ def _get_cookies(self) -> dict:
737
+ # Get Cookies #
738
+ url = (
739
+ self.USER_API_URL
740
+ + "oauth2/authorize?response_type=code&state=test&client_id="
741
+ + self.CLIENT_ID
742
+ + "&redirect_uri="
743
+ + self.USER_API_URL
744
+ + "oauth2/redirect"
745
+ )
746
+
747
+ _LOGGER.debug(f"{DOMAIN} - Get cookies request: {url}")
748
+ session = requests.Session()
749
+ _ = session.get(url)
750
+ _LOGGER.debug(f"{DOMAIN} - Get cookies response: {session.cookies.get_dict()}")
751
+ return session.cookies.get_dict()
752
+ # return session
753
+
754
+ def _get_authorization_code_with_redirect_url(
755
+ self, username, password, cookies
756
+ ) -> str:
757
+ url = self.USER_API_URL + "signin"
758
+ headers = {"Content-type": "application/json"}
759
+ data = {"email": username, "password": password}
760
+ response = requests.post(
761
+ url, json=data, headers=headers, cookies=cookies
762
+ ).json()
763
+ _LOGGER.debug(f"{DOMAIN} - Sign In Response: {response}")
764
+ parsed_url = urlparse(response["redirectUrl"])
765
+ authorization_code = "".join(parse_qs(parsed_url.query)["code"])
766
+ return authorization_code
767
+
768
+ def _get_access_token(self, stamp, authorization_code):
769
+ # Get Access Token #
770
+ url = self.USER_API_URL + "oauth2/token"
771
+ headers = {
772
+ "Authorization": self.BASIC_AUTHORIZATION,
773
+ "Stamp": stamp,
774
+ "Content-type": "application/x-www-form-urlencoded",
775
+ "Host": self.BASE_URL,
776
+ "Connection": "close",
777
+ "Accept-Encoding": "gzip, deflate",
778
+ "User-Agent": USER_AGENT_OK_HTTP,
779
+ }
780
+
781
+ data = (
782
+ "grant_type=authorization_code&redirect_uri=https%3A%2F%2F"
783
+ + self.BASE_DOMAIN
784
+ + "%3A8080%2Fapi%2Fv1%2Fuser%2Foauth2%2Fredirect&code="
785
+ + authorization_code
786
+ )
787
+ _LOGGER.debug(f"{DOMAIN} - Get Access Token Data: {headers}{data}")
788
+ response = requests.post(url, data=data, headers=headers)
789
+ response = response.json()
790
+ _LOGGER.debug(f"{DOMAIN} - Get Access Token Response: {response}")
791
+
792
+ token_type = response["token_type"]
793
+ access_token = token_type + " " + response["access_token"]
794
+ authorization_code = response["refresh_token"]
795
+ _LOGGER.debug(f"{DOMAIN} - Access Token Value {access_token}")
796
+ return token_type, access_token, authorization_code
797
+
798
+ def get_last_updated_at(self, value) -> dt.datetime:
799
+ _LOGGER.debug(f"{DOMAIN} - last_updated_at - before {value}")
800
+ if value is None:
801
+ value = dt.datetime(2000, 1, 1, tzinfo=self.data_timezone)
802
+ else:
803
+ m = re.match(r"(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})", value)
804
+ value = dt.datetime(
805
+ year=int(m.group(1)),
806
+ month=int(m.group(2)),
807
+ day=int(m.group(3)),
808
+ hour=int(m.group(4)),
809
+ minute=int(m.group(5)),
810
+ second=int(m.group(6)),
811
+ tzinfo=self.data_timezone,
812
+ )
813
+
814
+ _LOGGER.debug(f"{DOMAIN} - last_updated_at - after {value}")
815
+ return value
816
+
817
+ def _get_refresh_token(self, stamp, authorization_code):
818
+ # Get Refresh Token #
819
+ url = self.USER_API_URL + "oauth2/token"
820
+ headers = {
821
+ "Authorization": self.BASIC_AUTHORIZATION,
822
+ "Stamp": stamp,
823
+ "Content-type": "application/x-www-form-urlencoded",
824
+ "Host": self.BASE_URL,
825
+ "Connection": "close",
826
+ "Accept-Encoding": "gzip, deflate",
827
+ "User-Agent": USER_AGENT_OK_HTTP,
828
+ }
829
+
830
+ data = (
831
+ "grant_type=refresh_token&redirect_uri=https%3A%2F%2Fwww.getpostman.com%2Foauth2%2Fcallback&refresh_token=" # noqa
832
+ + authorization_code
833
+ )
834
+ _LOGGER.debug(f"{DOMAIN} - Get Refresh Token Data: {data}")
835
+ response = requests.post(url, data=data, headers=headers)
836
+ response = response.json()
837
+ _LOGGER.debug(f"{DOMAIN} - Get Refresh Token Response: {response}")
838
+ token_type = response["token_type"]
839
+ refresh_token = token_type + " " + response["access_token"]
840
+ return token_type, refresh_token
841
+
842
+ def _get_control_token(self, token: Token) -> Token:
843
+ url = self.USER_API_URL + "pin?token="
844
+ headers = {
845
+ "Authorization": token.access_token,
846
+ "Content-type": "application/json",
847
+ "Host": self.BASE_URL,
848
+ "Accept-Encoding": "gzip",
849
+ "User-Agent": USER_AGENT_OK_HTTP,
850
+ }
851
+
852
+ data = {"deviceId": token.device_id, "pin": token.pin}
853
+ _LOGGER.debug(f"{DOMAIN} - Get Control Token Data: {data}")
854
+ response = requests.put(url, json=data, headers=headers)
855
+ response = response.json()
856
+ _LOGGER.debug(f"{DOMAIN} - Get Control Token Response {response}")
857
+ control_token = "Bearer " + response["controlToken"]
858
+ control_token_expire_at = math.floor(
859
+ dt.datetime.now().timestamp() + response["expiresTime"]
860
+ )
861
+ return control_token, control_token_expire_at
@@ -20,6 +20,7 @@ from .KiaUvoApiCA import KiaUvoApiCA
20
20
  from .KiaUvoApiEU import KiaUvoApiEU
21
21
  from .KiaUvoApiCN import KiaUvoApiCN
22
22
  from .KiaUvoApiAU import KiaUvoApiAU
23
+ from .KiaUvoApiIN import KiaUvoApiIN
23
24
  from .Token import Token
24
25
  from .Vehicle import Vehicle
25
26
  from .const import (
@@ -33,6 +34,7 @@ from .const import (
33
34
  REGION_EUROPE,
34
35
  REGION_USA,
35
36
  REGION_CHINA,
37
+ REGION_INDIA,
36
38
  REGIONS,
37
39
  VEHICLE_LOCK_ACTION,
38
40
  CHARGE_PORT_ACTION,
@@ -312,5 +314,7 @@ class VehicleManager:
312
314
  return KiaUvoApiCN(region, brand, language)
313
315
  elif REGIONS[region] == REGION_AUSTRALIA:
314
316
  return KiaUvoApiAU(region, brand, language)
317
+ elif REGIONS[region] == REGION_INDIA:
318
+ return KiaUvoApiIN(brand)
315
319
  else:
316
320
  raise APIError(f"Unknown region {region}")
@@ -21,12 +21,14 @@ REGION_CANADA = "Canada"
21
21
  REGION_USA = "USA"
22
22
  REGION_CHINA = "China"
23
23
  REGION_AUSTRALIA = "Australia"
24
+ REGION_INDIA = "India"
24
25
  REGIONS = {
25
26
  1: REGION_EUROPE,
26
27
  2: REGION_CANADA,
27
28
  3: REGION_USA,
28
29
  4: REGION_CHINA,
29
30
  5: REGION_AUSTRALIA,
31
+ 6: REGION_INDIA,
30
32
  }
31
33
 
32
34
  LOGIN_TOKEN_LIFETIME = datetime.timedelta(hours=23)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyundai_kia_connect_api
3
- Version: 3.36.0
3
+ Version: 3.37.0
4
4
  Summary: Python Boilerplate contains all the boilerplate you need to create a Python package.
5
5
  Home-page: https://github.com/Hyundai-Kia-Connect/hyundai_kia_connect_api
6
6
  Author: Fuat Akgun
@@ -5,19 +5,20 @@ hyundai_kia_connect_api/KiaUvoApiAU.py,sha256=tJ_pQqjSA6qfRRO7_LgwlC0J_3x77U9wDf
5
5
  hyundai_kia_connect_api/KiaUvoApiCA.py,sha256=vP1WTSficCPSd3XrFh_FQZL2irJQbCYFVKm4OF2bWd8,32301
6
6
  hyundai_kia_connect_api/KiaUvoApiCN.py,sha256=WP-rRI3wZmjuLYZmPXeOSk2NNNc6UhTrpOMuYMG6-iE,47043
7
7
  hyundai_kia_connect_api/KiaUvoApiEU.py,sha256=vYmXY5so023HUTskBGPdOUgyyTpeTvRha0K_nwWpbKI,50171
8
+ hyundai_kia_connect_api/KiaUvoApiIN.py,sha256=UQuoL2wAyX4JzHEd03XQYFUDdNe-OMr6-CD9H6WIwJc,34293
8
9
  hyundai_kia_connect_api/KiaUvoApiUSA.py,sha256=DeA-a2qq78XPYGB8U4vzy2oAcCQJ1xJRN9tlAK_I94o,30404
9
10
  hyundai_kia_connect_api/Token.py,sha256=ZsPvXh1ID7FUTGHAqhZUZyrKT7xVbOtIn6FRJn4Ygf0,370
10
11
  hyundai_kia_connect_api/Vehicle.py,sha256=DPMwJ2fXpV3VxdTdX6JGXfIVX5etyH4r6cnk_Q0AlE8,19039
11
- hyundai_kia_connect_api/VehicleManager.py,sha256=nnHsTy49bf5nRhEC9EXFnnfx6M39JSaM2TxKK9vosbY,11733
12
+ hyundai_kia_connect_api/VehicleManager.py,sha256=qe2K-Eo14DImn3NyKsIvaovGXeXArkFJe-FVD3qgeCU,11872
12
13
  hyundai_kia_connect_api/__init__.py,sha256=IkyVeIMbcFJZgLaiiNnUVA1Ferxvrm1bXHKVg01cxvc,319
13
14
  hyundai_kia_connect_api/bluelink.py,sha256=JiNIHl-Qi8zwqyN6ywKg5CdXOLT74WkvpjVcn-rYEjI,19730
14
- hyundai_kia_connect_api/const.py,sha256=gFAhj9-YgrJNd7ZjYr4Qu1Yf4v-RhmyON1MJDN0eR90,2281
15
+ hyundai_kia_connect_api/const.py,sha256=quNZV943B_xzDvAeQyyMKD5qFjC3uTRvxSJrtfoCaWo,2325
15
16
  hyundai_kia_connect_api/exceptions.py,sha256=m7gyDnvA5OVAK4EXSP_ZwE0s0uV8HsGUV0tiYwqofK0,1343
16
17
  hyundai_kia_connect_api/utils.py,sha256=J0aXUX-nKIoS3XbelatNh-DZlHRU2_DYz_Mg_ZUKQJU,1957
17
- hyundai_kia_connect_api-3.36.0.dist-info/licenses/AUTHORS.rst,sha256=T77OE1hrQF6YyDE6NbdMKyL66inHt7dnjHAzblwuk2A,155
18
- hyundai_kia_connect_api-3.36.0.dist-info/licenses/LICENSE,sha256=49hmc755oyMwKdZ-2epiorjStRB0PfcZR1w5_NXZPgs,1068
19
- hyundai_kia_connect_api-3.36.0.dist-info/METADATA,sha256=JXa8EaCHcyMwYeJQXxuvCqQC9ZsJjStQf0YuOxocviw,7142
20
- hyundai_kia_connect_api-3.36.0.dist-info/WHEEL,sha256=MAQBAzGbXNI3bUmkDsiV_duv8i-gcdnLzw7cfUFwqhU,109
21
- hyundai_kia_connect_api-3.36.0.dist-info/entry_points.txt,sha256=XfrroRdyC_9q9VXjEZe5SdRPhkQyCCE4S7ZK6XSKelA,67
22
- hyundai_kia_connect_api-3.36.0.dist-info/top_level.txt,sha256=otZ7J_fN-s3EW4jD-kAearIo2OIzhQLR8DNEHIaFfds,24
23
- hyundai_kia_connect_api-3.36.0.dist-info/RECORD,,
18
+ hyundai_kia_connect_api-3.37.0.dist-info/licenses/AUTHORS.rst,sha256=T77OE1hrQF6YyDE6NbdMKyL66inHt7dnjHAzblwuk2A,155
19
+ hyundai_kia_connect_api-3.37.0.dist-info/licenses/LICENSE,sha256=49hmc755oyMwKdZ-2epiorjStRB0PfcZR1w5_NXZPgs,1068
20
+ hyundai_kia_connect_api-3.37.0.dist-info/METADATA,sha256=olxsZ_AU8KS9wiHOtyz2ET7zecd6GqW02Nq9yvr1LFA,7142
21
+ hyundai_kia_connect_api-3.37.0.dist-info/WHEEL,sha256=7wAbZI8A1UjN-j4-aYf66qBxOZ0Ioy0QNykkY5NcGJo,109
22
+ hyundai_kia_connect_api-3.37.0.dist-info/entry_points.txt,sha256=XfrroRdyC_9q9VXjEZe5SdRPhkQyCCE4S7ZK6XSKelA,67
23
+ hyundai_kia_connect_api-3.37.0.dist-info/top_level.txt,sha256=otZ7J_fN-s3EW4jD-kAearIo2OIzhQLR8DNEHIaFfds,24
24
+ hyundai_kia_connect_api-3.37.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (78.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any