Skip to content
Open
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,18 @@ Get vehicle data:
Get vehicle data with automatic wakeup:
`http://localhost:8080/api/1/vehicles/{VIN}/vehicle_data?wakeup=true`

Currently you will receive the following data:
By default you will receive the following data:

- charge_state
- climate_state

If you want to receive specific data, you can add the endpoints to the request. For example:
If you want to receive specific data, you can add the endpoints to the request. This also lets you request additional endpoints that are not part of the default response, such as `drive_state` (which includes the `odometer` field in miles, delivered over BLE in the drive state). For example:

`http://localhost:8080/api/1/vehicles/{VIN}/vehicle_data?endpoints=charge_state`

Request the drive state including the odometer:
`http://localhost:8080/api/1/vehicles/{VIN}/vehicle_data?endpoints=drive_state`

Get specific data with automatic wakeup:
`http://localhost:8080/api/1/vehicles/{VIN}/vehicle_data?endpoints=charge_state&wakeup=true`

Expand Down
12 changes: 12 additions & 0 deletions internal/api/models/states.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ type ChargeState struct {
MinutesToFullCharge int32 `json:"minutes_to_full_charge"` //
}

// DriveState contains the current drive states available from the vehicle.
type DriveState struct {
Timestamp int64 `json:"timestamp"` //
ShiftState string `json:"shift_state"` //
Speed float32 `json:"speed"` //
Power int32 `json:"power"` //
// Odometer is expressed in miles. The Tesla Fleet API historically returns
// the odometer in vehicle_state, but the BLE protocol delivers it in
// DriveState, so it is exposed here.
Odometer float64 `json:"odometer"` //
}

// ClimateState contains the current climate states available from the vehicle.
type ClimateState struct {
Timestamp int64 `json:"timestamp"` //
Expand Down
48 changes: 48 additions & 0 deletions internal/api/models/statesConverter.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,54 @@ MISSING
OffPeakChargingEnabled bool `json:"off_peak_charging_enabled"`
*/

// shiftStateFromBle converts the DriveState shift_state oneof into the string
// representation used by the Tesla Fleet API ("P", "R", "N", "D"). An unset or
// invalid shift state is returned as an empty string.
func shiftStateFromBle(shiftState *carserver.ShiftState) string {
switch shiftState.GetType().(type) {
case *carserver.ShiftState_P:
return "P"
case *carserver.ShiftState_R:
return "R"
case *carserver.ShiftState_N:
return "N"
case *carserver.ShiftState_D:
return "D"
case *carserver.ShiftState_SNA:
// SNA = signal not available; treat it like an unset shift state.
return ""
default:
return ""
}
}

// speedFromBle returns the vehicle speed, preferring the newer speed_float
// field. When speed_float is not present it falls back to the older integer
// speed field. Presence is detected via the optional oneof accessors, since a
// value of 0 cannot otherwise be distinguished from an unset field.
func speedFromBle(driveState *carserver.DriveState) float32 {
if driveState.GetOptionalSpeedFloat() != nil {
return driveState.GetSpeedFloat()
}
if driveState.GetOptionalSpeed() != nil {
return float32(driveState.GetSpeed())
}
return 0
}

func DriveStateFromBle(VehicleData *carserver.VehicleData) DriveState {
return DriveState{
Timestamp: VehicleData.DriveState.GetTimestamp().AsTime().Unix(),
ShiftState: shiftStateFromBle(VehicleData.DriveState.GetShiftState()),
Speed: speedFromBle(VehicleData.DriveState),
Power: VehicleData.DriveState.GetPower(),
// The Fleet API historically exposes the odometer in vehicle_state, but
// the BLE protocol delivers it in DriveState as hundredths of a mile, so
// convert it to miles and expose it here.
Odometer: float64(VehicleData.DriveState.GetOdometerInHundredthsOfAMile()) / 100.0,
}
}

func ClimateStateFromBle(VehicleData *carserver.VehicleData) ClimateState {
return ClimateState{
Timestamp: VehicleData.ClimateState.GetTimestamp().AsTime().Unix(),
Expand Down
101 changes: 101 additions & 0 deletions internal/api/models/statesConverter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package models

import (
"testing"

"github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/carserver"
)

func TestDriveStateFromBleOdometer(t *testing.T) {
vehicleData := &carserver.VehicleData{
DriveState: &carserver.DriveState{
OptionalOdometerInHundredthsOfAMile: &carserver.DriveState_OdometerInHundredthsOfAMile{
OdometerInHundredthsOfAMile: 1234567,
},
OptionalPower: &carserver.DriveState_Power{
Power: 42,
},
OptionalSpeedFloat: &carserver.DriveState_SpeedFloat{
SpeedFloat: 12.5,
},
ShiftState: &carserver.ShiftState{
Type: &carserver.ShiftState_D{D: &carserver.Void{}},
},
},
}

got := DriveStateFromBle(vehicleData)

if got.Odometer != 12345.67 {
t.Errorf("Odometer = %v, want %v", got.Odometer, 12345.67)
}
if got.Power != 42 {
t.Errorf("Power = %v, want %v", got.Power, 42)
}
if got.Speed != 12.5 {
t.Errorf("Speed = %v, want %v", got.Speed, 12.5)
}
if got.ShiftState != "D" {
t.Errorf("ShiftState = %q, want %q", got.ShiftState, "D")
}
}

func TestShiftStateFromBle(t *testing.T) {
tests := []struct {
name string
input *carserver.ShiftState
want string
}{
{"nil", nil, ""},
{"unset", &carserver.ShiftState{}, ""},
{"invalid", &carserver.ShiftState{Type: &carserver.ShiftState_Invalid{Invalid: &carserver.Void{}}}, ""},
{"park", &carserver.ShiftState{Type: &carserver.ShiftState_P{P: &carserver.Void{}}}, "P"},
{"reverse", &carserver.ShiftState{Type: &carserver.ShiftState_R{R: &carserver.Void{}}}, "R"},
{"neutral", &carserver.ShiftState{Type: &carserver.ShiftState_N{N: &carserver.Void{}}}, "N"},
{"drive", &carserver.ShiftState{Type: &carserver.ShiftState_D{D: &carserver.Void{}}}, "D"},
{"sna", &carserver.ShiftState{Type: &carserver.ShiftState_SNA{SNA: &carserver.Void{}}}, ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := shiftStateFromBle(tt.input); got != tt.want {
t.Errorf("shiftStateFromBle(%s) = %q, want %q", tt.name, got, tt.want)
}
})
}
}

func TestDriveStateFromBleSpeedFallback(t *testing.T) {
// When speed_float is not present, the conversion must fall back to the older
// integer speed field.
vehicleData := &carserver.VehicleData{
DriveState: &carserver.DriveState{
OptionalSpeed: &carserver.DriveState_Speed{
Speed: 37,
},
},
}

got := DriveStateFromBle(vehicleData)

if got.Speed != 37 {
t.Errorf("Speed = %v, want %v", got.Speed, 37)
}
}

func TestDriveStateFromBleMissingOptionalOdometer(t *testing.T) {
// When the optional odometer is not present, the conversion must default to 0
// instead of panicking or returning an unexpected value.
vehicleData := &carserver.VehicleData{
DriveState: &carserver.DriveState{},
}

got := DriveStateFromBle(vehicleData)

if got.Odometer != 0 {
t.Errorf("Odometer = %v, want %v", got.Odometer, 0)
}
if got.ShiftState != "" {
t.Errorf("ShiftState = %q, want empty string", got.ShiftState)
}
}
1 change: 1 addition & 0 deletions internal/tesla/commands/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var categoriesByName = map[string]vehicle.StateCategory{
"charge_state": vehicle.StateCategoryCharge,
"climate_state": vehicle.StateCategoryClimate,
"drive": vehicle.StateCategoryDrive,
"drive_state": vehicle.StateCategoryDrive,
"closures_state": vehicle.StateCategoryClosures,
"charge-schedule": vehicle.StateCategoryChargeSchedule,
"precondition-schedule": vehicle.StateCategoryPreconditioningSchedule,
Expand Down
4 changes: 3 additions & 1 deletion internal/tesla/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
)

var ExceptedCommands = []string{"vehicle_data", "auto_conditioning_start", "auto_conditioning_stop", "charge_port_door_open", "charge_port_door_close", "flash_lights", "wake_up", "set_charging_amps", "set_charge_limit", "charge_start", "charge_stop", "session_info", "honk_horn", "door_lock", "door_unlock", "set_sentry_mode"}
var ExceptedEndpoints = []string{"charge_state", "climate_state"}
var ExceptedEndpoints = []string{"charge_state", "climate_state", "drive_state"}

func (command *Command) Send(ctx context.Context, car *vehicle.Vehicle) (shouldRetry bool, err error) {
switch command.Command {
Expand Down Expand Up @@ -240,6 +240,8 @@ func (command *Command) Send(ctx context.Context, car *vehicle.Vehicle) (shouldR
converted = models.ChargeStateFromBle(data)
case "climate_state":
converted = models.ClimateStateFromBle(data)
case "drive_state":
converted = models.DriveStateFromBle(data)
}
d, err := json.Marshal(converted)
if err != nil {
Expand Down