Skip to content

Commit

Permalink
Merge pull request #62 from wimaha/dev
Browse files Browse the repository at this point in the history
Exposing vehicle data from BLE to the Http Proxy
  • Loading branch information
wimaha authored Dec 12, 2024
2 parents f0d705d + d2b09a5 commit ccab981
Show file tree
Hide file tree
Showing 8 changed files with 509 additions and 35 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.DEFAULT_GOAL := default

IMAGE ?= wimaha/tesla-ble-http-proxy
VERSION := 1.2.7
VERSION := 1.3.0

export DOCKER_CLI_EXPERIMENTAL=enabled

Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ The program stores the received requests in a queue and processes them one by on
- [Generate key for vehicle](#generate-key-for-vehicle)
- [Setup EVCC](#setup-evcc)
- [API](#api)
- [Vehicle Commands](#vehicle-commands)
- [Vehicle Data](#vehicle-data)

## How to install

Expand Down Expand Up @@ -133,6 +135,8 @@ vehicles:

## API

### Vehicle Commands

The program uses the same interfaces as the Tesla [Fleet API](https://developer.tesla.com/docs/fleet-api#vehicle-commands). Currently, the following requests are supported:

- wake_up
Expand All @@ -146,7 +150,33 @@ The program uses the same interfaces as the Tesla [Fleet API](https://developer.
- charge_port_door_close
- flash_lights

#### Example Request

Start charging:
`http://localhost:8080/api/1/vehicles/{VIN}/command/charge_start`

Stop charging:
`http://localhost:8080/api/1/vehicles/{VIN}/command/charge_stop`

Set charging amps to 5A:
`http://localhost:8080/api/1/vehicles/{VIN}/command/set_charging_amps` with body `{"charging_amps": "5"}`

### Vehicle Data

The vehicle data is fetched from the vehicle and returned in the response in the same format as the [Fleet API](https://developer.tesla.com/docs/fleet-api/endpoints/vehicle-endpoints#vehicle-data). Since a ble connection has to be established to fetch the data, it takes a few seconds before the data is returned.

#### Example Request

Get vehicle data:
`http://localhost:8080/api/1/vehicles/{VIN}/vehicle_data`

Currently 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:

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

This is recommended if you want to receive data frequently, since it will reduce the time it takes to receive the data.
45 changes: 42 additions & 3 deletions control/command.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,46 @@
package control

import (
"encoding/json"
"fmt"
"strings"
"sync"

"github.com/teslamotors/vehicle-command/pkg/vehicle"
)

type ApiResponse struct {
Wait *sync.WaitGroup
Result bool
Error string
Response json.RawMessage
}

type Command struct {
Command string
Vin string
Body map[string]interface{}
Command string
Vin string
Body map[string]interface{}
Response *ApiResponse
}

// 'charge_state', 'climate_state', 'closures_state', 'drive_state', 'gui_settings', 'location_data', 'charge_schedule_data', 'preconditioning_schedule_data', 'vehicle_config', 'vehicle_state', 'vehicle_data_combo'
var categoriesByName = map[string]vehicle.StateCategory{
"charge_state": vehicle.StateCategoryCharge,
"climate_state": vehicle.StateCategoryClimate,
"drive": vehicle.StateCategoryDrive,
"closures_state": vehicle.StateCategoryClosures,
"charge-schedule": vehicle.StateCategoryChargeSchedule,
"precondition-schedule": vehicle.StateCategoryPreconditioningSchedule,
"tire-pressure": vehicle.StateCategoryTirePressure,
"media": vehicle.StateCategoryMedia,
"media-detail": vehicle.StateCategoryMediaDetail,
"software-update": vehicle.StateCategorySoftwareUpdate,
"parental-controls": vehicle.StateCategoryParentalControls,
}

func GetCategory(nameStr string) (vehicle.StateCategory, error) {
if category, ok := categoriesByName[strings.ToLower(nameStr)]; ok {
return category, nil
}
return 0, fmt.Errorf("unrecognized state category '%s'", nameStr)
}
123 changes: 107 additions & 16 deletions control/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package control

import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
Expand All @@ -14,6 +15,8 @@ import (
"github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/universalmessage"
"github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/vcsec"
"github.com/teslamotors/vehicle-command/pkg/vehicle"
"github.com/wimaha/TeslaBleHttpProxy/converter"
"google.golang.org/protobuf/encoding/protojson"
)

var PublicKeyFile = "key/public.pem"
Expand All @@ -38,7 +41,8 @@ func CloseBleControl() {
type BleControl struct {
privateKey protocol.ECDHPrivateKey

commandStack chan Command
commandStack chan Command
providerStack chan Command
}

func NewBleControl() (*BleControl, error) {
Expand All @@ -51,8 +55,9 @@ func NewBleControl() (*BleControl, error) {
log.Debug("privateKeyFile loaded")

return &BleControl{
privateKey: privateKey,
commandStack: make(chan Command, 50),
privateKey: privateKey,
commandStack: make(chan Command, 50),
providerStack: make(chan Command),
}, nil
}

Expand All @@ -64,25 +69,27 @@ func (bc *BleControl) Loop() {
retryCommand = bc.connectToVehicleAndOperateConnection(retryCommand)
} else {
// Wait for the next command
command, ok := <-bc.commandStack
if ok {
retryCommand = bc.connectToVehicleAndOperateConnection(&command)
select {
case command, ok := <-bc.providerStack:
if ok {
retryCommand = bc.connectToVehicleAndOperateConnection(&command)
}
case command, ok := <-bc.commandStack:
if ok {
retryCommand = bc.connectToVehicleAndOperateConnection(&command)
}
}
}
}
}

func (bc *BleControl) PushCommand(command string, vin string, body map[string]interface{}) {
func (bc *BleControl) PushCommand(command string, vin string, body map[string]interface{}, response *ApiResponse) {
bc.commandStack <- Command{
Command: command,
Vin: vin,
Body: body,
Command: command,
Vin: vin,
Body: body,
Response: response,
}
/*bc.commandStack.Push(Command{
Command: command,
Vin: vin,
Body: body,
})*/
}

func (bc *BleControl) connectToVehicleAndOperateConnection(firstCommand *Command) *Command {
Expand Down Expand Up @@ -114,12 +121,26 @@ func (bc *BleControl) connectToVehicleAndOperateConnection(firstCommand *Command
} else if !retry {
//Failed but no retry possible
log.Error("can't connect to vehicle", "error", err)
if firstCommand.Response != nil {
firstCommand.Response.Error = err.Error()
firstCommand.Response.Result = false
if firstCommand.Response.Wait != nil {
firstCommand.Response.Wait.Done()
}
}
return nil
} else {
lastErr = err
}
}
log.Error(fmt.Sprintf("stop retrying after %d attempts", retryCount), "error", lastErr)
if firstCommand.Response != nil {
firstCommand.Response.Error = lastErr.Error()
firstCommand.Response.Result = false
if firstCommand.Response.Wait != nil {
firstCommand.Response.Wait.Done()
}
}
return nil
}

Expand Down Expand Up @@ -215,6 +236,21 @@ func (bc *BleControl) operateConnection(car *vehicle.Vehicle, firstCommand *Comm
case <-timeout:
log.Debug("connection Timeout")
return nil
case command, ok := <-bc.providerStack:
if !ok {
return nil
}

//If new VIN, close connection
if command.Vin != firstCommand.Vin {
log.Debug("new VIN, so close connection")
return &command
}

cmd, err := bc.executeCommand(car, &command)
if err != nil {
return cmd
}
case command, ok := <-bc.commandStack:
if !ok {
return nil
Expand All @@ -234,7 +270,7 @@ func (bc *BleControl) operateConnection(car *vehicle.Vehicle, firstCommand *Comm
}
}

func (bc *BleControl) executeCommand(car *vehicle.Vehicle, command *Command) (*Command, error) {
func (bc *BleControl) executeCommand(car *vehicle.Vehicle, command *Command) (retryCommand *Command, retErr error) {
log.Info("sending", "command", command.Command, "body", command.Body)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Expand All @@ -243,6 +279,20 @@ func (bc *BleControl) executeCommand(car *vehicle.Vehicle, command *Command) (*C
var retryCount = 3
var lastErr error

defer func() {
if command.Response != nil {
if retErr != nil {
command.Response.Error = retErr.Error()
command.Response.Result = false
} else {
command.Response.Result = true
}
if command.Response.Wait != nil && retryCommand == nil {
command.Response.Wait.Done()
}
}
}()

for i := 0; i < retryCount; i++ {
if i > 0 {
log.Warn(lastErr)
Expand Down Expand Up @@ -375,6 +425,47 @@ func (bc *BleControl) sendCommand(ctx context.Context, car *vehicle.Vehicle, com
} else {
log.Info(fmt.Sprintf("Sent add-key request to %s. Confirm by tapping NFC card on center console.", car.VIN()))
}
case "vehicle_data":
var endpoints = command.Body["endpoints"].([]string)

response := make(map[string]json.RawMessage)
for _, endpoint := range endpoints {
log.Debugf("get: %s", endpoint)
category, err := GetCategory(endpoint)
if err != nil {
return false, fmt.Errorf("unrecognized state category charge")
}
data, err := car.GetState(ctx, category)
if err != nil {
return true, fmt.Errorf("failed to get vehicle data: %s", err)
}
d, err := protojson.Marshal(data)
if err != nil {
return true, fmt.Errorf("failed to marshal vehicle data: %s", err)
}

//log.Debugf("data: %s", d)

var converted interface{}
switch endpoint {
case "charge_state":
converted = converter.ChargeStateFromBle(data)
case "climate_state":
converted = converter.ClimateStateFromBle(data)
}
d, err = json.Marshal(converted)
if err != nil {
return true, fmt.Errorf("failed to marshal vehicle data: %s", err)
}

response[endpoint] = d
}

responseJson, err := json.Marshal(response)
if err != nil {
return false, fmt.Errorf("failed to marshal vehicle data: %s", err)
}
command.Response.Response = responseJson
}

// everything fine
Expand Down
Loading

0 comments on commit ccab981

Please sign in to comment.