diff --git a/tesla_fleet_api/tesla/vehicle/fleet.py b/tesla_fleet_api/tesla/vehicle/fleet.py index 98e3dcd..04deeac 100644 --- a/tesla_fleet_api/tesla/vehicle/fleet.py +++ b/tesla_fleet_api/tesla/vehicle/fleet.py @@ -239,6 +239,14 @@ async def navigation_sc_request( json={"id": id, "order": order}, ) + async def navigation_waypoints_request(self, waypoints: str) -> dict[str, Any]: + """Sends a list of waypoints to the vehicle's navigation system.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/command/navigation_waypoints_request", + json={"waypoints": waypoints}, + ) + async def remote_auto_seat_climate_request( self, auto_seat_position: int | AutoSeat, diff --git a/tesla_fleet_api/teslemetry/vehicle.py b/tesla_fleet_api/teslemetry/vehicle.py index 12359ad..5250e08 100644 --- a/tesla_fleet_api/teslemetry/vehicle.py +++ b/tesla_fleet_api/teslemetry/vehicle.py @@ -4,9 +4,14 @@ from tesla_fleet_api.const import ( BluetoothConfirmation, + DistanceUnit, + EnergyDisplayFormat, Method, ClosureState, SeatHeaterLevel, + TemperatureUnit, + TimeDisplayFormat, + TirePressureUnit, ) from tesla_fleet_api.tesla.vehicle.vehicles import Vehicles from tesla_fleet_api.tesla.vehicle.fleet import VehicleFleet @@ -287,6 +292,357 @@ async def remove_key(self) -> dict[str, Any]: f"api/1/vehicles/{self.vin}/custom_command/remove_key", ) + async def get_charge_on_solar(self) -> dict[str, Any]: + """Gets the current charge-on-solar feature settings.""" + return await self._request( + Method.GET, + f"api/1/vehicles/{self.vin}/custom_command/charge_on_solar", + ) + + # Below: custom_command routes with no public OpenAPI schema. Body key + # names are inferred from tesla-protocol field names and the confirmed + # set_keep_accessory_power_mode {on: bool} shape, not independently + # verified against Tesla/Teslemetry documentation. + + async def set_front_zone_lights(self, level: int) -> dict[str, Any]: + """Sets front zone light level (0=off, 1=low, 2=med, 3=high). + + Named to match the BLE ``Commands.set_front_zone_lights`` sibling so + ``VehicleRouter`` failover can find this method by attribute name. + """ + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/front_zone_light", + json={"level": level}, + ) + + async def set_rear_zone_lights(self, level: int) -> dict[str, Any]: + """Sets rear zone light level (0=off, 1=low, 2=med, 3=high). + + Named to match the BLE ``Commands.set_rear_zone_lights`` sibling so + ``VehicleRouter`` failover can find this method by attribute name. + """ + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/rear_zone_light", + json={"level": level}, + ) + + async def set_outlets(self, request: int) -> dict[str, Any]: + """Sets outlets on/off (0=off, 1=cabin+bed, 2=cabin).""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_outlets", + json={"request": request}, + ) + + async def set_outlet_soc_limit(self, percent: int) -> dict[str, Any]: + """Sets the outlet SOC limit percentage.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_outlet_soc_limit", + json={"percent": percent}, + ) + + async def set_power_feed(self, request: int) -> dict[str, Any]: + """Sets power feed on/off (0=off, 1=feed1, 2=feed2, 3=both).""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_power_feed", + json={"request": request}, + ) + + async def set_power_feed_soc_limit(self, percent: int) -> dict[str, Any]: + """Sets the power feed SOC limit percentage.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_power_feed_soc_limit", + json={"percent": percent}, + ) + + async def set_lightbar_brightness(self, brightness: int) -> dict[str, Any]: + """Sets the lightbar brightness.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_lightbar_brightness", + json={"brightness": brightness}, + ) + + async def set_lightbar_middle(self, on: bool) -> dict[str, Any]: + """Enables or disables the lightbar middle light.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_lightbar_middle", + json={"on": on}, + ) + + async def set_lightbar_ditch(self, on: bool) -> dict[str, Any]: + """Enables or disables the ditch lights.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_lightbar_ditch", + json={"on": on}, + ) + + async def set_trailer_light_test(self, on: bool) -> dict[str, Any]: + """Starts or stops the trailer light test.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_trailer_light_test", + json={"on": on}, + ) + + async def set_truck_bed_light_auto(self, on: bool) -> dict[str, Any]: + """Sets truck bed light auto state.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_truck_bed_light_auto", + json={"on": on}, + ) + + async def set_truck_bed_light_brightness(self, brightness: int) -> dict[str, Any]: + """Sets truck bed light brightness.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_truck_bed_light_brightness", + json={"brightness": brightness}, + ) + + async def set_powershare_feature(self, on: bool) -> dict[str, Any]: + """Enables or disables the Powershare feature.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_powershare_feature", + json={"on": on}, + ) + + async def set_powershare_request(self, on: bool) -> dict[str, Any]: + """Enables or disables an active Powershare session.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_powershare_request", + json={"on": on}, + ) + + async def set_powershare_discharge_limit(self, percent: int) -> dict[str, Any]: + """Sets the Powershare discharge limit percentage.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_powershare_discharge_limit", + json={"percent": percent}, + ) + + async def set_recirculation(self, on: bool) -> dict[str, Any]: + """Sets HVAC recirculation mode on/off. + + Named to match the BLE ``Commands.set_recirculation`` sibling so + ``VehicleRouter`` failover can find this method by attribute name. + """ + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/hvac_recirculation", + json={"on": on}, + ) + + async def set_tent_mode(self, on: bool) -> dict[str, Any]: + """Enables or disables tent mode.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_tent_mode", + json={"on": on}, + ) + + async def set_suspension_level(self, level: int) -> dict[str, Any]: + """Sets the vehicle suspension level (1=entry, 2=low, 3=medium, 4=high, 5=very_high, 6=extract).""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_suspension_level", + json={"level": level}, + ) + + async def set_low_power_mode(self, on: bool) -> dict[str, Any]: + """Turns Low Power mode on and off, reducing standby power consumption while the vehicle is parked.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_low_power_mode", + json={"on": on}, + ) + + async def set_keep_accessory_power_mode(self, on: bool) -> dict[str, Any]: + """Turns Keep Accessory Power mode on and off, keeping 12V accessory power available while the vehicle is parked.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_keep_accessory_power_mode", + json={"on": on}, + ) + + async def set_temperature_unit(self, unit: TemperatureUnit | int) -> dict[str, Any]: + """Sets the vehicle's displayed temperature unit.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_temperature_unit", + json={"unit": unit}, + ) + + async def set_distance_unit(self, unit: DistanceUnit | int) -> dict[str, Any]: + """Sets the vehicle's displayed distance unit.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_distance_unit", + json={"unit": unit}, + ) + + async def set_time_display_format( + self, format: TimeDisplayFormat | int + ) -> dict[str, Any]: + """Sets the vehicle's displayed clock format (12h/24h).""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_time_display_format", + json={"format": format}, + ) + + async def set_tire_pressure_unit( + self, unit: TirePressureUnit | int + ) -> dict[str, Any]: + """Sets the vehicle's displayed tire pressure unit.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_tire_pressure_unit", + json={"unit": unit}, + ) + + async def set_energy_display_format( + self, format: EnergyDisplayFormat | int + ) -> dict[str, Any]: + """Sets the vehicle's displayed energy/range unit (percentage/distance).""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_energy_display_format", + json={"format": format}, + ) + + async def parental_controls(self, activate: bool, pin: str) -> dict[str, Any]: + """Activates or deactivates parental controls with a PIN.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/parental_controls", + json={"activate": activate, "pin": pin}, + ) + + async def parental_controls_clear_pin(self, pin: str) -> dict[str, Any]: + """Clears the parental controls PIN.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/parental_controls_clear_pin", + json={"pin": pin}, + ) + + async def parental_controls_clear_pin_admin(self) -> dict[str, Any]: + """Clears the parental controls PIN as admin (fleet manager/owner).""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/parental_controls_clear_pin_admin", + ) + + async def parental_controls_enable_setting( + self, setting: int, enable: bool + ) -> dict[str, Any]: + """Enables or disables a parental controls setting (1=speed_limit, 2=acceleration, 3=safety_features, 4=curfew).""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/parental_controls_enable_setting", + json={"setting": setting, "enable": enable}, + ) + + async def parental_controls_set_speed_limit( + self, limit_mph: float + ) -> dict[str, Any]: + """Sets the parental controls speed limit.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/parental_controls_set_speed_limit", + json={"limit_mph": limit_mph}, + ) + + async def navigation_gps_destination_request( + self, lat: float, lon: float, destination: str, order: int + ) -> dict[str, Any]: + """Navigates to coordinates with a named destination string. + + ``order`` is the Tesla remote-nav order integer: 1 replaces the + trip, 2 prepends a stop, and 3 appends a stop. Named to match the + BLE ``Commands.navigation_gps_destination_request`` sibling so + ``VehicleRouter`` failover can find this method by attribute name. + """ + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/navigation_gps_destination", + json={ + "lat": lat, + "lon": lon, + "destination": destination, + "order": order, + }, + ) + + async def auto_secure_vehicle(self) -> dict[str, Any]: + """Auto-secures the vehicle (locks, closes windows, etc.).""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/auto_secure_vehicle", + ) + + async def cancel_soh_test(self) -> dict[str, Any]: + """Cancels a State of Health test.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/cancel_soh_test", + ) + + async def get_nearby_charging_sites( + self, + count: int | None = None, + radius: int | None = None, + detail: bool | None = None, + ) -> dict[str, Any]: + """Returns the charging sites near the current location of the vehicle.""" + data: dict[str, Any] = {} + if count is not None: + data["count"] = count + if radius is not None: + data["radius"] = radius + if detail is not None: + data["detail"] = detail + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/get_nearby_charging_sites", + json=data, + ) + + async def get_rate_tariff(self) -> dict[str, Any]: + """Gets the current time-of-use rate tariff schedule.""" + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/get_rate_tariff", + ) + + async def trigger_rate_tariff_update(self) -> dict[str, Any]: + """Triggers a time-of-use rate tariff update with no schedule payload. + + Teslemetry's backend does not forward a schedule to the vehicle for + this route today, so it functions as a trigger, not a configurable + setter, until a real schema is published. Deliberately not named + ``set_rate_tariff`` - the BLE ``Commands.set_rate_tariff`` sibling + takes a required ``seasons``/``tariff`` schedule, and a same-named, + payload-free method here would raise ``TypeError`` if ``VehicleRouter`` + failover forwarded those arguments to it. + """ + return await self._request( + Method.POST, + f"api/1/vehicles/{self.vin}/custom_command/set_rate_tariff", + ) + class TeslemetryVehicles(Vehicles["Teslemetry"]): """Class containing and creating vehicles.""" diff --git a/tests/test_cross_transport_parity.py b/tests/test_cross_transport_parity.py index 349df69..e7c4860 100644 --- a/tests/test_cross_transport_parity.py +++ b/tests/test_cross_transport_parity.py @@ -235,3 +235,26 @@ async def test_note_survives_on_both_transports(self) -> None: await ble.take_drivenote("brake noise up front") action = _sent_vehicle_action(ble, send) self.assertEqual(action.takeDrivenoteAction.note, "brake noise up front") + + +class NavigationWaypointsRequestParityTests(MockedBleTransportTestCase): + """``navigation_waypoints_request`` must carry the same ``waypoints`` + string on both the new REST cloud path and its existing signed-command + BLE sibling.""" + + async def test_waypoints_survive_on_both_transports(self) -> None: + cloud, request = _make_fleet_vehicle(self.VIN) + await cloud.navigation_waypoints_request("some-waypoints-payload") + assert request.await_args is not None + self.assertEqual( + request.await_args.kwargs["json"], + {"waypoints": "some-waypoints-payload"}, + ) + + ble, send = self.make_vehicle() + send.return_value = infotainment_action_ok_reply() + await ble.navigation_waypoints_request("some-waypoints-payload") + action = _sent_vehicle_action(ble, send) + self.assertEqual( + action.navigationWaypointsRequest.waypoints, "some-waypoints-payload" + ) diff --git a/tests/test_teslemetry_vehicle_custom_commands.py b/tests/test_teslemetry_vehicle_custom_commands.py new file mode 100644 index 0000000..4d42fa3 --- /dev/null +++ b/tests/test_teslemetry_vehicle_custom_commands.py @@ -0,0 +1,377 @@ +"""Tests for the ``TeslemetryVehicle`` ``custom_command/*`` methods added for +command parity with Teslemetry's source-verified route inventory. + +These routes have no public OpenAPI schema, so each test only pins the +request shape (method, path, JSON body) this library sends - not a live +response contract. +""" + +from __future__ import annotations + +from unittest import IsolatedAsyncioTestCase +from unittest.mock import AsyncMock, MagicMock + +from tesla_fleet_api.const import ( + DistanceUnit, + EnergyDisplayFormat, + Method, + TemperatureUnit, + TimeDisplayFormat, + TirePressureUnit, +) +from tesla_fleet_api.teslemetry.vehicle import TeslemetryVehicle + +VIN = "5YJ3E1EA1JF000001" + + +def _make_vehicle() -> tuple[TeslemetryVehicle, AsyncMock]: + parent = MagicMock() + request = AsyncMock(return_value={"response": {"result": True}}) + parent._request = request # pyright: ignore[reportAttributeAccessIssue] + return TeslemetryVehicle(parent, VIN), request + + +class CustomCommandRequestShapeTests(IsolatedAsyncioTestCase): + """Each ``custom_command/*`` method sends the expected method/path/body.""" + + async def test_get_charge_on_solar(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.get_charge_on_solar() + request.assert_awaited_once_with( + Method.GET, f"api/1/vehicles/{VIN}/custom_command/charge_on_solar" + ) + + async def test_set_front_zone_lights(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_front_zone_lights(2) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/front_zone_light", + json={"level": 2}, + ) + + async def test_set_rear_zone_lights(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_rear_zone_lights(1) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/rear_zone_light", + json={"level": 1}, + ) + + async def test_set_outlets(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_outlets(1) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_outlets", + json={"request": 1}, + ) + + async def test_set_outlet_soc_limit(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_outlet_soc_limit(80) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_outlet_soc_limit", + json={"percent": 80}, + ) + + async def test_set_power_feed(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_power_feed(3) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_power_feed", + json={"request": 3}, + ) + + async def test_set_power_feed_soc_limit(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_power_feed_soc_limit(90) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_power_feed_soc_limit", + json={"percent": 90}, + ) + + async def test_set_lightbar_brightness(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_lightbar_brightness(50) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_lightbar_brightness", + json={"brightness": 50}, + ) + + async def test_set_lightbar_middle(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_lightbar_middle(True) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_lightbar_middle", + json={"on": True}, + ) + + async def test_set_lightbar_ditch(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_lightbar_ditch(False) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_lightbar_ditch", + json={"on": False}, + ) + + async def test_set_trailer_light_test(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_trailer_light_test(True) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_trailer_light_test", + json={"on": True}, + ) + + async def test_set_truck_bed_light_auto(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_truck_bed_light_auto(True) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_truck_bed_light_auto", + json={"on": True}, + ) + + async def test_set_truck_bed_light_brightness(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_truck_bed_light_brightness(75) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_truck_bed_light_brightness", + json={"brightness": 75}, + ) + + async def test_set_powershare_feature(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_powershare_feature(True) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_powershare_feature", + json={"on": True}, + ) + + async def test_set_powershare_request(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_powershare_request(True) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_powershare_request", + json={"on": True}, + ) + + async def test_set_powershare_discharge_limit(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_powershare_discharge_limit(20) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_powershare_discharge_limit", + json={"percent": 20}, + ) + + async def test_set_recirculation(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_recirculation(True) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/hvac_recirculation", + json={"on": True}, + ) + + async def test_set_tent_mode(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_tent_mode(True) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_tent_mode", + json={"on": True}, + ) + + async def test_set_suspension_level(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_suspension_level(4) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_suspension_level", + json={"level": 4}, + ) + + async def test_set_low_power_mode(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_low_power_mode(True) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_low_power_mode", + json={"on": True}, + ) + + async def test_set_keep_accessory_power_mode(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_keep_accessory_power_mode(True) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_keep_accessory_power_mode", + json={"on": True}, + ) + + async def test_set_temperature_unit(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_temperature_unit(TemperatureUnit.CELSIUS) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_temperature_unit", + json={"unit": TemperatureUnit.CELSIUS}, + ) + + async def test_set_distance_unit(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_distance_unit(DistanceUnit.KILOMETERS) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_distance_unit", + json={"unit": DistanceUnit.KILOMETERS}, + ) + + async def test_set_time_display_format(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_time_display_format(TimeDisplayFormat.HOUR_24) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_time_display_format", + json={"format": TimeDisplayFormat.HOUR_24}, + ) + + async def test_set_tire_pressure_unit(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_tire_pressure_unit(TirePressureUnit.BAR) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_tire_pressure_unit", + json={"unit": TirePressureUnit.BAR}, + ) + + async def test_set_energy_display_format(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.set_energy_display_format(EnergyDisplayFormat.DISTANCE) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_energy_display_format", + json={"format": EnergyDisplayFormat.DISTANCE}, + ) + + async def test_parental_controls(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.parental_controls(True, "1234") + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/parental_controls", + json={"activate": True, "pin": "1234"}, + ) + + async def test_parental_controls_clear_pin(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.parental_controls_clear_pin("1234") + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/parental_controls_clear_pin", + json={"pin": "1234"}, + ) + + async def test_parental_controls_clear_pin_admin(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.parental_controls_clear_pin_admin() + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/parental_controls_clear_pin_admin", + ) + + async def test_parental_controls_enable_setting(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.parental_controls_enable_setting(1, True) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/parental_controls_enable_setting", + json={"setting": 1, "enable": True}, + ) + + async def test_parental_controls_set_speed_limit(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.parental_controls_set_speed_limit(65.0) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/parental_controls_set_speed_limit", + json={"limit_mph": 65.0}, + ) + + async def test_navigation_gps_destination_request(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.navigation_gps_destination_request( + 37.3230, -122.0322, "Tesla HQ", 1 + ) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/navigation_gps_destination", + json={ + "lat": 37.3230, + "lon": -122.0322, + "destination": "Tesla HQ", + "order": 1, + }, + ) + + async def test_auto_secure_vehicle(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.auto_secure_vehicle() + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/auto_secure_vehicle", + ) + + async def test_cancel_soh_test(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.cancel_soh_test() + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/cancel_soh_test", + ) + + async def test_get_nearby_charging_sites_omits_unset_fields(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.get_nearby_charging_sites() + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/get_nearby_charging_sites", + json={}, + ) + + async def test_get_nearby_charging_sites_with_params(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.get_nearby_charging_sites(count=5, radius=25, detail=True) + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/get_nearby_charging_sites", + json={"count": 5, "radius": 25, "detail": True}, + ) + + async def test_get_rate_tariff(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.get_rate_tariff() + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/get_rate_tariff", + ) + + async def test_trigger_rate_tariff_update(self) -> None: + vehicle, request = _make_vehicle() + await vehicle.trigger_rate_tariff_update() + request.assert_awaited_once_with( + Method.POST, + f"api/1/vehicles/{VIN}/custom_command/set_rate_tariff", + ) diff --git a/uv.lock b/uv.lock index e11c317..270be5a 100644 --- a/uv.lock +++ b/uv.lock @@ -740,7 +740,7 @@ wheels = [ [[package]] name = "tesla-fleet-api" -version = "1.8.2" +version = "1.9.0" source = { editable = "." } dependencies = [ { name = "aiofiles" },