pySmartHashtag 0.9.1__tar.gz → 0.9.2__tar.gz
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.
- {pysmarthashtag-0.9.1/pySmartHashtag.egg-info → pysmarthashtag-0.9.2}/PKG-INFO +1 -1
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2/pySmartHashtag.egg-info}/PKG-INFO +1 -1
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pySmartHashtag.egg-info/SOURCES.txt +6 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/account.py +164 -1
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/const.py +1 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/control/climate.py +3 -2
- pysmarthashtag-0.9.2/pysmarthashtag/control/journal.py +95 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/common.py +36 -0
- pysmarthashtag-0.9.2/pysmarthashtag/tests/replys/journal_response.json +52 -0
- pysmarthashtag-0.9.2/pysmarthashtag/tests/replys/journal_toggle_success.json +9 -0
- pysmarthashtag-0.9.2/pysmarthashtag/tests/test_journal.py +236 -0
- pysmarthashtag-0.9.2/pysmarthashtag/tests/test_journal_control.py +67 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/battery.py +1 -2
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/climate.py +1 -2
- pysmarthashtag-0.9.2/pysmarthashtag/vehicle/journal.py +206 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/maintenance.py +1 -2
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/position.py +1 -2
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/running.py +1 -2
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/safety.py +1 -2
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/tires.py +1 -2
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/vehicle.py +17 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/requirements-test.txt +1 -1
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.devcontainer.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.github/copilot-instructions.md +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.github/dependabot.yml +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.github/workflows/python-package.yml +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.github/workflows/python-publish.yml +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.gitignore +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.pre-commit-config.yaml +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.vscode/launch.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.vscode/settings.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/AUTHORS +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/CODE_OF_CONDUCT.md +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/CONTRIBUTING.md +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/ChangeLog +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/LICENSE +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/README.md +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pySmartHashtag.egg-info/dependency_links.txt +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pySmartHashtag.egg-info/requires.txt +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pySmartHashtag.egg-info/top_level.txt +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pyproject.toml +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/__init__.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/api/__init__.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/api/authentication.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/api/client.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/api/log_sanitizer.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/api/ssl_context.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/api/utils.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/cli.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/control/charging.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/models.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/__init__.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/conftest.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/Human_and_vehicle_relationship_does_not_exist.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/api_access.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/auth_context.url +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/auth_intermediate.url +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/auth_result.url +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/charging_success.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/climate_success.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/login_result.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/ota_response.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/soc_80.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/soc_90.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/token_expired.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/vehicle_info.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/vehicle_info2.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/vehicle_info_dc_charging.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/vehicle_response.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/vehicle_result.json +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_account.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_actions.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_authentication_backoff.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_charging.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_dc_charging.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_endpoint_urls.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_log_sanitizer.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_missing_fields.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_ssl_context.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/__init__.py +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/requirements-cli.txt +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/requirements.txt +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/run.sh +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/setup.cfg +0 -0
- {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/setup.py +0 -0
|
@@ -38,6 +38,7 @@ pysmarthashtag/api/ssl_context.py
|
|
|
38
38
|
pysmarthashtag/api/utils.py
|
|
39
39
|
pysmarthashtag/control/charging.py
|
|
40
40
|
pysmarthashtag/control/climate.py
|
|
41
|
+
pysmarthashtag/control/journal.py
|
|
41
42
|
pysmarthashtag/tests/__init__.py
|
|
42
43
|
pysmarthashtag/tests/common.py
|
|
43
44
|
pysmarthashtag/tests/conftest.py
|
|
@@ -47,6 +48,8 @@ pysmarthashtag/tests/test_authentication_backoff.py
|
|
|
47
48
|
pysmarthashtag/tests/test_charging.py
|
|
48
49
|
pysmarthashtag/tests/test_dc_charging.py
|
|
49
50
|
pysmarthashtag/tests/test_endpoint_urls.py
|
|
51
|
+
pysmarthashtag/tests/test_journal.py
|
|
52
|
+
pysmarthashtag/tests/test_journal_control.py
|
|
50
53
|
pysmarthashtag/tests/test_log_sanitizer.py
|
|
51
54
|
pysmarthashtag/tests/test_missing_fields.py
|
|
52
55
|
pysmarthashtag/tests/test_ssl_context.py
|
|
@@ -57,6 +60,8 @@ pysmarthashtag/tests/replys/auth_intermediate.url
|
|
|
57
60
|
pysmarthashtag/tests/replys/auth_result.url
|
|
58
61
|
pysmarthashtag/tests/replys/charging_success.json
|
|
59
62
|
pysmarthashtag/tests/replys/climate_success.json
|
|
63
|
+
pysmarthashtag/tests/replys/journal_response.json
|
|
64
|
+
pysmarthashtag/tests/replys/journal_toggle_success.json
|
|
60
65
|
pysmarthashtag/tests/replys/login_result.json
|
|
61
66
|
pysmarthashtag/tests/replys/ota_response.json
|
|
62
67
|
pysmarthashtag/tests/replys/soc_80.json
|
|
@@ -70,6 +75,7 @@ pysmarthashtag/tests/replys/vehicle_result.json
|
|
|
70
75
|
pysmarthashtag/vehicle/__init__.py
|
|
71
76
|
pysmarthashtag/vehicle/battery.py
|
|
72
77
|
pysmarthashtag/vehicle/climate.py
|
|
78
|
+
pysmarthashtag/vehicle/journal.py
|
|
73
79
|
pysmarthashtag/vehicle/maintenance.py
|
|
74
80
|
pysmarthashtag/vehicle/position.py
|
|
75
81
|
pysmarthashtag/vehicle/running.py
|
|
@@ -41,6 +41,15 @@ class SmartAccount:
|
|
|
41
41
|
vehicles: dict[str, SmartVehicle] = field(default_factory=dict, init=False)
|
|
42
42
|
"""Vehicles associated with the account."""
|
|
43
43
|
|
|
44
|
+
_journal_grant_cache: dict[str, str] = field(default_factory=dict, init=False)
|
|
45
|
+
"""Per-VIN cache of the access_token under which the trip-journal
|
|
46
|
+
authorization grant was last accepted. ``{vin: access_token_string}``.
|
|
47
|
+
Used to skip the redundant grant POST when the same token is still
|
|
48
|
+
valid; auto-invalidates as soon as the token rotates (any reason —
|
|
49
|
+
expiry, manual relogin, future refresh-token flow). Maintained by
|
|
50
|
+
:meth:`grant_journal_authorization`.
|
|
51
|
+
"""
|
|
52
|
+
|
|
44
53
|
def __post_init__(self, password, log_responses):
|
|
45
54
|
"""Initialize the account."""
|
|
46
55
|
# Ensure endpoint_urls is set
|
|
@@ -134,7 +143,24 @@ class SmartAccount:
|
|
|
134
143
|
vehicle_info = await self.get_vehicle_information(vin)
|
|
135
144
|
vehicle_soc = await self.get_vehicle_soc(vin)
|
|
136
145
|
vehicle_ota_info = await self.get_vehicle_ota_info(vin)
|
|
137
|
-
|
|
146
|
+
# Trip journal is best-effort: the endpoint can return 8153
|
|
147
|
+
# ("data unavailable") on vehicles where on-vehicle trip
|
|
148
|
+
# recording is OFF, or transiently when the per-session auth
|
|
149
|
+
# grant hasn't been accepted yet. Never let an empty journal
|
|
150
|
+
# fail the whole refresh.
|
|
151
|
+
journal_response = None
|
|
152
|
+
try:
|
|
153
|
+
journal_response = await self.get_trip_journal(vin)
|
|
154
|
+
except Exception: # noqa: BLE001 # Best-effort: any failure (8153, transport, parse) must not break refresh.
|
|
155
|
+
_LOGGER.debug(
|
|
156
|
+
"Trip journal fetch failed for %s", sanitize_log_data(vin), exc_info=True
|
|
157
|
+
)
|
|
158
|
+
vehicle.combine_data(
|
|
159
|
+
vehicle_info,
|
|
160
|
+
charging_settings=vehicle_soc,
|
|
161
|
+
ota_info=vehicle_ota_info,
|
|
162
|
+
journal_response=journal_response,
|
|
163
|
+
)
|
|
138
164
|
|
|
139
165
|
async def select_active_vehicle(self, vin) -> None:
|
|
140
166
|
"""Select the active vehicle."""
|
|
@@ -257,6 +283,143 @@ class SmartAccount:
|
|
|
257
283
|
raise SmartAuthError("Could not get vehicle information")
|
|
258
284
|
return data
|
|
259
285
|
|
|
286
|
+
async def grant_journal_authorization(self, vin, force: bool = False) -> bool:
|
|
287
|
+
"""Grant cloud-side authorization for trip-journal data access.
|
|
288
|
+
|
|
289
|
+
``POST /remote-control/user/authorization/insert`` with
|
|
290
|
+
``{"serviceCode": "travelLogBusiCode", "authStatus": 1, "vin": <vin>}``.
|
|
291
|
+
This is a per-session handshake that unlocks the journalLogV4
|
|
292
|
+
endpoint — without it, journalLogV4 returns ``code: 8153``
|
|
293
|
+
("data unavailable") even on vehicles where the on-vehicle
|
|
294
|
+
recording flag is set and trip data exists cloud-side.
|
|
295
|
+
|
|
296
|
+
Cached per-VIN per-access-token. The grant POST is observed to
|
|
297
|
+
rotate the access_token server-side (every poll without the cache
|
|
298
|
+
returns ``1402 token invalid`` on the next call, forcing a
|
|
299
|
+
re-login). The cache stores the access_token under which the
|
|
300
|
+
grant was accepted; subsequent calls under the *same* token
|
|
301
|
+
skip the redundant POST. The cache auto-invalidates as soon as
|
|
302
|
+
the token rotates (relogin, refresh, expiry — any reason).
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
vin: Vehicle identification number.
|
|
306
|
+
force: If True, ignore the cache and re-issue the grant.
|
|
307
|
+
Use this for explicit init flows where you want to be
|
|
308
|
+
certain the grant is fresh.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
True if the grant succeeded (or was already cached);
|
|
312
|
+
False if the POST failed.
|
|
313
|
+
|
|
314
|
+
"""
|
|
315
|
+
token = self.config.authentication.api_access_token
|
|
316
|
+
if not force and token and self._journal_grant_cache.get(vin) == token:
|
|
317
|
+
_LOGGER.debug(
|
|
318
|
+
"Journal authorization cached for %s under current token; skipping POST",
|
|
319
|
+
sanitize_log_data(vin),
|
|
320
|
+
)
|
|
321
|
+
return True
|
|
322
|
+
|
|
323
|
+
_LOGGER.debug("Granting journal authorization for %s", sanitize_log_data(vin))
|
|
324
|
+
path = "/remote-control/user/authorization/insert"
|
|
325
|
+
body = json.dumps({"serviceCode": "travelLogBusiCode", "authStatus": 1, "vin": vin})
|
|
326
|
+
async with SmartClient(self.config) as client:
|
|
327
|
+
for retry in range(3):
|
|
328
|
+
try:
|
|
329
|
+
r = await client.post(
|
|
330
|
+
self.vehicles[vin].base_url + path,
|
|
331
|
+
headers={
|
|
332
|
+
**utils.generate_default_header(
|
|
333
|
+
client.config.authentication.device_id,
|
|
334
|
+
client.config.authentication.api_access_token,
|
|
335
|
+
params={},
|
|
336
|
+
method="POST",
|
|
337
|
+
url=path,
|
|
338
|
+
body=body,
|
|
339
|
+
)
|
|
340
|
+
},
|
|
341
|
+
content=body.encode("utf-8"),
|
|
342
|
+
)
|
|
343
|
+
payload = r.json()
|
|
344
|
+
success = bool(payload.get("success") or payload.get("code") == "1000")
|
|
345
|
+
if success:
|
|
346
|
+
# Record the token under which this grant was accepted.
|
|
347
|
+
# Re-read after the POST since the server may have rotated
|
|
348
|
+
# it during the call.
|
|
349
|
+
self._journal_grant_cache[vin] = (
|
|
350
|
+
self.config.authentication.api_access_token
|
|
351
|
+
)
|
|
352
|
+
return success
|
|
353
|
+
except SmartTokenRefreshNecessary:
|
|
354
|
+
_LOGGER.debug("Token refresh needed during auth-grant retry %d", retry)
|
|
355
|
+
continue
|
|
356
|
+
except SmartHumanCarConnectionError:
|
|
357
|
+
_LOGGER.debug("Human-car connection error during auth-grant retry %d", retry)
|
|
358
|
+
await self.select_active_vehicle(vin)
|
|
359
|
+
continue
|
|
360
|
+
break
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
async def get_trip_journal(self, vin, page_size: int = 20, window_days: int = 14) -> dict:
|
|
364
|
+
"""Fetch the most recent trip-journal entries for a vehicle.
|
|
365
|
+
|
|
366
|
+
Hits ``/geelyTCAccess/tcservices/vehicle/status/journalLogV4/{vin}``
|
|
367
|
+
— the endpoint that carries server-side reverse-geocoded start/end
|
|
368
|
+
addresses alongside per-trip energy/distance/speed metrics.
|
|
369
|
+
|
|
370
|
+
Sends ``startTime`` / ``endTime`` (ms-epoch window), ``pageIndex``,
|
|
371
|
+
``pageSize``, and ``userId`` as query params. The endpoint requires
|
|
372
|
+
a per-session authorization grant
|
|
373
|
+
(:meth:`grant_journal_authorization`), which this method calls
|
|
374
|
+
first; without it the endpoint returns ``code: 8153``. The grant
|
|
375
|
+
is cached per-VIN per-access-token, so subsequent calls under the
|
|
376
|
+
same token skip the grant POST.
|
|
377
|
+
|
|
378
|
+
Returns the raw response dict (with ``code``/``message``/``data``)
|
|
379
|
+
or an empty dict when the request fails server-side; raising is
|
|
380
|
+
avoided so a single flaky vehicle doesn't break the whole refresh.
|
|
381
|
+
"""
|
|
382
|
+
await self.grant_journal_authorization(vin)
|
|
383
|
+
_LOGGER.debug("Getting trip journal for vehicle")
|
|
384
|
+
end_ms = int(datetime.datetime.now(datetime.timezone.utc).timestamp() * 1000)
|
|
385
|
+
start_ms = end_ms - window_days * 86400 * 1000
|
|
386
|
+
params = {
|
|
387
|
+
"endTime": str(end_ms),
|
|
388
|
+
"pageIndex": "1",
|
|
389
|
+
"pageSize": str(page_size),
|
|
390
|
+
"startTime": str(start_ms),
|
|
391
|
+
"userId": str(self.config.authentication.api_user_id),
|
|
392
|
+
}
|
|
393
|
+
path = "/geelyTCAccess/tcservices/vehicle/status/journalLogV4/" + vin
|
|
394
|
+
url = path + "?" + utils.join_url_params(params)
|
|
395
|
+
data: dict = {}
|
|
396
|
+
async with SmartClient(self.config) as client:
|
|
397
|
+
for retry in range(3):
|
|
398
|
+
try:
|
|
399
|
+
r_journal = await client.get(
|
|
400
|
+
self.vehicles[vin].base_url + url,
|
|
401
|
+
headers={
|
|
402
|
+
**utils.generate_default_header(
|
|
403
|
+
client.config.authentication.device_id,
|
|
404
|
+
client.config.authentication.api_access_token,
|
|
405
|
+
params=params,
|
|
406
|
+
method="GET",
|
|
407
|
+
url=path,
|
|
408
|
+
)
|
|
409
|
+
},
|
|
410
|
+
)
|
|
411
|
+
_LOGGER.debug("Got response %d", r_journal.status_code)
|
|
412
|
+
data = r_journal.json()
|
|
413
|
+
except SmartTokenRefreshNecessary:
|
|
414
|
+
_LOGGER.debug("Got Token Error, retry: %d", retry)
|
|
415
|
+
continue
|
|
416
|
+
except SmartHumanCarConnectionError:
|
|
417
|
+
_LOGGER.debug("Got Human Car Connection Error, retry: %d", retry)
|
|
418
|
+
await self.select_active_vehicle(vin)
|
|
419
|
+
continue
|
|
420
|
+
break
|
|
421
|
+
return data
|
|
422
|
+
|
|
260
423
|
async def get_vehicle_ota_info(self, vin) -> dict:
|
|
261
424
|
"""Get information about a vehicle from OTA server."""
|
|
262
425
|
_LOGGER.debug("Getting OTA information for vehicle")
|
|
@@ -13,6 +13,7 @@ API_CARS_URL = "/device-platform/user/vehicle/secure"
|
|
|
13
13
|
API_SESION_URL = "/auth/account/session/secure"
|
|
14
14
|
API_SELECT_CAR_URL = "/device-platform/user/session/update"
|
|
15
15
|
API_TELEMATICS_URL = "/remote-control/vehicle/telematics/"
|
|
16
|
+
API_JOURNAL_TOGGLE_URL = "/remote-control/vehicle/status/journalLog/"
|
|
16
17
|
|
|
17
18
|
OTA_SERVER_URL = "https://ota.srv.smart.com/"
|
|
18
19
|
|
|
@@ -8,7 +8,7 @@ from typing import TypedDict
|
|
|
8
8
|
from pysmarthashtag.account import SmartAccount
|
|
9
9
|
from pysmarthashtag.api import utils
|
|
10
10
|
from pysmarthashtag.api.client import SmartClient
|
|
11
|
-
from pysmarthashtag.const import
|
|
11
|
+
from pysmarthashtag.const import API_TELEMATICS_URL
|
|
12
12
|
from pysmarthashtag.models import SmartHumanCarConnectionError, SmartTokenRefreshNecessary
|
|
13
13
|
|
|
14
14
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -112,7 +112,7 @@ class ClimateControll:
|
|
|
112
112
|
for retry in range(3):
|
|
113
113
|
try:
|
|
114
114
|
vehicles_response = await client.put(
|
|
115
|
-
|
|
115
|
+
self.account.vehicles[self.vin].base_url + API_TELEMATICS_URL + self.vin,
|
|
116
116
|
headers={
|
|
117
117
|
**utils.generate_default_header(
|
|
118
118
|
client.config.authentication.device_id,
|
|
@@ -133,3 +133,4 @@ class ClimateControll:
|
|
|
133
133
|
except SmartHumanCarConnectionError:
|
|
134
134
|
_LOGGER.debug("Got Human Car Connection Error, retry: %d", retry)
|
|
135
135
|
continue
|
|
136
|
+
return False
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Provides client-side control of the vehicle's on-vehicle trip-recording flag.
|
|
2
|
+
|
|
3
|
+
When the flag is OFF, the vehicle's TBox does not write to the cloud-side
|
|
4
|
+
journal log, so ``SmartAccount.get_trip_journal()`` returns ``code:8153``
|
|
5
|
+
"data unavailable". Toggling the flag ON causes subsequent trips to be
|
|
6
|
+
written to the journal log, after which ``get_trip_journal()`` returns
|
|
7
|
+
populated trip records.
|
|
8
|
+
|
|
9
|
+
Modeled on ``pysmarthashtag.control.charging.ChargingControl``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
from pysmarthashtag.account import SmartAccount
|
|
16
|
+
from pysmarthashtag.api import utils
|
|
17
|
+
from pysmarthashtag.api.client import SmartClient
|
|
18
|
+
from pysmarthashtag.const import API_JOURNAL_TOGGLE_URL
|
|
19
|
+
from pysmarthashtag.models import SmartHumanCarConnectionError, SmartTokenRefreshNecessary
|
|
20
|
+
|
|
21
|
+
_LOGGER = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class JournalRecordingControl:
|
|
25
|
+
"""Toggle the on-vehicle trip-recording flag (``journalLogState``)."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, account: SmartAccount, vin: str):
|
|
28
|
+
self.account = account
|
|
29
|
+
self.config = account.config
|
|
30
|
+
self.vin = vin
|
|
31
|
+
|
|
32
|
+
def _get_payload(self, enable: bool) -> str:
|
|
33
|
+
payload = {
|
|
34
|
+
"creator": "tc",
|
|
35
|
+
"timestamp": utils.create_correct_timestamp(),
|
|
36
|
+
"latest": False,
|
|
37
|
+
"command": "start" if enable else "stop",
|
|
38
|
+
"serviceId": "JOU",
|
|
39
|
+
"serviceParameters": [
|
|
40
|
+
{"key": "jou", "value": "enable" if enable else "disable"},
|
|
41
|
+
],
|
|
42
|
+
"operationScheduling": {
|
|
43
|
+
"scheduledTime": 0,
|
|
44
|
+
"interval": 0,
|
|
45
|
+
"occurs": 0,
|
|
46
|
+
"recurrentOperation": True,
|
|
47
|
+
"duration": 60,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
return json.dumps(payload, separators=(",", ":"))
|
|
51
|
+
|
|
52
|
+
async def enable_recording(self) -> bool:
|
|
53
|
+
"""Turn on-vehicle trip recording ON."""
|
|
54
|
+
return await self._set_recording(enable=True)
|
|
55
|
+
|
|
56
|
+
async def disable_recording(self) -> bool:
|
|
57
|
+
"""Turn on-vehicle trip recording OFF."""
|
|
58
|
+
return await self._set_recording(enable=False)
|
|
59
|
+
|
|
60
|
+
async def _set_recording(self, enable: bool) -> bool:
|
|
61
|
+
await self.account._ensure_ssl_context()
|
|
62
|
+
await self.account.select_active_vehicle(self.vin)
|
|
63
|
+
|
|
64
|
+
path = API_JOURNAL_TOGGLE_URL + self.vin
|
|
65
|
+
body = self._get_payload(enable)
|
|
66
|
+
action = "enable" if enable else "disable"
|
|
67
|
+
_LOGGER.debug("Setting trip-recording: %s", action)
|
|
68
|
+
|
|
69
|
+
async with SmartClient(self.config) as client:
|
|
70
|
+
for retry in range(3):
|
|
71
|
+
try:
|
|
72
|
+
response = await client.put(
|
|
73
|
+
self.account.vehicles[self.vin].base_url + path,
|
|
74
|
+
headers={
|
|
75
|
+
**utils.generate_default_header(
|
|
76
|
+
client.config.authentication.device_id,
|
|
77
|
+
client.config.authentication.api_access_token,
|
|
78
|
+
params={},
|
|
79
|
+
method="PUT",
|
|
80
|
+
url=path,
|
|
81
|
+
body=body,
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
content=body.encode("utf-8"),
|
|
85
|
+
)
|
|
86
|
+
api_result = response.json()
|
|
87
|
+
return bool(api_result.get("success"))
|
|
88
|
+
except SmartTokenRefreshNecessary:
|
|
89
|
+
_LOGGER.debug("Got Token Error, retry: %d", retry)
|
|
90
|
+
continue
|
|
91
|
+
except SmartHumanCarConnectionError:
|
|
92
|
+
_LOGGER.debug("Got Human Car Connection Error, retry: %d", retry)
|
|
93
|
+
await self.account.select_active_vehicle(self.vin)
|
|
94
|
+
continue
|
|
95
|
+
return False
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Fixtures for Smart tests."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
4
|
+
|
|
3
5
|
import respx
|
|
4
6
|
|
|
5
7
|
from pysmarthashtag.const import (
|
|
@@ -105,6 +107,40 @@ class SmartMockRouter(respx.MockRouter):
|
|
|
105
107
|
200,
|
|
106
108
|
json=load_response(RESPONSE_DIR / "soc_90.json"),
|
|
107
109
|
)
|
|
110
|
+
# journalLogV4 takes endTime/startTime/pageIndex/pageSize/userId
|
|
111
|
+
# query params; respx matches by URL prefix without query when
|
|
112
|
+
# we use route() with `host=` + `path__startswith=`. Keep
|
|
113
|
+
# URL-prefix matching simple by registering with a regex.
|
|
114
|
+
self.get(re.compile(re.escape(
|
|
115
|
+
base_url + "/geelyTCAccess/tcservices/vehicle/status/journalLogV4/TestVIN0000000001"
|
|
116
|
+
) + r".*")).respond(
|
|
117
|
+
200,
|
|
118
|
+
json=load_response(RESPONSE_DIR / "journal_response.json"),
|
|
119
|
+
)
|
|
120
|
+
self.get(re.compile(re.escape(
|
|
121
|
+
base_url + "/geelyTCAccess/tcservices/vehicle/status/journalLogV4/TestVIN0000000002"
|
|
122
|
+
) + r".*")).respond(
|
|
123
|
+
200,
|
|
124
|
+
json=load_response(RESPONSE_DIR / "journal_response.json"),
|
|
125
|
+
)
|
|
126
|
+
# Auth-grant handshake (called before each get_trip_journal)
|
|
127
|
+
self.post(base_url + "/remote-control/user/authorization/insert").respond(
|
|
128
|
+
200,
|
|
129
|
+
json={
|
|
130
|
+
"code": "1000",
|
|
131
|
+
"data": {"serviceCode": "travelLogBusiCode", "authStatus": 1},
|
|
132
|
+
"success": True,
|
|
133
|
+
"message": "operation succeed",
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
self.put(base_url + "/remote-control/vehicle/status/journalLog/TestVIN0000000001").respond(
|
|
137
|
+
200,
|
|
138
|
+
json=load_response(RESPONSE_DIR / "journal_toggle_success.json"),
|
|
139
|
+
)
|
|
140
|
+
self.put(base_url + "/remote-control/vehicle/status/journalLog/TestVIN0000000002").respond(
|
|
141
|
+
200,
|
|
142
|
+
json=load_response(RESPONSE_DIR / "journal_toggle_success.json"),
|
|
143
|
+
)
|
|
108
144
|
|
|
109
145
|
self.get(OTA_SERVER_URL + "app/info/TestVIN0000000001").respond(
|
|
110
146
|
200,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"code": "1000",
|
|
3
|
+
"message": "operation succeed",
|
|
4
|
+
"success": true,
|
|
5
|
+
"data": {
|
|
6
|
+
"pagination": {
|
|
7
|
+
"pageIndex": 1,
|
|
8
|
+
"sortField": "startTime",
|
|
9
|
+
"start": 1,
|
|
10
|
+
"pageSize": 20,
|
|
11
|
+
"totleSize": 42,
|
|
12
|
+
"direction": "desc"
|
|
13
|
+
},
|
|
14
|
+
"list": [
|
|
15
|
+
{
|
|
16
|
+
"tripId": 1734246000,
|
|
17
|
+
"startTime": 1734246000000,
|
|
18
|
+
"endTime": 1734247230000,
|
|
19
|
+
"startOdometer": 2950.0,
|
|
20
|
+
"endOdometer": 2965.4,
|
|
21
|
+
"traveledDistance": 15.4,
|
|
22
|
+
"avgSpeed": 45.0,
|
|
23
|
+
"electricConsumption": 20.8,
|
|
24
|
+
"tripStartAddr": "123 Test Street, Test City",
|
|
25
|
+
"tripEndAddr": "15 Other Road, Test City",
|
|
26
|
+
"tripRemark": null,
|
|
27
|
+
"tripLabel": null,
|
|
28
|
+
"trackpoints": [
|
|
29
|
+
{"position": {"altitude": 27, "latitude": 217414695, "longitude": 80626712, "posCanBeTrusted": true, "marsCoordinates": false}},
|
|
30
|
+
{"position": {"altitude": 28, "latitude": 217384510, "longitude": 80573036, "posCanBeTrusted": true, "marsCoordinates": false}}
|
|
31
|
+
],
|
|
32
|
+
"regeneratedEnergy": 0.8
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"tripId": 1734200000,
|
|
36
|
+
"startTime": 1734200000000,
|
|
37
|
+
"endTime": 1734200600000,
|
|
38
|
+
"startOdometer": 2942.9,
|
|
39
|
+
"endOdometer": 2950.0,
|
|
40
|
+
"traveledDistance": 7.1,
|
|
41
|
+
"avgSpeed": 40.0,
|
|
42
|
+
"electricConsumption": 21.1,
|
|
43
|
+
"tripStartAddr": "9 Elsewhere Ave, Test City",
|
|
44
|
+
"tripEndAddr": "123 Test Street, Test City",
|
|
45
|
+
"tripRemark": null,
|
|
46
|
+
"tripLabel": null,
|
|
47
|
+
"trackpoints": [],
|
|
48
|
+
"regeneratedEnergy": 0.3
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|