Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement new state commands #269

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@
# Dependency directories (remove the comment below to include it)
# vendor/
.env

# VSCode configurations
.vscode/
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ TeslaMateApi is a RESTful API to get data collected by self-hosted data logger *
- Endpoints return data in JSON format
- Send commands to your Tesla through the TeslaMateApi

### Table of Contents
## Table of Contents

- [How to use](#how-to-use)
- [Docker-compose](#docker-compose)
Expand Down Expand Up @@ -169,6 +169,10 @@ More detailed documentation of every endpoint will come..
- GET `/api/v1/cars/:CarID/logging`
- GET `/api/v1/cars/:CarID/status`
- GET `/api/v1/cars/:CarID/updates`
- GET `/api/v1/cars/:CarID/vehicle_data`
- GET `/api/v1/cars/:CarID/mobile_enabled`
- GET `/api/v1/cars/:CarID/nearby_charging_sites`
- GET `/api/v1/cars/:CarID/release_notes`
- POST `/api/v1/cars/:CarID/wake_up`
- GET `/api/v1/globalsettings`
- GET `/api/ping`
Expand Down
8 changes: 8 additions & 0 deletions src/CommandSupport.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ func initCommandAllowList() {
"/wake_up",
}

// https://tesla-api.timdorr.com/vehicle/state
CommandList["COMMANDS_STATE"] = []string{
"/vehicle_data",
"/mobile_enabled",
"/nearby_charging_sites",
"/release_notes",
}

// https://tesla-api.timdorr.com/vehicle/commands/alerts
CommandList["COMMANDS_ALERT"] = []string{
"/command/honk_horn",
Expand Down
139 changes: 139 additions & 0 deletions src/v1_TeslaMateAPICarsState.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package main

import (
"database/sql"
"encoding/json"
"io"
"log"
"net/http"
"strings"

"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
)

// TeslaMateAPICarsStateCommandV1 func
func TeslaMateAPICarsStateCommandV1(c *gin.Context) {

// creating required vars
var (
CarsCommandsError1 = "Unable to load cars."
TeslaAccessToken, TeslaVehicleID, TeslaEndpointUrl string
jsonData map[string]interface{}
err error
)

// check if commands are enabled.. if not we need to abort
if !getEnvAsBool("ENABLE_COMMANDS", false) {
log.Println("[warning] TeslaMateAPICarsStateCommandV1 ENABLE_COMMANDS is not true.. returning 403 forbidden.")
TeslaMateAPIHandleOtherResponse(c, http.StatusForbidden, "TeslaMateAPICarsStateCommandV1", gin.H{"error": "You are not allowed to access commands"})
return
}

// authentication for the endpoint
validToken, errorMessage := validateAuthToken(c)
if !validToken {
TeslaMateAPIHandleOtherResponse(c, http.StatusUnauthorized, "TeslaMateAPICarsStateCommandV1", gin.H{"error": errorMessage})
return
}

// getting CarID param from URL and validating that it's not zero
CarID := convertStringToInteger(c.Param("CarID"))
if CarID == 0 {
log.Println("[error] TeslaMateAPICarsStateCommandV1 CarID is invalid (zero)!")
TeslaMateAPIHandleOtherResponse(c, http.StatusBadRequest, "TeslaMateAPICarsStateCommandV1", gin.H{"error": "CarID invalid"})
return
}

// getting request body to pass to Tesla
reqBody, err := io.ReadAll(c.Request.Body)
if err != nil {
log.Println("[error] TeslaMateAPICarsStateCommandV1 error in first io.ReadAll", err)
TeslaMateAPIHandleOtherResponse(c, http.StatusInternalServerError, "TeslaMateAPICarsStateCommandV1", gin.H{"error": "internal io reading error"})
return
}

// getting :Command
command := ("/" + c.Param("Command"))

// Check valid comands
if !checkArrayContainsString(allowList, command) {
log.Println("[warning] TeslaMateAPICarsStateCommandV1 command not allowed!")
TeslaMateAPIHandleOtherResponse(c, http.StatusUnauthorized, "TeslaMateAPICarsStateCommandV1", gin.H{"error": "unauthorized"})
return
}

// get TeslaVehicleID and TeslaAccessToken
query := `
SELECT
eid as TeslaVehicleID,
(SELECT access FROM tokens LIMIT 1) as TeslaAccessToken
FROM cars
WHERE id = $1
LIMIT 1;`
row := db.QueryRow(query, CarID)

err = row.Scan(
&TeslaVehicleID,
&TeslaAccessToken,
)

switch err {
case sql.ErrNoRows:
TeslaMateAPIHandleErrorResponse(c, "TeslaMateAPICarsStateCommandV1", "No rows were returned!", err.Error())
return
case nil:
// nothing wrong.. continuing
break
default:
TeslaMateAPIHandleErrorResponse(c, "TeslaMateAPICarsStateCommandV1", CarsCommandsError1, err.Error())
return
}

// load ENCRYPTION_KEY environment variable
teslaMateEncryptionKey := getEnv("ENCRYPTION_KEY", "")
if teslaMateEncryptionKey == "" {
log.Println("[error] TeslaMateAPICarsStateCommandV1 can't get ENCRYPTION_KEY.. will fail to perform command.")
TeslaMateAPIHandleOtherResponse(c, http.StatusInternalServerError, "TeslaMateAPICarsStateCommandV1", gin.H{"error": "missing ENCRYPTION_KEY env variable"})
return
}

// decrypt access token
TeslaAccessToken = decryptAccessToken(TeslaAccessToken, teslaMateEncryptionKey)

switch getCarRegionAPI(TeslaAccessToken) {
case ChinaAPI:
TeslaEndpointUrl = "https://owner-api.vn.cloud.tesla.cn"
default:
TeslaEndpointUrl = "https://owner-api.teslamotors.com"
}

client := &http.Client{}
req, _ := http.NewRequest(c.Request.Method, TeslaEndpointUrl+"/api/1/vehicles/"+TeslaVehicleID+command, strings.NewReader(string(reqBody)))
req.Header.Set("Authorization", "Bearer "+TeslaAccessToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "TeslaMateApi/"+apiVersion+" (+https:/tobiasehlert/teslamateapi)")
resp, err := client.Do(req)

// check response error
if err != nil {
log.Println("[error] TeslaMateAPICarsStateCommandV1 error in http request to "+TeslaEndpointUrl, err)
TeslaMateAPIHandleOtherResponse(c, http.StatusInternalServerError, "TeslaMateAPICarsStateCommandV1", gin.H{"error": "internal http request error"})
return
}

defer resp.Body.Close()
defer client.CloseIdleConnections()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("[error] TeslaMateAPICarsStateCommandV1 error in second io.ReadAll:", err)
TeslaMateAPIHandleOtherResponse(c, http.StatusInternalServerError, "TeslaMateAPICarsStateCommandV1", gin.H{"error": "internal io reading error"})
return
}
json.Unmarshal([]byte(respBody), &jsonData)

// return jsonData
// use TeslaMateAPIHandleOtherResponse since we use the statusCode from Tesla API
TeslaMateAPIHandleOtherResponse(c, resp.StatusCode, "TeslaMateAPICarsStateCommandV1", jsonData)
}
3 changes: 3 additions & 0 deletions src/webserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ func main() {
// v1 /api/v1/cars/:CarID/wake_up endpoints
v1.POST("/cars/:CarID/wake_up", TeslaMateAPICarsCommandV1)

// v1 /api/v1/cars/:CarID/status_commands endpoints
v1.GET("/cars/:CarID/:Command", TeslaMateAPICarsStateCommandV1)

// v1 /api/v1/globalsettings endpoints
v1.GET("/globalsettings", TeslaMateAPIGlobalsettingsV1)
}
Expand Down