pySmartHashtag 0.12.2__tar.gz → 0.12.3__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 (99) hide show
  1. {pysmarthashtag-0.12.2/pySmartHashtag.egg-info → pysmarthashtag-0.12.3}/PKG-INFO +1 -1
  2. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3/pySmartHashtag.egg-info}/PKG-INFO +1 -1
  3. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pySmartHashtag.egg-info/SOURCES.txt +1 -0
  4. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pySmartHashtag.egg-info/scm_file_list.json +1 -0
  5. pysmarthashtag-0.12.3/pySmartHashtag.egg-info/scm_version.json +8 -0
  6. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/account.py +18 -1
  7. pysmarthashtag-0.12.3/pysmarthashtag/tests/test_ota_info.py +73 -0
  8. pysmarthashtag-0.12.2/pySmartHashtag.egg-info/scm_version.json +0 -8
  9. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/.devcontainer.json +0 -0
  10. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/.github/copilot-instructions.md +0 -0
  11. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/.github/dependabot.yml +0 -0
  12. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/.github/workflows/python-package.yml +0 -0
  13. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/.github/workflows/python-publish.yml +0 -0
  14. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/.gitignore +0 -0
  15. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/.pre-commit-config.yaml +0 -0
  16. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/.vscode/launch.json +0 -0
  17. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/.vscode/settings.json +0 -0
  18. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/AUTHORS +0 -0
  19. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/CODE_OF_CONDUCT.md +0 -0
  20. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/CONTRIBUTING.md +0 -0
  21. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/ChangeLog +0 -0
  22. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/LICENSE +0 -0
  23. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/README.md +0 -0
  24. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pySmartHashtag.egg-info/dependency_links.txt +0 -0
  25. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pySmartHashtag.egg-info/requires.txt +0 -0
  26. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pySmartHashtag.egg-info/top_level.txt +0 -0
  27. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pyproject.toml +0 -0
  28. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/__init__.py +0 -0
  29. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/api/__init__.py +0 -0
  30. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/api/authentication.py +0 -0
  31. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/api/client.py +0 -0
  32. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/api/log_sanitizer.py +0 -0
  33. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/api/ssl_context.py +0 -0
  34. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/api/utils.py +0 -0
  35. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/cli.py +0 -0
  36. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/const.py +0 -0
  37. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/control/charging.py +0 -0
  38. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/control/climate.py +0 -0
  39. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/control/journal.py +0 -0
  40. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/models.py +0 -0
  41. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/__init__.py +0 -0
  42. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/common.py +0 -0
  43. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/conftest.py +0 -0
  44. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/Human_and_vehicle_relationship_does_not_exist.json +0 -0
  45. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/api_access.json +0 -0
  46. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/auth_context.url +0 -0
  47. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/auth_intermediate.url +0 -0
  48. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/auth_result.url +0 -0
  49. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/charging_success.json +0 -0
  50. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/climate_success.json +0 -0
  51. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/journal_response.json +0 -0
  52. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/journal_toggle_success.json +0 -0
  53. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/login_result.json +0 -0
  54. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/ota_response.json +0 -0
  55. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/soc_80.json +0 -0
  56. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/soc_90.json +0 -0
  57. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/token_expired.json +0 -0
  58. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/trackpoints_response.json +0 -0
  59. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/vehicle_info.json +0 -0
  60. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/vehicle_info2.json +0 -0
  61. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/vehicle_info_dc_charging.json +0 -0
  62. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/vehicle_response.json +0 -0
  63. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/replys/vehicle_result.json +0 -0
  64. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_account.py +0 -0
  65. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_actions.py +0 -0
  66. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_authentication_backoff.py +0 -0
  67. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_charging.py +0 -0
  68. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_charging_states.py +0 -0
  69. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_dc_charging.py +0 -0
  70. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_endpoint_urls.py +0 -0
  71. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_journal.py +0 -0
  72. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_journal_control.py +0 -0
  73. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_journal_pagination.py +0 -0
  74. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_log_sanitizer.py +0 -0
  75. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_missing_fields.py +0 -0
  76. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_ssl_context.py +0 -0
  77. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_token_lifecycle.py +0 -0
  78. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_tracked_vins.py +0 -0
  79. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_trackpoints.py +0 -0
  80. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_vehicle_state.py +0 -0
  81. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/tests/test_vin_headers.py +0 -0
  82. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/vehicle/__init__.py +0 -0
  83. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/vehicle/battery.py +0 -0
  84. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/vehicle/climate.py +0 -0
  85. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/vehicle/journal.py +0 -0
  86. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/vehicle/maintenance.py +0 -0
  87. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/vehicle/position.py +0 -0
  88. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/vehicle/running.py +0 -0
  89. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/vehicle/safety.py +0 -0
  90. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/vehicle/tires.py +0 -0
  91. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/vehicle/trackpoints.py +0 -0
  92. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/vehicle/vehicle.py +0 -0
  93. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/pysmarthashtag/vehicle/vehicle_state.py +0 -0
  94. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/requirements-cli.txt +0 -0
  95. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/requirements-test.txt +0 -0
  96. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/requirements.txt +0 -0
  97. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/run.sh +0 -0
  98. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/setup.cfg +0 -0
  99. {pysmarthashtag-0.12.2 → pysmarthashtag-0.12.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pySmartHashtag
3
- Version: 0.12.2
3
+ Version: 0.12.3
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.12.2
3
+ Version: 0.12.3
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
@@ -56,6 +56,7 @@ pysmarthashtag/tests/test_journal_control.py
56
56
  pysmarthashtag/tests/test_journal_pagination.py
57
57
  pysmarthashtag/tests/test_log_sanitizer.py
58
58
  pysmarthashtag/tests/test_missing_fields.py
59
+ pysmarthashtag/tests/test_ota_info.py
59
60
  pysmarthashtag/tests/test_ssl_context.py
60
61
  pysmarthashtag/tests/test_token_lifecycle.py
61
62
  pysmarthashtag/tests/test_tracked_vins.py
@@ -65,6 +65,7 @@
65
65
  "pysmarthashtag/tests/test_journal_pagination.py",
66
66
  "pysmarthashtag/tests/test_log_sanitizer.py",
67
67
  "pysmarthashtag/tests/test_missing_fields.py",
68
+ "pysmarthashtag/tests/test_ota_info.py",
68
69
  "pysmarthashtag/tests/test_ssl_context.py",
69
70
  "pysmarthashtag/tests/test_token_lifecycle.py",
70
71
  "pysmarthashtag/tests/test_tracked_vins.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.12.3",
3
+ "distance": 0,
4
+ "node": "ge7f443b13dbb531926f2cb3f667e5c3e52ce96a9",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-20"
8
+ }
@@ -50,6 +50,13 @@ VALID_UNTIL_OFFSET = datetime.timedelta(seconds=10)
50
50
  # blip mid-loop doesn't fail the whole poll.
51
51
  _BENIGN_EMPTY_CODE = "8153"
52
52
 
53
+ # Cloud's code on the OTA endpoint for vehicles it has no update record
54
+ # for (reported on Smart #5). It is a property of the OTA service, not of
55
+ # the session: refreshing the token does not clear it, so a poll loop that
56
+ # treats it as an unmapped code refreshes the session on every cycle for
57
+ # nothing. OTA data is optional, so callers normalise this to "no OTA info".
58
+ _OTA_NO_DATA_CODE = "1003"
59
+
53
60
  _LOGGER = logging.getLogger(__name__)
54
61
 
55
62
 
@@ -931,8 +938,18 @@ class SmartAccount:
931
938
  await self.select_active_vehicle(vin)
932
939
  continue
933
940
  except httpx.HTTPStatusError as exc:
934
- # Unmapped code: one refresh in case the session is stale, then surface.
935
941
  code = _cloud_code(exc)
942
+ if code == _OTA_NO_DATA_CODE:
943
+ # The OTA service has nothing to report for this VIN.
944
+ # Optional data, and no session remedy applies — return
945
+ # empty rather than burning a refresh on every poll.
946
+ _LOGGER.debug(
947
+ "OTA endpoint returned cloud code %s for %s; no OTA info available",
948
+ code,
949
+ sanitize_log_data(vin),
950
+ )
951
+ return {}
952
+ # Unmapped code: one refresh in case the session is stale, then surface.
936
953
  if code is None or refreshed_unmapped:
937
954
  raise
938
955
  refreshed_unmapped = True
@@ -0,0 +1,73 @@
1
+ """Tests for OTA info fetching.
2
+
3
+ The OTA endpoint is optional data, so its failures must never cost the
4
+ session. In particular cloud code 1003 ("no OTA record for this VIN",
5
+ observed on Smart #5) used to fall into the unmapped-code branch, which
6
+ refreshed the authentication session on every single poll — 153 times in
7
+ one day in the report — without ever clearing the 1003.
8
+ """
9
+
10
+ import httpx
11
+ import pytest
12
+ import respx
13
+ from httpx import Response
14
+
15
+ from pysmarthashtag.account import SmartAccount
16
+ from pysmarthashtag.const import OTA_SERVER_URL
17
+ from pysmarthashtag.tests.conftest import prepare_account_with_vehicles
18
+
19
+ _OTA_URL = OTA_SERVER_URL + "app/info/TestVIN0000000001"
20
+
21
+
22
+ def _count_refreshes(account: SmartAccount) -> list[int]:
23
+ """Replace ``authentication.refresh`` with a counting no-op."""
24
+ calls: list[int] = []
25
+
26
+ async def fake_refresh() -> None:
27
+ calls.append(1)
28
+
29
+ account.config.authentication.refresh = fake_refresh
30
+ return calls
31
+
32
+
33
+ @pytest.mark.asyncio
34
+ async def test_ota_1003_returns_empty_without_refresh(smart_fixture: respx.Router) -> None:
35
+ """Code 1003 is normalised to "no OTA info" — no refresh, no retry."""
36
+ account = await prepare_account_with_vehicles()
37
+ refreshes = _count_refreshes(account)
38
+
39
+ route = smart_fixture.get(_OTA_URL).mock(
40
+ return_value=Response(200, json={"code": "1003", "message": "no ota info"})
41
+ )
42
+ # respx reuses the route registered by the fixture, so its counter already
43
+ # carries the OTA call made while setting the account up.
44
+ before = route.call_count
45
+
46
+ assert await account.get_vehicle_ota_info("TestVIN0000000001") == {}
47
+ assert refreshes == [], "1003 must not refresh the session"
48
+ assert route.call_count - before == 1, "1003 must not be retried"
49
+
50
+
51
+ @pytest.mark.asyncio
52
+ async def test_ota_1003_does_not_break_refresh_cycle(smart_fixture: respx.Router) -> None:
53
+ """A permanent 1003 still leaves every other vehicle sensor populated."""
54
+ smart_fixture.get(_OTA_URL).mock(return_value=Response(200, json={"code": "1003", "message": "no ota info"}))
55
+
56
+ account = await prepare_account_with_vehicles()
57
+
58
+ vehicle = account.vehicles["TestVIN0000000001"]
59
+ assert vehicle.vin == "TestVIN0000000001"
60
+ assert "ota" not in vehicle.data
61
+
62
+
63
+ @pytest.mark.asyncio
64
+ async def test_ota_unmapped_code_still_refreshes_once(smart_fixture: respx.Router) -> None:
65
+ """Regression guard: other unmapped codes keep the one-refresh remedy."""
66
+ account = await prepare_account_with_vehicles()
67
+ refreshes = _count_refreshes(account)
68
+
69
+ smart_fixture.get(_OTA_URL).mock(return_value=Response(200, json={"code": "9999", "message": "something new"}))
70
+
71
+ with pytest.raises(httpx.HTTPStatusError):
72
+ await account.get_vehicle_ota_info("TestVIN0000000001")
73
+ assert len(refreshes) == 1, "unmapped codes still get exactly one refresh"
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.12.2",
3
- "distance": 0,
4
- "node": "g800eaf2bed8475decf036d35ccdea3178897c327",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-15"
8
- }
File without changes
File without changes
File without changes