pySmartHashtag 0.14.2__tar.gz → 0.14.4__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 (113) hide show
  1. {pysmarthashtag-0.14.2/pySmartHashtag.egg-info → pysmarthashtag-0.14.4}/PKG-INFO +1 -1
  2. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4/pySmartHashtag.egg-info}/PKG-INFO +1 -1
  3. pysmarthashtag-0.14.4/pySmartHashtag.egg-info/scm_version.json +8 -0
  4. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/control/climate.py +35 -4
  5. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_charging_states.py +25 -0
  6. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_climate_zaf.py +50 -3
  7. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/vehicle/battery.py +8 -0
  8. pysmarthashtag-0.14.2/pySmartHashtag.egg-info/scm_version.json +0 -8
  9. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/.devcontainer.json +0 -0
  10. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/.github/copilot-instructions.md +0 -0
  11. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/.github/dependabot.yml +0 -0
  12. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/.github/workflows/python-package.yml +0 -0
  13. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/.github/workflows/python-publish.yml +0 -0
  14. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/.gitignore +0 -0
  15. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/.pre-commit-config.yaml +0 -0
  16. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/.vscode/launch.json +0 -0
  17. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/.vscode/settings.json +0 -0
  18. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/AUTHORS +0 -0
  19. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/CODE_OF_CONDUCT.md +0 -0
  20. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/CONTRIBUTING.md +0 -0
  21. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/ChangeLog +0 -0
  22. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/LICENSE +0 -0
  23. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/README.md +0 -0
  24. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pySmartHashtag.egg-info/SOURCES.txt +0 -0
  25. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pySmartHashtag.egg-info/dependency_links.txt +0 -0
  26. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pySmartHashtag.egg-info/requires.txt +0 -0
  27. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pySmartHashtag.egg-info/scm_file_list.json +0 -0
  28. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pySmartHashtag.egg-info/top_level.txt +0 -0
  29. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pyproject.toml +0 -0
  30. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/__init__.py +0 -0
  31. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/account.py +0 -0
  32. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/api/__init__.py +0 -0
  33. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/api/authentication.py +0 -0
  34. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/api/client.py +0 -0
  35. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/api/log_sanitizer.py +0 -0
  36. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/api/ssl_context.py +0 -0
  37. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/api/utils.py +0 -0
  38. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/cli.py +0 -0
  39. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/const.py +0 -0
  40. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/control/charging.py +0 -0
  41. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/control/horn_light.py +0 -0
  42. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/control/journal.py +0 -0
  43. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/control/lock.py +0 -0
  44. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/control/telematics.py +0 -0
  45. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/control/windows.py +0 -0
  46. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/models.py +0 -0
  47. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/__init__.py +0 -0
  48. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/common.py +0 -0
  49. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/conftest.py +0 -0
  50. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/Human_and_vehicle_relationship_does_not_exist.json +0 -0
  51. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/api_access.json +0 -0
  52. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/auth_context.url +0 -0
  53. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/auth_intermediate.url +0 -0
  54. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/auth_result.url +0 -0
  55. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/charging_success.json +0 -0
  56. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/climate_success.json +0 -0
  57. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/dc_charge_info.json +0 -0
  58. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/journal_response.json +0 -0
  59. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/journal_toggle_success.json +0 -0
  60. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/login_result.json +0 -0
  61. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/ota_response.json +0 -0
  62. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/soc_80.json +0 -0
  63. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/soc_90.json +0 -0
  64. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/token_expired.json +0 -0
  65. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/trackpoints_response.json +0 -0
  66. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/vehicle_info.json +0 -0
  67. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/vehicle_info2.json +0 -0
  68. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/vehicle_info_dc_charging.json +0 -0
  69. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/vehicle_response.json +0 -0
  70. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/replys/vehicle_result.json +0 -0
  71. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_account.py +0 -0
  72. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_actions.py +0 -0
  73. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_authentication_backoff.py +0 -0
  74. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_charging.py +0 -0
  75. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_charging_limit.py +0 -0
  76. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_cli.py +0 -0
  77. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_climate_status.py +0 -0
  78. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_dc_charge_info.py +0 -0
  79. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_dc_charging.py +0 -0
  80. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_endpoint_urls.py +0 -0
  81. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_horn_light_control.py +0 -0
  82. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_journal.py +0 -0
  83. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_journal_control.py +0 -0
  84. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_journal_pagination.py +0 -0
  85. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_lock_control.py +0 -0
  86. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_log_sanitizer.py +0 -0
  87. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_missing_fields.py +0 -0
  88. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_ota_info.py +0 -0
  89. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_send_destination.py +0 -0
  90. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_ssl_context.py +0 -0
  91. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_token_lifecycle.py +0 -0
  92. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_tracked_vins.py +0 -0
  93. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_trackpoints.py +0 -0
  94. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_vehicle_state.py +0 -0
  95. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_vin_headers.py +0 -0
  96. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/tests/test_window_control.py +0 -0
  97. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/vehicle/__init__.py +0 -0
  98. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/vehicle/climate.py +0 -0
  99. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/vehicle/journal.py +0 -0
  100. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/vehicle/maintenance.py +0 -0
  101. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/vehicle/position.py +0 -0
  102. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/vehicle/running.py +0 -0
  103. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/vehicle/safety.py +0 -0
  104. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/vehicle/tires.py +0 -0
  105. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/vehicle/trackpoints.py +0 -0
  106. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/vehicle/vehicle.py +0 -0
  107. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/pysmarthashtag/vehicle/vehicle_state.py +0 -0
  108. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/requirements-cli.txt +0 -0
  109. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/requirements-test.txt +0 -0
  110. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/requirements.txt +0 -0
  111. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/run.sh +0 -0
  112. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/setup.cfg +0 -0
  113. {pysmarthashtag-0.14.2 → pysmarthashtag-0.14.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pySmartHashtag
3
- Version: 0.14.2
3
+ Version: 0.14.4
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.14.2
3
+ Version: 0.14.4
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
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.14.4",
3
+ "distance": 0,
4
+ "node": "gb04b393f4789d1d8d298d6cb2657c81b47d0d764",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-15"
8
+ }
@@ -47,7 +47,7 @@ class RearHeatingLocation(str, Enum):
47
47
  # ZAF service parameter keys of the Smart #5 comfort functions
48
48
  ZAF_HEATING_KEYS = {
49
49
  HeatingLocation.DRIVER_SEAT: "SH.11",
50
- HeatingLocation.PASSENGER_SEAT: "SH.19",
50
+ HeatingLocation.PASSENGER_SEAT: "SH.12",
51
51
  HeatingLocation.STEERING_WHEEL: "SW",
52
52
  }
53
53
  ZAF_VENTILATION_KEYS = {
@@ -56,9 +56,10 @@ ZAF_VENTILATION_KEYS = {
56
56
  }
57
57
  ZAF_REAR_HEATING_KEYS = {
58
58
  RearHeatingLocation.REAR_LEFT: "SH.21",
59
- RearHeatingLocation.REAR_RIGHT: "SH.29",
59
+ RearHeatingLocation.REAR_RIGHT: "SH.22",
60
60
  }
61
- ZAF_ALL_COMFORT_KEYS = ("AC", "SH.11", "SH.19", "SH.21", "SH.29", "SW", "DF", "SV.11", "SV.19")
61
+ ZAF_SEAT_HEATING_DURATION = "15"
62
+ ZAF_ALL_COMFORT_KEYS = ("AC", "SH.11", "SH.12", "SH.21", "SH.22", "SW", "DF", "SV.11", "SV.19")
62
63
 
63
64
 
64
65
  class ClimateControll:
@@ -122,6 +123,10 @@ class ClimateControll:
122
123
  else:
123
124
  for key in ZAF_ALL_COMFORT_KEYS:
124
125
  params[key] = "false"
126
+ return self._build_zaf_payload(params)
127
+
128
+ def _build_zaf_payload(self, params: dict[str, str]) -> str:
129
+ """Wrap ZAF service parameters in the request body. Start and stop both use ``command: start``."""
125
130
  payload = {
126
131
  "serviceId": "ZAF",
127
132
  "command": "start",
@@ -140,6 +145,13 @@ class ClimateControll:
140
145
  }
141
146
  return json.dumps(payload).replace(" ", "")
142
147
 
148
+ async def _send_zaf_command(self, params: str) -> bool:
149
+ """Send a ZAF request and log what was sent and what the cloud answered."""
150
+ _LOGGER.debug("Sending ZAF command: %s", params)
151
+ result = await send_telematics_command(self.account, self.vin, params)
152
+ _LOGGER.debug("ZAF command result: %s", result)
153
+ return result is True
154
+
143
155
  def set_ventilation_level(self, location: VentilationLocation, level: int):
144
156
  """Set the (Smart #5) seat ventilation level; selecting one clears the seat's heating."""
145
157
  if not self.use_zaf:
@@ -214,7 +226,7 @@ class ClimateControll:
214
226
 
215
227
  _LOGGER.debug("Setting climate conditioning: active=%s, temp=%s", active, temp)
216
228
  if self.use_zaf:
217
- return (await send_telematics_command(self.account, self.vin, self._get_zaf_payload(active))) is True
229
+ return await self._send_zaf_command(self._get_zaf_payload(active))
218
230
  return await self._send_rce_command(self._get_payload(active))
219
231
 
220
232
  def _get_heating_payload(self, active: bool, levels: dict[HeatingLocation, int]) -> str:
@@ -256,8 +268,24 @@ class ClimateControll:
256
268
  raise ValueError("Heating level must be between 1 and 3 to start heating.")
257
269
 
258
270
  _LOGGER.debug("Setting heating: active=%s, levels=%s", active, levels)
271
+ if self.use_zaf:
272
+ return await self._send_zaf_command(self._get_zaf_heating_payload(active, levels))
259
273
  return await self._send_rce_command(self._get_heating_payload(active, levels))
260
274
 
275
+ def _get_zaf_heating_payload(self, active: bool, levels: dict[HeatingLocation, int]) -> str:
276
+ """Build the Smart #5 ZAF request that only touches the given seats / steering wheel."""
277
+ params: dict[str, str] = {"operation": "4"}
278
+ for loc, level in levels.items():
279
+ key = ZAF_HEATING_KEYS[loc]
280
+ if active:
281
+ params[key] = "true"
282
+ params[f"{key}.level"] = str(level)
283
+ if loc != HeatingLocation.STEERING_WHEEL:
284
+ params[f"{key}.duration"] = ZAF_SEAT_HEATING_DURATION
285
+ else:
286
+ params[key] = "false"
287
+ return self._build_zaf_payload(params)
288
+
261
289
  def _get_defrost_payload(self, active: bool) -> str:
262
290
  _payload = self.BASE_PAYLOAD_TEMPLATE.copy()
263
291
  _payload["command"] = "start" if active else "stop"
@@ -277,6 +305,9 @@ class ClimateControll:
277
305
  if not isinstance(active, bool):
278
306
  raise TypeError("Defrost state must be a boolean")
279
307
  _LOGGER.debug("Setting front windscreen defrost: active=%s", active)
308
+ if self.use_zaf:
309
+ params = {"operation": "4", "DF": "true" if active else "false"}
310
+ return await self._send_zaf_command(self._build_zaf_payload(params))
280
311
  return await self._send_rce_command(self._get_defrost_payload(active))
281
312
 
282
313
  async def _send_rce_command(self, params: str) -> bool:
@@ -117,3 +117,28 @@ class TestChargerConnectionState:
117
117
  assert battery is not None
118
118
  assert battery.charger_connection_status is None
119
119
  assert battery.charger_connection_state is None
120
+
121
+
122
+ class TestIsChargerConnected:
123
+ """The plug state must not depend on the car currently drawing power (issue #441)."""
124
+
125
+ def test_plugged_not_charging(self):
126
+ """Plugged in with chargerState NOT_CHARGING still counts as connected."""
127
+ battery = Battery.from_vehicle_data(create_vehicle_data("0", "2"))
128
+
129
+ assert battery is not None
130
+ assert battery.is_charger_connected is True
131
+
132
+ def test_not_connected(self):
133
+ """Unplugged stays disconnected."""
134
+ battery = Battery.from_vehicle_data(create_vehicle_data("0", "0"))
135
+
136
+ assert battery is not None
137
+ assert battery.is_charger_connected is False
138
+
139
+ def test_unknown_connection_code_does_not_connect(self):
140
+ """Codes the API adds later do not flip the plug state on their own."""
141
+ battery = Battery.from_vehicle_data(create_vehicle_data("0", "42"))
142
+
143
+ assert battery is not None
144
+ assert battery.is_charger_connected is False
@@ -31,7 +31,7 @@ def test_start_with_ventilation():
31
31
  assert params["operation"] == "4"
32
32
  assert params["AC"] == "true" and params["AC.temp"] == "21.0"
33
33
  assert params["SV.11"] == "true" and params["SV.11.level"] == "2"
34
- assert params["SH.19"] == "true" and params["SH.19.level"] == "3"
34
+ assert params["SH.12"] == "true" and params["SH.12.level"] == "3"
35
35
  assert params["SH.11"] == "false" and "SH.11.level" not in params
36
36
  assert params["SV.19"] == "false"
37
37
 
@@ -51,7 +51,7 @@ def test_stop_is_all_off():
51
51
  params = _params(ctrl._get_zaf_payload(False))
52
52
  assert json.loads(ctrl._get_zaf_payload(False))["command"] == "start"
53
53
  assert params["operation"] == "4"
54
- for key in ("AC", "SH.11", "SH.19", "SH.21", "SH.29", "SW", "DF", "SV.11", "SV.19"):
54
+ for key in ("AC", "SH.11", "SH.12", "SH.21", "SH.22", "SW", "DF", "SV.11", "SV.19"):
55
55
  assert params[key] == "false"
56
56
 
57
57
 
@@ -70,7 +70,7 @@ def test_rear_heating_and_all_keys_string_values():
70
70
  ctrl.set_rear_heating_level(RearHeatingLocation.REAR_LEFT, 2)
71
71
  params = _params(ctrl._get_zaf_payload(True))
72
72
  assert params["SH.21"] == "true" and params["SH.21.level"] == "2"
73
- assert params["SH.29"] == "false" and "SH.29.level" not in params
73
+ assert params["SH.22"] == "false" and "SH.22.level" not in params
74
74
  with pytest.raises(ValueError):
75
75
  ctrl.set_rear_heating_level(RearHeatingLocation.REAR_LEFT, 4)
76
76
  with pytest.raises(NotImplementedError):
@@ -92,3 +92,50 @@ async def test_stop_works_with_corrupt_temperature(monkeypatch):
92
92
  with pytest.raises(TypeError):
93
93
  await ctrl.set_climate_conditioning("garbage", True)
94
94
  assert len(sent) == 1
95
+
96
+
97
+ @pytest.mark.asyncio
98
+ async def test_set_heating_on_smart5_uses_zaf_without_ac(monkeypatch):
99
+ sent = []
100
+
101
+ async def fake_send(account, vin, payload):
102
+ sent.append(payload)
103
+ return True
104
+
105
+ monkeypatch.setattr("pysmarthashtag.control.climate.send_telematics_command", fake_send)
106
+ ctrl = _ctrl()
107
+ levels = {HeatingLocation.DRIVER_SEAT: 3, HeatingLocation.PASSENGER_SEAT: 2}
108
+ assert await ctrl.set_heating(True, levels) is True
109
+ payload = json.loads(sent[0])
110
+ assert payload["serviceId"] == "ZAF"
111
+ params = _params(sent[0])
112
+ assert params == {
113
+ "operation": "4",
114
+ "SH.11": "true",
115
+ "SH.11.level": "3",
116
+ "SH.11.duration": "15",
117
+ "SH.12": "true",
118
+ "SH.12.level": "2",
119
+ "SH.12.duration": "15",
120
+ }
121
+ assert ctrl.heating_levels[HeatingLocation.DRIVER_SEAT] == 0
122
+
123
+ assert await ctrl.set_heating(False, {HeatingLocation.STEERING_WHEEL: 0}) is True
124
+ assert _params(sent[1]) == {"operation": "4", "SW": "false"}
125
+
126
+
127
+ @pytest.mark.asyncio
128
+ async def test_set_defrost_on_smart5_uses_zaf(monkeypatch):
129
+ sent = []
130
+
131
+ async def fake_send(account, vin, payload):
132
+ sent.append(payload)
133
+ return True
134
+
135
+ monkeypatch.setattr("pysmarthashtag.control.climate.send_telematics_command", fake_send)
136
+ ctrl = _ctrl()
137
+ assert await ctrl.set_defrost(True) is True
138
+ assert json.loads(sent[0])["serviceId"] == "ZAF"
139
+ assert _params(sent[0]) == {"operation": "4", "DF": "true"}
140
+ await ctrl.set_defrost(False)
141
+ assert _params(sent[1]) == {"operation": "4", "DF": "false"}
@@ -304,6 +304,14 @@ class Battery(VehicleDataBase):
304
304
  if charger_conn is not None:
305
305
  retval["charger_connection_status"] = charger_conn
306
306
  retval["charger_connection_state"] = charger_connection_state_name(charger_conn)
307
+ # chargerState reports NOT_CHARGING for a plugged-in car that is not drawing power,
308
+ # so the connection code is the only reliable plug signal. See SmartHashtag issue #441.
309
+ if charger_conn in (
310
+ ChargerConnectionState.DC_CONNECTED,
311
+ ChargerConnectionState.PLUGGED_NOT_CHARGING,
312
+ ChargerConnectionState.CHARGING,
313
+ ):
314
+ retval["is_charger_connected"] = True
307
315
 
308
316
  charging_status = retval.get("charging_status")
309
317
  battery_percent = retval.get("remaining_battery_percent")
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.14.2",
3
- "distance": 0,
4
- "node": "gb73ae6cbc61e57756b26a7bb9ea3fd79957d7a89",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-10-10"
8
- }
File without changes
File without changes
File without changes