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 a03fdeb..86adfda 100644 --- a/src/saic_ismart_client_ng/api/vehicle/climate/__init__.py +++ b/src/saic_ismart_client_ng/api/vehicle/climate/__init__.py @@ -81,10 +81,10 @@ async def control_heated_seats( ) -> VehicleControlResp: rcv_params = [ RvcParams( - RvcParamsId.HEATED_SEAT_DRIVER, left_side_level.to_bytes(1, "big") + RvcParamsId.HEATED_SEAT_FRONT_LEFT, left_side_level.to_bytes(1, "big") ), RvcParams( - RvcParamsId.HEATED_SEAT_PASSENGER, right_side_level.to_bytes(1, "big") + RvcParamsId.HEATED_SEAT_FRONT_RIGHT, right_side_level.to_bytes(1, "big") ), RvcParams(RvcParamsId.PARAMS_MAX, b"\x00\x00\x00\x00"), ] diff --git a/src/saic_ismart_client_ng/api/vehicle/climate/schema.py b/src/saic_ismart_client_ng/api/vehicle/climate/schema.py index 86be7bf..daf46b7 100644 --- a/src/saic_ismart_client_ng/api/vehicle/climate/schema.py +++ b/src/saic_ismart_client_ng/api/vehicle/climate/schema.py @@ -6,10 +6,15 @@ class HeatedSeat(Enum): - """A single heated seat, by the parameter the iSmart app uses for it.""" + """A single heated seat, by the parameter the iSmart app uses for it. - FRONT_LEFT = RvcParamsId.HEATED_SEAT_DRIVER - FRONT_RIGHT = RvcParamsId.HEATED_SEAT_PASSENGER + Left and right are the physical sides of the car, the same on left- and + right-hand drive cars. They match ``frontLeftSeatHeatLevel`` / + ``frontRightSeatHeatLevel`` in the vehicle status. + """ + + FRONT_LEFT = RvcParamsId.HEATED_SEAT_FRONT_LEFT + FRONT_RIGHT = RvcParamsId.HEATED_SEAT_FRONT_RIGHT REAR_LEFT = RvcParamsId.HEATED_SEAT_REAR_LEFT REAR_RIGHT = RvcParamsId.HEATED_SEAT_REAR_RIGHT diff --git a/src/saic_ismart_client_ng/api/vehicle/schema.py b/src/saic_ismart_client_ng/api/vehicle/schema.py index cba2785..742207a 100644 --- a/src/saic_ismart_client_ng/api/vehicle/schema.py +++ b/src/saic_ismart_client_ng/api/vehicle/schema.py @@ -172,8 +172,15 @@ class RvcParamsId(Enum): WINDOW_3 = 11 WINDOW_4 = 12 WINDOW_OPEN_CLOSE = 13 - HEATED_SEAT_DRIVER = 17 - HEATED_SEAT_PASSENGER = 18 + # Heated seats are addressed by PHYSICAL side, not driver/passenger: + # 17 is the front-left seat and 18 the front-right on both left- and + # right-hand drive cars (confirmed on a right-hand drive MG S6 EV, where + # the app's front-left seat sends 17). The DRIVER/PASSENGER names are the + # original ones, kept as aliases; they're only right for left-hand drive. + HEATED_SEAT_FRONT_LEFT = 17 + HEATED_SEAT_FRONT_RIGHT = 18 + HEATED_SEAT_DRIVER = 17 # noqa: PIE796 - alias of HEATED_SEAT_FRONT_LEFT + HEATED_SEAT_PASSENGER = 18 # noqa: PIE796 - alias of HEATED_SEAT_FRONT_RIGHT FAN_SPEED = 19 TEMPERATURE = 20 AC_ON_OFF = 22 diff --git a/tests/test_heated_seat_sides.py b/tests/test_heated_seat_sides.py new file mode 100644 index 0000000..1ae7411 --- /dev/null +++ b/tests/test_heated_seat_sides.py @@ -0,0 +1,53 @@ +"""Front heated seats are named by physical side, not driver/passenger. + +Parameters 17 and 18 are the front-left and front-right seats on both left- +and right-hand drive cars: on a right-hand drive MG S6 EV the iSmart app's +front-left (passenger) seat sends 17. HEATED_SEAT_FRONT_LEFT/RIGHT are the +new names; HEATED_SEAT_DRIVER/PASSENGER are kept as aliases, and the bytes +sent are unchanged. +""" + +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 +from saic_ismart_client_ng.api.vehicle.climate import HeatedSeat +from saic_ismart_client_ng.model import SaicApiConfiguration + + +class TestHeatedSeatSides(unittest.IsolatedAsyncioTestCase): + def test_front_seats_are_physical_sides(self) -> None: + assert RvcParamsId.HEATED_SEAT_FRONT_LEFT.value == 17 + assert RvcParamsId.HEATED_SEAT_FRONT_RIGHT.value == 18 + assert HeatedSeat.FRONT_LEFT.value is RvcParamsId.HEATED_SEAT_FRONT_LEFT + assert HeatedSeat.FRONT_RIGHT.value is RvcParamsId.HEATED_SEAT_FRONT_RIGHT + + def test_old_names_still_work(self) -> None: + # Looked up by name: mypy doesn't know enum aliases share a member. + assert RvcParamsId["HEATED_SEAT_DRIVER"] is RvcParamsId.HEATED_SEAT_FRONT_LEFT + assert ( + RvcParamsId["HEATED_SEAT_PASSENGER"] is RvcParamsId.HEATED_SEAT_FRONT_RIGHT + ) + + async def test_both_front_seats_command_unchanged(self) -> None: + 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_seats( + "OFFLINE0000000000", left_side_level=1, right_side_level=3 + ) + await api.close() + params = [ + (p.paramId, base64.b64decode(p.paramValue)) + for p in captured["body"].rvcParams + ] + assert params == [(17, b"\x01"), (18, b"\x03"), (255, b"\x00\x00\x00\x00")]