Skip to content

fix(vehicle): standardize auto seat climate on 1-indexed AutoSeat (fixes #11)#41

Merged
Bre77 merged 1 commit into
mainfrom
fm/tfa-autoseat-fix-a9
Jul 2, 2026
Merged

fix(vehicle): standardize auto seat climate on 1-indexed AutoSeat (fixes #11)#41
Bre77 merged 1 commit into
mainfrom
fm/tfa-autoseat-fix-a9

Conversation

@Bre77

@Bre77 Bre77 commented Jul 2, 2026

Copy link
Copy Markdown
Member

Intent

Fix issue #11: the auto-seat-climate command (remote_auto_seat_climate_request) is 1-indexed on Tesla's wire (AutoSeatPosition_FrontLeft=1, FrontRight=2), but the library's two backends disagreed with each other and with the 0-indexed Seat enum, so Seat.FRONT_LEFT=0 was rejected and Seat.FRONT_RIGHT=1 controlled the front-left seat. DECISION (made by the maintainer, do not re-litigate): standardize BOTH backends on the 1-indexed AutoSeat enum (FL=1, FR=2), which already matches the REST wire values and the proto AutoSeatPosition_* enum. Changes: (1) fleet.py REST path — change type hint 'int | Seat' to 'int | AutoSeat' and import AutoSeat; this is a correctness/clarity change with NO behavior change since the REST path already forwarded the value verbatim to the 1-indexed endpoint. (2) commands.py signed/protobuf path — map the 1-indexed int directly onto the proto enum (via typing.cast for pyright strict; protobuf accepts the raw int at runtime) INSTEAD of indexing the removed 0-based AutoSeatClimatePositions tuple, so AutoSeat.FRONT_RIGHT=2 no longer raises IndexError; type hint updated to 'int | AutoSeat'. This is a DELIBERATE behavior change on the signed path: its expected index moves 0->1, so a direct caller of the signed method must now pass 1-indexed values — this is intended and called out in the commit body. (3) AutoSeat already sits alongside Seat in const.py so it is importable from the same module. (4) Added regression tests (tests/test_auto_seat_climate.py) asserting the Fleet JSON body carries auto_seat_position=1/2 and the signed builder emits AutoSeatPosition_FrontLeft/FrontRight for the same 1-indexed input. The manual seat-heater/cooler paths and the 0-indexed Seat enum are intentionally left unchanged (they are correct). Also recorded the dual seat-indexing gotcha in AGENTS.md. Out of scope but noted for the maintainer: the Home Assistant tesla_fleet integration must also pass 1-indexed AutoSeat values to fully close the end-user symptom; HA is not touched here.

What Changed

  • remote_auto_seat_climate_request now standardizes both backends on the 1-indexed AutoSeat enum (FRONT_LEFT=1, FRONT_RIGHT=2), matching Tesla's REST wire values and the proto AutoSeatPosition_* enum; the REST path in fleet.py gains the int | AutoSeat type hint and AutoSeat import (value already forwarded verbatim, no behavior change).
  • The signed/protobuf path in commands.py now casts the 1-indexed int directly onto the proto enum instead of indexing the removed 0-based AutoSeatClimatePositions tuple, so AutoSeat.FRONT_RIGHT=2 no longer raises IndexError — a deliberate index shift (0→1) for direct callers of the signed method.
  • Added regression tests (tests/test_auto_seat_climate.py) asserting the Fleet JSON carries auto_seat_position=1/2 and the signed builder emits AutoSeatPosition_FrontLeft/FrontRight for the same input, and documented the dual seat-indexing gotcha in AGENTS.md.

Risk Assessment

✅ Low: A tightly-bounded correctness fix that standardizes both backends on the 1-indexed AutoSeat convention matching the proto enum and REST wire values, with matching regression tests and no other affected call sites.

Testing

Baseline uv sync plus uv run pytest tests (41 passed, 8 subtests) confirm no regressions from the signature/import changes; the new tests/test_auto_seat_climate.py (4 tests) passes and asserts both backends agree on 1-indexed values. Since this is a library (no UI surface), the end-user-experience evidence is a runnable demo capturing the real command payloads: the Fleet REST path forwards auto_seat_position 1/2 verbatim and the signed path maps them onto AutoSeatPosition_FrontLeft/FrontRight, while a reproduction of the removed 0-indexed tuple lookup shows the old bug (IndexError for FRONT_RIGHT=2, and FRONT_LEFT=1 wrongly emitting FrontRight) that the fix eliminates. No visual artifact applies because there is no rendered UI. Transient egg-info change from the editable install was reverted; worktree is clean.

Evidence: Auto-seat-climate end-to-end demo (payloads + old-vs-new behavior)

### Caller: remote_auto_seat_climate_request(1, True) [Fleet REST] POST body -> {'auto_seat_position': <AutoSeat.FRONT_LEFT: 1>, 'auto_climate_on': True} [Signed/proto] seat_position -> 1 (AutoSeatPosition_FrontLeft) ### Caller: remote_auto_seat_climate_request(2, True) [Fleet REST] POST body -> {'auto_seat_position': <AutoSeat.FRONT_RIGHT: 2>, 'auto_climate_on': True} [Signed/proto] seat_position -> 2 (AutoSeatPosition_FrontRight) ### Regression: OLD signed-path bug OLD with AutoSeat.FRONT_RIGHT(=2): IndexError: tuple index out of range OLD with AutoSeat.FRONT_LEFT(=1): emitted AutoSeatPosition_FrontRight (wrong seat!) FIX confirmed: new code emits the correct seat for both, no IndexError.

"""End-to-end demo of the issue #11 fix: auto-seat-climate uses 1-indexed AutoSeat
consistently on BOTH the Fleet REST backend and the signed/protobuf backend.

Shows the actual command payloads an end user's call produces, and reproduces the
OLD broken signed-path behavior for contrast.
"""
import asyncio
from unittest.mock import AsyncMock, MagicMock
from cryptography.hazmat.primitives.asymmetric import ec

from tesla_fleet_api.const import AutoSeat
from tesla_fleet_api.tesla.vehicle.fleet import VehicleFleet
from tesla_fleet_api.tesla.vehicle.commands import Commands
from tesla_fleet_api.tesla.vehicle.proto.car_server_pb2 import Action, AutoSeatClimateAction

VIN = "5YJXCAE43LF123456"
PROTO_NAME = {1: "AutoSeatPosition_FrontLeft", 2: "AutoSeatPosition_FrontRight"}


class _C(Commands):
    _auth_method = "hmac"
    async def _send(self, msg, requires):
        raise NotImplementedError


def fleet():
    parent = MagicMock()
    parent._request = AsyncMock(return_value={"response": {"result": True}})
    return VehicleFleet(parent, VIN), parent._request


def signed():
    parent = MagicMock()
    parent.private_key = ec.generate_private_key(ec.SECP256R1())
    v = _C(parent, VIN)
    v._sendInfotainment = AsyncMock(return_value={"response": {"result": True}})
    return v, v._sendInfotainment


async def main():
    print("=" * 68)
    print("AutoSeat enum (1-indexed, matches Tesla wire + proto):")
    print(f"  AutoSeat.FRONT_LEFT  = {int(AutoSeat.FRONT_LEFT)}")
    print(f"  AutoSeat.FRONT_RIGHT = {int(AutoSeat.FRONT_RIGHT)}")
    print("=" * 68)

    for seat in (AutoSeat.FRONT_LEFT, AutoSeat.FRONT_RIGHT):
        print(f"\n### Caller: remote_auto_seat_climate_request({seat!s}, True)")

        v, req = fleet()
        await v.remote_auto_seat_climate_request(seat, True)
        body = req.await_args.kwargs["json"]
        print(f"  [Fleet REST]  POST body -> {body}")
        assert body["auto_seat_position"] == int(seat)

        v, send = signed()
        await v.remote_auto_seat_climate_request(seat, True)
        action: Action = send.await_args.args[0]
        pos = action.vehicleAction.autoSeatClimateAction.carseat[0].seat_position
        print(f"  [Signed/proto] seat_position -> {pos} ({PROTO_NAME[pos]})")
        assert pos == int(seat)
        assert AutoSeatClimateAction.AutoSeatPosition_E.Name(pos) == PROTO_NAME[pos]

    print("\n" + "=" * 68)
    print("Both backends agree: FRONT_LEFT->1->FrontLeft, FRONT_RIGHT->2->FrontRight")
    print("=" * 68)

    print("\n### Regression: reproduce the OLD signed-path bug (pre-fix)")
    print("Old code did: AutoSeatClimatePositions[auto_seat_position]  (0-indexed tuple)")
    OLD = (
        AutoSeatClimateAction.AutoSeatPosition_FrontLeft,   # index 0
        AutoSeatClimateAction.AutoSeatPosition_FrontRight,  # index 1
    )
    try:
        _ = OLD[int(AutoSeat.FRONT_RIGHT)]  # index 2 -> boom
        print("  UNEXPECTED: no error")
    except IndexError as e:
        print(f"  OLD behavior with AutoSeat.FRONT_RIGHT(=2): IndexError: {e}")
    old_fl = OLD[int(AutoSeat.FRONT_LEFT)]  # index 1 -> WRONG seat (FrontRight)
    print(f"  OLD behavior with AutoSeat.FRONT_LEFT(=1): emitted "
          f"{AutoSeatClimateAction.AutoSeatPosition_E.Name(old_fl)} (wrong seat!)")
    print("\nFIX confirmed: new code emits the correct seat for both, no IndexError.")


asyncio.run(main())

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ tesla_fleet_api/tesla/vehicle/commands.py:1012 - The signed path previously validated the seat index implicitly: AutoSeatClimatePositions[auto_seat_position] raised IndexError for out-of-range values. The new cast(...) forwards any int verbatim to the proto enum, so an out-of-range value (e.g. old 0-indexed callers passing 0) now silently serializes as AutoSeatPosition_Unknown (proto value 0) instead of failing fast. This matches the REST path (which also never validated) and is consistent with the deliberate 1-indexed standardization, so it's an acceptable tradeoff — noted only for awareness.
✅ **Test** - passed

✅ No issues found.

  • uv run pytest tests/test_auto_seat_climate.py -v — 4 regression tests (Fleet JSON carries auto_seat_position=1/2; signed builder emits AutoSeatPosition_FrontLeft/FrontRight for same 1-indexed input)
  • uv run pytest tests -q — full suite, 41 passed + 8 subtests
  • uv run python demo_auto_seat_climate.py — end-to-end demo printing the Fleet REST POST body and signed proto seat_position for FRONT_LEFT/FRONT_RIGHT, plus reproduction of the old 0-indexed-tuple bug (IndexError at index 2, wrong-seat at index 1)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

The auto-seat-climate command is 1-indexed on Tesla's wire (FrontLeft=1,
FrontRight=2), but the two backends disagreed with each other and with the
0-indexed `Seat` enum, so `Seat.FRONT_LEFT`=0 was rejected and
`Seat.FRONT_RIGHT`=1 controlled the front-left seat.

Both backends now standardize on the 1-indexed `AutoSeat` enum (FL=1, FR=2),
matching the REST wire values and the proto `AutoSeatPosition_*` enum:

- fleet.py: type hint `int | Seat` -> `int | AutoSeat` (behavior unchanged;
  the Fleet REST path already forwarded the value verbatim to the 1-indexed
  endpoint).
- commands.py: the signed/protobuf path now maps the 1-indexed int directly
  onto the proto enum instead of indexing a 0-based tuple, so
  `AutoSeat.FRONT_RIGHT`=2 no longer raises IndexError. Type hint updated to
  `int | AutoSeat`.

`AutoSeat` sits alongside `Seat` in const.py, so consumers can import it from
the same module.

BEHAVIOR CHANGE (signed path): the expected `auto_seat_position` index moves
from 0-based to 1-based. A direct caller of the signed
`remote_auto_seat_climate_request` must now pass 1-indexed values.

Regression tests cover both backends for front-left and front-right. The
manual seat-heater/cooler paths and the 0-indexed `Seat` enum are unchanged.

Downstream follow-up (out of scope here): the Home Assistant `tesla_fleet`
integration must pass 1-indexed `AutoSeat` values to fully close the reported
end-user symptom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Bre77 Bre77 merged commit a9b5306 into main Jul 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant