162 lines
3.5 KiB
Go
162 lines
3.5 KiB
Go
package psa
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/andig/evcc/util"
|
|
"github.com/andig/evcc/util/request"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
// https://github.com/flobz/psa_car_controller
|
|
// https://developer.groupe-psa.io/webapi/b2c/api-reference/specification
|
|
|
|
// BaseURL is the API base url
|
|
const BaseURL = "https://api.groupe-psa.com/connectedcar/v4"
|
|
|
|
// API is an api.Vehicle implementation for PSA cars
|
|
type API struct {
|
|
*request.Helper
|
|
brand, realm string
|
|
id, secret string // client
|
|
}
|
|
|
|
// NewAPI creates a new vehicle
|
|
func NewAPI(log *util.Logger, brand, realm, id, secret string) *API {
|
|
v := &API{
|
|
Helper: request.NewHelper(log),
|
|
brand: brand,
|
|
realm: realm,
|
|
id: id,
|
|
secret: secret,
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
// Login performs the login
|
|
func (v *API) Login(user, password string) error {
|
|
config := oauth2.Config{
|
|
ClientID: v.id,
|
|
ClientSecret: v.secret,
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: "https://api.mpsa.com/api/connectedcar/v2/oauth/authorize",
|
|
TokenURL: fmt.Sprintf("https://idpcvs.%s/am/oauth2/access_token", v.brand),
|
|
AuthStyle: oauth2.AuthStyleInHeader,
|
|
},
|
|
Scopes: []string{"openid profile"},
|
|
}
|
|
|
|
ctx := context.WithValue(
|
|
context.Background(),
|
|
oauth2.HTTPClient,
|
|
v.Client,
|
|
)
|
|
|
|
// replace client with authenticated oauth client
|
|
token, err := config.PasswordCredentialsToken(ctx, user, password)
|
|
if err == nil {
|
|
v.Client = config.Client(ctx, token)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Vehicle is a single vehicle
|
|
type Vehicle struct {
|
|
ID string `json:"id"`
|
|
Label string `json:"label"`
|
|
Pictures []string `json:"pictures"`
|
|
VIN string `json:"vin"`
|
|
}
|
|
|
|
// Vehicles implements the /vehicles response
|
|
func (v *API) Vehicles() ([]Vehicle, error) {
|
|
data := url.Values{
|
|
"client_id": []string{v.id},
|
|
}
|
|
|
|
uri := fmt.Sprintf("%s/user/vehicles?%s", BaseURL, data.Encode())
|
|
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
|
|
"Accept": "application/hal+json",
|
|
"X-Introspect-Realm": v.realm,
|
|
})
|
|
|
|
var res struct {
|
|
Embedded struct {
|
|
Vehicles []Vehicle
|
|
} `json:"_embedded"`
|
|
}
|
|
if err == nil {
|
|
err = v.DoJSON(req, &res)
|
|
}
|
|
|
|
return res.Embedded.Vehicles, err
|
|
}
|
|
|
|
// Status is the /status response
|
|
type Status struct {
|
|
Battery struct {
|
|
Capacity int64
|
|
Health struct {
|
|
Capacity int64
|
|
Resistance int64
|
|
}
|
|
}
|
|
Charging struct {
|
|
ChargingMode string
|
|
ChargingRate int64
|
|
NextDelayedTime string
|
|
Plugged bool
|
|
RemainingTime string
|
|
Status string
|
|
}
|
|
Preconditionning struct {
|
|
AirConditioning struct {
|
|
UpdatedAt time.Time
|
|
Status string // Disabled
|
|
}
|
|
}
|
|
Energy []Energy
|
|
}
|
|
|
|
// Energy is the /status partial energy response
|
|
type Energy struct {
|
|
UpdatedAt time.Time
|
|
Type string // Electric
|
|
Level int
|
|
Autonomy int
|
|
Charging struct {
|
|
Plugged bool
|
|
Status string // InProgress
|
|
RemainingTime Duration
|
|
ChargingRate int
|
|
ChargingMode string // "Slow"
|
|
NextDelayedTime Duration
|
|
}
|
|
}
|
|
|
|
// Status implements the /vehicles/<vid>/status response
|
|
func (v *API) Status(vid string) (Status, error) {
|
|
data := url.Values{
|
|
"client_id": []string{v.id},
|
|
}
|
|
|
|
// BaseURL is the API base url
|
|
uri := fmt.Sprintf("%s/user/vehicles/%s/status?%s", BaseURL, vid, data.Encode())
|
|
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
|
|
"Accept": "application/hal+json",
|
|
"X-Introspect-Realm": v.realm,
|
|
})
|
|
|
|
var status Status
|
|
if err == nil {
|
|
err = v.DoJSON(req, &status)
|
|
}
|
|
|
|
return status, err
|
|
}
|