diff --git a/.gitignore b/.gitignore index 238d5cf..40c7e47 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ # Dependency directories (remove the comment below to include it) # vendor/ .env + +# VSCode configurations +.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index 9b32218..572bd98 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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` diff --git a/src/CommandSupport.go b/src/CommandSupport.go index 092fe08..d069dbe 100644 --- a/src/CommandSupport.go +++ b/src/CommandSupport.go @@ -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", diff --git a/src/v1_TeslaMateAPICarsState.go b/src/v1_TeslaMateAPICarsState.go new file mode 100644 index 0000000..b73d151 --- /dev/null +++ b/src/v1_TeslaMateAPICarsState.go @@ -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://github.com/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) +} diff --git a/src/webserver.go b/src/webserver.go index 0785900..72b32a4 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -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) }