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
27 changes: 15 additions & 12 deletions src/saic_ismart_client_ng/api/vehicle/climate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand All @@ -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,
Expand All @@ -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,
Expand Down
15 changes: 9 additions & 6 deletions tests/test_heated_seats.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down
12 changes: 7 additions & 5 deletions tests/test_heated_steering_wheel.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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")]
Loading