pySmartHashtag 0.14.0__tar.gz → 0.14.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 (113) hide show
  1. {pysmarthashtag-0.14.0/pySmartHashtag.egg-info → pysmarthashtag-0.14.2}/PKG-INFO +9 -1
  2. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/README.md +8 -0
  3. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2/pySmartHashtag.egg-info}/PKG-INFO +9 -1
  4. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/SOURCES.txt +1 -0
  5. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/scm_file_list.json +1 -0
  6. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/scm_version.json +2 -2
  7. pysmarthashtag-0.14.2/pysmarthashtag/control/climate.py +314 -0
  8. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_actions.py +71 -0
  9. pysmarthashtag-0.14.2/pysmarthashtag/tests/test_climate_zaf.py +94 -0
  10. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/vehicle.py +5 -1
  11. pysmarthashtag-0.14.0/pysmarthashtag/control/climate.py +0 -161
  12. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.devcontainer.json +0 -0
  13. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.github/copilot-instructions.md +0 -0
  14. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.github/dependabot.yml +0 -0
  15. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.github/workflows/python-package.yml +0 -0
  16. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.github/workflows/python-publish.yml +0 -0
  17. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.gitignore +0 -0
  18. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.pre-commit-config.yaml +0 -0
  19. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.vscode/launch.json +0 -0
  20. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.vscode/settings.json +0 -0
  21. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/AUTHORS +0 -0
  22. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/CODE_OF_CONDUCT.md +0 -0
  23. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/CONTRIBUTING.md +0 -0
  24. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/ChangeLog +0 -0
  25. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/LICENSE +0 -0
  26. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/dependency_links.txt +0 -0
  27. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/requires.txt +0 -0
  28. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/top_level.txt +0 -0
  29. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pyproject.toml +0 -0
  30. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/__init__.py +0 -0
  31. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/account.py +0 -0
  32. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/api/__init__.py +0 -0
  33. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/api/authentication.py +0 -0
  34. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/api/client.py +0 -0
  35. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/api/log_sanitizer.py +0 -0
  36. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/api/ssl_context.py +0 -0
  37. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/api/utils.py +0 -0
  38. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/cli.py +0 -0
  39. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/const.py +0 -0
  40. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/control/charging.py +0 -0
  41. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/control/horn_light.py +0 -0
  42. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/control/journal.py +0 -0
  43. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/control/lock.py +0 -0
  44. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/control/telematics.py +0 -0
  45. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/control/windows.py +0 -0
  46. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/models.py +0 -0
  47. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/__init__.py +0 -0
  48. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/common.py +0 -0
  49. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/conftest.py +0 -0
  50. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/Human_and_vehicle_relationship_does_not_exist.json +0 -0
  51. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/api_access.json +0 -0
  52. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/auth_context.url +0 -0
  53. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/auth_intermediate.url +0 -0
  54. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/auth_result.url +0 -0
  55. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/charging_success.json +0 -0
  56. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/climate_success.json +0 -0
  57. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/dc_charge_info.json +0 -0
  58. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/journal_response.json +0 -0
  59. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/journal_toggle_success.json +0 -0
  60. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/login_result.json +0 -0
  61. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/ota_response.json +0 -0
  62. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/soc_80.json +0 -0
  63. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/soc_90.json +0 -0
  64. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/token_expired.json +0 -0
  65. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/trackpoints_response.json +0 -0
  66. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_info.json +0 -0
  67. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_info2.json +0 -0
  68. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_info_dc_charging.json +0 -0
  69. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_response.json +0 -0
  70. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_result.json +0 -0
  71. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_account.py +0 -0
  72. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_authentication_backoff.py +0 -0
  73. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_charging.py +0 -0
  74. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_charging_limit.py +0 -0
  75. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_charging_states.py +0 -0
  76. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_cli.py +0 -0
  77. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_climate_status.py +0 -0
  78. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_dc_charge_info.py +0 -0
  79. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_dc_charging.py +0 -0
  80. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_endpoint_urls.py +0 -0
  81. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_horn_light_control.py +0 -0
  82. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_journal.py +0 -0
  83. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_journal_control.py +0 -0
  84. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_journal_pagination.py +0 -0
  85. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_lock_control.py +0 -0
  86. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_log_sanitizer.py +0 -0
  87. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_missing_fields.py +0 -0
  88. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_ota_info.py +0 -0
  89. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_send_destination.py +0 -0
  90. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_ssl_context.py +0 -0
  91. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_token_lifecycle.py +0 -0
  92. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_tracked_vins.py +0 -0
  93. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_trackpoints.py +0 -0
  94. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_vehicle_state.py +0 -0
  95. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_vin_headers.py +0 -0
  96. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_window_control.py +0 -0
  97. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/__init__.py +0 -0
  98. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/battery.py +0 -0
  99. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/climate.py +0 -0
  100. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/journal.py +0 -0
  101. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/maintenance.py +0 -0
  102. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/position.py +0 -0
  103. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/running.py +0 -0
  104. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/safety.py +0 -0
  105. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/tires.py +0 -0
  106. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/trackpoints.py +0 -0
  107. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/vehicle_state.py +0 -0
  108. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/requirements-cli.txt +0 -0
  109. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/requirements-test.txt +0 -0
  110. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/requirements.txt +0 -0
  111. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/run.sh +0 -0
  112. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/setup.cfg +0 -0
  113. {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pySmartHashtag
3
- Version: 0.14.0
3
+ Version: 0.14.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
@@ -62,6 +62,14 @@ Get this custom integration into homeassistant
62
62
 
63
63
  [![Project Maintenance][maintenance-shield]](https://platinenmacher.tech)
64
64
 
65
+ ## Credits
66
+
67
+ The Smart #5 seat comfort functions (ZAF request format) exist thanks to community contributors:
68
+
69
+ - [@radzatm](https://github.com/radzatm) captured the working ZAF seat heating request of the Smart #5 with a man-in-the-middle proxy and tested the debug builds ([SmartHashtag#478](https://github.com/DasBasti/SmartHashtag/issues/478)).
70
+ - [@ooooli](https://github.com/ooooli) documented and confirmed the ZAF request format for climate, seat heating, steering wheel heating and front seat ventilation ([SmartHashtag#479](https://github.com/DasBasti/SmartHashtag/issues/479)).
71
+ - [@krlimi69](https://github.com/krlimi69) reported the failing seat heating on the Smart #5 Brabus and tested the fixes ([SmartHashtag#478](https://github.com/DasBasti/SmartHashtag/issues/478)).
72
+
65
73
  ## AI Disclosure
66
74
 
67
75
  This project uses AI to assist with development.
@@ -15,6 +15,14 @@ Get this custom integration into homeassistant
15
15
 
16
16
  [![Project Maintenance][maintenance-shield]](https://platinenmacher.tech)
17
17
 
18
+ ## Credits
19
+
20
+ The Smart #5 seat comfort functions (ZAF request format) exist thanks to community contributors:
21
+
22
+ - [@radzatm](https://github.com/radzatm) captured the working ZAF seat heating request of the Smart #5 with a man-in-the-middle proxy and tested the debug builds ([SmartHashtag#478](https://github.com/DasBasti/SmartHashtag/issues/478)).
23
+ - [@ooooli](https://github.com/ooooli) documented and confirmed the ZAF request format for climate, seat heating, steering wheel heating and front seat ventilation ([SmartHashtag#479](https://github.com/DasBasti/SmartHashtag/issues/479)).
24
+ - [@krlimi69](https://github.com/krlimi69) reported the failing seat heating on the Smart #5 Brabus and tested the fixes ([SmartHashtag#478](https://github.com/DasBasti/SmartHashtag/issues/478)).
25
+
18
26
  ## AI Disclosure
19
27
 
20
28
  This project uses AI to assist with development.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pySmartHashtag
3
- Version: 0.14.0
3
+ Version: 0.14.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
@@ -62,6 +62,14 @@ Get this custom integration into homeassistant
62
62
 
63
63
  [![Project Maintenance][maintenance-shield]](https://platinenmacher.tech)
64
64
 
65
+ ## Credits
66
+
67
+ The Smart #5 seat comfort functions (ZAF request format) exist thanks to community contributors:
68
+
69
+ - [@radzatm](https://github.com/radzatm) captured the working ZAF seat heating request of the Smart #5 with a man-in-the-middle proxy and tested the debug builds ([SmartHashtag#478](https://github.com/DasBasti/SmartHashtag/issues/478)).
70
+ - [@ooooli](https://github.com/ooooli) documented and confirmed the ZAF request format for climate, seat heating, steering wheel heating and front seat ventilation ([SmartHashtag#479](https://github.com/DasBasti/SmartHashtag/issues/479)).
71
+ - [@krlimi69](https://github.com/krlimi69) reported the failing seat heating on the Smart #5 Brabus and tested the fixes ([SmartHashtag#478](https://github.com/DasBasti/SmartHashtag/issues/478)).
72
+
65
73
  ## AI Disclosure
66
74
 
67
75
  This project uses AI to assist with development.
@@ -56,6 +56,7 @@ pysmarthashtag/tests/test_charging_limit.py
56
56
  pysmarthashtag/tests/test_charging_states.py
57
57
  pysmarthashtag/tests/test_cli.py
58
58
  pysmarthashtag/tests/test_climate_status.py
59
+ pysmarthashtag/tests/test_climate_zaf.py
59
60
  pysmarthashtag/tests/test_dc_charge_info.py
60
61
  pysmarthashtag/tests/test_dc_charging.py
61
62
  pysmarthashtag/tests/test_endpoint_urls.py
@@ -66,6 +66,7 @@
66
66
  "pysmarthashtag/tests/test_charging_states.py",
67
67
  "pysmarthashtag/tests/test_cli.py",
68
68
  "pysmarthashtag/tests/test_climate_status.py",
69
+ "pysmarthashtag/tests/test_climate_zaf.py",
69
70
  "pysmarthashtag/tests/test_dc_charge_info.py",
70
71
  "pysmarthashtag/tests/test_dc_charging.py",
71
72
  "pysmarthashtag/tests/test_endpoint_urls.py",
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "0.14.0",
2
+ "tag": "0.14.2",
3
3
  "distance": 0,
4
- "node": "g05515738dee6f771bb6c3defdb08901548b04be0",
4
+ "node": "gb73ae6cbc61e57756b26a7bb9ea3fd79957d7a89",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-10-10"
@@ -0,0 +1,314 @@
1
+ """Provides an accessible controll of the vehicle's climate functions."""
2
+
3
+ import json
4
+ import logging
5
+ from enum import Enum
6
+ from typing import TypedDict
7
+
8
+ from pysmarthashtag.account import SmartAccount
9
+ from pysmarthashtag.api import utils
10
+ from pysmarthashtag.api.client import SmartClient
11
+ from pysmarthashtag.const import API_TELEMATICS_URL
12
+ from pysmarthashtag.control.telematics import send_telematics_command
13
+ from pysmarthashtag.models import SmartHumanCarConnectionError, SmartTokenRefreshNecessary
14
+
15
+ _LOGGER = logging.getLogger(__name__)
16
+
17
+
18
+ class ServiceParameter(TypedDict):
19
+ """TypedDict for service parameters."""
20
+
21
+ key: str
22
+ value: str
23
+
24
+
25
+ class HeatingLocation(str, Enum):
26
+ """Enum for heating locations in the vehicle."""
27
+
28
+ DRIVER_SEAT = "front-left"
29
+ PASSENGER_SEAT = "front-right"
30
+ STEERING_WHEEL = "steering_wheel"
31
+
32
+
33
+ class VentilationLocation(str, Enum):
34
+ """Enum for ventilated seats (Smart #5 only)."""
35
+
36
+ DRIVER_SEAT = "front-left"
37
+ PASSENGER_SEAT = "front-right"
38
+
39
+
40
+ class RearHeatingLocation(str, Enum):
41
+ """Enum for heated rear seats (Smart #5 only)."""
42
+
43
+ REAR_LEFT = "rear-left"
44
+ REAR_RIGHT = "rear-right"
45
+
46
+
47
+ # ZAF service parameter keys of the Smart #5 comfort functions
48
+ ZAF_HEATING_KEYS = {
49
+ HeatingLocation.DRIVER_SEAT: "SH.11",
50
+ HeatingLocation.PASSENGER_SEAT: "SH.19",
51
+ HeatingLocation.STEERING_WHEEL: "SW",
52
+ }
53
+ ZAF_VENTILATION_KEYS = {
54
+ VentilationLocation.DRIVER_SEAT: "SV.11",
55
+ VentilationLocation.PASSENGER_SEAT: "SV.19",
56
+ }
57
+ ZAF_REAR_HEATING_KEYS = {
58
+ RearHeatingLocation.REAR_LEFT: "SH.21",
59
+ RearHeatingLocation.REAR_RIGHT: "SH.29",
60
+ }
61
+ ZAF_ALL_COMFORT_KEYS = ("AC", "SH.11", "SH.19", "SH.21", "SH.29", "SW", "DF", "SV.11", "SV.19")
62
+
63
+
64
+ class ClimateControll:
65
+ """Provides an accessible controll of the vehicle's climate functions."""
66
+
67
+ BASE_PAYLOAD_TEMPLATE = {
68
+ "creator": "tc",
69
+ "operationScheduling": {
70
+ "duration": 180,
71
+ "interval": 0,
72
+ "occurs": 1,
73
+ "recurrentOperation": False,
74
+ },
75
+ "serviceId": "RCE_2",
76
+ }
77
+ DEFROST_DURATION = 90
78
+
79
+ def __init__(self, account: SmartAccount, vin: str, use_zaf: bool = False):
80
+ """Initialize the vehicle.
81
+
82
+ ``use_zaf`` selects the newer ZAF request format used by the Smart #5
83
+ instead of the legacy RCE_2 payload of the #1/#3.
84
+ """
85
+ self.use_zaf = use_zaf
86
+ self.account = account
87
+ self.config = account.config
88
+ self.vin = vin
89
+ self.conditioning_temp = 20.0
90
+ self.heating_levels: dict[HeatingLocation, int] = {
91
+ HeatingLocation.DRIVER_SEAT: 0,
92
+ HeatingLocation.PASSENGER_SEAT: 0,
93
+ HeatingLocation.STEERING_WHEEL: 0,
94
+ }
95
+ self.ventilation_levels: dict[VentilationLocation, int] = {
96
+ VentilationLocation.DRIVER_SEAT: 0,
97
+ VentilationLocation.PASSENGER_SEAT: 0,
98
+ }
99
+ self.rear_heating_levels: dict[RearHeatingLocation, int] = {
100
+ RearHeatingLocation.REAR_LEFT: 0,
101
+ RearHeatingLocation.REAR_RIGHT: 0,
102
+ }
103
+
104
+ def _get_zaf_payload(self, active: bool) -> str:
105
+ """Build the Smart #5 ZAF request. Start and stop both use ``command: start``."""
106
+ params: dict[str, str] = {"operation": "4"}
107
+ if active:
108
+ params["AC"] = "true"
109
+ params["AC.temp"] = f"{self.conditioning_temp:.1f}"
110
+ params["AC.duration"] = "180"
111
+ for key in ZAF_ALL_COMFORT_KEYS[1:]:
112
+ params[key] = "false"
113
+ comfort = {
114
+ **{ZAF_HEATING_KEYS[loc]: lvl for loc, lvl in self.heating_levels.items()},
115
+ **{ZAF_VENTILATION_KEYS[loc]: lvl for loc, lvl in self.ventilation_levels.items()},
116
+ **{ZAF_REAR_HEATING_KEYS[loc]: lvl for loc, lvl in self.rear_heating_levels.items()},
117
+ }
118
+ for key, level in comfort.items():
119
+ if level > 0:
120
+ params[key] = "true"
121
+ params[f"{key}.level"] = str(level)
122
+ else:
123
+ for key in ZAF_ALL_COMFORT_KEYS:
124
+ params[key] = "false"
125
+ payload = {
126
+ "serviceId": "ZAF",
127
+ "command": "start",
128
+ "creator": "tc",
129
+ "latest": False,
130
+ "timeStamp": utils.create_correct_timestamp(),
131
+ "userId": str(self.config.authentication.api_user_id),
132
+ "operationScheduling": {
133
+ "scheduledTime": None,
134
+ "interval": 0,
135
+ "occurs": 0,
136
+ "recurrentOperation": 1,
137
+ "duration": 60,
138
+ },
139
+ "serviceParameters": [{"key": k, "value": v} for k, v in params.items()],
140
+ }
141
+ return json.dumps(payload).replace(" ", "")
142
+
143
+ def set_ventilation_level(self, location: VentilationLocation, level: int):
144
+ """Set the (Smart #5) seat ventilation level; selecting one clears the seat's heating."""
145
+ if not self.use_zaf:
146
+ raise NotImplementedError("Seat ventilation is only supported on the Smart #5")
147
+ if not isinstance(level, int) or isinstance(level, bool):
148
+ raise TypeError("Ventilation level must be an integer")
149
+ if level > 3 or level < 0:
150
+ raise ValueError("Seat ventilation level must be between 0 and 3.")
151
+ self.ventilation_levels[location] = level
152
+ if level > 0:
153
+ self.heating_levels[HeatingLocation(location.value)] = 0
154
+
155
+ def set_rear_heating_level(self, location: RearHeatingLocation, level: int):
156
+ """Set the (Smart #5) rear seat heating level."""
157
+ if not self.use_zaf:
158
+ raise NotImplementedError("Rear seat heating is only supported on the Smart #5")
159
+ if not isinstance(level, int) or isinstance(level, bool):
160
+ raise TypeError("Heating level must be an integer")
161
+ if level > 3 or level < 0:
162
+ raise ValueError("Seat heating level must be between 0 and 3.")
163
+ self.rear_heating_levels[location] = level
164
+
165
+ def _get_payload(self, active: bool) -> str:
166
+ _payload = self.BASE_PAYLOAD_TEMPLATE.copy()
167
+ _payload["command"] = "stop" if not active else "start"
168
+ _payload["timestamp"] = utils.create_correct_timestamp()
169
+ _payload["serviceParameters"] = []
170
+ _payload["serviceParameters"].append({"key": "rce.conditioner", "value": "1"})
171
+ _payload["serviceParameters"].append({"key": "rce.temp", "value": f"{self.conditioning_temp:.1f}"})
172
+ for loc, level in self.heating_levels.items():
173
+ if level > 0:
174
+ _payload["serviceParameters"] += self._add_rce_heating_service(loc, f"{level}")
175
+
176
+ return json.dumps(_payload).replace(" ", "")
177
+
178
+ def _add_rce_heating_service(self, value: str, level: int) -> list[ServiceParameter]:
179
+ """Create heating service parameters for the payload.
180
+
181
+ Args:
182
+ ----
183
+ value: The heating location identifier
184
+ level: The heating level as a string
185
+
186
+ Returns:
187
+ -------
188
+ List of service parameter dictionaries
189
+
190
+ """
191
+ payload = [{"key": "rce.heat", "value": value}, {"key": "rce.level", "value": level}]
192
+ return payload
193
+
194
+ def set_heating_level(self, location: HeatingLocation, level: int):
195
+ """Set heating level for the specified location."""
196
+ if not isinstance(level, int):
197
+ raise TypeError("Heating level must be an integer")
198
+ if level > 3 or level < 0:
199
+ raise ValueError("Seat heating level must be between 0 and 3.")
200
+ self.heating_levels[location] = level
201
+ if level > 0 and self.use_zaf and location != HeatingLocation.STEERING_WHEEL:
202
+ self.ventilation_levels[VentilationLocation(location.value)] = 0
203
+
204
+ async def set_climate_conditioning(self, temp: float, active: bool) -> bool:
205
+ """Set the climate conditioning."""
206
+
207
+ # A complete stop on the #5 sends all comfort functions off and needs no temperature
208
+ if active or not self.use_zaf:
209
+ if not isinstance(temp, (int, float)):
210
+ raise TypeError("Temperature must be a number")
211
+ if temp < 16 or temp > 30:
212
+ raise ValueError("Temperature must be between 16 and 30 degrees.")
213
+ self.conditioning_temp = float(temp)
214
+
215
+ _LOGGER.debug("Setting climate conditioning: active=%s, temp=%s", active, temp)
216
+ if self.use_zaf:
217
+ return (await send_telematics_command(self.account, self.vin, self._get_zaf_payload(active))) is True
218
+ return await self._send_rce_command(self._get_payload(active))
219
+
220
+ def _get_heating_payload(self, active: bool, levels: dict[HeatingLocation, int]) -> str:
221
+ _payload = self.BASE_PAYLOAD_TEMPLATE.copy()
222
+ _payload["command"] = "start" if active else "stop"
223
+ _payload["timestamp"] = utils.create_correct_timestamp()
224
+ _payload["serviceParameters"] = []
225
+ if active:
226
+ for loc, level in levels.items():
227
+ _payload["serviceParameters"] += self._add_rce_heating_service(loc, f"{level}")
228
+ else:
229
+ # Same as the Hello Smart app: list every location, then level 0
230
+ for loc in levels:
231
+ _payload["serviceParameters"].append({"key": "rce.heat", "value": loc})
232
+ _payload["serviceParameters"].append({"key": "rce.level", "value": "0"})
233
+ return json.dumps(_payload).replace(" ", "")
234
+
235
+ async def set_heating(self, active: bool, levels: dict[HeatingLocation, int]) -> bool:
236
+ """Start or stop seat / steering wheel heating without climate conditioning.
237
+
238
+ Unlike set_climate_conditioning, the RCE_2 command carries no
239
+ rce.conditioner / rce.temp, so the air conditioning is not started.
240
+ The levels given here are used as-is and do not change heating_levels.
241
+
242
+ Args:
243
+ ----
244
+ active: True to start heating, False to stop it
245
+ levels: Heating level (1-3) per location; only the keys are used to stop
246
+
247
+ """
248
+ if not isinstance(active, bool):
249
+ raise TypeError("Heating state must be a boolean")
250
+ if not levels:
251
+ raise ValueError("At least one heating location is required.")
252
+ for level in levels.values():
253
+ if not isinstance(level, int):
254
+ raise TypeError("Heating level must be an integer")
255
+ if active and (level > 3 or level < 1):
256
+ raise ValueError("Heating level must be between 1 and 3 to start heating.")
257
+
258
+ _LOGGER.debug("Setting heating: active=%s, levels=%s", active, levels)
259
+ return await self._send_rce_command(self._get_heating_payload(active, levels))
260
+
261
+ def _get_defrost_payload(self, active: bool) -> str:
262
+ _payload = self.BASE_PAYLOAD_TEMPLATE.copy()
263
+ _payload["command"] = "start" if active else "stop"
264
+ _payload["timestamp"] = utils.create_correct_timestamp()
265
+ _payload["operationScheduling"] = {
266
+ **self.BASE_PAYLOAD_TEMPLATE["operationScheduling"],
267
+ "duration": self.DEFROST_DURATION if active else 0,
268
+ }
269
+ _payload["serviceParameters"] = [
270
+ {"key": "rce.conditioner", "value": "2"},
271
+ {"key": "rce.level", "value": "2"},
272
+ ]
273
+ return json.dumps(_payload).replace(" ", "")
274
+
275
+ async def set_defrost(self, active: bool) -> bool:
276
+ """Start or stop the front windscreen defrost."""
277
+ if not isinstance(active, bool):
278
+ raise TypeError("Defrost state must be a boolean")
279
+ _LOGGER.debug("Setting front windscreen defrost: active=%s", active)
280
+ return await self._send_rce_command(self._get_defrost_payload(active))
281
+
282
+ async def _send_rce_command(self, params: str) -> bool:
283
+ """Send a remote climate command to the vehicle."""
284
+ # Ensure SSL context is created before using the client
285
+ await self.account._ensure_ssl_context()
286
+
287
+ await self.account.select_active_vehicle(self.vin)
288
+
289
+ async with SmartClient(self.config) as client:
290
+ for retry in range(3):
291
+ try:
292
+ vehicles_response = await client.put(
293
+ self.account.vehicles[self.vin].base_url + API_TELEMATICS_URL + self.vin,
294
+ headers={
295
+ **utils.generate_default_header(
296
+ client.config.authentication.device_id,
297
+ client.config.authentication.api_access_token,
298
+ params={},
299
+ method="PUT",
300
+ url=API_TELEMATICS_URL + self.vin,
301
+ body=params,
302
+ )
303
+ },
304
+ content=params.encode("utf-8"),
305
+ )
306
+ api_result = vehicles_response.json()
307
+ return api_result["success"]
308
+ except SmartTokenRefreshNecessary:
309
+ _LOGGER.debug("Got Token Error, retry: %d", retry)
310
+ continue
311
+ except SmartHumanCarConnectionError:
312
+ _LOGGER.debug("Got Human Car Connection Error, retry: %d", retry)
313
+ continue
314
+ return False
@@ -131,3 +131,74 @@ async def test_set_defrost_invalid_state(smart_fixture: respx.Router):
131
131
 
132
132
  with pytest.raises(TypeError):
133
133
  await climate_ctrl.set_defrost("on")
134
+
135
+
136
+ @pytest.mark.asyncio
137
+ async def test_set_heating_start_sends_heat_only(smart_fixture: respx.Router):
138
+ """Test that set_heating starts heating without climate conditioning parameters."""
139
+ account = await prepare_account_with_vehicles()
140
+ await account.get_vehicle_information("TestVIN0000000001")
141
+ climate_ctrl = account.vehicles["TestVIN0000000001"].climate_control
142
+
143
+ result = await climate_ctrl.set_heating(True, {HeatingLocation.DRIVER_SEAT: 3, HeatingLocation.PASSENGER_SEAT: 1})
144
+
145
+ assert result
146
+ payload = _last_telematics_payload(smart_fixture)
147
+ assert payload["serviceId"] == "RCE_2"
148
+ assert payload["command"] == "start"
149
+ assert payload["serviceParameters"] == [
150
+ {"key": "rce.heat", "value": "front-left"},
151
+ {"key": "rce.level", "value": "3"},
152
+ {"key": "rce.heat", "value": "front-right"},
153
+ {"key": "rce.level", "value": "1"},
154
+ ]
155
+
156
+
157
+ @pytest.mark.asyncio
158
+ async def test_set_heating_stop_sets_level_zero(smart_fixture: respx.Router):
159
+ """Test that set_heating stops all given locations with level 0."""
160
+ account = await prepare_account_with_vehicles()
161
+ await account.get_vehicle_information("TestVIN0000000001")
162
+ climate_ctrl = account.vehicles["TestVIN0000000001"].climate_control
163
+
164
+ await climate_ctrl.set_heating(False, {HeatingLocation.STEERING_WHEEL: 0})
165
+
166
+ payload = _last_telematics_payload(smart_fixture)
167
+ assert payload["command"] == "stop"
168
+ assert payload["serviceParameters"] == [
169
+ {"key": "rce.heat", "value": "steering_wheel"},
170
+ {"key": "rce.level", "value": "0"},
171
+ ]
172
+
173
+
174
+ @pytest.mark.asyncio
175
+ async def test_set_heating_does_not_change_conditioning_levels(smart_fixture: respx.Router):
176
+ """Test that set_heating leaves the levels used for climate conditioning untouched."""
177
+ account = await prepare_account_with_vehicles()
178
+ await account.get_vehicle_information("TestVIN0000000001")
179
+ climate_ctrl = account.vehicles["TestVIN0000000001"].climate_control
180
+
181
+ await climate_ctrl.set_heating(True, {HeatingLocation.DRIVER_SEAT: 3})
182
+
183
+ assert climate_ctrl.heating_levels[HeatingLocation.DRIVER_SEAT] == 0
184
+
185
+
186
+ @pytest.mark.asyncio
187
+ @pytest.mark.parametrize(
188
+ ("active", "levels", "exc"),
189
+ [
190
+ (True, {}, ValueError),
191
+ (True, {HeatingLocation.DRIVER_SEAT: 0}, ValueError),
192
+ (True, {HeatingLocation.DRIVER_SEAT: 4}, ValueError),
193
+ (True, {HeatingLocation.DRIVER_SEAT: "3"}, TypeError),
194
+ ("on", {HeatingLocation.DRIVER_SEAT: 3}, TypeError),
195
+ ],
196
+ )
197
+ async def test_set_heating_invalid_input(smart_fixture: respx.Router, active, levels, exc):
198
+ """Test that set_heating rejects invalid input."""
199
+ account = await prepare_account_with_vehicles()
200
+ await account.get_vehicle_information("TestVIN0000000001")
201
+ climate_ctrl = account.vehicles["TestVIN0000000001"].climate_control
202
+
203
+ with pytest.raises(exc):
204
+ await climate_ctrl.set_heating(active, levels)
@@ -0,0 +1,94 @@
1
+ """Tests for the Smart #5 ZAF climate payload with seat ventilation."""
2
+
3
+ import json
4
+ from types import SimpleNamespace
5
+
6
+ import pytest
7
+
8
+ from pysmarthashtag.control.climate import ClimateControll, HeatingLocation, VentilationLocation
9
+
10
+
11
+ def _ctrl(use_zaf: bool = True) -> ClimateControll:
12
+ account = SimpleNamespace(config=SimpleNamespace(authentication=SimpleNamespace(api_user_id=42)))
13
+ return ClimateControll(account, "VIN", use_zaf=use_zaf)
14
+
15
+
16
+ def _params(payload: str) -> dict:
17
+ return {p["key"]: p["value"] for p in json.loads(payload)["serviceParameters"]}
18
+
19
+
20
+ def test_start_with_ventilation():
21
+ ctrl = _ctrl()
22
+ ctrl.conditioning_temp = 21.0
23
+ ctrl.set_ventilation_level(VentilationLocation.DRIVER_SEAT, 2)
24
+ ctrl.set_heating_level(HeatingLocation.PASSENGER_SEAT, 3)
25
+ payload = json.loads(ctrl._get_zaf_payload(True))
26
+ assert payload["serviceId"] == "ZAF"
27
+ assert payload["command"] == "start"
28
+ assert payload["userId"] == "42"
29
+ assert payload["latest"] is False
30
+ params = _params(json.dumps(payload))
31
+ assert params["operation"] == "4"
32
+ assert params["AC"] == "true" and params["AC.temp"] == "21.0"
33
+ assert params["SV.11"] == "true" and params["SV.11.level"] == "2"
34
+ assert params["SH.19"] == "true" and params["SH.19.level"] == "3"
35
+ assert params["SH.11"] == "false" and "SH.11.level" not in params
36
+ assert params["SV.19"] == "false"
37
+
38
+
39
+ def test_heating_and_ventilation_are_exclusive():
40
+ ctrl = _ctrl()
41
+ ctrl.set_heating_level(HeatingLocation.DRIVER_SEAT, 1)
42
+ ctrl.set_ventilation_level(VentilationLocation.DRIVER_SEAT, 3)
43
+ assert ctrl.heating_levels[HeatingLocation.DRIVER_SEAT] == 0
44
+ ctrl.set_heating_level(HeatingLocation.DRIVER_SEAT, 2)
45
+ assert ctrl.ventilation_levels[VentilationLocation.DRIVER_SEAT] == 0
46
+
47
+
48
+ def test_stop_is_all_off():
49
+ ctrl = _ctrl()
50
+ ctrl.set_ventilation_level(VentilationLocation.DRIVER_SEAT, 2)
51
+ params = _params(ctrl._get_zaf_payload(False))
52
+ assert json.loads(ctrl._get_zaf_payload(False))["command"] == "start"
53
+ assert params["operation"] == "4"
54
+ for key in ("AC", "SH.11", "SH.19", "SH.21", "SH.29", "SW", "DF", "SV.11", "SV.19"):
55
+ assert params[key] == "false"
56
+
57
+
58
+ def test_invalid_level_and_legacy_rejected():
59
+ ctrl = _ctrl()
60
+ with pytest.raises(ValueError):
61
+ ctrl.set_ventilation_level(VentilationLocation.DRIVER_SEAT, 4)
62
+ with pytest.raises(NotImplementedError):
63
+ _ctrl(use_zaf=False).set_ventilation_level(VentilationLocation.DRIVER_SEAT, 1)
64
+
65
+
66
+ def test_rear_heating_and_all_keys_string_values():
67
+ from pysmarthashtag.control.climate import RearHeatingLocation
68
+
69
+ ctrl = _ctrl()
70
+ ctrl.set_rear_heating_level(RearHeatingLocation.REAR_LEFT, 2)
71
+ params = _params(ctrl._get_zaf_payload(True))
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
74
+ with pytest.raises(ValueError):
75
+ ctrl.set_rear_heating_level(RearHeatingLocation.REAR_LEFT, 4)
76
+ with pytest.raises(NotImplementedError):
77
+ _ctrl(use_zaf=False).set_rear_heating_level(RearHeatingLocation.REAR_LEFT, 1)
78
+
79
+
80
+ @pytest.mark.asyncio
81
+ async def test_stop_works_with_corrupt_temperature(monkeypatch):
82
+ sent = []
83
+
84
+ async def fake_send(account, vin, payload):
85
+ sent.append(payload)
86
+ return True
87
+
88
+ monkeypatch.setattr("pysmarthashtag.control.climate.send_telematics_command", fake_send)
89
+ ctrl = _ctrl()
90
+ assert await ctrl.set_climate_conditioning("garbage", False) is True
91
+ assert all(p["value"] == "false" or p["key"] == "operation" for p in json.loads(sent[0])["serviceParameters"])
92
+ with pytest.raises(TypeError):
93
+ await ctrl.set_climate_conditioning("garbage", True)
94
+ assert len(sent) == 1
@@ -162,7 +162,11 @@ class SmartVehicle:
162
162
 
163
163
  from pysmarthashtag.control.climate import ClimateControll
164
164
 
165
- self.climate_control = ClimateControll(self.account, self.vin)
165
+ self.climate_control = ClimateControll(
166
+ self.account,
167
+ self.vin,
168
+ use_zaf=str(self.data.get("seriesCodeVs", "")).startswith(SERIES_CODE_PREFIX_SMART_5),
169
+ )
166
170
 
167
171
  from pysmarthashtag.control.charging import ChargingControl
168
172
 
@@ -1,161 +0,0 @@
1
- """Provides an accessible controll of the vehicle's climate functions."""
2
-
3
- import json
4
- import logging
5
- from enum import Enum
6
- from typing import TypedDict
7
-
8
- from pysmarthashtag.account import SmartAccount
9
- from pysmarthashtag.api import utils
10
- from pysmarthashtag.api.client import SmartClient
11
- from pysmarthashtag.const import API_TELEMATICS_URL
12
- from pysmarthashtag.models import SmartHumanCarConnectionError, SmartTokenRefreshNecessary
13
-
14
- _LOGGER = logging.getLogger(__name__)
15
-
16
-
17
- class ServiceParameter(TypedDict):
18
- """TypedDict for service parameters."""
19
-
20
- key: str
21
- value: str
22
-
23
-
24
- class HeatingLocation(str, Enum):
25
- """Enum for heating locations in the vehicle."""
26
-
27
- DRIVER_SEAT = "front-left"
28
- PASSENGER_SEAT = "front-right"
29
- STEERING_WHEEL = "steering_wheel"
30
-
31
-
32
- class ClimateControll:
33
- """Provides an accessible controll of the vehicle's climate functions."""
34
-
35
- BASE_PAYLOAD_TEMPLATE = {
36
- "creator": "tc",
37
- "operationScheduling": {
38
- "duration": 180,
39
- "interval": 0,
40
- "occurs": 1,
41
- "recurrentOperation": False,
42
- },
43
- "serviceId": "RCE_2",
44
- }
45
- DEFROST_DURATION = 90
46
-
47
- def __init__(self, account: SmartAccount, vin: str):
48
- """Initialize the vehicle."""
49
- self.account = account
50
- self.config = account.config
51
- self.vin = vin
52
- self.conditioning_temp = 20.0
53
- self.heating_levels: dict[HeatingLocation, int] = {
54
- HeatingLocation.DRIVER_SEAT: 0,
55
- HeatingLocation.PASSENGER_SEAT: 0,
56
- HeatingLocation.STEERING_WHEEL: 0,
57
- }
58
-
59
- def _get_payload(self, active: bool) -> str:
60
- _payload = self.BASE_PAYLOAD_TEMPLATE.copy()
61
- _payload["command"] = "stop" if not active else "start"
62
- _payload["timestamp"] = utils.create_correct_timestamp()
63
- _payload["serviceParameters"] = []
64
- _payload["serviceParameters"].append({"key": "rce.conditioner", "value": "1"})
65
- _payload["serviceParameters"].append({"key": "rce.temp", "value": f"{self.conditioning_temp:.1f}"})
66
- for loc, level in self.heating_levels.items():
67
- if level > 0:
68
- _payload["serviceParameters"] += self._add_rce_heating_service(loc, f"{level}")
69
-
70
- return json.dumps(_payload).replace(" ", "")
71
-
72
- def _add_rce_heating_service(self, value: str, level: int) -> list[ServiceParameter]:
73
- """Create heating service parameters for the payload.
74
-
75
- Args:
76
- ----
77
- value: The heating location identifier
78
- level: The heating level as a string
79
-
80
- Returns:
81
- -------
82
- List of service parameter dictionaries
83
-
84
- """
85
- payload = [{"key": "rce.heat", "value": value}, {"key": "rce.level", "value": level}]
86
- return payload
87
-
88
- def set_heating_level(self, location: HeatingLocation, level: int):
89
- """Set heating level for the specified location."""
90
- if not isinstance(level, int):
91
- raise TypeError("Heating level must be an integer")
92
- if level > 3 or level < 0:
93
- raise ValueError("Seat heating level must be between 0 and 3.")
94
- self.heating_levels[location] = level
95
-
96
- async def set_climate_conditioning(self, temp: float, active: bool) -> bool:
97
- """Set the climate conditioning."""
98
-
99
- if not isinstance(temp, (int, float)):
100
- raise TypeError("Temperature must be a number")
101
- if temp < 16 or temp > 30:
102
- raise ValueError("Temperature must be between 16 and 30 degrees.")
103
- self.conditioning_temp = float(temp)
104
-
105
- _LOGGER.debug("Setting climate conditioning: active=%s, temp=%.1f", active, self.conditioning_temp)
106
- return await self._send_rce_command(self._get_payload(active))
107
-
108
- def _get_defrost_payload(self, active: bool) -> str:
109
- _payload = self.BASE_PAYLOAD_TEMPLATE.copy()
110
- _payload["command"] = "start" if active else "stop"
111
- _payload["timestamp"] = utils.create_correct_timestamp()
112
- _payload["operationScheduling"] = {
113
- **self.BASE_PAYLOAD_TEMPLATE["operationScheduling"],
114
- "duration": self.DEFROST_DURATION if active else 0,
115
- }
116
- _payload["serviceParameters"] = [
117
- {"key": "rce.conditioner", "value": "2"},
118
- {"key": "rce.level", "value": "2"},
119
- ]
120
- return json.dumps(_payload).replace(" ", "")
121
-
122
- async def set_defrost(self, active: bool) -> bool:
123
- """Start or stop the front windscreen defrost."""
124
- if not isinstance(active, bool):
125
- raise TypeError("Defrost state must be a boolean")
126
- _LOGGER.debug("Setting front windscreen defrost: active=%s", active)
127
- return await self._send_rce_command(self._get_defrost_payload(active))
128
-
129
- async def _send_rce_command(self, params: str) -> bool:
130
- """Send a remote climate command to the vehicle."""
131
- # Ensure SSL context is created before using the client
132
- await self.account._ensure_ssl_context()
133
-
134
- await self.account.select_active_vehicle(self.vin)
135
-
136
- async with SmartClient(self.config) as client:
137
- for retry in range(3):
138
- try:
139
- vehicles_response = await client.put(
140
- self.account.vehicles[self.vin].base_url + API_TELEMATICS_URL + self.vin,
141
- headers={
142
- **utils.generate_default_header(
143
- client.config.authentication.device_id,
144
- client.config.authentication.api_access_token,
145
- params={},
146
- method="PUT",
147
- url=API_TELEMATICS_URL + self.vin,
148
- body=params,
149
- )
150
- },
151
- content=params.encode("utf-8"),
152
- )
153
- api_result = vehicles_response.json()
154
- return api_result["success"]
155
- except SmartTokenRefreshNecessary:
156
- _LOGGER.debug("Got Token Error, retry: %d", retry)
157
- continue
158
- except SmartHumanCarConnectionError:
159
- _LOGGER.debug("Got Human Car Connection Error, retry: %d", retry)
160
- continue
161
- return False
File without changes
File without changes
File without changes