From 6f9a7401f26e957a0bb874b03a7d8864cdb40ebd Mon Sep 17 00:00:00 2001 From: Paul Mundt Date: Wed, 17 Jun 2020 12:08:01 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + LICENSE | 22 ++ README.md | 72 +++++++ authentication.go | 11 + client.go | 88 ++++++++ examples/car-simulator-auth/README.md | 34 +++ examples/car-simulator-auth/main.go | 126 ++++++++++++ go.mod | 5 + go.sum | 13 ++ models.go | 284 ++++++++++++++++++++++++++ vehicle.go | 60 ++++++ 11 files changed, 716 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 authentication.go create mode 100644 client.go create mode 100644 examples/car-simulator-auth/README.md create mode 100644 examples/car-simulator-auth/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 models.go create mode 100644 vehicle.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..92cdda7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2019 Adaptant Solutions AG +Copyright (c) 2019 Paul Mundt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a51c8ef --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Go SDK for Mercedes-Benz Connected Vehicle API + +## Overview + +The [Mercedes-Benz Connected Vehicle API][connected_vehicle_api] is an experimental API for accessing connected vehicle +data and for prototyping connected vehicle services. It provides extensive information about the vehicle itself (vehicle +information, tire pressure, door status, location & heading, odometer, fuel level, as well as the state of battery +charge for electric vehicles), while also providing limited actuation controls (door lock / unlock). + +[connected_vehicle_api]: https://developer.mercedes-benz.com/products/connected_vehicle/ + +## Installation + +If not using Go modules, the SDK can be installed the same way as for other Go projects: + +``` +$ go get github.com/adaptant-labs/mercedes-connectedvehicle-go +``` + +## Getting Started + +1\. Initiate a new client connection with your API key +``` +// Initiate a new client connection. Set 'false' for tryout, 'true' for production API. +client := mercedes.NewClient(, false) +``` + +2\. Obtain a list of vehicles +``` +vehicles, err := client.GetVehicles(context.TODO()) +``` + +3\. Choose a vehicle to perform operations on +``` +vehicle := client.NewVehicle(vehicles[0].Id) +``` + +4\. Carry out vehicle-specific operations +``` +// Vehicle APIs +detail, err := vehicle.GetVehicleDetail(context.TODO()) +tires, err := vehicle.GetTirePressure(context.TODO()) +err := vehicle.LockDoors(context.TODO()) +err := vehicle.UnockDoors(context.TODO()) +doors, err := vehicle.GetDoorStatus(context.TODO()) +location, err := vehicle.GetLocation(context.TODO()) +distance, err := vehicle.GetDistanceDriven(context.TODO()) +level, err := vehicle.GetFuelLevel(context.TODO()) +charge, err := vehicle.GetStateOfCharge(context.TODO()) +``` + +## Testing with the Mercedes-Benz Car Simulator + +The SDK itself can be used together with the [Mercedes-Benz Car Simulator][simulator], but must go through the +appropriate Oauth2 authentication flows in order to become accessible from the Connected Vehicle API. Note that in this +case, the client must be configured for using the production API, and initiate the connection with the exchanged access +token. A simple example of this is included in the `examples` directory. + +[simulator]: https://car-simulator.developer.mercedes-benz.com/ + +## Features and bugs + +Please file feature requests and bugs concerning the SDK itself in the [issue tracker][tracker]. Note that as this is a +third-party SDK and we have no direct affiliation with Mercedes-Benz, we are unable to handle feature requests for the +REST API itself. + +[tracker]: https://github.com/adaptant-labs/mercedes-connectedvehicle-go/issues + +## License + +`mercedes-connectedvehicle-go` is released under the terms of the MIT license, the full +version of which can be found in the LICENSE file included in the distribution. \ No newline at end of file diff --git a/authentication.go b/authentication.go new file mode 100644 index 0000000..f22ab91 --- /dev/null +++ b/authentication.go @@ -0,0 +1,11 @@ +package mercedes + +import "golang.org/x/oauth2" + +var ( + DefaultScopes = []string{"mb:user:pool:reader", "mb:vehicle:status:general"} + Endpoint = oauth2.Endpoint{ + AuthURL: "https://api.secure.mercedes-benz.com/oidc10/auth/oauth/v2/authorize", + TokenURL: "https://api.secure.mercedes-benz.com/oidc10/auth/oauth/v2/token", + } +) diff --git a/client.go b/client.go new file mode 100644 index 0000000..572c807 --- /dev/null +++ b/client.go @@ -0,0 +1,88 @@ +package mercedes + +import ( + "bytes" + "context" + "encoding/json" + "net/http" +) + +var ( + apiTryoutBase = "https://api.mercedes-benz.com/experimental/connectedvehicle_tryout/v1" + apiProductionBase = "https://api.mercedes-benz.com/experimental/connectedvehicle/v1" +) + +// Provide a default HTTP transport mechanism that wraps requests with the API key / Bearer token. +type apiKeyHttpTransport struct { + apiKey string +} + +func (a *apiKeyHttpTransport) RoundTrip(r *http.Request) (*http.Response, error) { + r.Header.Add("Authorization", "Bearer "+a.apiKey) + return http.DefaultTransport.RoundTrip(r) +} + +type ConnectedVehicleClient struct { + apiBase string + httpClient *http.Client +} + +func (m *ConnectedVehicleClient) getJson(ctx context.Context, path string, target interface{}) error { + req, _ := http.NewRequestWithContext(ctx, "GET", m.apiBase+path, nil) + r, err := m.httpClient.Do(req) + if err != nil { + return err + } + defer r.Body.Close() + + if r.StatusCode == http.StatusUnauthorized { + println("unauthorized") + return err + } + return json.NewDecoder(r.Body).Decode(target) +} + +func (m *ConnectedVehicleClient) postJson(ctx context.Context, path string, payload interface{}) error { + b := new(bytes.Buffer) + err := json.NewEncoder(b).Encode(payload) + if err != nil { + return err + } + + req, _ := http.NewRequestWithContext(ctx, "POST", m.apiBase+path, b) + req.Header.Add("Content-Type", "application/json") + + _, err = m.httpClient.Do(req) + if err != nil { + return err + } + + return nil +} + +func NewClient(apiKey string, productionMode bool) *ConnectedVehicleClient { + m := &ConnectedVehicleClient{ + httpClient: &http.Client{Transport: &apiKeyHttpTransport{apiKey: apiKey}}, + } + + if productionMode == true { + m.apiBase = apiProductionBase + } else { + m.apiBase = apiTryoutBase + } + + return m +} + +func (m *ConnectedVehicleClient) NewVehicle(vehicleId string) *ConnectedVehicle { + return &ConnectedVehicle{ + VehicleID: vehicleId, + client: m, + } +} + +func (m *ConnectedVehicleClient) GetVehicles(ctx context.Context) ([]Vehicle, error) { + var vehicles []Vehicle + ret := m.getJson(ctx, "/vehicles", &vehicles) + return vehicles, ret +} diff --git a/examples/car-simulator-auth/README.md b/examples/car-simulator-auth/README.md new file mode 100644 index 0000000..8bfd9a3 --- /dev/null +++ b/examples/car-simulator-auth/README.md @@ -0,0 +1,34 @@ +# car-simulator-auth + +This is a simple example of negotiating the Oauth2 login and consent flows described by the +[Mercedes-Benz OAuth Authentication Documentation][oauth2-docs] in order to access vehicle data, to be used in +conjunction with the [Mercedes-Benz Simulator][simulator]. Once consent has been given by the user, the door locks of the +simulated vehicle will be toggled, which can be observed directly in the simulator: + +``` +11:16:01 - Mercedes-Benz Simulator - Starting initialization +11:16:01 - Mercedes-Benz Simulator - Initialized capabilities +11:16:02 - Mercedes-Benz Simulator - Car with serial number 7410ED2522A7AA3256 initialized +11:19:55 - Sandbox device - CapabilityManager - Incoming telematics message for Door Locks - ACASAQAEAQABAQ== +11:19:55 - Sandbox device - Door Locks - Lock doors +11:20:01 - Sandbox device - CapabilityManager - Incoming telematics message for Door Locks - ACAA +11:20:01 - Sandbox device - Door Locks - Get Lock State +11:20:07 - Sandbox device - CapabilityManager - Incoming telematics message for Door Locks - ACASAQAEAQABAA== +11:20:07 - Sandbox device - Door Locks - Unlock doors +``` + +# Environment Variables + +In order to carry out the OAuth flow, the client ID and secret must be provided. These are to be set in the following +environment variables: + +``` +MERCEDES_CLIENT_ID +MERCEDES_CLIENT_SECRET +``` + +These are available from the [Mercedes-Benz Developer Console][console]. + +[oauth2-docs]: https://developer.mercedes-benz.com/content-page/oauth-documentation +[simulator]: https://car-simulator.developer.mercedes-benz.com/ +[console]: https://developer.mercedes-benz.com/console diff --git a/examples/car-simulator-auth/main.go b/examples/car-simulator-auth/main.go new file mode 100644 index 0000000..1b90ff9 --- /dev/null +++ b/examples/car-simulator-auth/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "context" + "errors" + "fmt" + "github.com/adaptant-labs/mercedes-connectedvehicle-go" + "golang.org/x/oauth2" + "log" + "math/rand" + "net/http" + "os" + "time" +) + +var ( + oauth2Config = oauth2.Config{ + ClientID: os.Getenv("MERCEDES_CLIENT_ID"), + ClientSecret: os.Getenv("MERCEDES_CLIENT_SECRET"), + RedirectURL: "http://localhost:8080/callback", + Scopes: mercedes.DefaultScopes, + Endpoint: mercedes.Endpoint, + } + + oauthStateString string +) + +func randomString(n int) string { + var letter = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + + b := make([]rune, n) + for i := range b { + b[i] = letter[rand.Intn(len(letter))] + } + return string(b) +} + +func handleIndex(w http.ResponseWriter, r *http.Request) { + var html = ` + + Mercedes Log in + + +` + fmt.Fprintf(w, html) +} + +func handleLogin(w http.ResponseWriter, r *http.Request) { + url := oauth2Config.AuthCodeURL(oauthStateString) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func handleCallback(w http.ResponseWriter, r *http.Request) { + err := toggleDoorLocks(r.FormValue("state"), r.FormValue("code")) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } +} + +func toggleDoorLocks(state string, code string) error { + // Make sure we have a valid request + if state != oauthStateString { + return errors.New("oauth state strings do not match") + } + + // Exchange code for access token + token, err := oauth2Config.Exchange(context.TODO(), code) + if err != nil { + return err + } + + // Use the new access token for subsequent API calls + client := mercedes.NewClient(token.AccessToken, true) + vehicles, err := client.GetVehicles(context.TODO()) + if err != nil { + panic(err) + } + + // Make sure there are vehicles available to the user + if len(vehicles) == 0 { + return errors.New("no vehicles discovered") + } + + // Fetch a vehicle to operate on + vehicle := client.NewVehicle(vehicles[0].Id) + + // Obtain more detailed information about the vehicle + detail, err := vehicle.GetVehicleDetail(context.TODO()) + if err != nil { + return err + } + + go func() { + // Lock doors + fmt.Printf("Locking doors for vehicle %s (VIN %s)...\n", vehicle.VehicleID, detail.VIN) + _ = vehicle.LockDoors(context.TODO()) + + time.Sleep(5 * time.Second) + + // Fetch door lock status + doors, _ := vehicle.GetDoorStatus(context.TODO()) + + println("Front left door is now", doors.DoorLockStatusFrontLeft.Value) + time.Sleep(5 * time.Second) + + // Unlock doors + println("Unlocking...") + _ = vehicle.UnlockDoors(context.TODO()) + }() + + return nil +} + +func main() { + // Generate a random state string for validation + rand.Seed(time.Now().UnixNano()) + oauthStateString = randomString(16) + + // Handlers for Oauth2 login and consent flows + http.HandleFunc("/", handleIndex) + http.HandleFunc("/login", handleLogin) + http.HandleFunc("/callback", handleCallback) + + println("Awaiting completion of login and consent flow at http://localhost:8080...") + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c50a891 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/adaptant-labs/mercedes-connectedvehicle-go + +go 1.13 + +require golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e3ba8b0 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/models.go b/models.go new file mode 100644 index 0000000..d8650a1 --- /dev/null +++ b/models.go @@ -0,0 +1,284 @@ +package mercedes + +import ( + "encoding/json" + "errors" +) + +type Vehicle struct { + Id string `json:"id"` + LicensePlate string `json:"licenseplate"` + VIN string `json:"finorvin"` +} + +type VehicleDetail struct { + Id string `json:"id"` + LicensePlate string `json:"licenseplate"` + SalesDesignation string `json:"salesdesignation"` + VIN string `json:"finorvin"` + ModelYear string `json:"modelyear"` + ColorName string `json:"colorname"` + FuelType string `json:"fueltype"` + PowerHP string `json:"powerhp"` + PowerKW string `json:"powerkw"` + NumberOfDoors string `json:"numberofdoors"` + NumberOfSeats string `json:"numberofseats"` +} + +type RetrievalStatus string + +const ( + Valid RetrievalStatus = "VALID" + Initialized = "INITIALIZED" + Invalid = "INVALID" + NotSupported = "NOT_SUPPORTED" +) + +func (rs *RetrievalStatus) UnmarshalJSON(b []byte) error { + // Define a secondary type to avoid ending up with a recursive call to json.Unmarshal + type RS RetrievalStatus + var r = (*RS)(rs) + err := json.Unmarshal(b, &r) + if err != nil { + return err + } + + switch *rs { + case Valid, Initialized, Invalid, NotSupported: + return nil + } + + return errors.New("invalid retrieval status") +} + +type PressureUnit string + +const ( + KiloPascal PressureUnit = "KILOPASCAL" +) + +func (pu *PressureUnit) UnmarshalJSON(b []byte) error { + // Define a secondary type to avoid ending up with a recursive call to json.Unmarshal + type PU PressureUnit + var r = (*PU)(pu) + err := json.Unmarshal(b, &r) + if err != nil { + return err + } + + switch *pu { + case KiloPascal: + return nil + } + + return errors.New("invalid pressure unit") +} + +type TirePressureStatus struct { + Unit PressureUnit `json:"unit"` + Value float64 `json:"value"` + RetrievalStatus RetrievalStatus `json:"retrievalstatus"` + Timestamp int64 `json:"timestamp"` +} + +type Tires struct { + TirePressureFrontLeft TirePressureStatus `json:"tirepressurefrontleft"` + TirePressureFrontRight TirePressureStatus `json:"tirepressurefrontright"` + TirePressureRearLeft TirePressureStatus `json:"tirepressurerearleft"` + TirePressureRearRight TirePressureStatus `json:"tirepressurerearright"` +} + +type DoorState string + +const ( + Open DoorState = "OPEN" + Closed = "CLOSED" +) + +func (ds *DoorState) UnmarshalJSON(b []byte) error { + // Define a secondary type to avoid ending up with a recursive call to json.Unmarshal + type DS DoorState + var r = (*DS)(ds) + err := json.Unmarshal(b, &r) + if err != nil { + return err + } + + switch *ds { + case Open, Closed: + return nil + } + + return errors.New("invalid door state") +} + +type DoorOpenStatus struct { + Value DoorState `json:"value"` + RetrievalStatus RetrievalStatus `json:"retrievalstatus"` + Timestamp int64 `json:"timestamp"` +} + +type DoorLockState string + +const ( + Locked DoorLockState = "LOCKED" + Unlocked = "UNLOCKED" +) + +func (ds *DoorLockState) UnmarshalJSON(b []byte) error { + // Define a secondary type to avoid ending up with a recursive call to json.Unmarshal + type DS DoorLockState + var r = (*DS)(ds) + err := json.Unmarshal(b, &r) + if err != nil { + return err + } + + switch *ds { + case Locked, Unlocked: + return nil + } + + return errors.New("invalid door lock state") +} + +type DoorLockStatus struct { + Value DoorLockState `json:"value"` + RetrievalStatus RetrievalStatus `json:"retrievalstatus"` + Timestamp int64 `json:"timestamp"` +} + +type Doors struct { + DoorStatusFrontLeft DoorOpenStatus `json:"doorstatusfrontleft"` + DoorStatusFrontRight DoorOpenStatus `json:"doorstatusfrontright"` + DoorStatusRearLeft DoorOpenStatus `json:"doorstatusrearleft"` + DoorStatusRearRight DoorOpenStatus `json:"doorstatusrearright"` + DoorLockStatusFrontLeft DoorLockStatus `json:"doorlockstatusfrontleft"` + DoorLockStatusFrontRight DoorLockStatus `json:"doorlockstatusfrontright"` + DoorLockStatusRearLeft DoorLockStatus `json:"doorlockstatusrearleft"` + DoorLockStatusRearRight DoorLockStatus `json:"doorlockstatusrearright"` + DoorLockStatusDeckLid DoorLockStatus `json:"doorlockstatusdecklid"` + DoorLockStatusGas DoorLockStatus `json:"doorlockstatusgas"` + DoorLockStatusVehicle DoorLockStatus `json:"doorlockstatusvehicle"` +} + +type DoorLockCommand string + +const ( + Lock DoorLockCommand = "LOCK" + Unlock = "UNLOCK" +) + +func (dl *DoorLockCommand) UnmarshalJSON(b []byte) error { + // Define a secondary type to avoid ending up with a recursive call to json.Unmarshal + type DL DoorLockCommand + var r = (*DL)(dl) + err := json.Unmarshal(b, &r) + if err != nil { + return err + } + + switch *dl { + case Lock, Unlock: + return nil + } + + return errors.New("invalid door lock/unlock command") +} + +type DoorLockChangeRequestBody struct { + Command DoorLockCommand `json:"command"` +} + +type LocationCoordinate struct { + Value float64 `json:"value"` + RetrievalStatus RetrievalStatus `json:"retrievalstatus"` + Timestamp int64 `json:"timestamp"` +} + +type Location struct { + Latitude LocationCoordinate `json:"latitude"` + Longitude LocationCoordinate `json:"longitude"` + Heading LocationCoordinate `json:"heading"` +} + +type DistanceUnit string + +const ( + Kilometers DistanceUnit = "KILOMETERS" +) + +func (du *DistanceUnit) UnmarshalJSON(b []byte) error { + // Define a secondary type to avoid ending up with a recursive call to json.Unmarshal + type DU DistanceUnit + var r = (*DU)(du) + err := json.Unmarshal(b, &r) + if err != nil { + return err + } + + switch *du { + case Kilometers: + return nil + } + + return errors.New("invalid distance unit") +} + +type DistanceDriven struct { + Unit DistanceUnit `json:"unit"` + Value int `json:"value"` + RetrievalStatus RetrievalStatus `json:"retrievalstatus"` + Timestamp int64 `json:"timestamp"` +} + +type DistanceDrivenResponse struct { + Odometer DistanceDriven `json:"odometer"` + DistanceSinceReset DistanceDriven `json:"distancesincereset"` + DistanceSinceStart DistanceDriven `json:"distancesincestart"` +} + +type PercentUnit string + +const ( + Percent PercentUnit = "PERCENT" +) + +func (pu *PercentUnit) UnmarshalJSON(b []byte) error { + // Define a secondary type to avoid ending up with a recursive call to json.Unmarshal + type PU PercentUnit + var r = (*PU)(pu) + err := json.Unmarshal(b, &r) + if err != nil { + return err + } + + switch *pu { + case Percent: + return nil + } + + return errors.New("invalid percent unit") +} + +type FuelLevel struct { + Unit PercentUnit `json:"unit"` + Value int `json:"value"` + RetrievalStatus RetrievalStatus `json:"retrievalstatus"` + Timestamp int64 `json:"timestamp"` +} + +type FuelLevelResponse struct { + FuelLevelPercent FuelLevel `json:"fuellevelpercent"` +} + +type StateOfCharge struct { + Unit PercentUnit `json:"unit"` + Value int `json:"value"` + RetrievalStatus RetrievalStatus `json:"retrievalstatus"` + Timestamp int64 `json:"timestamp"` +} + +type StateOfChargeResponse struct { + StateOfCharge StateOfCharge `json:"stateofcharge"` +} diff --git a/vehicle.go b/vehicle.go new file mode 100644 index 0000000..4345d62 --- /dev/null +++ b/vehicle.go @@ -0,0 +1,60 @@ +package mercedes + +import "context" + +type ConnectedVehicle struct { + VehicleID string + client *ConnectedVehicleClient +} + +func (v *ConnectedVehicle) GetVehicleDetail(ctx context.Context) (VehicleDetail, error) { + var detail VehicleDetail + ret := v.client.getJson(ctx, "/vehicles/"+v.VehicleID, &detail) + return detail, ret +} + +func (v *ConnectedVehicle) GetTirePressure(ctx context.Context) (Tires, error) { + var tires Tires + ret := v.client.getJson(ctx, "/vehicles/"+v.VehicleID+"/tires", &tires) + return tires, ret +} + +func (v *ConnectedVehicle) GetDoorStatus(ctx context.Context) (Doors, error) { + var doors Doors + ret := v.client.getJson(ctx, "/vehicles/"+v.VehicleID+"/doors", &doors) + return doors, ret +} + +func (v *ConnectedVehicle) LockDoors(ctx context.Context) error { + cmd := DoorLockChangeRequestBody{Command: Lock} + return v.client.postJson(ctx, "/vehicles/"+v.VehicleID+"/doors", &cmd) +} + +func (v *ConnectedVehicle) UnlockDoors(ctx context.Context) error { + cmd := DoorLockChangeRequestBody{Command: Unlock} + return v.client.postJson(ctx, "/vehicles/"+v.VehicleID+"/doors", &cmd) +} + +func (v *ConnectedVehicle) GetLocation(ctx context.Context) (Location, error) { + var location Location + ret := v.client.getJson(ctx, "/vehicles/"+v.VehicleID+"/location", &location) + return location, ret +} + +func (v *ConnectedVehicle) GetDistanceDriven(ctx context.Context) (DistanceDrivenResponse, error) { + var response DistanceDrivenResponse + ret := v.client.getJson(ctx, "/vehicles/"+v.VehicleID+"/odometer", &response) + return response, ret +} + +func (v *ConnectedVehicle) GetFuelLevel(ctx context.Context) (FuelLevelResponse, error) { + var level FuelLevelResponse + ret := v.client.getJson(ctx, "/vehicles/"+v.VehicleID+"/fuel", &level) + return level, ret +} + +func (v *ConnectedVehicle) GetStateOfCharge(ctx context.Context) (StateOfChargeResponse, error) { + var state StateOfChargeResponse + ret := v.client.getJson(ctx, "/vehicles/"+v.VehicleID+"/stateofcharge", &state) + return state, ret +}