diff --git a/.gitignore b/.gitignore index 66fd13c..238d5cf 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ # Dependency directories (remove the comment below to include it) # vendor/ +.env diff --git a/README.md b/README.md index fcc7210..3f870ca 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ -# teslamateapi -TeslaMate RESTful API plugin to get data collected by self-hosted data logger TeslaMate +# TeslaMateApi + +TeslaMate RESTful API plugin to get data collected by self-hosted data logger TeslaMate. + +- Written in **[Golang](https://golang.org/)** +- Data is collected from TeslaMate **Postgres** database and local **MQTT** Broker + +## How to use + +*Information will be updated..* + +## API documentation + +*Information will be updated..* + +## Credits + +- Authors: Tobias Lindberg – [List of contributors](https://github.com/tobiasehlert/teslamateapi/graphs/contributors) +- Distributed under MIT License diff --git a/src/TeslaMateAPICars.go b/src/TeslaMateAPICars.go new file mode 100644 index 0000000..52d9efd --- /dev/null +++ b/src/TeslaMateAPICars.go @@ -0,0 +1,170 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + + _ "github.com/lib/pq" +) + +// TeslaMateAPICars func +func TeslaMateAPICars() (string, bool) { + + // creating structs for /cars + // CarDetails struct - child of Cars + type CarDetails struct { + EID int64 `json:"eid"` // bigint + VID int64 `json:"vid"` // bigint + Vin string `json:"vin"` // text + Model string `json:"model"` // character varying(255) + TrimBadging string `json:"trim_badging"` // text + Efficiency float64 `json:"efficiency"` // double precision + } + // CarExterior struct - child of Cars + type CarExterior struct { + ExteriorColor string `json:"exterior_color"` // text + SpoilerType string `json:"spoiler_type"` // text + WheelType string `json:"wheel_type"` // text + } + // CarSettings struct - child of Cars + type CarSettings struct { + SuspendMin int `json:"suspend_min"` // int + SuspendAfterIdleMin int `json:"suspend_after_idle_min"` // int + ReqNotUnlocked bool `json:"req_not_unlocked"` // bool + FreeSupercharging bool `json:"free_supercharging"` // bool + UseStreamingAPI bool `json:"use_streaming_api"` // bool + } + // TeslaMateDetails struct - child of Cars + type TeslaMateDetails struct { + InsertedAt string `json:"inserted_at"` // timestamp(0) without time zone + UpdatedAt string `json:"updated_at"` // timestamp(0) without time zone + } + // TeslaMateStats struct - child of Cars + type TeslaMateStats struct { + TotalCharges int `json:"total_charges"` // int + TotalDrives int `json:"total_drives"` // int + TotalUpdates int `json:"total_updates"` // int + } + // Cars struct - child of Data + type Cars struct { + ID int `json:"id"` // smallint + Name string `json:"name"` // text + CarDetails CarDetails `json:"car_details"` // struct + CarExterior CarExterior `json:"car_exterior"` // struct + CarSettings CarSettings `json:"car_settings"` // struct + TeslaMateDetails TeslaMateDetails `json:"teslamate_details"` // struct + TeslaMateStats TeslaMateStats `json:"teslamate_stats"` // struct + } + // Information struct - child of JSONData + type Data struct { + Cars []Cars `json:"cars"` + } + // JSONData struct - main + type JSONData struct { + Data Data `json:"data"` + } + + // creating required vars + var CarsData []Cars + var ValidResponse bool // default is false + + // getting data from database + query := ` + SELECT + cars.id, + eid, + vid, + model, + efficiency, + inserted_at, + updated_at, + vin, + name, + trim_badging, + exterior_color, + spoiler_type, + wheel_type, + suspend_min, + suspend_after_idle_min, + req_not_unlocked, + free_supercharging, + use_streaming_api, + (SELECT COUNT(*) FROM charging_processes WHERE car_id=cars.id) as total_charges, + (SELECT COUNT(*) FROM drives WHERE car_id=cars.id) as total_drives, + (SELECT COUNT(*) FROM updates WHERE car_id=cars.id) as total_charges + FROM cars + LEFT JOIN car_settings ON cars.id = car_settings.id + ORDER BY id;` + rows, err := db.Query(query) + + // checking for errors in query + if err != nil { + log.Fatal(err) + } + + // defer closing rows + defer rows.Close() + + // looping through all results + for rows.Next() { + + // creating car object based on struct + car := Cars{} + + // scanning row and putting values into the car + err = rows.Scan( + &car.ID, + &car.CarDetails.EID, + &car.CarDetails.VID, + &car.CarDetails.Model, + &car.CarDetails.Efficiency, + &car.TeslaMateDetails.InsertedAt, + &car.TeslaMateDetails.UpdatedAt, + &car.CarDetails.Vin, + &car.Name, + &car.CarDetails.TrimBadging, + &car.CarExterior.ExteriorColor, + &car.CarExterior.SpoilerType, + &car.CarExterior.WheelType, + &car.CarSettings.SuspendMin, + &car.CarSettings.SuspendAfterIdleMin, + &car.CarSettings.ReqNotUnlocked, + &car.CarSettings.FreeSupercharging, + &car.CarSettings.UseStreamingAPI, + &car.TeslaMateStats.TotalCharges, + &car.TeslaMateStats.TotalDrives, + &car.TeslaMateStats.TotalUpdates, + ) + + // checking for errors after scanning + if err != nil { + log.Fatal(err) + } + + // appending car to CarsData + CarsData = append(CarsData, car) + ValidResponse = true + } + + // checking for errors in the rows result + err = rows.Err() + if err != nil { + log.Fatal(err) + } + + // + // build the data-blob + jsonData := JSONData{ + Data{ + Cars: CarsData, + }, + } + + // print readable output to log + log.Println("data for /cars created:") + + js, _ := json.Marshal(jsonData) + fmt.Printf("%s\n", js) + return string(js), ValidResponse +} diff --git a/src/webserver.go b/src/webserver.go new file mode 100644 index 0000000..122bf8e --- /dev/null +++ b/src/webserver.go @@ -0,0 +1,156 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "os" + "strconv" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" +) + +// defining db var +var db *sql.DB + +// main function +func main() { + + // init of API with connection to database + initAPI() + defer db.Close() + + // kicking off Gin in value r + r := gin.Default() + + // /cars endpoint + r.GET("/cars", func(c *gin.Context) { + // TeslaMateAPICars to get cars + result, ValidResponse := TeslaMateAPICars() + + c.Header("Content-Type", "application/json") + if ValidResponse { + c.String(http.StatusOK, result) + } else { + c.String(http.StatusNotFound, result) + } + }) + + // /ping endpoint + r.GET("/ping", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) + }) + + // run this! :) + r.Run() +} + +// initAPI func +func initAPI() { + + // declare error var for use insite initAPI + var err error + + // creating connection string towards postgres + dbhost := getEnv("TM_DB_HOST", "database") + dbport := getEnvAsInt("TM_DB_PORT", 5432) + dbuser := getEnv("TM_DB_USER", "teslamate") + dbpass := getEnv("TM_DB_PASS", "secret") + dbname := getEnv("TM_DB_NAME", "teslamate") + psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", dbhost, dbport, dbuser, dbpass, dbname) + + // opening connection to postgres + db, err = sql.Open("postgres", psqlInfo) + if err != nil { + log.Panic(err) + } + + // doing ping to database to test connection + err = db.Ping() + if err != nil { + log.Panic(err) + } + + // showing database successfully connected + log.Println("successfully connected to postgres.") +} + +// getEnv func - read an environment or return a default value +func getEnv(key string, defaultVal string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return defaultVal +} + +// getEnvAsInt func - read an environment variable into integer or return a default value +func getEnvAsInt(name string, defaultVal int) int { + valueStr := getEnv(name, "") + if value, err := strconv.Atoi(valueStr); err == nil { + return value + } + return defaultVal +} + +// getEnvAsBool func - read an environment variable into a bool or return default value +func getEnvAsBool(name string, defaultVal bool) bool { + valStr := getEnv(name, "") + if val, err := strconv.ParseBool(valStr); err == nil { + return val + } + return defaultVal +} + +// convertStringToBool func +func convertStringToBool(data string) bool { + if value, err := strconv.ParseBool(data); err == nil { + return value + } + // else.. + log.Println("Error: convertStringToBool could not return value correct.. returning false") + return false +} + +// convertStringToFloat func +func convertStringToFloat(data string) float64 { + if value, err := strconv.ParseFloat(data, 64); err == nil { + return value + } + // else.. + log.Println("Error: convertStringToFloat could not return value correct.. returning 0.0") + return 0.0 +} + +// convertStringToInteger func +func convertStringToInteger(data string) int { + if value, err := strconv.Atoi(data); err == nil { + return value + } + // else.. + log.Println("Error: convertStringToInteger could not return value correct.. returning 0") + return 0 +} + +// kilometersToMiles func +func kilometersToMiles(km float64) float64 { + return (km * 0.62137119223733) +} + +// milesToKilometers func +func milesToKilometers(mi float64) float64 { + return (mi * 1.609344) +} + +// celsiusToFahrenheit func +func celsiusToFahrenheit(c float64) float64 { + return (c*9/5 + 32) +} + +// fahrenheitToCelsius func +func fahrenheitToCelsius(f float64) float64 { + return ((f - 32) * 5 / 9) +}