Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Supported functionality (partial list, check the code for more)
* stop rear window heating
* remote climate
* heated seats
* heated steering wheel


## Prerequisites
Expand Down
24 changes: 24 additions & 0 deletions src/saic_ismart_client_ng/api/vehicle/climate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,30 @@ async def control_heated_seats(
)
return await self.send_vehicle_control_command(body, vin)

async def control_heated_steering_wheel(
self, vin: str, *, enable: bool
) -> VehicleControlResp:
"""Turn the heated steering wheel on or off.

Matches the traffic captured from the iSmart app on an MG S6 EV:
request type 8 with a single parameter, 24 = 1 (on) or 0 (off), and
no end-of-parameters marker. Other app builds or models may send
more than this; only the S6 EV has been checked. The car turns the
heater off by itself after about 10 minutes. Its state is reported
back in ``BasicVehicleStatus.steeringHeatLevel``.
"""
rvc_params = [
RvcParams(
RvcParamsId.HEATED_STEERING_WHEEL, b"\x01" if enable else b"\x00"
),
]
body = VehicleControlReq(
rvc_req_type=RvcReqType.HEATED_STEERING_WHEEL,
rvc_params=rvc_params,
vin=sha256_hex_digest(vin),
)
return await self.send_vehicle_control_command(body, vin)

async def control_rear_window_heat(
self, vin: str, *, enable: bool
) -> VehicleControlResp:
Expand Down
2 changes: 2 additions & 0 deletions src/saic_ismart_client_ng/api/vehicle/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ class RvcParamsId(Enum):
TEMPERATURE = 20
AC_ON_OFF = 22
REMOTE_HEAT_REAR_WINDOW = 23
HEATED_STEERING_WHEEL = 24
PARAMS_MAX = 0xFF


Expand All @@ -195,6 +196,7 @@ class RvcReqType(Enum):
HEATED_SEATS = "5"
CLIMATE = "6"
AIR_CLEAN = "7"
HEATED_STEERING_WHEEL = "8"
ENGINE_CONTROL = "17"
REMOTE_REFRESH = "18"
REMOTE_IMMOBILIZER = "19"
Expand Down
49 changes: 49 additions & 0 deletions tests/test_heated_steering_wheel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Heated steering wheel control (#33).

Checks the request body against what the iSmart app sends (decrypted MG S6 EV
traffic): request type 8, a single parameter 24 = 1 (on) or 0 (off), and no
end-of-parameters marker.
"""

from __future__ import annotations

import base64
from typing import Any
import unittest

from saic_ismart_client_ng import SaicApi
from saic_ismart_client_ng.api.vehicle import RvcParamsId, RvcReqType
from saic_ismart_client_ng.crypto_utils import sha256_hex_digest
from saic_ismart_client_ng.model import SaicApiConfiguration

VIN = "OFFLINE0000000000"


class TestHeatedSteeringWheel(unittest.IsolatedAsyncioTestCase):
async def _sent(self, *, enable: bool) -> Any:
api = SaicApi(SaicApiConfiguration(username="user", password="pass")) # noqa: S106
captured: dict[str, Any] = {}

async def capture(body: Any, vin: str) -> None:
captured["body"] = body
captured["vin"] = vin

api.send_vehicle_control_command = capture # type: ignore[method-assign,assignment]
await api.control_heated_steering_wheel(VIN, enable=enable)
await api.close()
assert captured["vin"] == VIN
return captured["body"]

async def test_on(self) -> None:
body = await self._sent(enable=True)
assert body.rvcReqType == RvcReqType.HEATED_STEERING_WHEEL.value
assert body.rvcReqType == "8"
params = [(p.paramId, base64.b64decode(p.paramValue)) for p in body.rvcParams]
assert params == [(RvcParamsId.HEATED_STEERING_WHEEL.value, b"\x01")]
assert params[0][0] == 24
assert body.vin == sha256_hex_digest(VIN)

async def test_off(self) -> None:
body = await self._sent(enable=False)
params = [(p.paramId, base64.b64decode(p.paramValue)) for p in body.rvcParams]
assert params == [(24, b"\x00")]
Loading