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")]