From 24531edfcdb741c5cafc1ff797653e7ef355e90d Mon Sep 17 00:00:00 2001 From: Leland Sindt Date: Tue, 23 Mar 2021 20:50:57 -0500 Subject: [PATCH 1/4] add support for teslamate logging resume/suspend --- README.md | 6 +- src/CommandSupport.go | 7 ++ src/v1_TeslaMateAPICarsLoggingCommand.go | 110 +++++++++++++++++++++++ src/webserver.go | 4 + 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 src/v1_TeslaMateAPICarsLoggingCommand.go diff --git a/README.md b/README.md index 712a4f5..b3ab39c 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Basically the same environment variables for the database, mqqt and timezone nee - **DATABASE_NAME** string *(default: teslamate)* - **DATABASE_HOST** string *(default: database)* - **MQTT_HOST** string *(default: mosquitto)* +- **TESLAMATE_URL** string *(default: http://teslamate:4000)* - **TZ** string *(default: Europe/Berlin)* **Optional** environment variables @@ -118,6 +119,7 @@ Basically the same environment variables for the database, mqqt and timezone nee - **ENABLE_COMMANDS** boolean *(default: false)* - **COMMANDS_ALL** boolean *(default: false)* - **COMMANDS_ALLOWLIST** string *(default: allow_list.json)* +- **COMMANDS_LOGGING** boolean *(deafault: false)* - **COMMANDS_WAKE** boolean *(default: false)* - **COMMANDS_ALERT** boolean *(default: false)* - **COMMANDS_REMOTESTART** boolean *(default: false)* @@ -151,6 +153,8 @@ More detailed documentation of every endpoint will come.. - POST `/api/v1/cars/:CarID/command/:Command` - GET `/api/v1/cars/:CarID/drives` - GET `/api/v1/cars/:CarID/drives/:DriveID` +- PUT `/api/v1/cars/:CarID/logging/:Command` +- GET `/api/v1/cars/:CarID/logging` - GET `/api/v1/cars/:CarID/status` - GET `/api/v1/cars/:CarID/updates` - POST `/api/v1/cars/:CarID/wake_up` @@ -159,7 +163,7 @@ More detailed documentation of every endpoint will come.. ### Authentication -If you want to use command endpoints such as `/api/v1/cars/:CarID/command/:Command` and `/api/v1/cars/:CarID/wake_up`, you need to add authentication to your request. +If you want to use command or logging endpoints such as `/api/v1/cars/:CarID/command/:Command`, `/api/v1/cars/:CarID/wake_up`, or `/api/v1/cars/:CarID/logging/:Command` you need to add authentication to your request. You need to specify a token yourself (called **API_TOKEN**) in the environment variables file, to set it. The token has the requirement to be a minimum of 32 characters long. diff --git a/src/CommandSupport.go b/src/CommandSupport.go index 685af04..6a26b08 100644 --- a/src/CommandSupport.go +++ b/src/CommandSupport.go @@ -16,6 +16,13 @@ func initCommandAllowList() { // allow all commands available below allowAll := getEnvAsBool("COMMANDS_ALL", false) + // https://github.com/adriankumpf/teslamate/discussions/1433 + if getEnvAsBool("COMMANDS_LOGGING", false) || allowAll { + allowList = append(allowList, + "/logging/resume", + "/logging/suspend") + } + // https://tesla-api.timdorr.com/vehicle/commands/wake if getEnvAsBool("COMMANDS_WAKE", false) || allowAll { allowList = append(allowList, "/wake_up") diff --git a/src/v1_TeslaMateAPICarsLoggingCommand.go b/src/v1_TeslaMateAPICarsLoggingCommand.go new file mode 100644 index 0000000..ff40c5b --- /dev/null +++ b/src/v1_TeslaMateAPICarsLoggingCommand.go @@ -0,0 +1,110 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" +) + +// TeslaMateAPICarsLoggingCommandV1 func +func TeslaMateAPICarsLoggingCommandV1(c *gin.Context) { + + // creating required vars + var jsonData map[string]interface{} + var err error + + // check if commands are enabled.. if not we need to abort + if getEnvAsBool("ENABLE_COMMANDS", false) == false { + log.Println("[warning] TeslaMateAPICarsLoggingCommandV1 ENABLE_COMMANDS is not true.. returning 403 forbidden.") + c.JSON(http.StatusForbidden, gin.H{"error": "You are not allowed to access logging commands"}) + return + } + + // if request method is GET return list of commands + if c.Request.Method == http.MethodGet { + c.JSON(http.StatusOK, gin.H{"enabled_commands": allowList}) + return + } + + // authentication for the endpoint + validToken, errorMessage := validateAuthToken(c) + if !validToken { + c.JSON(http.StatusUnauthorized, gin.H{"error": errorMessage}) + return + } + + // getting CarID param from URL + ParamCarID := c.Param("CarID") + var CarID int + if ParamCarID != "" { + CarID = convertStringToInteger(ParamCarID) + } + + // validating that CarID is not zero + if CarID == 0 { + log.Println("[error] TeslaMateAPICarsLoggingCommandV1 CarID is invalid (zero)!") + c.JSON(http.StatusBadRequest, gin.H{"error": "CarID invalid"}) + return + } + + // getting request body to pass to Tesla + reqBody, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + log.Println("[error] TeslaMateAPICarsLoggingCommandV1 error in first ioutil.ReadAll", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) + return + } + + // getting :Command + command := ("/logging/" + c.Param("Command")) + log.Println("[debug] TeslaMateAPICarsLoggingCommandV1 command received:", command) + + if !checkArrayContainsString(allowList, command) { + log.Print("[warning] TeslaMateAPICarsCommandV1 command: " + command + " not allowed") + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + + client := &http.Client{} + putURL := getEnv("TESLAMATE_URL", "http://teslamate:4000") + "/api/car/" + ParamCarID + command + req, _ := http.NewRequest(http.MethodPut, putURL, strings.NewReader(string(reqBody))) + 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] TeslaMateAPICarsLoggingCommandV1 error in http request to http://teslamate:", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) + return + } + + defer resp.Body.Close() + defer client.CloseIdleConnections() + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println("[error] TeslaMateAPICarsLoggingCommandV1 error in second ioutil.ReadAll:", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) + return + } + json.Unmarshal([]byte(respBody), &jsonData) + + // print to log about request + if gin.IsDebugging() { + log.Println("[debug] TeslaMateAPICarsLoggingCommandV1 " + c.Request.RequestURI + " returned data:") + js, _ := json.Marshal(jsonData) + log.Printf("[debug] %s\n", js) + } + + if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNoContent { + log.Println("[info] TeslaMateAPICarsLoggingCommandV1 " + c.Request.RequestURI + " executed successful.") + } else { + log.Println("[error] TeslaMateAPICarsLoggingCommandV1 " + c.Request.RequestURI + " error in execution!") + } + c.JSON(resp.StatusCode, jsonData) +} diff --git a/src/webserver.go b/src/webserver.go index bf6862e..43a19dd 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -94,6 +94,10 @@ func main() { v1.GET("/cars/:CarID/drives", TeslaMateAPICarsDrivesV1) v1.GET("/cars/:CarID/drives/:DriveID", TeslaMateAPICarsDrivesDetailsV1) + // v1 /api/v1/cars/:CarID/logging endpoints + v1.GET("/cars/:CarID/logging", TeslaMateAPICarsLoggingCommandV1) + v1.PUT("/cars/:CarID/logging/:Command", TeslaMateAPICarsLoggingCommandV1) + // v1 /api/v1/cars/:CarID/status endpoints v1.GET("/cars/:CarID/status", TeslaMateAPICarsStatusV1) From 5bebf85e1b1e6ac82a71bfa22f467f0c938d0fed Mon Sep 17 00:00:00 2001 From: Leland Sindt Date: Mon, 3 May 2021 20:12:17 -0500 Subject: [PATCH 2/4] rename TeslaMateAPICarsLoggingCommandV1 -> TeslaMateAPICarsLoggingV1 --- ...mmand.go => v1_TeslaMateAPICarsLogging.go} | 24 +++++++++---------- src/webserver.go | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) rename src/{v1_TeslaMateAPICarsLoggingCommand.go => v1_TeslaMateAPICarsLogging.go} (70%) diff --git a/src/v1_TeslaMateAPICarsLoggingCommand.go b/src/v1_TeslaMateAPICarsLogging.go similarity index 70% rename from src/v1_TeslaMateAPICarsLoggingCommand.go rename to src/v1_TeslaMateAPICarsLogging.go index ff40c5b..eaaf22a 100644 --- a/src/v1_TeslaMateAPICarsLoggingCommand.go +++ b/src/v1_TeslaMateAPICarsLogging.go @@ -11,8 +11,8 @@ import ( _ "github.com/lib/pq" ) -// TeslaMateAPICarsLoggingCommandV1 func -func TeslaMateAPICarsLoggingCommandV1(c *gin.Context) { +// TeslaMateAPICarsLoggingV1 func +func TeslaMateAPICarsLoggingV1(c *gin.Context) { // creating required vars var jsonData map[string]interface{} @@ -20,7 +20,7 @@ func TeslaMateAPICarsLoggingCommandV1(c *gin.Context) { // check if commands are enabled.. if not we need to abort if getEnvAsBool("ENABLE_COMMANDS", false) == false { - log.Println("[warning] TeslaMateAPICarsLoggingCommandV1 ENABLE_COMMANDS is not true.. returning 403 forbidden.") + log.Println("[warning] TeslaMateAPICarsLoggingV1 ENABLE_COMMANDS is not true.. returning 403 forbidden.") c.JSON(http.StatusForbidden, gin.H{"error": "You are not allowed to access logging commands"}) return } @@ -47,7 +47,7 @@ func TeslaMateAPICarsLoggingCommandV1(c *gin.Context) { // validating that CarID is not zero if CarID == 0 { - log.Println("[error] TeslaMateAPICarsLoggingCommandV1 CarID is invalid (zero)!") + log.Println("[error] TeslaMateAPICarsLoggingV1 CarID is invalid (zero)!") c.JSON(http.StatusBadRequest, gin.H{"error": "CarID invalid"}) return } @@ -55,17 +55,17 @@ func TeslaMateAPICarsLoggingCommandV1(c *gin.Context) { // getting request body to pass to Tesla reqBody, err := ioutil.ReadAll(c.Request.Body) if err != nil { - log.Println("[error] TeslaMateAPICarsLoggingCommandV1 error in first ioutil.ReadAll", err) + log.Println("[error] TeslaMateAPICarsLoggingV1 error in first ioutil.ReadAll", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } // getting :Command command := ("/logging/" + c.Param("Command")) - log.Println("[debug] TeslaMateAPICarsLoggingCommandV1 command received:", command) + log.Println("[debug] TeslaMateAPICarsLoggingV1 command received:", command) if !checkArrayContainsString(allowList, command) { - log.Print("[warning] TeslaMateAPICarsCommandV1 command: " + command + " not allowed") + log.Print("[warning] TeslaMateAPICarsLoggingV1 command: " + command + " not allowed") c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } @@ -78,7 +78,7 @@ func TeslaMateAPICarsLoggingCommandV1(c *gin.Context) { // check response error if err != nil { - log.Println("[error] TeslaMateAPICarsLoggingCommandV1 error in http request to http://teslamate:", err) + log.Println("[error] TeslaMateAPICarsLoggingV1 error in http request to http://teslamate:", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } @@ -88,7 +88,7 @@ func TeslaMateAPICarsLoggingCommandV1(c *gin.Context) { respBody, err := ioutil.ReadAll(resp.Body) if err != nil { - log.Println("[error] TeslaMateAPICarsLoggingCommandV1 error in second ioutil.ReadAll:", err) + log.Println("[error] TeslaMateAPICarsLoggingV1 error in second ioutil.ReadAll:", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } @@ -96,15 +96,15 @@ func TeslaMateAPICarsLoggingCommandV1(c *gin.Context) { // print to log about request if gin.IsDebugging() { - log.Println("[debug] TeslaMateAPICarsLoggingCommandV1 " + c.Request.RequestURI + " returned data:") + log.Println("[debug] TeslaMateAPICarsLoggingV1 " + c.Request.RequestURI + " returned data:") js, _ := json.Marshal(jsonData) log.Printf("[debug] %s\n", js) } if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNoContent { - log.Println("[info] TeslaMateAPICarsLoggingCommandV1 " + c.Request.RequestURI + " executed successful.") + log.Println("[info] TeslaMateAPICarsLoggingV1 " + c.Request.RequestURI + " executed successful.") } else { - log.Println("[error] TeslaMateAPICarsLoggingCommandV1 " + c.Request.RequestURI + " error in execution!") + log.Println("[error] TeslaMateAPICarsLoggingV1 " + c.Request.RequestURI + " error in execution!") } c.JSON(resp.StatusCode, jsonData) } diff --git a/src/webserver.go b/src/webserver.go index 43a19dd..e9bef05 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -95,8 +95,8 @@ func main() { v1.GET("/cars/:CarID/drives/:DriveID", TeslaMateAPICarsDrivesDetailsV1) // v1 /api/v1/cars/:CarID/logging endpoints - v1.GET("/cars/:CarID/logging", TeslaMateAPICarsLoggingCommandV1) - v1.PUT("/cars/:CarID/logging/:Command", TeslaMateAPICarsLoggingCommandV1) + v1.GET("/cars/:CarID/logging", TeslaMateAPICarsLoggingV1) + v1.PUT("/cars/:CarID/logging/:Command", TeslaMateAPICarsLoggingV1) // v1 /api/v1/cars/:CarID/status endpoints v1.GET("/cars/:CarID/status", TeslaMateAPICarsStatusV1) From 6605fde6fb6b50dd7201afc0412732c6c54d903f Mon Sep 17 00:00:00 2001 From: Leland Sindt Date: Mon, 3 May 2021 20:31:58 -0500 Subject: [PATCH 3/4] break out teslamate Protocol, FQDN, Port --- README.md | 4 +++- src/v1_TeslaMateAPICarsLogging.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b3ab39c..4483e35 100644 --- a/README.md +++ b/README.md @@ -96,11 +96,13 @@ Basically the same environment variables for the database, mqqt and timezone nee - **DATABASE_NAME** string *(default: teslamate)* - **DATABASE_HOST** string *(default: database)* - **MQTT_HOST** string *(default: mosquitto)* -- **TESLAMATE_URL** string *(default: http://teslamate:4000)* - **TZ** string *(default: Europe/Berlin)* **Optional** environment variables +- **TESLAMATE_PROTOCOL** string *(default: http)* +- **TESLAMATE_FQDN** string *(default: teslamate)* +- **TESLAMATE_PORT** string *(default: 4000)* - **API_TOKEN** string *(default: )* - **DATABASE_PORT** integer *(default: 5432)* - **DATABASE_TIMEOUT** integer *(default: 60000)* diff --git a/src/v1_TeslaMateAPICarsLogging.go b/src/v1_TeslaMateAPICarsLogging.go index eaaf22a..474122b 100644 --- a/src/v1_TeslaMateAPICarsLogging.go +++ b/src/v1_TeslaMateAPICarsLogging.go @@ -71,7 +71,7 @@ func TeslaMateAPICarsLoggingV1(c *gin.Context) { } client := &http.Client{} - putURL := getEnv("TESLAMATE_URL", "http://teslamate:4000") + "/api/car/" + ParamCarID + command + putURL := getEnv("TESLAMATE_PROTOCOL", "http") + "://" + getEnv("TESLAMATE_FQDN", "teslamate") + ":" + getEnv("TESLAMATE_PORT", "4000") + "/api/car/" + ParamCarID + command req, _ := http.NewRequest(http.MethodPut, putURL, strings.NewReader(string(reqBody))) req.Header.Set("User-Agent", "TeslaMateApi/"+apiVersion+" https://github.com/tobiasehlert/teslamateapi") resp, err := client.Do(req) From 331a05e1ce3d4d769cbb73e6a9603037225c0684 Mon Sep 17 00:00:00 2001 From: Leland Sindt Date: Tue, 4 May 2021 18:25:06 -0500 Subject: [PATCH 4/4] SSL: true|false, FQDN -> HOST --- README.md | 4 ++-- src/v1_TeslaMateAPICarsLogging.go | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4483e35..ea86787 100644 --- a/README.md +++ b/README.md @@ -100,8 +100,8 @@ Basically the same environment variables for the database, mqqt and timezone nee **Optional** environment variables -- **TESLAMATE_PROTOCOL** string *(default: http)* -- **TESLAMATE_FQDN** string *(default: teslamate)* +- **TESLAMATE_SSL** boolean *(default: false)* +- **TESLAMATE_HOST** string *(default: teslamate)* - **TESLAMATE_PORT** string *(default: 4000)* - **API_TOKEN** string *(default: )* - **DATABASE_PORT** integer *(default: 5432)* diff --git a/src/v1_TeslaMateAPICarsLogging.go b/src/v1_TeslaMateAPICarsLogging.go index 474122b..27b7e9a 100644 --- a/src/v1_TeslaMateAPICarsLogging.go +++ b/src/v1_TeslaMateAPICarsLogging.go @@ -71,7 +71,13 @@ func TeslaMateAPICarsLoggingV1(c *gin.Context) { } client := &http.Client{} - putURL := getEnv("TESLAMATE_PROTOCOL", "http") + "://" + getEnv("TESLAMATE_FQDN", "teslamate") + ":" + getEnv("TESLAMATE_PORT", "4000") + "/api/car/" + ParamCarID + command + putURL := "" + if getEnvAsBool("TESLAMATE_SSL", false) { + putURL = "https://" + } else { + putURL = "http://" + } + putURL = putURL + getEnv("TESLAMATE_HOST", "teslamate") + ":" + getEnv("TESLAMATE_PORT", "4000") + "/api/car/" + ParamCarID + command req, _ := http.NewRequest(http.MethodPut, putURL, strings.NewReader(string(reqBody))) req.Header.Set("User-Agent", "TeslaMateApi/"+apiVersion+" https://github.com/tobiasehlert/teslamateapi") resp, err := client.Do(req)