forked from evcc-io/evcc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Jaguar/Landrover api (evcc-io#2468)
- Loading branch information
Showing
7 changed files
with
456 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
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" | ||
"github.com/google/uuid" | ||
) | ||
|
||
// https://github.com/ardevd/jlrpy | ||
|
||
// JLR is an api.Vehicle implementation for Jaguar LandRover cars | ||
type JLR struct { | ||
*embed | ||
*jlr.Provider | ||
} | ||
|
||
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) | ||
|
||
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() | ||
if err != nil { | ||
return nil, fmt.Errorf("login failed: %w", err) | ||
} | ||
|
||
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) | ||
|
||
user, err := api.User(cc.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.Cache) | ||
} | ||
|
||
return v, err | ||
} | ||
|
||
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, | ||
"authorization_token": t.AuthToken, | ||
"expires_in": "86400", | ||
"deviceID": device} | ||
|
||
uri := fmt.Sprintf("%s/users/%s/clients", jlr.IFOP_BASE_URL, url.PathEscape(user)) | ||
|
||
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 = c.DoBody(req) | ||
} | ||
|
||
return err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
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{ | ||
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": request.JSONContent, | ||
"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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package jlr | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"time" | ||
|
||
"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, | ||
} | ||
} | ||
|
||
// 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{ | ||
"Authorization": "Basic YXM6YXNwYXNz", | ||
"Content-type": request.JSONContent, | ||
"X-Device-Id": v.device, | ||
}) | ||
|
||
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 | ||
func (v *Identity) Login() (Token, 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 token, 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 | ||
} |
Oops, something went wrong.