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.
Files changed (85) hide show
  1. {pysmarthashtag-0.9.1/pySmartHashtag.egg-info → pysmarthashtag-0.9.2}/PKG-INFO +1 -1
  2. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2/pySmartHashtag.egg-info}/PKG-INFO +1 -1
  3. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pySmartHashtag.egg-info/SOURCES.txt +6 -0
  4. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/account.py +164 -1
  5. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/const.py +1 -0
  6. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/control/climate.py +3 -2
  7. pysmarthashtag-0.9.2/pysmarthashtag/control/journal.py +95 -0
  8. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/common.py +36 -0
  9. pysmarthashtag-0.9.2/pysmarthashtag/tests/replys/journal_response.json +52 -0
  10. pysmarthashtag-0.9.2/pysmarthashtag/tests/replys/journal_toggle_success.json +9 -0
  11. pysmarthashtag-0.9.2/pysmarthashtag/tests/test_journal.py +236 -0
  12. pysmarthashtag-0.9.2/pysmarthashtag/tests/test_journal_control.py +67 -0
  13. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/battery.py +1 -2
  14. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/climate.py +1 -2
  15. pysmarthashtag-0.9.2/pysmarthashtag/vehicle/journal.py +206 -0
  16. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/maintenance.py +1 -2
  17. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/position.py +1 -2
  18. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/running.py +1 -2
  19. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/safety.py +1 -2
  20. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/tires.py +1 -2
  21. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/vehicle.py +17 -0
  22. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/requirements-test.txt +1 -1
  23. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.devcontainer.json +0 -0
  24. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.github/copilot-instructions.md +0 -0
  25. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.github/dependabot.yml +0 -0
  26. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.github/workflows/python-package.yml +0 -0
  27. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.github/workflows/python-publish.yml +0 -0
  28. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.gitignore +0 -0
  29. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.pre-commit-config.yaml +0 -0
  30. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.vscode/launch.json +0 -0
  31. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/.vscode/settings.json +0 -0
  32. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/AUTHORS +0 -0
  33. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/CODE_OF_CONDUCT.md +0 -0
  34. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/CONTRIBUTING.md +0 -0
  35. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/ChangeLog +0 -0
  36. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/LICENSE +0 -0
  37. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/README.md +0 -0
  38. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pySmartHashtag.egg-info/dependency_links.txt +0 -0
  39. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pySmartHashtag.egg-info/requires.txt +0 -0
  40. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pySmartHashtag.egg-info/top_level.txt +0 -0
  41. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pyproject.toml +0 -0
  42. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/__init__.py +0 -0
  43. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/api/__init__.py +0 -0
  44. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/api/authentication.py +0 -0
  45. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/api/client.py +0 -0
  46. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/api/log_sanitizer.py +0 -0
  47. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/api/ssl_context.py +0 -0
  48. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/api/utils.py +0 -0
  49. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/cli.py +0 -0
  50. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/control/charging.py +0 -0
  51. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/models.py +0 -0
  52. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/__init__.py +0 -0
  53. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/conftest.py +0 -0
  54. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/Human_and_vehicle_relationship_does_not_exist.json +0 -0
  55. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/api_access.json +0 -0
  56. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/auth_context.url +0 -0
  57. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/auth_intermediate.url +0 -0
  58. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/auth_result.url +0 -0
  59. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/charging_success.json +0 -0
  60. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/climate_success.json +0 -0
  61. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/login_result.json +0 -0
  62. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/ota_response.json +0 -0
  63. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/soc_80.json +0 -0
  64. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/soc_90.json +0 -0
  65. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/token_expired.json +0 -0
  66. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/vehicle_info.json +0 -0
  67. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/vehicle_info2.json +0 -0
  68. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/vehicle_info_dc_charging.json +0 -0
  69. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/vehicle_response.json +0 -0
  70. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/replys/vehicle_result.json +0 -0
  71. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_account.py +0 -0
  72. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_actions.py +0 -0
  73. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_authentication_backoff.py +0 -0
  74. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_charging.py +0 -0
  75. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_dc_charging.py +0 -0
  76. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_endpoint_urls.py +0 -0
  77. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_log_sanitizer.py +0 -0
  78. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_missing_fields.py +0 -0
  79. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/tests/test_ssl_context.py +0 -0
  80. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/pysmarthashtag/vehicle/__init__.py +0 -0
  81. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/requirements-cli.txt +0 -0
  82. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/requirements.txt +0 -0
  83. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/run.sh +0 -0
  84. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/setup.cfg +0 -0
  85. {pysmarthashtag-0.9.1 → pysmarthashtag-0.9.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pySmartHashtag
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: A python library to get information from Smart #1, #3 and #5 web API
5
5
  Home-page: https://github.com/dasBasti/pySmartHashtag
6
6
  Author: dasBasti
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pySmartHashtag
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: A python library to get information from Smart #1, #3 and #5 web API
5
5
  Home-page: https://github.com/dasBasti/pySmartHashtag
6
6
  Author: dasBasti
@@ -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
- vehicle.combine_data(vehicle_info, charging_settings=vehicle_soc, ota_info=vehicle_ota_info)
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 API_BASE_URL, API_TELEMATICS_URL
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
- API_BASE_URL + API_TELEMATICS_URL + self.vin,
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
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "code": "1000",
3
+ "data": {},
4
+ "success": true,
5
+ "hint": null,
6
+ "httpStatus": "OK",
7
+ "sessionId": "test-session-journal-toggle",
8
+ "message": "operation succeed"
9
+ }