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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ Source protobuf definitions live in `proto/`; generated Python files live in `te
- **BLE infotainment boot-delay gotcha**: `wake_up()` (VCSEC) returns as soon as the vehicle-security computer acks it, well before the infotainment computer is ready to complete a signed-command handshake. An INFO-domain read/command issued immediately after `wake_up()` can raise `BluetoothTimeout` on the handshake through no fault of the command itself. Live-verified: waiting ~10s after `wake_up()` before the first INFO read is sufficient on the test rig; callers doing INFO work right after waking should retry-with-backoff rather than treat one timeout as failure.
- **BLE `vehicle_data()` response-size cap**: the vehicle's signed-command implementation enforces its own response-size limit independent of the BLE transport's packet reassembly. Live-verified: a single-endpoint `vehicle_data()` call (or any of the dedicated per-substate readers like `charge_state()`) succeeds, but requesting as few as two `BluetoothVehicleData` endpoints together reliably raises `TeslaFleetMessageFaultResponseSizeExceedsMTU` (`exceptions.py`). This is why `vehicle_data()`'s `endpoints` arg has no all-endpoints default (unlike the cloud method) - prefer the per-substate readers, or a single-endpoint `vehicle_data()` call, over a multi-endpoint composite. Auto-chunking (split under the cap, merge replies) would fix this properly but is not implemented.
- **BLE individual-door powered-close gotcha**: `open_*_door()` unlatches a door over VCSEC; on a Model 3 there is no reliable powered close. Live-verified: `close_rear_passenger_door()` returned an OK ack (`{"result": True}`) but `closures_state().door_open_passenger_rear` stayed `True` - the ack only means the car accepted the command, not that the door physically re-latched (a human has to push it shut). Never chain an automated snapshot→act→verify→restore cycle across an individual door-open command; treat the 8 door commands as ack-verified only, or require a human to confirm the physical re-close before trusting `closures_state()` again.
- **`remote_heater_control_enabled` gate**: `climate_state().remote_heater_control_enabled` is a read-only vehicle-side setting (no command exists to flip it) that gates every "remote comfort" action - live-verified on `commands.py`: `remote_seat_heater_request`, `remote_auto_seat_climate_request`, `remote_steering_wheel_heater_request`, `remote_steering_wheel_heat_level_request`, `remote_auto_steering_wheel_heat_climate_request`. With it `false`, the vehicle ACKs `{"result": false, "reason": "cabin comfort remote settings not enabled"}` and leaves state untouched (not a library bug, not a partial mutation) - this is presumably the touchscreen/app "Remote Climate" or comfort-access toggle, outside this library's command surface. Check this field before treating a comfort-command rejection as a regression.
- **Protobuf oneof-by-string-kwargs bypasses pyright**: `remote_seat_heater_request`/`remote_seat_cooler_request` (`commands.py`) build their `HvacSeatHeaterAction`/`HvacSeatCoolerAction` via a `dict` of literal field-name strings expanded as `**kwargs` into the message constructor (with a `# pyright: ignore[reportUnknownArgumentType]` already on the call) - a typo in one of those strings (e.g. `SEAT_HEATER_MEDIUM` vs. the proto's actual `SEAT_HEATER_MED`, fixed live during PR-4) raises at call time, not at type-check time. Cross-check any new field-name string against the proto (`proto/car_server.proto`) rather than trusting pyright to catch it.

## Maintaining this file

Expand Down
15 changes: 15 additions & 0 deletions docs/bluetooth_vehicles.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ request. The infotainment computer can take longer to become ready, so
INFO-domain reads immediately after waking, such as `charge_state()` or
`vehicle_data()`, should retry `BluetoothTimeout` with backoff.

## Climate Commands

Bluetooth vehicles support the same signed climate command methods as
`VehicleSigned`, including `auto_conditioning_start()`,
`auto_conditioning_stop()`, `set_temps()`, `set_climate_keeper_mode()`,
`set_cabin_overheat_protection()`, `set_cop_temp()`,
`set_bioweapon_mode()`, `set_preconditioning_max()`,
`set_recirculation()`, the remote seat heater/cooler methods, and the
remote steering-wheel heat methods.

Remote seat and steering-wheel comfort commands can be rejected by the vehicle
with `cabin comfort remote settings not enabled` when
`climate_state().remote_heater_control_enabled` is false. That field is a
read-only vehicle setting; the library has no command to enable it.

## Open/Close Individual Doors (Bluetooth Only)

The individual door closure commands are Bluetooth-only and are not available via Fleet API signed commands.
Expand Down
14 changes: 14 additions & 0 deletions docs/fleet_api_signed_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,20 @@ async def main():
asyncio.run(main())
```

## Climate Commands

`VehicleSigned` supports signed climate command methods including
`auto_conditioning_start()`, `auto_conditioning_stop()`, `set_temps()`,
`set_climate_keeper_mode()`, `set_cabin_overheat_protection()`,
`set_cop_temp()`, `set_bioweapon_mode()`, `set_preconditioning_max()`,
`set_recirculation()`, the remote seat heater/cooler methods, and the
remote steering-wheel heat methods.

Remote seat and steering-wheel comfort commands can be rejected by the vehicle
with `cabin comfort remote settings not enabled` when
`climate_state().remote_heater_control_enabled` is false. That field is a
read-only vehicle setting; the library has no command to enable it.

## Low Power / Keep Accessory Power Modes

These are signed-only commands with no Fleet API REST equivalent. `set_low_power_mode` reduces standby power consumption while the vehicle is parked, and `set_keep_accessory_power_mode` keeps 12V accessory power available while parked. Both take a single `bool` to turn the mode on or off:
Expand Down
39 changes: 29 additions & 10 deletions tesla_fleet_api/tesla/vehicle/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,8 @@ async def remote_auto_seat_climate_request(

``auto_seat_position`` is 1-indexed (``AutoSeat.FRONT_LEFT`` == 1),
matching Tesla's wire values and the proto ``AutoSeatPosition_*`` enum.
The vehicle can reject remote comfort commands when
``climate_state().remote_heater_control_enabled`` is false.
"""
# AutoSeatPosition_FrontLeft = 1;
# AutoSeatPosition_FrontRight = 2;
Expand Down Expand Up @@ -1026,7 +1028,11 @@ async def remote_auto_seat_climate_request(
async def remote_auto_steering_wheel_heat_climate_request(
self, on: bool
) -> dict[str, Any]:
"""Sets automatic steering wheel heating on/off."""
"""Sets automatic steering wheel heating on/off.

The vehicle can reject remote comfort commands when
``climate_state().remote_heater_control_enabled`` is false.
"""
return await self._sendInfotainment(
Action(
vehicleAction=VehicleAction(
Expand Down Expand Up @@ -1084,12 +1090,16 @@ async def remote_seat_cooler_request(
async def remote_seat_heater_request(
self, seat_position: int, seat_heater_level: int
) -> dict[str, Any]:
"""Sets seat heating."""
# HvacSeatCoolerLevel_Unknown = 0;
# HvacSeatCoolerLevel_Off = 1;
# HvacSeatCoolerLevel_Low = 2;
# HvacSeatCoolerLevel_Med = 3;
# HvacSeatCoolerLevel_High = 4;
"""Sets seat heating.

The vehicle can reject remote comfort commands when
``climate_state().remote_heater_control_enabled`` is false.
"""
# Void SEAT_HEATER_UNKNOWN = 1;
# Void SEAT_HEATER_OFF = 2;
# Void SEAT_HEATER_LOW = 3;
# Void SEAT_HEATER_MED = 4;
# Void SEAT_HEATER_HIGH = 5;
# Void CAR_SEAT_UNKNOWN = 6;
# Void CAR_SEAT_FRONT_LEFT = 7;
# Void CAR_SEAT_FRONT_RIGHT = 8;
Expand Down Expand Up @@ -1129,7 +1139,7 @@ async def remote_seat_heater_request(
case 1:
heater_action_dict["SEAT_HEATER_LOW"] = Void()
case 2:
heater_action_dict["SEAT_HEATER_MEDIUM"] = Void()
heater_action_dict["SEAT_HEATER_MED"] = Void()
case 3:
heater_action_dict["SEAT_HEATER_HIGH"] = Void()
case _:
Expand All @@ -1155,7 +1165,11 @@ async def remote_start_drive(self) -> dict[str, Any]:
async def remote_steering_wheel_heat_level_request(
self, level: int
) -> dict[str, Any]:
"""Sets steering wheel heat level."""
"""Sets steering wheel heat level.

The vehicle can reject remote comfort commands when
``climate_state().remote_heater_control_enabled`` is false.
"""
return await self._sendInfotainment(
Action(
vehicleAction=VehicleAction(
Expand All @@ -1167,7 +1181,12 @@ async def remote_steering_wheel_heat_level_request(
)

async def remote_steering_wheel_heater_request(self, on: bool) -> dict[str, Any]:
"""Sets steering wheel heating on/off. For vehicles that do not support auto steering wheel heat."""
"""Sets steering wheel heating on/off.

For vehicles that do not support auto steering wheel heat. The vehicle
can reject remote comfort commands when
``climate_state().remote_heater_control_enabled`` is false.
"""
return await self._sendInfotainment(
Action(
vehicleAction=VehicleAction(
Expand Down
27 changes: 23 additions & 4 deletions tesla_fleet_api/tesla/vehicle/fleet.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ async def remote_auto_seat_climate_request(

``auto_seat_position`` is 1-indexed (``AutoSeat.FRONT_LEFT`` == 1),
matching Tesla's wire values for this endpoint.
The vehicle can reject remote comfort commands when
``climate_state().remote_heater_control_enabled`` is false.
"""
return await self._request(
Method.POST,
Expand All @@ -256,7 +258,11 @@ async def remote_auto_seat_climate_request(
async def remote_auto_steering_wheel_heat_climate_request(
self, on: bool
) -> dict[str, Any]:
"""Sets automatic steering wheel heating on/off."""
"""Sets automatic steering wheel heating on/off.

The vehicle can reject remote comfort commands when
``climate_state().remote_heater_control_enabled`` is false.
"""
return await self._request(
Method.POST,
f"api/1/vehicles/{self.vin}/command/remote_auto_steering_wheel_heat_climate_request",
Expand Down Expand Up @@ -291,7 +297,11 @@ async def remote_seat_heater_request(
seat_position: Seat | int,
seat_heater_level: Level | int,
) -> dict[str, Any]:
"""Sets seat heating."""
"""Sets seat heating.

The vehicle can reject remote comfort commands when
``climate_state().remote_heater_control_enabled`` is false.
"""
return await self._request(
Method.POST,
f"api/1/vehicles/{self.vin}/command/remote_seat_heater_request",
Expand All @@ -310,15 +320,24 @@ async def remote_start_drive(self) -> dict[str, Any]:
async def remote_steering_wheel_heat_level_request(
self, level: Level | int
) -> dict[str, Any]:
"""Sets steering wheel heat level."""
"""Sets steering wheel heat level.

The vehicle can reject remote comfort commands when
``climate_state().remote_heater_control_enabled`` is false.
"""
return await self._request(
Method.POST,
f"api/1/vehicles/{self.vin}/command/remote_steering_wheel_heat_level_request",
json={"level": level},
)

async def remote_steering_wheel_heater_request(self, on: bool) -> dict[str, Any]:
"""Sets steering wheel heating on/off. For vehicles that do not support auto steering wheel heat."""
"""Sets steering wheel heating on/off.

For vehicles that do not support auto steering wheel heat. The vehicle
can reject remote comfort commands when
``climate_state().remote_heater_control_enabled`` is false.
"""
return await self._request(
Method.POST,
f"api/1/vehicles/{self.vin}/command/remote_steering_wheel_heater_request",
Expand Down
Loading
Loading