From 1c287052274435f88148e69173af138da63149ea Mon Sep 17 00:00:00 2001 From: Wilko Date: Fri, 6 Dec 2024 16:08:00 +0100 Subject: [PATCH 1/6] Integrate possibility to query charge state. User: /api/1/vehicles/{vin}/vehicle_data --- Makefile | 2 +- control/command.go | 42 +++++++++++++++++++++++++-- control/control.go | 69 ++++++++++++++++++++++++++++++++++---------- main.go | 71 ++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 156 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index 3b1b4ec..349d802 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/control/command.go b/control/command.go index 5460aa6..6cbfe32 100644 --- a/control/command.go +++ b/control/command.go @@ -1,7 +1,43 @@ package control +import ( + "fmt" + "strings" + + "github.com/teslamotors/vehicle-command/pkg/vehicle" +) + +type ApiResponse struct { + Finished bool + Result bool + Error string + Response interface{} +} + type Command struct { - Command string - Vin string - Body map[string]interface{} + Command string + Vin string + Body map[string]interface{} + Response *ApiResponse +} + +var categoriesByName = map[string]vehicle.StateCategory{ + "charge": vehicle.StateCategoryCharge, + "climate": vehicle.StateCategoryClimate, + "drive": vehicle.StateCategoryDrive, + "closures": 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) } diff --git a/control/control.go b/control/control.go index ef51e51..0fdb1a4 100644 --- a/control/control.go +++ b/control/control.go @@ -14,6 +14,7 @@ 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" + "google.golang.org/protobuf/encoding/protojson" ) var PublicKeyFile = "key/public.pem" @@ -38,7 +39,8 @@ func CloseBleControl() { type BleControl struct { privateKey protocol.ECDHPrivateKey - commandStack chan Command + commandStack chan Command + providerStack chan Command } func NewBleControl() (*BleControl, error) { @@ -51,8 +53,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 } @@ -64,25 +67,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 { @@ -215,6 +220,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 @@ -268,6 +288,11 @@ func (bc *BleControl) executeCommand(car *vehicle.Vehicle, command *Command) (*C } } log.Error("canceled", "command", command.Command, "body", command.Body, "err", lastErr) + if command.Response != nil { + command.Response.Error = lastErr.Error() + command.Response.Result = false + command.Response.Finished = true + } return nil, lastErr } @@ -375,6 +400,20 @@ 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": + category, err := GetCategory("charge") + 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) + } + dataStr := protojson.Format(data) + command.Response.Response = dataStr + command.Response.Result = true + command.Response.Finished = true + //log.Info("vehicle data", "response", *command.Response) } // everything fine diff --git a/main.go b/main.go index 5b8de0f..7ec7a1d 100644 --- a/main.go +++ b/main.go @@ -8,9 +8,9 @@ import ( "os" "slices" "strings" + "time" "github.com/charmbracelet/log" - "github.com/teslamotors/vehicle-command/pkg/connector/ble" "github.com/wimaha/TeslaBleHttpProxy/control" "github.com/wimaha/TeslaBleHttpProxy/html" @@ -22,25 +22,26 @@ type Ret struct { } type Response struct { - Result bool `json:"result"` - Reason string `json:"reason"` - Vin string `json:"vin"` - Command string `json:"command"` + Result bool `json:"result"` + Reason string `json:"reason"` + Vin string `json:"vin"` + Command string `json:"command"` + Response *interface{} `json:"response,omitempty"` } -var exceptedCommands = []string{"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"} +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"} //go:embed static/* var static embed.FS func main() { - log.Info("TeslaBleHttpProxy 1.2.7 is loading ...") + log.Info("TeslaBleHttpProxy 1.3.0 is loading ...") envLogLevel := os.Getenv("logLevel") if envLogLevel == "debug" { log.SetLevel(log.DebugLevel) log.Debug("LogLevel set to debug") - ble.SetDebugLog() + //ble.SetDebugLog() } addr := os.Getenv("httpListenAddress") @@ -56,6 +57,7 @@ func main() { // Define the endpoints ///api/1/vehicles/{vehicle_tag}/command/set_charging_amps router.HandleFunc("/api/1/vehicles/{vin}/command/{command}", receiveCommand).Methods("POST") + router.HandleFunc("/api/1/vehicles/{vin}/vehicle_data", receiveVehicleData).Methods("GET") router.HandleFunc("/dashboard", html.ShowDashboard).Methods("GET") router.HandleFunc("/gen_keys", html.GenKeys).Methods("GET") router.HandleFunc("/remove_keys", html.RemoveKeys).Methods("GET") @@ -115,12 +117,63 @@ func receiveCommand(w http.ResponseWriter, r *http.Request) { return } - control.BleControlInstance.PushCommand(command, vin, body) + control.BleControlInstance.PushCommand(command, vin, body, nil) response.Result = true response.Reason = "The command was successfully received and will be processed shortly." } +func receiveVehicleData(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + vin := params["vin"] + command := "vehicle_data" + + var apiResponse control.ApiResponse + + control.BleControlInstance.PushCommand(command, vin, nil, &apiResponse) + + var response Response + response.Vin = vin + response.Command = command + + defer func() { + //var ret Ret + //ret.Response = response + + w.Header().Set("Content-Type", "application/json") + if response.Result { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusServiceUnavailable) + } + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Fatal("failed to send response", "error", err) + } + }() + + if control.BleControlInstance == nil { + response.Reason = "BleControl is not initialized. Maybe private.pem is missing." + response.Result = false + return + } + + for { + if apiResponse.Finished { + break + } + time.Sleep(100 * time.Millisecond) + } + + if apiResponse.Result { + response.Result = true + response.Reason = "The command was successfully processed." + response.Response = &apiResponse.Response + } else { + response.Result = false + response.Reason = apiResponse.Error + } +} + /*func pushCommand(command string, vin string, body map[string]interface{}) error { if bleControl == nil { return fmt.Errorf("BleControl is not initialized. Maybe private.pem is missing.") From b9585025a9aa7544c865e39a8bbc9e9c2c4580d1 Mon Sep 17 00:00:00 2001 From: Wilko Date: Mon, 9 Dec 2024 12:37:47 +0100 Subject: [PATCH 2/6] Fix Marshal response and Fix flatten variables like chargingState --- control/command.go | 3 ++- control/control.go | 22 ++++++++++++++++++++-- main.go | 10 +++++----- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/control/command.go b/control/command.go index 6cbfe32..bcd2e52 100644 --- a/control/command.go +++ b/control/command.go @@ -1,6 +1,7 @@ package control import ( + "encoding/json" "fmt" "strings" @@ -11,7 +12,7 @@ type ApiResponse struct { Finished bool Result bool Error string - Response interface{} + Response json.RawMessage } type Command struct { diff --git a/control/control.go b/control/control.go index 0fdb1a4..c0f04ea 100644 --- a/control/control.go +++ b/control/control.go @@ -1,9 +1,11 @@ package control import ( + "bytes" "context" "fmt" "os" + "regexp" "strconv" "strings" "time" @@ -409,8 +411,24 @@ func (bc *BleControl) sendCommand(ctx context.Context, car *vehicle.Vehicle, com if err != nil { return true, fmt.Errorf("failed to get vehicle data: %s", err) } - dataStr := protojson.Format(data) - command.Response.Response = dataStr + d, err := protojson.Marshal(data) + if err != nil { + return true, fmt.Errorf("failed to marshal vehicle data: %s", err) + } + + //Flatten the response to a single level of keys + var r = regexp.MustCompile(`":{"(?P[a-zA-Z]*)":{}}`) + match := r.FindAllSubmatch(d, -1) + + for _, sm := range match { + if len(sm) != 2 { + continue + } + tb := append(append([]byte(`":"`), sm[1]...), '"') + d = bytes.ReplaceAll(d, sm[0], tb) + } + + command.Response.Response = d command.Response.Result = true command.Response.Finished = true //log.Info("vehicle data", "response", *command.Response) diff --git a/main.go b/main.go index 7ec7a1d..f3eaa50 100644 --- a/main.go +++ b/main.go @@ -22,11 +22,11 @@ type Ret struct { } type Response struct { - Result bool `json:"result"` - Reason string `json:"reason"` - Vin string `json:"vin"` - Command string `json:"command"` - Response *interface{} `json:"response,omitempty"` + Result bool `json:"result"` + Reason string `json:"reason"` + Vin string `json:"vin"` + Command string `json:"command"` + Response *json.RawMessage `json:"response,omitempty"` } 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"} From 4ab6f619caee8673ed90ef68cda6ebf0b4b7b93a Mon Sep 17 00:00:00 2001 From: Wilko Date: Mon, 9 Dec 2024 22:05:09 +0100 Subject: [PATCH 3/6] Update vehicle_data to match http api - You can know query vor vehicle_data like with the http endpoint api --- control/command.go | 7 ++- control/control.go | 71 +++++++++++++++-------- converter/converter.go | 124 +++++++++++++++++++++++++++++++++++++++++ converter/states.go | 102 +++++++++++++++++++++++++++++++++ converter/text.txt | 39 +++++++++++++ main.go | 22 +++++--- 6 files changed, 332 insertions(+), 33 deletions(-) create mode 100644 converter/converter.go create mode 100644 converter/states.go create mode 100644 converter/text.txt diff --git a/control/command.go b/control/command.go index bcd2e52..bebe4d1 100644 --- a/control/command.go +++ b/control/command.go @@ -22,11 +22,12 @@ type Command struct { 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": vehicle.StateCategoryCharge, - "climate": vehicle.StateCategoryClimate, + "charge_state": vehicle.StateCategoryCharge, + "climate_state": vehicle.StateCategoryClimate, "drive": vehicle.StateCategoryDrive, - "closures": vehicle.StateCategoryClosures, + "closures_state": vehicle.StateCategoryClosures, "charge-schedule": vehicle.StateCategoryChargeSchedule, "precondition-schedule": vehicle.StateCategoryPreconditioningSchedule, "tire-pressure": vehicle.StateCategoryTirePressure, diff --git a/control/control.go b/control/control.go index c0f04ea..a2c836b 100644 --- a/control/control.go +++ b/control/control.go @@ -1,11 +1,10 @@ package control import ( - "bytes" "context" + "encoding/json" "fmt" "os" - "regexp" "strconv" "strings" "time" @@ -16,6 +15,7 @@ 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" ) @@ -403,32 +403,57 @@ func (bc *BleControl) sendCommand(ctx context.Context, car *vehicle.Vehicle, com log.Info(fmt.Sprintf("Sent add-key request to %s. Confirm by tapping NFC card on center console.", car.VIN())) } case "vehicle_data": - category, err := GetCategory("charge") - 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) - } + 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) + } - //Flatten the response to a single level of keys - var r = regexp.MustCompile(`":{"(?P[a-zA-Z]*)":{}}`) - match := r.FindAllSubmatch(d, -1) + log.Debugf("data: %s", d) - for _, sm := range match { - if len(sm) != 2 { - continue + 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) } - tb := append(append([]byte(`":"`), sm[1]...), '"') - d = bytes.ReplaceAll(d, sm[0], tb) + + /*//Flatten the response to a single level of keys + var r = regexp.MustCompile(`":{"(?P[a-zA-Z]*)":{}}`) + match := r.FindAllSubmatch(d, -1) + + for _, sm := range match { + if len(sm) != 2 { + continue + } + tb := append(append([]byte(`":"`), sm[1]...), '"') + d = bytes.ReplaceAll(d, sm[0], tb) + }*/ + response[endpoint] = d } - command.Response.Response = d + responseJson, err := json.Marshal(response) + if err != nil { + return false, fmt.Errorf("failed to marshal vehicle data: %s", err) + } + command.Response.Response = responseJson command.Response.Result = true command.Response.Finished = true //log.Info("vehicle data", "response", *command.Response) diff --git a/converter/converter.go b/converter/converter.go new file mode 100644 index 0000000..155153f --- /dev/null +++ b/converter/converter.go @@ -0,0 +1,124 @@ +package converter + +import ( + "strings" + + "github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/carserver" +) + +func flatten(s string) string { + return strings.ReplaceAll(s, ":{}", "") +} + +func ChargeStateFromBle(VehicleData *carserver.VehicleData) ChargeState { + return ChargeState{ + Timestamp: VehicleData.ChargeState.GetTimestamp().AsTime().Unix(), + ChargingState: flatten(VehicleData.ChargeState.GetChargingState().String()), + ChargeLimitSoc: VehicleData.ChargeState.GetChargeLimitSoc(), + ChargeLimitSocStd: VehicleData.ChargeState.GetChargeLimitSocStd(), + ChargeLimitSocMin: VehicleData.ChargeState.GetChargeLimitSocMin(), + ChargeLimitSocMax: VehicleData.ChargeState.GetChargeLimitSocMax(), + MaxRangeChargeCounter: VehicleData.ChargeState.GetMaxRangeChargeCounter(), + FastChargerPresent: VehicleData.ChargeState.GetFastChargerPresent(), + FastChargerType: flatten(VehicleData.ChargeState.GetFastChargerType().String()), + BatteryRange: VehicleData.ChargeState.GetBatteryRange(), + EstBatteryRange: VehicleData.ChargeState.GetEstBatteryRange(), + IdealBatteryRange: VehicleData.ChargeState.GetIdealBatteryRange(), + BatteryLevel: VehicleData.ChargeState.GetBatteryLevel(), + UsableBatteryLevel: VehicleData.ChargeState.GetUsableBatteryLevel(), + ChargeEnergyAdded: VehicleData.ChargeState.GetChargeEnergyAdded(), + ChargeMilesAddedRated: VehicleData.ChargeState.GetChargeMilesAddedRated(), + ChargeMilesAddedIdeal: VehicleData.ChargeState.GetChargeMilesAddedIdeal(), + ChargerVoltage: VehicleData.ChargeState.GetChargerVoltage(), + ChargerPilotCurrent: VehicleData.ChargeState.GetChargerPilotCurrent(), + ChargerActualCurrent: VehicleData.ChargeState.GetChargerActualCurrent(), + ChargerPower: VehicleData.ChargeState.GetChargerPower(), + TripCharging: VehicleData.ChargeState.GetTripCharging(), + ChargeRate: VehicleData.ChargeState.GetChargeRateMphFloat(), + ChargePortDoorOpen: VehicleData.ChargeState.GetChargePortDoorOpen(), + ScheduledChargingMode: flatten(VehicleData.ChargeState.GetScheduledChargingMode().String()), + ScheduledDepatureTime: VehicleData.ChargeState.GetScheduledDepartureTime().AsTime().Unix(), + ScheduledDepatureTimeMinutes: VehicleData.ChargeState.GetScheduledDepartureTimeMinutes(), + SuperchargerSessionTripPlanner: VehicleData.ChargeState.GetSuperchargerSessionTripPlanner(), + ScheduledChargingStartTime: VehicleData.ChargeState.GetScheduledChargingStartTime(), + ScheduledChargingPending: VehicleData.ChargeState.GetScheduledChargingPending(), + UserChargeEnableRequest: VehicleData.ChargeState.GetUserChargeEnableRequest(), + ChargeEnableRequest: VehicleData.ChargeState.GetChargeEnableRequest(), + ChargerPhases: VehicleData.ChargeState.GetChargerPhases(), + ChargePortLatch: flatten(VehicleData.ChargeState.GetChargePortLatch().String()), + ChargeCurrentRequest: VehicleData.ChargeState.GetChargeCurrentRequest(), + ChargeCurrentRequestMax: VehicleData.ChargeState.GetChargeCurrentRequestMax(), + ChargeAmps: VehicleData.ChargeState.GetChargingAmps(), + OffPeakChargingTimes: flatten(VehicleData.ChargeState.GetOffPeakChargingTimes().String()), + OffPeakHoursEndTime: VehicleData.ChargeState.GetOffPeakHoursEndTime(), + PreconditioningEnabled: VehicleData.ChargeState.GetPreconditioningEnabled(), + PreconditioningTimes: flatten(VehicleData.ChargeState.GetPreconditioningTimes().String()), + ManagedChargingActive: VehicleData.ChargeState.GetManagedChargingActive(), + ManagedChargingUserCanceled: VehicleData.ChargeState.GetManagedChargingUserCanceled(), + ManagedChargingStartTime: VehicleData.ChargeState.GetManagedChargingStartTime(), + ChargePortcoldWeatherMode: VehicleData.ChargeState.GetChargePortColdWeatherMode(), + ChargePortColor: flatten(VehicleData.ChargeState.GetChargePortColor().String()), + ConnChargeCable: flatten(VehicleData.ChargeState.GetConnChargeCable().String()), + FastChargerBrand: flatten(VehicleData.ChargeState.GetFastChargerBrand().String()), + MinutesToFullCharge: VehicleData.ChargeState.GetMinutesToFullCharge(), + } +} + +/* +MISSING + BatteryHeaterOn bool `json:"battery_heater_on"` + NotEnoughPowerToHeat bool `json:"not_enough_power_to_heat"` + TimeToFullCharge float64 `json:"time_to_full_charge"` + OffPeakChargingEnabled bool `json:"off_peak_charging_enabled"` +*/ + +func ClimateStateFromBle(VehicleData *carserver.VehicleData) ClimateState { + return ClimateState{ + Timestamp: VehicleData.ClimateState.GetTimestamp().AsTime().Unix(), + AllowCabinOverheatProtection: VehicleData.ClimateState.GetAllowCabinOverheatProtection(), + AutoSeatClimateLeft: VehicleData.ClimateState.GetAutoSeatClimateLeft(), + AutoSeatClimateRight: VehicleData.ClimateState.GetAutoSeatClimateRight(), + AutoSteeringWheelHeat: VehicleData.ClimateState.GetAutoSteeringWheelHeat(), + BioweaponMode: VehicleData.ClimateState.GetBioweaponModeOn(), + CabinOverheatProtection: flatten(VehicleData.ClimateState.GetCabinOverheatProtection().String()), + CabinOverheatProtectionActivelyCooling: VehicleData.ClimateState.GetCabinOverheatProtectionActivelyCooling(), + CopActivationTemperature: flatten(VehicleData.ClimateState.GetCopActivationTemperature().String()), + InsideTemp: VehicleData.ClimateState.GetInsideTempCelsius(), + OutsideTemp: VehicleData.ClimateState.GetOutsideTempCelsius(), + DriverTempSetting: VehicleData.ClimateState.GetDriverTempSetting(), + PassengerTempSetting: VehicleData.ClimateState.GetPassengerTempSetting(), + LeftTempDirection: VehicleData.ClimateState.GetLeftTempDirection(), + RightTempDirection: VehicleData.ClimateState.GetRightTempDirection(), + IsAutoConditioningOn: VehicleData.ClimateState.GetIsAutoConditioningOn(), + IsFrontDefrosterOn: VehicleData.ClimateState.GetIsFrontDefrosterOn(), + IsRearDefrosterOn: VehicleData.ClimateState.GetIsRearDefrosterOn(), + FanStatus: VehicleData.ClimateState.GetFanStatus(), + HvacAutoRequest: flatten(VehicleData.ClimateState.GetHvacAutoRequest().String()), + IsClimateOn: VehicleData.ClimateState.GetIsClimateOn(), + MinAvailTemp: VehicleData.ClimateState.GetMinAvailTempCelsius(), + MaxAvailTemp: VehicleData.ClimateState.GetMaxAvailTempCelsius(), + SeatHeaterLeft: VehicleData.ClimateState.GetSeatHeaterLeft(), + SeatHeaterRight: VehicleData.ClimateState.GetSeatHeaterRight(), + SeatHeaterRearLeft: VehicleData.ClimateState.GetSeatHeaterRearLeft(), + SeatHeaterRearRight: VehicleData.ClimateState.GetSeatHeaterRearRight(), + SeatHeaterRearCenter: VehicleData.ClimateState.GetSeatHeaterRearCenter(), + SeatHeaterRearRightBack: VehicleData.ClimateState.GetSeatHeaterRearRightBack(), + SeatHeaterRearLeftBack: VehicleData.ClimateState.GetSeatHeaterRearLeftBack(), + SteeringWheelHeatLevel: int32(*VehicleData.ClimateState.GetSteeringWheelHeatLevel().Enum()), + SteeringWheelHeater: VehicleData.ClimateState.GetSteeringWheelHeater(), + SupportsFanOnlyCabinOverheatProtection: VehicleData.ClimateState.GetSupportsFanOnlyCabinOverheatProtection(), + BatteryHeater: VehicleData.ClimateState.GetBatteryHeater(), + BatteryHeaterNoPower: VehicleData.ClimateState.GetBatteryHeaterNoPower(), + ClimateKeeperMode: flatten(VehicleData.ClimateState.GetClimateKeeperMode().String()), + DefrostMode: flatten(VehicleData.ClimateState.GetDefrostMode().String()), + IsPreconditioning: VehicleData.ClimateState.GetIsPreconditioning(), + RemoteHeaterControlEnabled: VehicleData.ClimateState.GetRemoteHeaterControlEnabled(), + SideMirrorHeaters: VehicleData.ClimateState.GetSideMirrorHeaters(), + WiperBladeHeater: VehicleData.ClimateState.GetWiperBladeHeater(), + } +} + +/* +MISSING + SmartPreconditioning bool `json:"smart_preconditioning"` +*/ diff --git a/converter/states.go b/converter/states.go new file mode 100644 index 0000000..30edbf1 --- /dev/null +++ b/converter/states.go @@ -0,0 +1,102 @@ +package converter + +// ChargeState contains the current charge states that exist within the vehicle. +type ChargeState struct { + Timestamp int64 `json:"timestamp"` // + ChargingState string `json:"charging_state"` // + ChargeLimitSoc int32 `json:"charge_limit_soc"` // + ChargeLimitSocStd int32 `json:"charge_limit_soc_std"` // + ChargeLimitSocMin int32 `json:"charge_limit_soc_min"` // + ChargeLimitSocMax int32 `json:"charge_limit_soc_max"` // + BatteryHeaterOn bool `json:"battery_heater_on"` // + NotEnoughPowerToHeat bool `json:"not_enough_power_to_heat"` // + MaxRangeChargeCounter int32 `json:"max_range_charge_counter"` // + FastChargerPresent bool `json:"fast_charger_present"` // + FastChargerType string `json:"fast_charger_type"` // + BatteryRange float32 `json:"battery_range"` // + EstBatteryRange float32 `json:"est_battery_range"` // + IdealBatteryRange float32 `json:"ideal_battery_range"` // + BatteryLevel int32 `json:"battery_level"` // + UsableBatteryLevel int32 `json:"usable_battery_level"` // + ChargeEnergyAdded float32 `json:"charge_energy_added"` // + ChargeMilesAddedRated float32 `json:"charge_miles_added_rated"` // + ChargeMilesAddedIdeal float32 `json:"charge_miles_added_ideal"` // + ChargerVoltage int32 `json:"charger_voltage"` // + ChargerPilotCurrent int32 `json:"charger_pilot_current"` // + ChargerActualCurrent int32 `json:"charger_actual_current"` // + ChargerPower int32 `json:"charger_power"` // + TripCharging bool `json:"trip_charging"` // + ChargeRate float32 `json:"charge_rate"` // + ChargePortDoorOpen bool `json:"charge_port_door_open"` // + ScheduledChargingMode string `json:"scheduled_charging_mode"` // + ScheduledDepatureTime int64 `json:"scheduled_departure_time"` // + ScheduledDepatureTimeMinutes uint32 `json:"scheduled_departure_time_minutes"` // + SuperchargerSessionTripPlanner bool `json:"supercharger_session_trip_planner"` // + ScheduledChargingStartTime uint64 `json:"scheduled_charging_start_time"` // + ScheduledChargingPending bool `json:"scheduled_charging_pending"` // + UserChargeEnableRequest interface{} `json:"user_charge_enable_request"` // + ChargeEnableRequest bool `json:"charge_enable_request"` // + ChargerPhases int32 `json:"charger_phases"` // + ChargePortLatch string `json:"charge_port_latch"` // + ChargeCurrentRequest int32 `json:"charge_current_request"` // + ChargeCurrentRequestMax int32 `json:"charge_current_request_max"` // + ChargeAmps int32 `json:"charge_amps"` // + OffPeakChargingEnabled bool `json:"off_peak_charging_enabled"` // + OffPeakChargingTimes string `json:"off_peak_charging_times"` // + OffPeakHoursEndTime uint32 `json:"off_peak_hours_end_time"` // + PreconditioningEnabled bool `json:"preconditioning_enabled"` // + PreconditioningTimes string `json:"preconditioning_times"` // + ManagedChargingActive bool `json:"managed_charging_active"` // + ManagedChargingUserCanceled bool `json:"managed_charging_user_canceled"` // + ManagedChargingStartTime interface{} `json:"managed_charging_start_time"` // + ChargePortcoldWeatherMode bool `json:"charge_port_cold_weather_mode"` // + ChargePortColor string `json:"charge_port_color"` // + ConnChargeCable string `json:"conn_charge_cable"` // + FastChargerBrand string `json:"fast_charger_brand"` // + MinutesToFullCharge int32 `json:"minutes_to_full_charge"` // +} + +// ClimateState contains the current climate states available from the vehicle. +type ClimateState struct { + Timestamp int64 `json:"timestamp"` // + AllowCabinOverheatProtection bool `json:"allow_cabin_overheat_protection"` // + AutoSeatClimateLeft bool `json:"auto_seat_climate_left"` // + AutoSeatClimateRight bool `json:"auto_seat_climate_right"` // + AutoSteeringWheelHeat bool `json:"auto_steering_wheel_heat"` // + BioweaponMode bool `json:"bioweapon_mode"` // + CabinOverheatProtection string `json:"cabin_overheat_protection"` // + CabinOverheatProtectionActivelyCooling bool `json:"cabin_overheat_protection_actively_cooling"` // + CopActivationTemperature string `json:"cop_activation_temperature"` // + InsideTemp float32 `json:"inside_temp"` // + OutsideTemp float32 `json:"outside_temp"` // + DriverTempSetting float32 `json:"driver_temp_setting"` // + PassengerTempSetting float32 `json:"passenger_temp_setting"` // + LeftTempDirection int32 `json:"left_temp_direction"` // + RightTempDirection int32 `json:"right_temp_direction"` // + IsAutoConditioningOn bool `json:"is_auto_conditioning_on"` // + IsFrontDefrosterOn bool `json:"is_front_defroster_on"` // + IsRearDefrosterOn bool `json:"is_rear_defroster_on"` // + FanStatus int32 `json:"fan_status"` // + HvacAutoRequest string `json:"hvac_auto_request"` // + IsClimateOn bool `json:"is_climate_on"` // + MinAvailTemp float32 `json:"min_avail_temp"` // + MaxAvailTemp float32 `json:"max_avail_temp"` // + SeatHeaterLeft int32 `json:"seat_heater_left"` // + SeatHeaterRight int32 `json:"seat_heater_right"` // + SeatHeaterRearLeft int32 `json:"seat_heater_rear_left"` // + SeatHeaterRearRight int32 `json:"seat_heater_rear_right"` // + SeatHeaterRearCenter int32 `json:"seat_heater_rear_center"` // + SeatHeaterRearRightBack int32 `json:"seat_heater_rear_right_back"` + SeatHeaterRearLeftBack int32 `json:"seat_heater_rear_left_back"` + SteeringWheelHeatLevel int32 `json:"steering_wheel_heat_level"` // + SteeringWheelHeater bool `json:"steering_wheel_heater"` // + SupportsFanOnlyCabinOverheatProtection bool `json:"supports_fan_only_cabin_overheat_protection"` // + BatteryHeater bool `json:"battery_heater"` // + BatteryHeaterNoPower interface{} `json:"battery_heater_no_power"` // + ClimateKeeperMode string `json:"climate_keeper_mode"` // + DefrostMode string `json:"defrost_mode"` // + IsPreconditioning bool `json:"is_preconditioning"` // + RemoteHeaterControlEnabled bool `json:"remote_heater_control_enabled"` // + SideMirrorHeaters bool `json:"side_mirror_heaters"` // + WiperBladeHeater bool `json:"wiper_blade_heater"` // +} diff --git a/converter/text.txt b/converter/text.txt new file mode 100644 index 0000000..52465e4 --- /dev/null +++ b/converter/text.txt @@ -0,0 +1,39 @@ +//"allow_cabin_overheat_protection": true, +//"auto_seat_climate_left": false, +//"auto_seat_climate_right": false, +//"auto_steering_wheel_heat": false, +//"battery_heater": false, +//"battery_heater_no_power": null, +//"bioweapon_mode": false, +//"cabin_overheat_protection": "On", +//"cabin_overheat_protection_actively_cooling": true, +//"climate_keeper_mode": "off", +//"cop_activation_temperature": "High", +//"defrost_mode": 0, +//"driver_temp_setting": 21, +//"fan_status": 0, +//"hvac_auto_request": "On", +//"inside_temp": 38.4, +//"is_auto_conditioning_on": true, +//"is_climate_on": false, +//"is_front_defroster_on": false, +//"is_preconditioning": false, +//"is_rear_defroster_on": false, +//"left_temp_direction": -293, +//"max_avail_temp": 28, +//"min_avail_temp": 15, +//"outside_temp": 36.5, +//"passenger_temp_setting": 21, +//"remote_heater_control_enabled": false, +//"right_temp_direction": -276, +//"seat_heater_left": 0, +//"seat_heater_rear_center": 0, +//"seat_heater_rear_left": 0, +//"seat_heater_rear_right": 0, +//"seat_heater_right": 0, +//"side_mirror_heaters": false, +//"steering_wheel_heat_level": 0, +//"steering_wheel_heater": false, +//"supports_fan_only_cabin_overheat_protection": true, +//"timestamp": 1692141038419, +//"wiper_blade_heater": false \ No newline at end of file diff --git a/main.go b/main.go index f3eaa50..c9befff 100644 --- a/main.go +++ b/main.go @@ -22,11 +22,11 @@ type Ret struct { } type Response struct { - Result bool `json:"result"` - Reason string `json:"reason"` - Vin string `json:"vin"` - Command string `json:"command"` - Response *json.RawMessage `json:"response,omitempty"` + Result bool `json:"result"` + Reason string `json:"reason"` + Vin string `json:"vin"` + Command string `json:"command"` + Response json.RawMessage `json:"response,omitempty"` } 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"} @@ -128,9 +128,17 @@ func receiveVehicleData(w http.ResponseWriter, r *http.Request) { vin := params["vin"] command := "vehicle_data" + var endpoints []string + entpointsString := r.URL.Query().Get("endpoints") + if entpointsString != "" { + endpoints = strings.Split(entpointsString, ";") + } else { + endpoints = []string{"charge_state", "climate_state", "closures_state"} //'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 apiResponse control.ApiResponse - control.BleControlInstance.PushCommand(command, vin, nil, &apiResponse) + control.BleControlInstance.PushCommand(command, vin, map[string]interface{}{"endpoints": endpoints}, &apiResponse) var response Response response.Vin = vin @@ -167,7 +175,7 @@ func receiveVehicleData(w http.ResponseWriter, r *http.Request) { if apiResponse.Result { response.Result = true response.Reason = "The command was successfully processed." - response.Response = &apiResponse.Response + response.Response = apiResponse.Response } else { response.Result = false response.Reason = apiResponse.Error From 30a6c03440f4cc04eb51355c205713cb1ab4cbba Mon Sep 17 00:00:00 2001 From: Wilko Date: Mon, 9 Dec 2024 23:03:24 +0100 Subject: [PATCH 4/6] Change to use waitGroup to be thread safe --- control/command.go | 3 ++- control/control.go | 51 +++++++++++++++++++++++++++------------------- main.go | 22 +++++--------------- 3 files changed, 37 insertions(+), 39 deletions(-) diff --git a/control/command.go b/control/command.go index bebe4d1..4cb2af4 100644 --- a/control/command.go +++ b/control/command.go @@ -4,12 +4,13 @@ import ( "encoding/json" "fmt" "strings" + "sync" "github.com/teslamotors/vehicle-command/pkg/vehicle" ) type ApiResponse struct { - Finished bool + Wait *sync.WaitGroup Result bool Error string Response json.RawMessage diff --git a/control/control.go b/control/control.go index a2c836b..4a2e00a 100644 --- a/control/control.go +++ b/control/control.go @@ -121,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 } @@ -256,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() @@ -265,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) @@ -290,11 +318,6 @@ func (bc *BleControl) executeCommand(car *vehicle.Vehicle, command *Command) (*C } } log.Error("canceled", "command", command.Command, "body", command.Body, "err", lastErr) - if command.Response != nil { - command.Response.Error = lastErr.Error() - command.Response.Result = false - command.Response.Finished = true - } return nil, lastErr } @@ -421,7 +444,7 @@ func (bc *BleControl) sendCommand(ctx context.Context, car *vehicle.Vehicle, com return true, fmt.Errorf("failed to marshal vehicle data: %s", err) } - log.Debugf("data: %s", d) + //log.Debugf("data: %s", d) var converted interface{} switch endpoint { @@ -435,17 +458,6 @@ func (bc *BleControl) sendCommand(ctx context.Context, car *vehicle.Vehicle, com return true, fmt.Errorf("failed to marshal vehicle data: %s", err) } - /*//Flatten the response to a single level of keys - var r = regexp.MustCompile(`":{"(?P[a-zA-Z]*)":{}}`) - match := r.FindAllSubmatch(d, -1) - - for _, sm := range match { - if len(sm) != 2 { - continue - } - tb := append(append([]byte(`":"`), sm[1]...), '"') - d = bytes.ReplaceAll(d, sm[0], tb) - }*/ response[endpoint] = d } @@ -454,9 +466,6 @@ func (bc *BleControl) sendCommand(ctx context.Context, car *vehicle.Vehicle, com return false, fmt.Errorf("failed to marshal vehicle data: %s", err) } command.Response.Response = responseJson - command.Response.Result = true - command.Response.Finished = true - //log.Info("vehicle data", "response", *command.Response) } // everything fine diff --git a/main.go b/main.go index c9befff..2fcfb9e 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "os" "slices" "strings" - "time" + "sync" "github.com/charmbracelet/log" "github.com/wimaha/TeslaBleHttpProxy/control" @@ -137,7 +137,10 @@ func receiveVehicleData(w http.ResponseWriter, r *http.Request) { } var apiResponse control.ApiResponse + wg := sync.WaitGroup{} + apiResponse.Wait = &wg + wg.Add(1) control.BleControlInstance.PushCommand(command, vin, map[string]interface{}{"endpoints": endpoints}, &apiResponse) var response Response @@ -165,12 +168,7 @@ func receiveVehicleData(w http.ResponseWriter, r *http.Request) { return } - for { - if apiResponse.Finished { - break - } - time.Sleep(100 * time.Millisecond) - } + wg.Wait() if apiResponse.Result { response.Result = true @@ -181,13 +179,3 @@ func receiveVehicleData(w http.ResponseWriter, r *http.Request) { response.Reason = apiResponse.Error } } - -/*func pushCommand(command string, vin string, body map[string]interface{}) error { - if bleControl == nil { - return fmt.Errorf("BleControl is not initialized. Maybe private.pem is missing.") - } - - bleControl.PushCommand(command, vin, body) - - return nil -}*/ From 7d517a1d973ef083c3403c3c8ecc56e1e23cf182 Mon Sep 17 00:00:00 2001 From: Wilko Date: Mon, 9 Dec 2024 23:13:48 +0100 Subject: [PATCH 5/6] Update Readme with vehicle_data api --- README.md | 28 ++++++++++++++++++++++++++++ main.go | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 622e24f..c0da893 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,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 @@ -146,7 +148,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. diff --git a/main.go b/main.go index 2fcfb9e..6796a4b 100644 --- a/main.go +++ b/main.go @@ -133,7 +133,7 @@ func receiveVehicleData(w http.ResponseWriter, r *http.Request) { if entpointsString != "" { endpoints = strings.Split(entpointsString, ";") } else { - endpoints = []string{"charge_state", "climate_state", "closures_state"} //'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' + endpoints = []string{"charge_state", "climate_state"} //'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 apiResponse control.ApiResponse From d2b09a5be64d9513ece9b90f4c70c09ed7fe0dec Mon Sep 17 00:00:00 2001 From: Wilko Date: Mon, 9 Dec 2024 23:15:35 +0100 Subject: [PATCH 6/6] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c0da893..6655164 100644 --- a/README.md +++ b/README.md @@ -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