From 075c72895927e12717dae49a7bfc79f4b0b54577 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 1 Feb 2022 22:01:28 +0100 Subject: [PATCH 1/7] Add Jaguar/Landrover api --- vehicle/ford/identity.go | 2 +- vehicle/jlr.go | 104 +++++++++++++++++++++++++++++++++++++ vehicle/jlr/api.go | 109 +++++++++++++++++++++++++++++++++++++++ vehicle/jlr/identity.go | 77 +++++++++++++++++++++++++++ vehicle/jlr/provider.go | 77 +++++++++++++++++++++++++++ vehicle/jlr/types.go | 66 ++++++++++++++++++++++++ 6 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 vehicle/jlr.go create mode 100644 vehicle/jlr/api.go create mode 100644 vehicle/jlr/identity.go create mode 100644 vehicle/jlr/provider.go create mode 100644 vehicle/jlr/types.go diff --git a/vehicle/ford/identity.go b/vehicle/ford/identity.go index ec19eb2f8..f03a1fae2 100644 --- a/vehicle/ford/identity.go +++ b/vehicle/ford/identity.go @@ -82,7 +82,7 @@ func (v *Identity) login() (oauth.Token, error) { return token, err } -// Refresh implements oauth.TokenRefresher +// RefreshToken implements oauth.TokenRefresher func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { data := map[string]string{ "refresh_token": token.RefreshToken, diff --git a/vehicle/jlr.go b/vehicle/jlr.go new file mode 100644 index 000000000..e3a8f361f --- /dev/null +++ b/vehicle/jlr.go @@ -0,0 +1,104 @@ +package vehicle + +import ( + "fmt" + "net/http" + "net/url" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/vehicle/jlr" +) + +// https://github.com/ardevd/jlrpy + +// JLR is an api.Vehicle implementation for Jaguar LandRover cars +type JLR struct { + *embed + // *JLR.Provider + *request.Helper + user, password string +} + +func init() { + registry.Add("jaguar", NewJLRFromConfig) + registry.Add("landrover", NewJLRFromConfig) +} + +// NewJLRFromConfig creates a new vehicle +func NewJLRFromConfig(other map[string]interface{}) (api.Vehicle, error) { + cc := struct { + embed `mapstructure:",squash"` + User, Password, VIN string + DeviceID string + Expiry time.Duration + Cache time.Duration + }{ + Expiry: expiry, + Cache: interval, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + if cc.User == "" || cc.Password == "" { + return nil, api.ErrMissingCredentials + } + + v := &JLR{ + embed: &cc.embed, + } + + log := util.NewLogger("jlr").Redact(cc.User, cc.Password, cc.VIN, cc.DeviceID) + + // uid := uuid.New() + // deviceId := uid.String() + // fmt.Println("use this deviceid:", deviceId) + cc.DeviceID = "d565375c-49a1-4b3d-93f6-79044033c414" + + identity := jlr.NewIdentity(log, cc.User, cc.Password, cc.DeviceID) + + err := identity.Login() + if err != nil { + return nil, fmt.Errorf("login failed: %w", err) + } + + api := jlr.NewAPI(log, cc.DeviceID, identity) + + user, err := api.User(v.user) + if err != nil { + return nil, fmt.Errorf("login failed: %w", err) + } + + cc.VIN, err = ensureVehicle(cc.VIN, func() ([]string, error) { + return api.Vehicles(user.UserId) + }) + + // if err == nil { + // v.Provider = jlr.NewProvider(api, cc.VIN, cc.Expiry, cc.Cache) + // } + + return v, err +} + +func (v *JLR) RegisterDevice(device string) error { + var t jlr.Token + + data := map[string]string{ + "access_token": t.AccessToken, + "authorization_token": t.AuthToken, + "expires_in": "86400", + "deviceID": device} + + uri := fmt.Sprintf("%s/users/%s/clients", jlr.IFOP_BASE_URL, url.PathEscape(v.user)) + + req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + if err == nil { + err = v.DoJSON(req, nil) + } + + return err +} diff --git a/vehicle/jlr/api.go b/vehicle/jlr/api.go new file mode 100644 index 000000000..15672fe8b --- /dev/null +++ b/vehicle/jlr/api.go @@ -0,0 +1,109 @@ +package jlr + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" + "golang.org/x/oauth2" +) + +const ( + IF9_BASE_URL = "https://if9.prod-row.jlrmotor.com/if9/jlr" + IFOP_BASE_URL = "https://ifop.prod-row.jlrmotor.com/ifop/jlr" +) + +// API is the Jaguar/Landrover api client +type API struct { + *request.Helper +} + +// NewAPI creates a new api client +func NewAPI(log *util.Logger, device string, ts oauth2.TokenSource) *API { + v := &API{ + Helper: request.NewHelper(log), + } + + // v.Client.Transport = &transport.Decorator{ + // Base: &oauth2.Transport{ + // Source: oauth2.StaticTokenSource(&t.Token), + // Base: v.Transport, + // }, + // Decorator: transport.DecorateHeaders(map[string]string{ + // "X-Device-Id": device, + // "x-telematicsprogramtype": "jlrpy", + // }), + // } + + v.Client.Transport = &transport.Decorator{ + Decorator: func(req *http.Request) error { + token, err := ts.Token() + if err == nil { + for k, v := range map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", token.AccessToken), + "Content-type": request.JSONContent, + "X-Device-Id": device, + "x-telematicsprogramtype": "jlrpy", + } { + req.Header.Set(k, v) + } + } + return err + }, + Base: v.Client.Transport, + } + + return v +} + +func (v *API) User(name string) (User, error) { + var res User + + uri := fmt.Sprintf("%s/users?loginName=%s", IF9_BASE_URL, url.QueryEscape(name)) + req, err := request.New(http.MethodGet, uri, nil, map[string]string{ + "Content-Type": "application/json", + "Accept": "application/vnd.wirelesscar.ngtp.if9.User-v3+json", + }) + + if err == nil { + err = v.DoJSON(req, &res) + } + + return res, err +} + +func (v *API) Vehicles(user string) ([]string, error) { + var vehicles []string + var resp VehiclesResponse + + uri := fmt.Sprintf("%s/users/%s/vehicles?primaryOnly=true", IF9_BASE_URL, user) + + err := v.GetJSON(uri, &resp) + if err == nil { + for _, v := range resp.Vehicles { + vehicles = append(vehicles, v.VIN) + } + } + + return vehicles, nil +} + +// Status returns the vehicle status +func (v *API) Status(vin string) (StatusResponse, error) { + var status StatusResponse + + uri := fmt.Sprintf("%s/vehicles/%s/status?includeInactive=true", IF9_BASE_URL, vin) + req, err := request.New(http.MethodGet, uri, nil, map[string]string{ + "Content-Type": "application/json", + "Accept": "application/vnd.ngtp.org.if9.healthstatus-v3+json", + }) + + if err == nil { + err = v.DoJSON(req, &status) + } + + return status, err +} diff --git a/vehicle/jlr/identity.go b/vehicle/jlr/identity.go new file mode 100644 index 000000000..3eba25460 --- /dev/null +++ b/vehicle/jlr/identity.go @@ -0,0 +1,77 @@ +package jlr + +import ( + "fmt" + "net/http" + + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/oauth" + "github.com/evcc-io/evcc/util/request" + "golang.org/x/oauth2" +) + +// https://github.com/ardevd/jlrpy + +const ( + IFAS_BASE_URL = "https://ifas.prod-row.jlrmotor.com/ifas/jlr" +) + +type Identity struct { + *request.Helper + user, password, device string + oauth2.TokenSource +} + +// NewIdentity creates Fiat identity +func NewIdentity(log *util.Logger, user, password, device string) *Identity { + return &Identity{ + Helper: request.NewHelper(log), + user: user, + password: password, + device: device, + } +} + +func (v *Identity) login(data map[string]string) (Token, error) { + uri := fmt.Sprintf("%s/tokens", IFAS_BASE_URL) + req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), map[string]string{ + "Authorization": "Basic YXM6YXNwYXNz", + "Content-type": request.JSONContent, + "X-Device-Id": v.device, + }) + + var token Token + if err == nil { + err = v.DoJSON(req, &token) + } + + return token, err +} + +// Login authenticates with username/password to get new aws credentials +func (v *Identity) Login() error { + data := map[string]string{ + "grant_type": "password", + "username": v.user, + "password": v.password, + } + + token, err := v.login(data) + if err == nil { + v.TokenSource = oauth.RefreshTokenSource(&token.Token, v) + } + + return err +} + +// RefreshToken implements oauth.TokenRefresher +func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { + data := map[string]string{ + "grant_type": "refresh_token", + "refresh_token": token.RefreshToken, + } + + res, err := v.login(data) + + return &res.Token, err +} diff --git a/vehicle/jlr/provider.go b/vehicle/jlr/provider.go new file mode 100644 index 000000000..91feab9a8 --- /dev/null +++ b/vehicle/jlr/provider.go @@ -0,0 +1,77 @@ +package jlr + +import ( + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/provider" +) + +type Provider struct { + statusG func() (interface{}, error) +} + +func NewProvider(api *API, vin string, expiry, cache time.Duration) *Provider { + impl := &Provider{ + statusG: provider.NewCached(func() (interface{}, error) { + return api.Status(vin) + }, cache).InterfaceGetter(), + } + + return impl +} + +var _ api.Battery = (*Provider)(nil) + +// SoC implements the api.Battery interface +func (v *Provider) SoC() (float64, error) { + res, err := v.statusG() + if res, ok := res.(StatusResponse); err == nil && ok { + return res.VehicleStatus.BatteryFillLevel.Value, nil + } + + return 0, err +} + +var _ api.VehicleRange = (*Provider)(nil) + +// Range implements the api.VehicleRange interface +func (v *Provider) Range() (int64, error) { + res, err := v.statusG() + if res, ok := res.(StatusResponse); err == nil && ok { + return int64(res.VehicleStatus.ElVehDTE.Value), 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.statusG() +// if res, ok := res.(StatusResponse); err == nil && ok { +// if res.VehicleStatus.PlugStatus.Value == 1 { +// status = api.StatusB // connected, not charging +// } +// if res.VehicleStatus.ChargingStatus.Value == "ChargingAC" { +// status = api.StatusC // charging +// } +// } + +// return status, err +// } + +// var _ api.VehicleOdometer = (*Provider)(nil) + +// // Odometer implements the api.VehicleOdometer interface +// func (v *Provider) Odometer() (float64, error) { +// res, err := v.statusG() +// if res, ok := res.(StatusResponse); err == nil && ok { +// return res.VehicleStatus.Odometer.Value, nil +// } + +// return 0, err +// } diff --git a/vehicle/jlr/types.go b/vehicle/jlr/types.go new file mode 100644 index 000000000..133dc8b9b --- /dev/null +++ b/vehicle/jlr/types.go @@ -0,0 +1,66 @@ +package jlr + +import ( + "errors" + "strconv" + + "golang.org/x/oauth2" +) + +type Token struct { + AuthToken string `json:"authorization_token"` + oauth2.Token +} + +type User struct { + HomeMarket string `json:"homeMarket"` + UserId string `json:"userId"` +} + +type Vehicle struct { + UserId string `json:"userId"` + VIN string `json:"vin"` + Role string `json:"role"` +} + +type VehiclesResponse struct { + Vehicles []Vehicle +} + +type KeyValue struct { + Key, Value string +} + +type KeyValueList []KeyValue + +type StatusResponse struct { + VehicleStatus struct { + CoreStatus KeyValueList + EvStatus KeyValueList + } +} + +func (l KeyValueList) StringVal(key string) (string, error) { + for _, el := range l { + if el.Key == key { + return el.Value, nil + } + } + return "", errors.New("key not found") +} + +func (l KeyValueList) FloatVal(key string) (float64, error) { + s, err := l.StringVal(key) + if err != nil { + return 0, err + } + return strconv.ParseFloat(s, 64) +} + +func (l KeyValueList) IntVal(key string) (int64, error) { + s, err := l.StringVal(key) + if err != nil { + return 0, err + } + return strconv.ParseInt(s, 10, 64) +} From 0e3e47976859f5a029fb3b47ef16406d79918130 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 2 Feb 2022 12:37:16 +0100 Subject: [PATCH 2/7] wip --- vehicle/jlr.go | 52 ++++++++++++++++++++------------- vehicle/jlr/identity.go | 6 ++-- vehicle/jlr/provider.go | 64 ++++++++++++++++++++++------------------- vehicle/jlr/types.go | 1 + 4 files changed, 72 insertions(+), 51 deletions(-) diff --git a/vehicle/jlr.go b/vehicle/jlr.go index e3a8f361f..81c6d454b 100644 --- a/vehicle/jlr.go +++ b/vehicle/jlr.go @@ -10,6 +10,7 @@ import ( "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/vehicle/jlr" + "github.com/google/uuid" ) // https://github.com/ardevd/jlrpy @@ -17,9 +18,7 @@ import ( // JLR is an api.Vehicle implementation for Jaguar LandRover cars type JLR struct { *embed - // *JLR.Provider - *request.Helper - user, password string + *jlr.Provider } func init() { @@ -52,23 +51,30 @@ func NewJLRFromConfig(other map[string]interface{}) (api.Vehicle, error) { embed: &cc.embed, } - log := util.NewLogger("jlr").Redact(cc.User, cc.Password, cc.VIN, cc.DeviceID) - - // uid := uuid.New() - // deviceId := uid.String() - // fmt.Println("use this deviceid:", deviceId) - cc.DeviceID = "d565375c-49a1-4b3d-93f6-79044033c414" - + log := util.NewLogger("jlr") + // .Redact(cc.User, cc.Password, cc.VIN, cc.DeviceID) identity := jlr.NewIdentity(log, cc.User, cc.Password, cc.DeviceID) - err := identity.Login() + token, err := identity.Login() if err != nil { return nil, fmt.Errorf("login failed: %w", err) } + // cc.DeviceID = "d565375c-49a1-4b3d-93f6-79044033c414" + if cc.DeviceID == "" { + uid := uuid.New() + cc.DeviceID = uid.String() + + if err := v.RegisterDevice(log, cc.User, cc.DeviceID, token); err != nil { + return nil, fmt.Errorf("device registry failed: %w", err) + } + + log.WARN.Println("new device id registered, add to config:", cc.DeviceID) + } + api := jlr.NewAPI(log, cc.DeviceID, identity) - user, err := api.User(v.user) + user, err := api.User(cc.User) if err != nil { return nil, fmt.Errorf("login failed: %w", err) } @@ -77,15 +83,15 @@ func NewJLRFromConfig(other map[string]interface{}) (api.Vehicle, error) { return api.Vehicles(user.UserId) }) - // if err == nil { - // v.Provider = jlr.NewProvider(api, cc.VIN, cc.Expiry, cc.Cache) - // } + if err == nil { + v.Provider = jlr.NewProvider(api, cc.VIN, cc.Cache) + } return v, err } -func (v *JLR) RegisterDevice(device string) error { - var t jlr.Token +func (v *JLR) RegisterDevice(log *util.Logger, user, device string, t jlr.Token) error { + c := request.NewHelper(log) data := map[string]string{ "access_token": t.AccessToken, @@ -93,11 +99,17 @@ func (v *JLR) RegisterDevice(device string) error { "expires_in": "86400", "deviceID": device} - uri := fmt.Sprintf("%s/users/%s/clients", jlr.IFOP_BASE_URL, url.PathEscape(v.user)) + uri := fmt.Sprintf("%s/users/%s/clients", jlr.IFOP_BASE_URL, url.PathEscape(user)) - req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), map[string]string{ + "Authorization": "Bearer " + t.AccessToken, + "Content-type": "application/json", + "Accept": "application/json", + "X-Device-Id": device, + "x-telematicsprogramtype": "jlrpy", + }) if err == nil { - err = v.DoJSON(req, nil) + _, err = c.DoBody(req) } return err diff --git a/vehicle/jlr/identity.go b/vehicle/jlr/identity.go index 3eba25460..0a7cb9fdf 100644 --- a/vehicle/jlr/identity.go +++ b/vehicle/jlr/identity.go @@ -3,6 +3,7 @@ package jlr import ( "fmt" "net/http" + "time" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/oauth" @@ -43,13 +44,14 @@ func (v *Identity) login(data map[string]string) (Token, error) { var token Token if err == nil { err = v.DoJSON(req, &token) + token.Expiry = time.Now().Add(time.Duration(token.ExpiresIn) * time.Second) } return token, err } // Login authenticates with username/password to get new aws credentials -func (v *Identity) Login() error { +func (v *Identity) Login() (Token, error) { data := map[string]string{ "grant_type": "password", "username": v.user, @@ -61,7 +63,7 @@ func (v *Identity) Login() error { v.TokenSource = oauth.RefreshTokenSource(&token.Token, v) } - return err + return token, err } // RefreshToken implements oauth.TokenRefresher diff --git a/vehicle/jlr/provider.go b/vehicle/jlr/provider.go index 91feab9a8..bfb15a26b 100644 --- a/vehicle/jlr/provider.go +++ b/vehicle/jlr/provider.go @@ -11,7 +11,7 @@ type Provider struct { statusG func() (interface{}, error) } -func NewProvider(api *API, vin string, expiry, cache time.Duration) *Provider { +func NewProvider(api *API, vin string, cache time.Duration) *Provider { impl := &Provider{ statusG: provider.NewCached(func() (interface{}, error) { return api.Status(vin) @@ -25,53 +25,59 @@ var _ api.Battery = (*Provider)(nil) // SoC implements the api.Battery interface func (v *Provider) SoC() (float64, error) { + var val float64 res, err := v.statusG() if res, ok := res.(StatusResponse); err == nil && ok { - return res.VehicleStatus.BatteryFillLevel.Value, nil + val, err = res.VehicleStatus.EvStatus.FloatVal("EV_RANGE_VSC_INITIAL_HV_BATT_ENERGYx100") } - return 0, err + return val, err } var _ api.VehicleRange = (*Provider)(nil) // Range implements the api.VehicleRange interface func (v *Provider) Range() (int64, error) { + var val int64 res, err := v.statusG() if res, ok := res.(StatusResponse); err == nil && ok { - return int64(res.VehicleStatus.ElVehDTE.Value), nil + val, err = res.VehicleStatus.EvStatus.IntVal("EV_RANGE_ON_BATTERY_KM") } - return 0, err + return val, err } -// var _ api.ChargeState = (*Provider)(nil) +var _ api.ChargeState = (*Provider)(nil) -// // Status implements the api.ChargeState interface -// func (v *Provider) Status() (api.ChargeStatus, error) { -// status := api.StatusA // disconnected +// Status implements the api.ChargeState interface +func (v *Provider) Status() (api.ChargeStatus, error) { + status := api.StatusA // disconnected -// res, err := v.statusG() -// if res, ok := res.(StatusResponse); err == nil && ok { -// if res.VehicleStatus.PlugStatus.Value == 1 { -// status = api.StatusB // connected, not charging -// } -// if res.VehicleStatus.ChargingStatus.Value == "ChargingAC" { -// status = api.StatusC // charging -// } -// } + res, err := v.statusG() + if res, ok := res.(StatusResponse); err == nil && ok { + if s, err := res.VehicleStatus.EvStatus.StringVal("EV_IS_PLUGGED_IN"); err == nil && s == "CONNECTED" { + // fmt.Println("EV_IS_PLUGGED_IN", s, err) + status = api.StatusB + } + + if s, err := res.VehicleStatus.EvStatus.StringVal("EV_CHARGING_STATUS"); err == nil && s == "CHARGING" { + // fmt.Println("EV_CHARGING_STATUS", s, err) + status = api.StatusC + } + } -// return status, err -// } + return status, err +} -// var _ api.VehicleOdometer = (*Provider)(nil) +var _ api.VehicleOdometer = (*Provider)(nil) -// // Odometer implements the api.VehicleOdometer interface -// func (v *Provider) Odometer() (float64, error) { -// res, err := v.statusG() -// if res, ok := res.(StatusResponse); err == nil && ok { -// return res.VehicleStatus.Odometer.Value, nil -// } +// Odometer implements the api.VehicleOdometer interface +func (v *Provider) Odometer() (float64, error) { + var val float64 + res, err := v.statusG() + if res, ok := res.(StatusResponse); err == nil && ok { + val, err = res.VehicleStatus.CoreStatus.FloatVal("ODOMETER") + } -// return 0, err -// } + return val / 1e3, err +} diff --git a/vehicle/jlr/types.go b/vehicle/jlr/types.go index 133dc8b9b..0163c2272 100644 --- a/vehicle/jlr/types.go +++ b/vehicle/jlr/types.go @@ -9,6 +9,7 @@ import ( type Token struct { AuthToken string `json:"authorization_token"` + ExpiresIn int `json:"expires_in,string"` oauth2.Token } From 7556d11b010d7b2a4627a4e2124d006414bee3be Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 2 Feb 2022 12:46:18 +0100 Subject: [PATCH 3/7] wip --- vehicle/jlr/provider.go | 15 ++++++++++++++- vehicle/jlr/types.go | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/vehicle/jlr/provider.go b/vehicle/jlr/provider.go index bfb15a26b..df8b06ce8 100644 --- a/vehicle/jlr/provider.go +++ b/vehicle/jlr/provider.go @@ -28,7 +28,7 @@ func (v *Provider) SoC() (float64, error) { var val float64 res, err := v.statusG() if res, ok := res.(StatusResponse); err == nil && ok { - val, err = res.VehicleStatus.EvStatus.FloatVal("EV_RANGE_VSC_INITIAL_HV_BATT_ENERGYx100") + val, err = res.VehicleStatus.EvStatus.FloatVal("EV_STATE_OF_CHARGE") } return val, err @@ -69,6 +69,19 @@ func (v *Provider) Status() (api.ChargeStatus, error) { return status, err } +var _ api.VehicleFinishTimer = (*Provider)(nil) + +// FinishTime implements the api.VehicleFinishTimer interface +func (v *Provider) FinishTime() (time.Time, error) { + res, err := v.statusG() + if res, ok := res.(StatusResponse); err == nil && ok { + i, err := res.VehicleStatus.CoreStatus.IntVal("EV_MINUTES_TO_FULLY_CHARGED") + return time.Now().Add(time.Duration(i) * time.Minute), err + } + + return time.Time{}, err +} + var _ api.VehicleOdometer = (*Provider)(nil) // Odometer implements the api.VehicleOdometer interface diff --git a/vehicle/jlr/types.go b/vehicle/jlr/types.go index 0163c2272..d66cea86f 100644 --- a/vehicle/jlr/types.go +++ b/vehicle/jlr/types.go @@ -1,9 +1,9 @@ package jlr import ( - "errors" "strconv" + "github.com/evcc-io/evcc/api" "golang.org/x/oauth2" ) @@ -47,7 +47,7 @@ func (l KeyValueList) StringVal(key string) (string, error) { return el.Value, nil } } - return "", errors.New("key not found") + return "", api.ErrNotAvailable } func (l KeyValueList) FloatVal(key string) (float64, error) { From 7fb10080ce79283137bbdcba8724b605c9bfbdf1 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 2 Feb 2022 13:48:35 +0100 Subject: [PATCH 4/7] wip --- vehicle/jlr.go | 22 ++++++++++------------ vehicle/jlr/identity.go | 7 +++---- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/vehicle/jlr.go b/vehicle/jlr.go index 81c6d454b..c3e1e1ef4 100644 --- a/vehicle/jlr.go +++ b/vehicle/jlr.go @@ -51,8 +51,14 @@ func NewJLRFromConfig(other map[string]interface{}) (api.Vehicle, error) { embed: &cc.embed, } - log := util.NewLogger("jlr") - // .Redact(cc.User, cc.Password, cc.VIN, cc.DeviceID) + log := util.NewLogger("jlr").Redact(cc.User, cc.Password, cc.VIN, cc.DeviceID) + + if cc.DeviceID == "" { + uid := uuid.New() + cc.DeviceID = uid.String() + log.WARN.Println("new device id generated, add `deviceid` to config:", cc.DeviceID) + } + identity := jlr.NewIdentity(log, cc.User, cc.Password, cc.DeviceID) token, err := identity.Login() @@ -60,16 +66,8 @@ func NewJLRFromConfig(other map[string]interface{}) (api.Vehicle, error) { return nil, fmt.Errorf("login failed: %w", err) } - // cc.DeviceID = "d565375c-49a1-4b3d-93f6-79044033c414" - if cc.DeviceID == "" { - uid := uuid.New() - cc.DeviceID = uid.String() - - if err := v.RegisterDevice(log, cc.User, cc.DeviceID, token); err != nil { - return nil, fmt.Errorf("device registry failed: %w", err) - } - - log.WARN.Println("new device id registered, add to config:", cc.DeviceID) + if err := v.RegisterDevice(log, cc.User, cc.DeviceID, token); err != nil { + return nil, fmt.Errorf("device registry failed: %w", err) } api := jlr.NewAPI(log, cc.DeviceID, identity) diff --git a/vehicle/jlr/identity.go b/vehicle/jlr/identity.go index 0a7cb9fdf..c04c68523 100644 --- a/vehicle/jlr/identity.go +++ b/vehicle/jlr/identity.go @@ -13,9 +13,7 @@ import ( // https://github.com/ardevd/jlrpy -const ( - IFAS_BASE_URL = "https://ifas.prod-row.jlrmotor.com/ifas/jlr" -) +const IFAS_BASE_URL = "https://ifas.prod-row.jlrmotor.com/ifas/jlr" type Identity struct { *request.Helper @@ -33,6 +31,7 @@ func NewIdentity(log *util.Logger, user, password, device string) *Identity { } } +// Login authenticates with given payload func (v *Identity) login(data map[string]string) (Token, error) { uri := fmt.Sprintf("%s/tokens", IFAS_BASE_URL) req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), map[string]string{ @@ -50,7 +49,7 @@ func (v *Identity) login(data map[string]string) (Token, error) { return token, err } -// Login authenticates with username/password to get new aws credentials +// Login authenticates with username/password func (v *Identity) Login() (Token, error) { data := map[string]string{ "grant_type": "password", From d39c05f5956ff0764c480d9ab7ebf83368c95113 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 2 Feb 2022 15:40:07 +0100 Subject: [PATCH 5/7] wip --- vehicle/jlr/api.go | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/vehicle/jlr/api.go b/vehicle/jlr/api.go index 15672fe8b..216a62b2a 100644 --- a/vehicle/jlr/api.go +++ b/vehicle/jlr/api.go @@ -27,24 +27,13 @@ func NewAPI(log *util.Logger, device string, ts oauth2.TokenSource) *API { Helper: request.NewHelper(log), } - // v.Client.Transport = &transport.Decorator{ - // Base: &oauth2.Transport{ - // Source: oauth2.StaticTokenSource(&t.Token), - // Base: v.Transport, - // }, - // Decorator: transport.DecorateHeaders(map[string]string{ - // "X-Device-Id": device, - // "x-telematicsprogramtype": "jlrpy", - // }), - // } - v.Client.Transport = &transport.Decorator{ Decorator: func(req *http.Request) error { token, err := ts.Token() if err == nil { for k, v := range map[string]string{ "Authorization": fmt.Sprintf("Bearer %s", token.AccessToken), - "Content-type": request.JSONContent, + "Content-Type": request.JSONContent, "X-Device-Id": device, "x-telematicsprogramtype": "jlrpy", } { @@ -64,7 +53,7 @@ func (v *API) User(name string) (User, error) { uri := fmt.Sprintf("%s/users?loginName=%s", IF9_BASE_URL, url.QueryEscape(name)) req, err := request.New(http.MethodGet, uri, nil, map[string]string{ - "Content-Type": "application/json", + "Content-Type": request.JSONContent, "Accept": "application/vnd.wirelesscar.ngtp.if9.User-v3+json", }) From a5d7f97ab0b99cc819e474f9f05a9c51d2b47d26 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 2 Feb 2022 15:53:37 +0100 Subject: [PATCH 6/7] wip --- vehicle/jlr/provider.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/vehicle/jlr/provider.go b/vehicle/jlr/provider.go index df8b06ce8..b1eee2597 100644 --- a/vehicle/jlr/provider.go +++ b/vehicle/jlr/provider.go @@ -55,14 +55,15 @@ func (v *Provider) Status() (api.ChargeStatus, error) { res, err := v.statusG() if res, ok := res.(StatusResponse); err == nil && ok { - if s, err := res.VehicleStatus.EvStatus.StringVal("EV_IS_PLUGGED_IN"); err == nil && s == "CONNECTED" { - // fmt.Println("EV_IS_PLUGGED_IN", s, err) - status = api.StatusB - } - - if s, err := res.VehicleStatus.EvStatus.StringVal("EV_CHARGING_STATUS"); err == nil && s == "CHARGING" { - // fmt.Println("EV_CHARGING_STATUS", s, err) - status = api.StatusC + if s, err := res.VehicleStatus.EvStatus.StringVal("EV_CHARGING_STATUS"); err == nil { + switch s { + case "NOTCONNECTED": + status = api.StatusA + case "INITIALIZATION", "PAUSED": + status = api.StatusB + case "CHARGING": + status = api.StatusC + } } } From 08a459b79f894c74f017a94e489ba71fd6c87ec2 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 2 Feb 2022 15:55:29 +0100 Subject: [PATCH 7/7] wip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 086dc0cdd..232e0075a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Evcc is an extensible EV Charge Controller with PV integration implemented in [G - Build-your-own: Phoenix (includes ESL Walli), [EVSE DIN](https://www.evse-wifi.de/produkt-schlagwort/simple-evse-wb/) - Smart-Home outlets: FritzDECT, Shelly, Tasmota, TP-Link - multiple [meters](https://docs.evcc.io/docs/devices/meters): ModBus (Eastron SDM, MPM3PM, SBC ALE3 and many more), Discovergy (using HTTP plugin), SMA Sunny Home Manager and Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC, ...), Tesla PowerWall, LG ESS HOME -- wide support of vendor-specific [vehicles](https://docs.evcc.io/docs/devices/vehicles) interfaces (remote charge, battery and preconditioning status): Audi, BMW, Fiat, Ford, Hyundai, Kia, Mini, Nissan, Niu, Porsche, Renault, Seat, Smart, Skoda, Tesla, Volkswagen, Volvo, Tronity +- wide support of vendor-specific [vehicles](https://docs.evcc.io/docs/devices/vehicles) interfaces (remote charge, battery and preconditioning status): Audi, BMW, Fiat, Ford, Hyundai, Jaguar, Kia, Landrover, Mini, Nissan, Niu, Porsche, Renault, Seat, Smart, Skoda, Tesla, Volkswagen, Volvo, Tronity - [plugins](https://docs.evcc.io/docs/reference/plugins) for integrating with any charger/ meter/ vehicle: Modbus (meters and grid inverters), HTTP, MQTT, Javascript, WebSockets and shell scripts - status [notifications](https://docs.evcc.io/docs/reference/configuration/messaging) using [Telegram](https://telegram.org), [PushOver](https://pushover.net) and [many more](https://containrrr.dev/shoutrrr/) - logging using [InfluxDB](https://www.influxdata.com) and [Grafana](https://grafana.com/grafana/)