From 0cacb44e8a876704f64a9b84946c89bd5dc4e631 Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Sun, 25 Oct 2026 16:04:31 +0100 Subject: [PATCH] fix: end the heated seat and steering wheel commands with PARAMS_MAX The iSmart app appends parameter 255 to these commands. Send it with the value 0, encoded like the other control commands, so they follow the app and the rest of the client. Door windows are unchanged: the app sends no 255 there. --- .../api/vehicle/climate/__init__.py | 27 ++++++++++--------- tests/test_heated_seats.py | 15 ++++++----- tests/test_heated_steering_wheel.py | 12 +++++---- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/saic_ismart_client_ng/api/vehicle/climate/__init__.py b/src/saic_ismart_client_ng/api/vehicle/climate/__init__.py index 86adfda..643eb22 100644 --- a/src/saic_ismart_client_ng/api/vehicle/climate/__init__.py +++ b/src/saic_ismart_client_ng/api/vehicle/climate/__init__.py @@ -100,11 +100,11 @@ async def control_heated_seat( ) -> VehicleControlResp: """Set one heated seat, leaving the others alone. - Matches what the iSmart app sends (decrypted MG S6 EV traffic): request - type 5 with a single parameter for the chosen seat (17 front left, - 18 front right, 25 rear left, 26 rear right) and no end-of-parameters - marker. Unlike ``control_heated_seats`` it doesn't have to re-send the - other front seat's level, and it can reach the rear seats. + Sends request type 5 with a single parameter for the chosen seat (17 + front left, 18 front right, 25 rear left, 26 rear right), followed by + the end-of-parameters marker (255) like the other control commands. + Unlike ``control_heated_seats`` it doesn't have to re-send the other + front seat's level, and it can reach the rear seats. Front seats: 0 off, 1 low, 2 medium, 3 high. Rear seats are on/off only (in the app and in the car): send ``REAR_HEATED_SEAT_ON_LEVEL`` @@ -113,7 +113,10 @@ async def control_heated_seat( if not 0 <= level <= 3: msg = f"Heated seat level must be 0-3, got {level}" raise ValueError(msg) - rvc_params = [RvcParams(seat.value, level.to_bytes(1, "big"))] + rvc_params = [ + RvcParams(seat.value, level.to_bytes(1, "big")), + RvcParams(RvcParamsId.PARAMS_MAX, b"\x00\x00\x00\x00"), + ] body = VehicleControlReq( rvc_req_type=RvcReqType.HEATED_SEATS, rvc_params=rvc_params, @@ -126,17 +129,17 @@ async def control_heated_steering_wheel( ) -> 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``. + Sends request type 8 with parameter 24 = 1 (on) or 0 (off), followed + by the end-of-parameters marker (255) like the other control + commands. 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" ), + RvcParams(RvcParamsId.PARAMS_MAX, b"\x00\x00\x00\x00"), ] body = VehicleControlReq( rvc_req_type=RvcReqType.HEATED_STEERING_WHEEL, diff --git a/tests/test_heated_seats.py b/tests/test_heated_seats.py index 7920c54..c262e96 100644 --- a/tests/test_heated_seats.py +++ b/tests/test_heated_seats.py @@ -1,9 +1,9 @@ """Per-seat heated seat control, front and rear (#34). -Checks the request body against what the iSmart app sends (decrypted MG S6 EV -traffic): request type 5, a single parameter for the chosen seat (17 front -left, 18 front right, 25 rear left, 26 rear right) and no end-of-parameters -marker. Rear seats are on/off only; the app sends 3 for "on". +Checks the request body: request type 5, a single parameter for the chosen seat +(17 front left, 18 front right, 25 rear left, 26 rear right), then the +end-of-parameters marker (255). Rear seats are on/off only; the app sends 3 for +"on". """ from __future__ import annotations @@ -58,7 +58,10 @@ async def test_one_seat_per_command(self) -> None: (p.paramId, base64.b64decode(p.paramValue)) for p in body.rvcParams ] - assert params == [(param_id, bytes([level]))] + assert params == [ + (param_id, bytes([level])), + (255, b"\x00\x00\x00\x00"), + ] assert body.vin == sha256_hex_digest(VIN) async def test_rear_seat_on_is_the_apps_level(self) -> None: @@ -67,7 +70,7 @@ async def test_rear_seat_on_is_the_apps_level(self) -> None: seat=HeatedSeat.REAR_LEFT, level=REAR_HEATED_SEAT_ON_LEVEL ) params = [(p.paramId, base64.b64decode(p.paramValue)) for p in body.rvcParams] - assert params == [(25, b"\x03")] + assert params == [(25, b"\x03"), (255, b"\x00\x00\x00\x00")] async def test_out_of_range_levels_are_rejected(self) -> None: for level in (-1, 4): diff --git a/tests/test_heated_steering_wheel.py b/tests/test_heated_steering_wheel.py index 8d3c56d..ece4a24 100644 --- a/tests/test_heated_steering_wheel.py +++ b/tests/test_heated_steering_wheel.py @@ -1,8 +1,7 @@ """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. +Checks the request body: request type 8, parameter 24 = 1 (on) or 0 (off), +then the end-of-parameters marker (255). """ from __future__ import annotations @@ -39,11 +38,14 @@ async def test_on(self) -> None: 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 == [ + (RvcParamsId.HEATED_STEERING_WHEEL.value, b"\x01"), + (255, b"\x00\x00\x00\x00"), + ] 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")] + assert params == [(24, b"\x00"), (255, b"\x00\x00\x00\x00")]