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.
- {pysmarthashtag-0.14.0/pySmartHashtag.egg-info → pysmarthashtag-0.14.2}/PKG-INFO +9 -1
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/README.md +8 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2/pySmartHashtag.egg-info}/PKG-INFO +9 -1
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/SOURCES.txt +1 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/scm_file_list.json +1 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/scm_version.json +2 -2
- pysmarthashtag-0.14.2/pysmarthashtag/control/climate.py +314 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_actions.py +71 -0
- pysmarthashtag-0.14.2/pysmarthashtag/tests/test_climate_zaf.py +94 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/vehicle.py +5 -1
- pysmarthashtag-0.14.0/pysmarthashtag/control/climate.py +0 -161
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.devcontainer.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.github/copilot-instructions.md +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.github/dependabot.yml +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.github/workflows/python-package.yml +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.github/workflows/python-publish.yml +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.gitignore +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.pre-commit-config.yaml +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.vscode/launch.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/.vscode/settings.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/AUTHORS +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/CODE_OF_CONDUCT.md +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/CONTRIBUTING.md +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/ChangeLog +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/LICENSE +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/dependency_links.txt +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/requires.txt +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/top_level.txt +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pyproject.toml +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/__init__.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/account.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/api/__init__.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/api/authentication.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/api/client.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/api/log_sanitizer.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/api/ssl_context.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/api/utils.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/cli.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/const.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/control/charging.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/control/horn_light.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/control/journal.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/control/lock.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/control/telematics.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/control/windows.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/models.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/__init__.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/common.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/conftest.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/Human_and_vehicle_relationship_does_not_exist.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/api_access.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/auth_context.url +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/auth_intermediate.url +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/auth_result.url +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/charging_success.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/climate_success.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/dc_charge_info.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/journal_response.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/journal_toggle_success.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/login_result.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/ota_response.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/soc_80.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/soc_90.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/token_expired.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/trackpoints_response.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_info.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_info2.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_info_dc_charging.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_response.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_result.json +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_account.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_authentication_backoff.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_charging.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_charging_limit.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_charging_states.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_cli.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_climate_status.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_dc_charge_info.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_dc_charging.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_endpoint_urls.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_horn_light_control.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_journal.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_journal_control.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_journal_pagination.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_lock_control.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_log_sanitizer.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_missing_fields.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_ota_info.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_send_destination.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_ssl_context.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_token_lifecycle.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_tracked_vins.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_trackpoints.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_vehicle_state.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_vin_headers.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_window_control.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/__init__.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/battery.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/climate.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/journal.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/maintenance.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/position.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/running.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/safety.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/tires.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/trackpoints.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/vehicle/vehicle_state.py +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/requirements-cli.txt +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/requirements-test.txt +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/requirements.txt +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/run.sh +0 -0
- {pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/setup.cfg +0 -0
- {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.
|
|
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.
|
|
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",
|
|
@@ -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(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pySmartHashtag.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/auth_context.url
RENAMED
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/auth_intermediate.url
RENAMED
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/charging_success.json
RENAMED
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/climate_success.json
RENAMED
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/dc_charge_info.json
RENAMED
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/journal_response.json
RENAMED
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/login_result.json
RENAMED
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/ota_response.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/token_expired.json
RENAMED
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_info.json
RENAMED
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_info2.json
RENAMED
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_response.json
RENAMED
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/replys/vehicle_result.json
RENAMED
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_authentication_backoff.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_charging_states.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_horn_light_control.py
RENAMED
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_journal_control.py
RENAMED
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_journal_pagination.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_send_destination.py
RENAMED
|
File without changes
|
|
File without changes
|
{pysmarthashtag-0.14.0 → pysmarthashtag-0.14.2}/pysmarthashtag/tests/test_token_lifecycle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|