Renault: refactor structure (#3950)
This commit is contained in:
parent
5398850037
commit
abf4f76568
6 changed files with 509 additions and 452 deletions
|
|
@ -2,18 +2,14 @@ package vehicle
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/provider"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"golang.org/x/exp/slices"
|
||||
"github.com/evcc-io/evcc/vehicle/renault"
|
||||
"github.com/evcc-io/evcc/vehicle/renault/gigya"
|
||||
"github.com/evcc-io/evcc/vehicle/renault/kamereon"
|
||||
"github.com/evcc-io/evcc/vehicle/renault/keys"
|
||||
)
|
||||
|
||||
// Credits to
|
||||
|
|
@ -23,109 +19,10 @@ import (
|
|||
// https://github.com/jamesremuscat/pyze
|
||||
// https://muscatoxblog.blogspot.com/2019/07/delving-into-renaults-new-api.html
|
||||
|
||||
const (
|
||||
keyStore = "https://renault-wrd-prod-1-euw1-myrapp-one.s3-eu-west-1.amazonaws.com/configuration/android/config_%s.json"
|
||||
)
|
||||
|
||||
type configResponse struct {
|
||||
Servers configServers
|
||||
}
|
||||
|
||||
type configServers struct {
|
||||
GigyaProd configServer `json:"gigyaProd"`
|
||||
WiredProd configServer `json:"wiredProd"`
|
||||
}
|
||||
|
||||
type configServer struct {
|
||||
Target string `json:"target"`
|
||||
APIKey string `json:"apikey"`
|
||||
}
|
||||
|
||||
type gigyaResponse struct {
|
||||
ErrorCode int `json:"errorCode"` // /accounts.login
|
||||
ErrorMessage string `json:"errorMessage"` // /accounts.login
|
||||
SessionInfo gigyaSessionInfo `json:"sessionInfo"` // /accounts.login
|
||||
IDToken string `json:"id_token"` // /accounts.getJWT
|
||||
Data gigyaData `json:"data"` // /accounts.getAccountInfo
|
||||
}
|
||||
|
||||
type gigyaSessionInfo struct {
|
||||
CookieValue string `json:"cookieValue"`
|
||||
}
|
||||
|
||||
type gigyaData struct {
|
||||
PersonID string `json:"personId"`
|
||||
}
|
||||
|
||||
type kamereonResponse struct {
|
||||
Accounts []kamereonAccount `json:"accounts"` // /commerce/v1/persons/%s
|
||||
AccessToken string `json:"accessToken"` // /commerce/v1/accounts/%s/kamereon/token
|
||||
VehicleLinks []kamereonVehicle `json:"vehicleLinks"` // /commerce/v1/accounts/%s/vehicles
|
||||
Data kamereonData `json:"data"` // /commerce/v1/accounts/%s/kamereon/kca/car-adapter/vX/cars/%s/...
|
||||
}
|
||||
|
||||
type kamereonAccount struct {
|
||||
AccountID string `json:"accountId"`
|
||||
}
|
||||
|
||||
type kamereonVehicle struct {
|
||||
Brand string `json:"brand"`
|
||||
VIN string `json:"vin"`
|
||||
Status string `json:"status"`
|
||||
ConnectedDriver connectedDriver `json:"ConnectedDriver"`
|
||||
}
|
||||
|
||||
func (v *kamereonVehicle) Available() error {
|
||||
if strings.ToUpper(v.Status) != "ACTIVE" {
|
||||
return errors.New("vehicle is not active")
|
||||
}
|
||||
|
||||
if len(v.ConnectedDriver.Role) == 0 {
|
||||
return errors.New("vehicle is not connected to driver")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type connectedDriver struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type kamereonData struct {
|
||||
Attributes attributes `json:"attributes"`
|
||||
}
|
||||
|
||||
type attributes struct {
|
||||
// battery-status
|
||||
Timestamp string `json:"timestamp"`
|
||||
ChargingStatus float32 `json:"chargingStatus"`
|
||||
InstantaneousPower int `json:"instantaneousPower"`
|
||||
RangeHvacOff int `json:"rangeHvacOff"`
|
||||
BatteryAutonomy int `json:"batteryAutonomy"`
|
||||
BatteryLevel int `json:"batteryLevel"`
|
||||
BatteryTemperature int `json:"batteryTemperature"`
|
||||
PlugStatus int `json:"plugStatus"`
|
||||
LastUpdateTime string `json:"lastUpdateTime"`
|
||||
ChargePower int `json:"chargePower"`
|
||||
RemainingTime *int `json:"chargingRemainingTime"`
|
||||
// hvac-status
|
||||
ExternalTemperature float64 `json:"externalTemperature"`
|
||||
HvacStatus string `json:"hvacStatus"`
|
||||
// cockpit
|
||||
TotalMileage float64 `json:"totalMileage"`
|
||||
}
|
||||
|
||||
// Renault is an api.Vehicle implementation for Renault cars
|
||||
type Renault struct {
|
||||
*embed
|
||||
*request.Helper
|
||||
user, password, vin string
|
||||
gigya, kamereon configServer
|
||||
gigyaJwtToken string
|
||||
accountID string
|
||||
batteryG func() (kamereonResponse, error)
|
||||
cockpitG func() (kamereonResponse, error)
|
||||
hvacG func() (kamereonResponse, error)
|
||||
*renault.Provider
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -151,24 +48,33 @@ func NewRenaultFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
log := util.NewLogger("renault").Redact(cc.User, cc.Password, cc.VIN)
|
||||
|
||||
v := &Renault{
|
||||
embed: &cc.embed,
|
||||
Helper: request.NewHelper(log),
|
||||
user: cc.User,
|
||||
password: cc.Password,
|
||||
embed: &cc.embed,
|
||||
}
|
||||
|
||||
err := v.apiKeys(cc.Region)
|
||||
if err == nil {
|
||||
err = v.authFlow()
|
||||
keys := keys.New(log)
|
||||
keys.Load(cc.Region)
|
||||
|
||||
identity := gigya.NewIdentity(log, keys.Gigya)
|
||||
if err := identity.Login(cc.User, cc.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var car kamereonVehicle
|
||||
api := kamereon.New(log, keys.Kamereon, identity, func() error {
|
||||
return identity.Login(cc.User, cc.Password)
|
||||
})
|
||||
|
||||
accountID, err := api.Person(identity.PersonID)
|
||||
if err == nil && accountID == "" {
|
||||
return nil, errors.New("missing accountID")
|
||||
}
|
||||
|
||||
var car kamereon.Vehicle
|
||||
if err == nil {
|
||||
v.vin, car, err = ensureVehicleWithFeature(cc.VIN,
|
||||
func() ([]kamereonVehicle, error) {
|
||||
return v.kamereonVehicles(v.accountID)
|
||||
cc.VIN, car, err = ensureVehicleWithFeature(cc.VIN,
|
||||
func() ([]kamereon.Vehicle, error) {
|
||||
return api.Vehicles(accountID)
|
||||
},
|
||||
func(v kamereonVehicle) (string, kamereonVehicle) {
|
||||
func(v kamereon.Vehicle) (string, kamereon.Vehicle) {
|
||||
return v.VIN, v
|
||||
},
|
||||
)
|
||||
|
|
@ -178,338 +84,7 @@ func NewRenaultFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
err = car.Available()
|
||||
}
|
||||
|
||||
v.batteryG = provider.Cached(v.batteryAPI, cc.Cache)
|
||||
v.cockpitG = provider.Cached(v.cockpitAPI, cc.Cache)
|
||||
v.hvacG = provider.Cached(v.hvacAPI, cc.Cache)
|
||||
v.Provider = renault.NewProvider(api, accountID, cc.VIN, cc.Cache)
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (v *Renault) apiKeys(region string) error {
|
||||
uri := fmt.Sprintf(keyStore, region)
|
||||
|
||||
var cr configResponse
|
||||
err := v.GetJSON(uri, &cr)
|
||||
if err != nil {
|
||||
// Use of old fixed keys if keyStore is not accessible
|
||||
v.gigya = configServer{"https://accounts.eu1.gigya.com", "3_7PLksOyBRkHv126x5WhHb-5pqC1qFR8pQjxSeLB6nhAnPERTUlwnYoznHSxwX668"}
|
||||
v.kamereon = configServer{"https://api-wired-prod-1-euw1.wrd-aws.com", "VAX7XYKGfa92yMvXculCkEFyfZbuM7Ss"}
|
||||
return nil
|
||||
} else {
|
||||
v.gigya = cr.Servers.GigyaProd
|
||||
v.kamereon = cr.Servers.WiredProd
|
||||
// Temporary fix of wrong kamereon APIKey in keyStore
|
||||
v.kamereon.APIKey = "VAX7XYKGfa92yMvXculCkEFyfZbuM7Ss"
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Renault) authFlow() error {
|
||||
sessionCookie, err := v.sessionCookie(v.user, v.password)
|
||||
|
||||
if err == nil {
|
||||
v.gigyaJwtToken, err = v.jwtToken(sessionCookie)
|
||||
|
||||
if err == nil {
|
||||
if v.accountID != "" {
|
||||
// personID, accountID and VIN have already been read, skip remainder of flow
|
||||
return nil
|
||||
}
|
||||
|
||||
var personID string
|
||||
personID, err = v.personID(sessionCookie)
|
||||
if personID == "" {
|
||||
return errors.New("missing personID")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
v.accountID, err = v.kamereonPerson(personID)
|
||||
if v.accountID == "" {
|
||||
return errors.New("missing accountID")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *Renault) request(uri string, params url.Values, body io.Reader, headers ...map[string]string) (*http.Request, error) {
|
||||
method := http.MethodGet
|
||||
if body != nil {
|
||||
method = http.MethodPost
|
||||
}
|
||||
|
||||
req, err := request.New(method, uri, body, headers...)
|
||||
if err == nil {
|
||||
req.URL.RawQuery = params.Encode()
|
||||
}
|
||||
|
||||
return req, err
|
||||
}
|
||||
|
||||
func (v *Renault) sessionCookie(user, password string) (string, error) {
|
||||
uri := fmt.Sprintf("%s/accounts.login", v.gigya.Target)
|
||||
|
||||
data := url.Values{
|
||||
"loginID": []string{user},
|
||||
"password": []string{password},
|
||||
"apiKey": []string{v.gigya.APIKey},
|
||||
}
|
||||
|
||||
req, err := v.request(uri, data, nil)
|
||||
|
||||
var res gigyaResponse
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
if err == nil && res.ErrorCode > 0 {
|
||||
err = errors.New(res.ErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return res.SessionInfo.CookieValue, err
|
||||
}
|
||||
|
||||
func (v *Renault) personID(sessionCookie string) (string, error) {
|
||||
uri := fmt.Sprintf("%s/accounts.getAccountInfo", v.gigya.Target)
|
||||
|
||||
data := url.Values{
|
||||
"apiKey": []string{v.gigya.APIKey},
|
||||
"login_token": []string{sessionCookie},
|
||||
}
|
||||
|
||||
req, err := v.request(uri, data, nil)
|
||||
|
||||
var res gigyaResponse
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return res.Data.PersonID, err
|
||||
}
|
||||
|
||||
func (v *Renault) jwtToken(sessionCookie string) (string, error) {
|
||||
uri := fmt.Sprintf("%s/accounts.getJWT", v.gigya.Target)
|
||||
|
||||
data := url.Values{
|
||||
"apiKey": []string{v.gigya.APIKey},
|
||||
"login_token": []string{sessionCookie},
|
||||
"fields": []string{"data.personId,data.gigyaDataCenter"},
|
||||
"expiration": []string{"900"},
|
||||
}
|
||||
|
||||
req, err := v.request(uri, data, nil)
|
||||
|
||||
var res gigyaResponse
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return res.IDToken, err
|
||||
}
|
||||
|
||||
func (v *Renault) kamereonRequest(uri string, body io.Reader) (kamereonResponse, error) {
|
||||
params := url.Values{"country": []string{"DE"}}
|
||||
headers := map[string]string{
|
||||
"x-gigya-id_token": v.gigyaJwtToken,
|
||||
"apikey": v.kamereon.APIKey,
|
||||
}
|
||||
|
||||
var res kamereonResponse
|
||||
req, err := v.request(uri, params, body, headers)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (v *Renault) kamereonPerson(personID string) (string, error) {
|
||||
uri := fmt.Sprintf("%s/commerce/v1/persons/%s", v.kamereon.Target, personID)
|
||||
res, err := v.kamereonRequest(uri, nil)
|
||||
|
||||
if len(res.Accounts) == 0 {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return res.Accounts[0].AccountID, err
|
||||
}
|
||||
|
||||
func (v *Renault) kamereonVehicles(configVIN string) ([]kamereonVehicle, error) {
|
||||
uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/vehicles", v.kamereon.Target, v.accountID)
|
||||
res, err := v.kamereonRequest(uri, nil)
|
||||
return res.VehicleLinks, err
|
||||
}
|
||||
|
||||
// batteryAPI provides battery-status api response
|
||||
func (v *Renault) batteryAPI() (kamereonResponse, error) {
|
||||
uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v2/cars/%s/battery-status", v.kamereon.Target, v.accountID, v.vin)
|
||||
res, err := v.kamereonRequest(uri, nil)
|
||||
|
||||
// repeat auth if error
|
||||
if err != nil {
|
||||
if err = v.authFlow(); err == nil {
|
||||
res, err = v.kamereonRequest(uri, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// hvacAPI provides hvac-status api response
|
||||
func (v *Renault) hvacAPI() (kamereonResponse, error) {
|
||||
uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v1/cars/%s/hvac-status", v.kamereon.Target, v.accountID, v.vin)
|
||||
res, err := v.kamereonRequest(uri, nil)
|
||||
|
||||
// repeat auth if error
|
||||
if err != nil {
|
||||
if err = v.authFlow(); err == nil {
|
||||
res, err = v.kamereonRequest(uri, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// cockpitAPI provides cockpit api response
|
||||
func (v *Renault) cockpitAPI() (kamereonResponse, error) {
|
||||
uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v2/cars/%s/cockpit", v.kamereon.Target, v.accountID, v.vin)
|
||||
res, err := v.kamereonRequest(uri, nil)
|
||||
|
||||
// repeat auth if error
|
||||
if err != nil {
|
||||
if err = v.authFlow(); err == nil {
|
||||
res, err = v.kamereonRequest(uri, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// SoC implements the api.Vehicle interface
|
||||
func (v *Renault) SoC() (float64, error) {
|
||||
res, err := v.batteryG()
|
||||
|
||||
if err == nil {
|
||||
return float64(res.Data.Attributes.BatteryLevel), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.ChargeState = (*Renault)(nil)
|
||||
|
||||
// Status implements the api.ChargeState interface
|
||||
func (v *Renault) Status() (api.ChargeStatus, error) {
|
||||
status := api.StatusA // disconnected
|
||||
|
||||
res, err := v.batteryG()
|
||||
if err == nil {
|
||||
if res.Data.Attributes.PlugStatus > 0 {
|
||||
status = api.StatusB
|
||||
}
|
||||
if res.Data.Attributes.ChargingStatus >= 1.0 {
|
||||
status = api.StatusC
|
||||
}
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
var _ api.VehicleRange = (*Renault)(nil)
|
||||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *Renault) Range() (int64, error) {
|
||||
res, err := v.batteryG()
|
||||
|
||||
if err == nil {
|
||||
return int64(res.Data.Attributes.BatteryAutonomy), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.VehicleOdometer = (*Renault)(nil)
|
||||
|
||||
// Odometer implements the api.VehicleOdometer interface
|
||||
func (v *Renault) Odometer() (float64, error) {
|
||||
res, err := v.cockpitG()
|
||||
|
||||
if err == nil {
|
||||
return res.Data.Attributes.TotalMileage, nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.VehicleFinishTimer = (*Renault)(nil)
|
||||
|
||||
// FinishTime implements the api.VehicleFinishTimer interface
|
||||
func (v *Renault) FinishTime() (time.Time, error) {
|
||||
res, err := v.batteryG()
|
||||
|
||||
if err == nil {
|
||||
timestamp, err := time.Parse(time.RFC3339, res.Data.Attributes.Timestamp)
|
||||
|
||||
if res.Data.Attributes.RemainingTime == nil {
|
||||
return time.Time{}, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
return timestamp.Add(time.Duration(*res.Data.Attributes.RemainingTime) * time.Minute), err
|
||||
}
|
||||
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
var _ api.VehicleClimater = (*Renault)(nil)
|
||||
|
||||
// Climater implements the api.VehicleClimater interface
|
||||
func (v *Renault) Climater() (active bool, outsideTemp, targetTemp float64, err error) {
|
||||
res, err := v.hvacG()
|
||||
|
||||
// Zoe Ph2
|
||||
if err, ok := err.(request.StatusError); ok && err.HasStatus(http.StatusForbidden) {
|
||||
return false, 0, 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
state := strings.ToLower(res.Data.Attributes.HvacStatus)
|
||||
|
||||
if state == "" {
|
||||
return false, 0, 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
active := !slices.Contains([]string{"off", "false", "invalid", "error"}, state)
|
||||
|
||||
return active, res.Data.Attributes.ExternalTemperature, 20, nil
|
||||
}
|
||||
|
||||
return false, 0, 0, err
|
||||
}
|
||||
|
||||
var _ api.AlarmClock = (*Renault)(nil)
|
||||
|
||||
// WakeUp implements the api.AlarmClock interface
|
||||
func (v *Renault) WakeUp() error {
|
||||
uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kcm/v1/vehicles/%s/charge/pause-resume", v.kamereon.Target, v.accountID, v.vin)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"type": "ChargePauseResume",
|
||||
"attributes": map[string]interface{}{
|
||||
"action": "resume",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := v.kamereonRequest(uri, request.MarshalJSON(data))
|
||||
|
||||
// repeat auth if error
|
||||
if err != nil {
|
||||
if err = v.authFlow(); err == nil {
|
||||
_, err = v.kamereonRequest(uri, request.MarshalJSON(data))
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
103
vehicle/renault/gigya/identity.go
Normal file
103
vehicle/renault/gigya/identity.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package gigya
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/renault/keys"
|
||||
)
|
||||
|
||||
type gigyaResponse struct {
|
||||
ErrorCode int `json:"errorCode"` // /accounts.login
|
||||
ErrorMessage string `json:"errorMessage"` // /accounts.login
|
||||
SessionInfo gigyaSessionInfo `json:"sessionInfo"` // /accounts.login
|
||||
IDToken string `json:"id_token"` // /accounts.getJWT
|
||||
Data gigyaData `json:"data"` // /accounts.getAccountInfo
|
||||
}
|
||||
|
||||
type gigyaSessionInfo struct {
|
||||
CookieValue string `json:"cookieValue"`
|
||||
}
|
||||
|
||||
type gigyaData struct {
|
||||
PersonID string `json:"personId"`
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
*request.Helper
|
||||
gigya keys.ConfigServer
|
||||
Token string
|
||||
PersonID string
|
||||
}
|
||||
|
||||
func NewIdentity(log *util.Logger, gigya keys.ConfigServer) *Identity {
|
||||
return &Identity{
|
||||
Helper: request.NewHelper(log),
|
||||
gigya: gigya,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Identity) Login(user, password string) error {
|
||||
sessionCookie, err := v.sessionCookie(user, password)
|
||||
|
||||
if err == nil {
|
||||
v.Token, err = v.jwtToken(sessionCookie)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if v.PersonID, err = v.personID(sessionCookie); v.PersonID == "" {
|
||||
err = errors.New("missing personID")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *Identity) sessionCookie(user, password string) (string, error) {
|
||||
data := url.Values{
|
||||
"loginID": []string{user},
|
||||
"password": []string{password},
|
||||
"apiKey": []string{v.gigya.APIKey},
|
||||
}
|
||||
|
||||
var res gigyaResponse
|
||||
uri := fmt.Sprintf("%s/accounts.login?%s", v.gigya.Target, data.Encode())
|
||||
|
||||
err := v.GetJSON(uri, &res)
|
||||
if err == nil && res.ErrorCode > 0 {
|
||||
err = errors.New(res.ErrorMessage)
|
||||
}
|
||||
|
||||
return res.SessionInfo.CookieValue, err
|
||||
}
|
||||
|
||||
func (v *Identity) personID(sessionCookie string) (string, error) {
|
||||
data := url.Values{
|
||||
"apiKey": []string{v.gigya.APIKey},
|
||||
"login_token": []string{sessionCookie},
|
||||
}
|
||||
|
||||
var res gigyaResponse
|
||||
uri := fmt.Sprintf("%s/accounts.getAccountInfo?%s", v.gigya.Target, data.Encode())
|
||||
err := v.GetJSON(uri, &res)
|
||||
|
||||
return res.Data.PersonID, err
|
||||
}
|
||||
|
||||
func (v *Identity) jwtToken(sessionCookie string) (string, error) {
|
||||
data := url.Values{
|
||||
"apiKey": []string{v.gigya.APIKey},
|
||||
"login_token": []string{sessionCookie},
|
||||
"fields": []string{"data.personId,data.gigyaDataCenter"},
|
||||
"expiration": []string{"900"},
|
||||
}
|
||||
|
||||
var res gigyaResponse
|
||||
uri := fmt.Sprintf("%s/accounts.getJWT?%s", v.gigya.Target, data.Encode())
|
||||
err := v.GetJSON(uri, &res)
|
||||
|
||||
return res.IDToken, err
|
||||
}
|
||||
113
vehicle/renault/kamereon/api.go
Normal file
113
vehicle/renault/kamereon/api.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package kamereon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/renault/gigya"
|
||||
"github.com/evcc-io/evcc/vehicle/renault/keys"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
*request.Helper
|
||||
keys keys.ConfigServer
|
||||
identity *gigya.Identity
|
||||
login func() error
|
||||
}
|
||||
|
||||
func New(log *util.Logger, keys keys.ConfigServer, identity *gigya.Identity, login func() error) *API {
|
||||
return &API{
|
||||
Helper: request.NewHelper(log),
|
||||
keys: keys,
|
||||
identity: identity,
|
||||
login: login,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *API) request_(uri string, body io.Reader) (Response, error) {
|
||||
params := url.Values{"country": []string{"DE"}}
|
||||
headers := map[string]string{
|
||||
"x-gigya-id_token": v.identity.Token,
|
||||
"apikey": v.keys.APIKey,
|
||||
}
|
||||
|
||||
method := http.MethodGet
|
||||
if body != nil {
|
||||
method = http.MethodPost
|
||||
}
|
||||
|
||||
var res Response
|
||||
req, err := request.New(method, uri+"?"+params.Encode(), body, headers)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (v *API) request(uri string, body io.Reader) (Response, error) {
|
||||
res, err := v.request_(uri, body)
|
||||
|
||||
// repeat auth if error
|
||||
if err != nil {
|
||||
if err = v.login(); err == nil {
|
||||
res, err = v.request_(uri, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (v *API) Person(personID string) (string, error) {
|
||||
uri := fmt.Sprintf("%s/commerce/v1/persons/%s", v.keys.Target, personID)
|
||||
res, err := v.request(uri, nil)
|
||||
|
||||
if len(res.Accounts) == 0 {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return res.Accounts[0].AccountID, err
|
||||
}
|
||||
|
||||
func (v *API) Vehicles(accountID string) ([]Vehicle, error) {
|
||||
uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/vehicles", v.keys.Target, accountID)
|
||||
res, err := v.request(uri, nil)
|
||||
return res.VehicleLinks, err
|
||||
}
|
||||
|
||||
// Battery provides battery-status api response
|
||||
func (v *API) Battery(accountID string, vin string) (Response, error) {
|
||||
uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v2/cars/%s/battery-status", v.keys.Target, accountID, vin)
|
||||
return v.request(uri, nil)
|
||||
}
|
||||
|
||||
// Hvac provides hvac-status api response
|
||||
func (v *API) Hvac(accountID string, vin string) (Response, error) {
|
||||
uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v1/cars/%s/hvac-status", v.keys.Target, accountID, vin)
|
||||
return v.request(uri, nil)
|
||||
}
|
||||
|
||||
// Cockpit provides cockpit api response
|
||||
func (v *API) Cockpit(accountID string, vin string) (Response, error) {
|
||||
uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v2/cars/%s/cockpit", v.keys.Target, accountID, vin)
|
||||
return v.request(uri, nil)
|
||||
}
|
||||
|
||||
func (v *API) WakeUp(accountID string, vin string) (Response, error) {
|
||||
uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kcm/v1/vehicles/%s/charge/pause-resume", v.keys.Target, accountID, vin)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"type": "ChargePauseResume",
|
||||
"attributes": map[string]interface{}{
|
||||
"action": "resume",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return v.request(uri, request.MarshalJSON(data))
|
||||
}
|
||||
64
vehicle/renault/kamereon/types.go
Normal file
64
vehicle/renault/kamereon/types.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package kamereon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Accounts []Account `json:"accounts"` // /commerce/v1/persons/%s
|
||||
AccessToken string `json:"accessToken"` // /commerce/v1/accounts/%s/kamereon/token
|
||||
VehicleLinks []Vehicle `json:"vehicleLinks"` // /commerce/v1/accounts/%s/vehicles
|
||||
Data Data `json:"data"` // /commerce/v1/accounts/%s/kamereon/kca/car-adapter/vX/cars/%s/...
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
AccountID string `json:"accountId"`
|
||||
}
|
||||
|
||||
type Vehicle struct {
|
||||
Brand string
|
||||
VIN string
|
||||
Status string
|
||||
ConnectedDriver connectedDriver
|
||||
}
|
||||
|
||||
func (v *Vehicle) Available() error {
|
||||
if strings.ToUpper(v.Status) != "ACTIVE" {
|
||||
return errors.New("vehicle is not active")
|
||||
}
|
||||
|
||||
if len(v.ConnectedDriver.Role) == 0 {
|
||||
return errors.New("vehicle is not connected to driver")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type connectedDriver struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
Attributes attributes `json:"attributes"`
|
||||
}
|
||||
|
||||
type attributes struct {
|
||||
// battery-status
|
||||
Timestamp string `json:"timestamp"`
|
||||
ChargingStatus float32 `json:"chargingStatus"`
|
||||
InstantaneousPower int `json:"instantaneousPower"`
|
||||
RangeHvacOff int `json:"rangeHvacOff"`
|
||||
BatteryAutonomy int `json:"batteryAutonomy"`
|
||||
BatteryLevel int `json:"batteryLevel"`
|
||||
BatteryTemperature int `json:"batteryTemperature"`
|
||||
PlugStatus int `json:"plugStatus"`
|
||||
LastUpdateTime string `json:"lastUpdateTime"`
|
||||
ChargePower int `json:"chargePower"`
|
||||
RemainingTime *int `json:"chargingRemainingTime"`
|
||||
// hvac-status
|
||||
ExternalTemperature float64 `json:"externalTemperature"`
|
||||
HvacStatus string `json:"hvacStatus"`
|
||||
// cockpit
|
||||
TotalMileage float64 `json:"totalMileage"`
|
||||
}
|
||||
51
vehicle/renault/keys/keys.go
Normal file
51
vehicle/renault/keys/keys.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package keys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
)
|
||||
|
||||
const keyStore = "https://renault-wrd-prod-1-euw1-myrapp-one.s3-eu-west-1.amazonaws.com/configuration/android/config_%s.json"
|
||||
|
||||
type configResponse struct {
|
||||
Servers configServers
|
||||
}
|
||||
|
||||
type configServers struct {
|
||||
GigyaProd ConfigServer `json:"gigyaProd"`
|
||||
WiredProd ConfigServer `json:"wiredProd"`
|
||||
}
|
||||
|
||||
type ConfigServer struct {
|
||||
Target string `json:"target"`
|
||||
APIKey string `json:"apikey"`
|
||||
}
|
||||
|
||||
type Keys struct {
|
||||
*request.Helper
|
||||
Gigya, Kamereon ConfigServer
|
||||
}
|
||||
|
||||
func New(log *util.Logger) *Keys {
|
||||
return &Keys{
|
||||
Helper: request.NewHelper(log),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Keys) Load(region string) {
|
||||
uri := fmt.Sprintf(keyStore, region)
|
||||
|
||||
var cr configResponse
|
||||
if err := v.GetJSON(uri, &cr); err == nil {
|
||||
v.Gigya = cr.Servers.GigyaProd
|
||||
v.Kamereon = cr.Servers.WiredProd
|
||||
// temporary fix of wrong kamereon APIKey in keyStore
|
||||
v.Kamereon.APIKey = "VAX7XYKGfa92yMvXculCkEFyfZbuM7Ss"
|
||||
} else {
|
||||
// use old fixed keys if keyStore is not accessible
|
||||
v.Gigya = ConfigServer{"https://accounts.eu1.gigya.com", "3_7PLksOyBRkHv126x5WhHb-5pqC1qFR8pQjxSeLB6nhAnPERTUlwnYoznHSxwX668"}
|
||||
v.Kamereon = ConfigServer{"https://api-wired-prod-1-euw1.wrd-aws.com", "VAX7XYKGfa92yMvXculCkEFyfZbuM7Ss"}
|
||||
}
|
||||
}
|
||||
151
vehicle/renault/provider.go
Normal file
151
vehicle/renault/provider.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package renault
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/provider"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/renault/kamereon"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Provider is an api.Vehicle implementation for PSA cars
|
||||
type Provider struct {
|
||||
batteryG func() (kamereon.Response, error)
|
||||
cockpitG func() (kamereon.Response, error)
|
||||
hvacG func() (kamereon.Response, error)
|
||||
wakeup func() (kamereon.Response, error)
|
||||
}
|
||||
|
||||
// NewProvider creates a new vehicle
|
||||
func NewProvider(api *kamereon.API, accountID, vin string, cache time.Duration) *Provider {
|
||||
impl := &Provider{
|
||||
batteryG: provider.Cached(func() (kamereon.Response, error) {
|
||||
return api.Battery(accountID, vin)
|
||||
}, cache),
|
||||
cockpitG: provider.Cached(func() (kamereon.Response, error) {
|
||||
return api.Cockpit(accountID, vin)
|
||||
}, cache),
|
||||
hvacG: provider.Cached(func() (kamereon.Response, error) {
|
||||
return api.Hvac(accountID, vin)
|
||||
}, cache),
|
||||
wakeup: func() (kamereon.Response, error) {
|
||||
return api.WakeUp(accountID, vin)
|
||||
},
|
||||
}
|
||||
return impl
|
||||
}
|
||||
|
||||
var _ api.Battery = (*Provider)(nil)
|
||||
|
||||
// SoC implements the api.Vehicle interface
|
||||
func (v *Provider) SoC() (float64, error) {
|
||||
res, err := v.batteryG()
|
||||
|
||||
if err == nil {
|
||||
return float64(res.Data.Attributes.BatteryLevel), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.ChargeState = (*Provider)(nil)
|
||||
|
||||
// Status implements the api.ChargeState interface
|
||||
func (v *Provider) Status() (api.ChargeStatus, error) {
|
||||
status := api.StatusA // disconnected
|
||||
|
||||
res, err := v.batteryG()
|
||||
if err == nil {
|
||||
if res.Data.Attributes.PlugStatus > 0 {
|
||||
status = api.StatusB
|
||||
}
|
||||
if res.Data.Attributes.ChargingStatus >= 1.0 {
|
||||
status = api.StatusC
|
||||
}
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
var _ api.VehicleRange = (*Provider)(nil)
|
||||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *Provider) Range() (int64, error) {
|
||||
res, err := v.batteryG()
|
||||
|
||||
if err == nil {
|
||||
return int64(res.Data.Attributes.BatteryAutonomy), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.VehicleOdometer = (*Provider)(nil)
|
||||
|
||||
// Odometer implements the api.VehicleOdometer interface
|
||||
func (v *Provider) Odometer() (float64, error) {
|
||||
res, err := v.cockpitG()
|
||||
|
||||
if err == nil {
|
||||
return res.Data.Attributes.TotalMileage, nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.VehicleFinishTimer = (*Provider)(nil)
|
||||
|
||||
// FinishTime implements the api.VehicleFinishTimer interface
|
||||
func (v *Provider) FinishTime() (time.Time, error) {
|
||||
res, err := v.batteryG()
|
||||
|
||||
if err == nil {
|
||||
timestamp, err := time.Parse(time.RFC3339, res.Data.Attributes.Timestamp)
|
||||
|
||||
if res.Data.Attributes.RemainingTime == nil {
|
||||
return time.Time{}, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
return timestamp.Add(time.Duration(*res.Data.Attributes.RemainingTime) * time.Minute), err
|
||||
}
|
||||
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
var _ api.VehicleClimater = (*Provider)(nil)
|
||||
|
||||
// Climater implements the api.VehicleClimater interface
|
||||
func (v *Provider) Climater() (active bool, outsideTemp float64, targetTemp float64, err error) {
|
||||
res, err := v.hvacG()
|
||||
|
||||
// Zoe Ph2
|
||||
if err, ok := err.(request.StatusError); ok && err.HasStatus(http.StatusForbidden) {
|
||||
return false, 0, 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
state := strings.ToLower(res.Data.Attributes.HvacStatus)
|
||||
|
||||
if state == "" {
|
||||
return false, 0, 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
active := !slices.Contains([]string{"off", "false", "invalid", "error"}, state)
|
||||
|
||||
return active, res.Data.Attributes.ExternalTemperature, 20, nil
|
||||
}
|
||||
|
||||
return false, 0, 0, err
|
||||
}
|
||||
|
||||
var _ api.AlarmClock = (*Provider)(nil)
|
||||
|
||||
// WakeUp implements the api.AlarmClock interface
|
||||
func (v *Provider) WakeUp() error {
|
||||
_, err := v.wakeup()
|
||||
return err
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue