hyundai-kia-connect-api 3.36.0__py2.py3-none-any.whl → 3.38.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.
- hyundai_kia_connect_api/KiaUvoApiAU.py +19 -12
- hyundai_kia_connect_api/KiaUvoApiIN.py +861 -0
- hyundai_kia_connect_api/VehicleManager.py +12 -0
- hyundai_kia_connect_api/const.py +5 -0
- {hyundai_kia_connect_api-3.36.0.dist-info → hyundai_kia_connect_api-3.38.0.dist-info}/METADATA +2 -2
- {hyundai_kia_connect_api-3.36.0.dist-info → hyundai_kia_connect_api-3.38.0.dist-info}/RECORD +11 -10
- {hyundai_kia_connect_api-3.36.0.dist-info → hyundai_kia_connect_api-3.38.0.dist-info}/WHEEL +1 -1
- {hyundai_kia_connect_api-3.36.0.dist-info → hyundai_kia_connect_api-3.38.0.dist-info}/entry_points.txt +0 -0
- {hyundai_kia_connect_api-3.36.0.dist-info → hyundai_kia_connect_api-3.38.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {hyundai_kia_connect_api-3.36.0.dist-info → hyundai_kia_connect_api-3.38.0.dist-info}/licenses/LICENSE +0 -0
- {hyundai_kia_connect_api-3.36.0.dist-info → hyundai_kia_connect_api-3.38.0.dist-info}/top_level.txt +0 -0
@@ -28,7 +28,10 @@ from .const import (
|
|
28
28
|
BRAND_HYUNDAI,
|
29
29
|
BRAND_KIA,
|
30
30
|
BRANDS,
|
31
|
+
REGIONS,
|
31
32
|
DOMAIN,
|
33
|
+
REGION_AUSTRALIA,
|
34
|
+
REGION_NZ,
|
32
35
|
DISTANCE_UNITS,
|
33
36
|
TEMPERATURE_UNITS,
|
34
37
|
SEAT_STATUS,
|
@@ -56,16 +59,30 @@ class KiaUvoApiAU(ApiImplType1):
|
|
56
59
|
|
57
60
|
def __init__(self, region: int, brand: int, language: str) -> None:
|
58
61
|
self.brand = brand
|
59
|
-
if BRANDS[brand] == BRAND_KIA:
|
62
|
+
if BRANDS[brand] == BRAND_KIA and REGIONS[region] == REGION_AUSTRALIA:
|
60
63
|
self.BASE_URL: str = "au-apigw.ccs.kia.com.au:8082"
|
61
64
|
self.CCSP_SERVICE_ID: str = "8acb778a-b918-4a8d-8624-73a0beb64289"
|
62
65
|
self.APP_ID: str = "4ad4dcde-be23-48a8-bc1c-91b94f5c06f8" # Android app ID
|
63
66
|
self.BASIC_AUTHORIZATION: str = "Basic OGFjYjc3OGEtYjkxOC00YThkLTg2MjQtNzNhMGJlYjY0Mjg5OjdTY01NbTZmRVlYZGlFUEN4YVBhUW1nZVlkbFVyZndvaDRBZlhHT3pZSVMyQ3U5VA==" # noqa
|
67
|
+
self.cfb = base64.b64decode(
|
68
|
+
"IDbMgWBXgic4MAyMgf5PFFRAdGX5O3IyC3uvN3scCs0gDpTFDuyvBorlAH9JMM2/wMc="
|
69
|
+
)
|
64
70
|
elif BRANDS[brand] == BRAND_HYUNDAI:
|
65
71
|
self.BASE_URL: str = "au-apigw.ccs.hyundai.com.au:8080"
|
66
72
|
self.CCSP_SERVICE_ID: str = "855c72df-dfd7-4230-ab03-67cbf902bb1c"
|
67
73
|
self.APP_ID: str = "f9ccfdac-a48d-4c57-bd32-9116963c24ed" # Android app ID
|
68
74
|
self.BASIC_AUTHORIZATION: str = "Basic ODU1YzcyZGYtZGZkNy00MjMwLWFiMDMtNjdjYmY5MDJiYjFjOmU2ZmJ3SE0zMllOYmhRbDBwdmlhUHAzcmY0dDNTNms5MWVjZUEzTUpMZGJkVGhDTw==" # noqa
|
75
|
+
self.cfb = base64.b64decode(
|
76
|
+
"V60WkEmyRQaAfrBF1623/7QL62MjLVbCHdItGzQ1g5T/hkmKmMVTaMHv4cKGzgD3gL8="
|
77
|
+
)
|
78
|
+
elif BRANDS[brand] == BRAND_KIA and REGIONS[region] == REGION_NZ:
|
79
|
+
self.BASE_URL: str = "au-apigw.ccs.kia.com.au:8082"
|
80
|
+
self.CCSP_SERVICE_ID: str = "4ab606a7-cea4-48a0-a216-ed9c14a4a38c"
|
81
|
+
self.APP_ID: str = "97745337-cac6-4a5b-afc3-e65ace81c994" # Android app ID
|
82
|
+
self.BASIC_AUTHORIZATION: str = "Basic NGFiNjA2YTctY2VhNC00OGEwLWEyMTYtZWQ5YzE0YTRhMzhjOjBoYUZxWFRrS2t0Tktmemt4aFowYWt1MzFpNzRnMHlRRm01b2QybXo0TGRJNW1MWQ==" # noqa
|
83
|
+
self.cfb = base64.b64decode(
|
84
|
+
"IDbMgWBXgic4MAyMgf5PFGsDas7YzQmN/lcPSkArm/KVUTutuiJAZ+4ZFoVxsHFNNy4="
|
85
|
+
)
|
69
86
|
|
70
87
|
self.USER_API_URL: str = "https://" + self.BASE_URL + "/api/v1/user/"
|
71
88
|
self.SPA_API_URL: str = "https://" + self.BASE_URL + "/api/v1/spa/"
|
@@ -777,18 +794,8 @@ class KiaUvoApiAU(ApiImplType1):
|
|
777
794
|
return None
|
778
795
|
|
779
796
|
def _get_stamp(self) -> str:
|
780
|
-
if BRANDS[self.brand] == BRAND_KIA:
|
781
|
-
cfb = base64.b64decode(
|
782
|
-
"IDbMgWBXgic4MAyMgf5PFFRAdGX5O3IyC3uvN3scCs0gDpTFDuyvBorlAH9JMM2/wMc="
|
783
|
-
)
|
784
|
-
elif BRANDS[self.brand] == BRAND_HYUNDAI:
|
785
|
-
cfb = base64.b64decode(
|
786
|
-
"V60WkEmyRQaAfrBF1623/7QL62MjLVbCHdItGzQ1g5T/hkmKmMVTaMHv4cKGzgD3gL8="
|
787
|
-
)
|
788
|
-
else:
|
789
|
-
raise ValueError("Invalid brand")
|
790
797
|
raw_data = f"{self.APP_ID}:{int(dt.datetime.now().timestamp())}".encode()
|
791
|
-
result = bytes(b1 ^ b2 for b1, b2 in zip(cfb, raw_data))
|
798
|
+
result = bytes(b1 ^ b2 for b1, b2 in zip(self.cfb, raw_data))
|
792
799
|
return base64.b64encode(result).decode("utf-8")
|
793
800
|
|
794
801
|
def _get_device_id(self, stamp):
|
@@ -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,8 @@ from .const import (
|
|
33
34
|
REGION_EUROPE,
|
34
35
|
REGION_USA,
|
35
36
|
REGION_CHINA,
|
37
|
+
REGION_NZ,
|
38
|
+
REGION_INDIA,
|
36
39
|
REGIONS,
|
37
40
|
VEHICLE_LOCK_ACTION,
|
38
41
|
CHARGE_PORT_ACTION,
|
@@ -312,5 +315,14 @@ class VehicleManager:
|
|
312
315
|
return KiaUvoApiCN(region, brand, language)
|
313
316
|
elif REGIONS[region] == REGION_AUSTRALIA:
|
314
317
|
return KiaUvoApiAU(region, brand, language)
|
318
|
+
elif REGIONS[region] == REGION_NZ:
|
319
|
+
if BRANDS[brand] == BRAND_KIA:
|
320
|
+
return KiaUvoApiAU(region, brand, language)
|
321
|
+
else:
|
322
|
+
raise APIError(
|
323
|
+
f"Unknown brand {BRANDS[brand]} for region {REGIONS[region]}"
|
324
|
+
)
|
325
|
+
elif REGIONS[region] == REGION_INDIA:
|
326
|
+
return KiaUvoApiIN(brand)
|
315
327
|
else:
|
316
328
|
raise APIError(f"Unknown region {region}")
|
hyundai_kia_connect_api/const.py
CHANGED
@@ -21,12 +21,17 @@ REGION_CANADA = "Canada"
|
|
21
21
|
REGION_USA = "USA"
|
22
22
|
REGION_CHINA = "China"
|
23
23
|
REGION_AUSTRALIA = "Australia"
|
24
|
+
REGION_NZ = "New Zealand"
|
25
|
+
|
26
|
+
REGION_INDIA = "India"
|
24
27
|
REGIONS = {
|
25
28
|
1: REGION_EUROPE,
|
26
29
|
2: REGION_CANADA,
|
27
30
|
3: REGION_USA,
|
28
31
|
4: REGION_CHINA,
|
29
32
|
5: REGION_AUSTRALIA,
|
33
|
+
6: REGION_INDIA,
|
34
|
+
7: REGION_NZ,
|
30
35
|
}
|
31
36
|
|
32
37
|
LOGIN_TOKEN_LIFETIME = datetime.timedelta(hours=23)
|
{hyundai_kia_connect_api-3.36.0.dist-info → hyundai_kia_connect_api-3.38.0.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hyundai_kia_connect_api
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.38.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
|
@@ -70,7 +70,7 @@ Python 3.10 or newer is required to use this package. Vehicle manager is the key
|
|
70
70
|
|
71
71
|
Key values for the int exist in the `const.py <https://github.com/Hyundai-Kia-Connect/hyundai_kia_connect_api/blob/master/hyundai_kia_connect_api/const.py>`_ file as::
|
72
72
|
|
73
|
-
REGIONS = {1: REGION_EUROPE, 2: REGION_CANADA, 3: REGION_USA, 4: REGION_CHINA, 5: REGION_AUSTRALIA}
|
73
|
+
REGIONS = {1: REGION_EUROPE, 2: REGION_CANADA, 3: REGION_USA, 4: REGION_CHINA, 5: REGION_AUSTRALIA, 6: REGION_NZ}
|
74
74
|
BRANDS = {1: BRAND_KIA, 2: BRAND_HYUNDAI, 3: BRAND_GENESIS}
|
75
75
|
GEO_LOCATION_PROVIDERS = {1: OPENSTREETMAP, 2: GOOGLE}
|
76
76
|
|
{hyundai_kia_connect_api-3.36.0.dist-info → hyundai_kia_connect_api-3.38.0.dist-info}/RECORD
RENAMED
@@ -1,23 +1,24 @@
|
|
1
1
|
hyundai_kia_connect_api/ApiImpl.py,sha256=UeG2FH4KCdU5LvGp5Ks793vrWbwBx8MN_DedbVRRouM,9612
|
2
2
|
hyundai_kia_connect_api/ApiImplType1.py,sha256=puAO4lN7vgzNJ2ZVE-rreJQNWN0Iwt7RYE87m47TX_o,37892
|
3
3
|
hyundai_kia_connect_api/HyundaiBlueLinkApiUSA.py,sha256=TNuFJIZ6Kpd5vbV5rVJjcqp7z-Ys3blzsz6YYWZThiA,36710
|
4
|
-
hyundai_kia_connect_api/KiaUvoApiAU.py,sha256=
|
4
|
+
hyundai_kia_connect_api/KiaUvoApiAU.py,sha256=433Db-rp3drhyY8ZvN-GiVP85pl90Wwu4A2uAdkYH1c,36100
|
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=
|
12
|
+
hyundai_kia_connect_api/VehicleManager.py,sha256=BbMeM5HU38nimNTN4QsSFuEcsbeUYCcFdC7-MCU52AY,12183
|
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=
|
15
|
+
hyundai_kia_connect_api/const.py,sha256=ImISRt4QarbN8BOBlDGYRB3F69NKCcGu8ddFvNPhhXU,2370
|
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.
|
18
|
-
hyundai_kia_connect_api-3.
|
19
|
-
hyundai_kia_connect_api-3.
|
20
|
-
hyundai_kia_connect_api-3.
|
21
|
-
hyundai_kia_connect_api-3.
|
22
|
-
hyundai_kia_connect_api-3.
|
23
|
-
hyundai_kia_connect_api-3.
|
18
|
+
hyundai_kia_connect_api-3.38.0.dist-info/licenses/AUTHORS.rst,sha256=T77OE1hrQF6YyDE6NbdMKyL66inHt7dnjHAzblwuk2A,155
|
19
|
+
hyundai_kia_connect_api-3.38.0.dist-info/licenses/LICENSE,sha256=49hmc755oyMwKdZ-2epiorjStRB0PfcZR1w5_NXZPgs,1068
|
20
|
+
hyundai_kia_connect_api-3.38.0.dist-info/METADATA,sha256=xtGzWqUN6AyL3We2NBqoE41vaQAOabSUytUGavECX7Q,7156
|
21
|
+
hyundai_kia_connect_api-3.38.0.dist-info/WHEEL,sha256=7wAbZI8A1UjN-j4-aYf66qBxOZ0Ioy0QNykkY5NcGJo,109
|
22
|
+
hyundai_kia_connect_api-3.38.0.dist-info/entry_points.txt,sha256=XfrroRdyC_9q9VXjEZe5SdRPhkQyCCE4S7ZK6XSKelA,67
|
23
|
+
hyundai_kia_connect_api-3.38.0.dist-info/top_level.txt,sha256=otZ7J_fN-s3EW4jD-kAearIo2OIzhQLR8DNEHIaFfds,24
|
24
|
+
hyundai_kia_connect_api-3.38.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
{hyundai_kia_connect_api-3.36.0.dist-info → hyundai_kia_connect_api-3.38.0.dist-info}/top_level.txt
RENAMED
File without changes
|