Mercedes: sunset BYOC api (#9923)
This commit is contained in:
parent
cc1e21bea7
commit
9e0ceb91a4
6 changed files with 0 additions and 413 deletions
|
|
@ -1,77 +0,0 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/vehicle/mercedes"
|
||||
)
|
||||
|
||||
// Mercedes is an api.Vehicle implementation for Mercedes cars
|
||||
type Mercedes struct {
|
||||
*embed
|
||||
api.AuthProvider
|
||||
*mercedes.Provider
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.Add("mercedes", NewMercedesFromConfig)
|
||||
}
|
||||
|
||||
// NewMercedesFromConfig creates a new Mercedes vehicle
|
||||
func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
||||
cc := struct {
|
||||
embed `mapstructure:",squash"`
|
||||
ClientID, ClientSecret string
|
||||
VIN string
|
||||
Sandbox bool
|
||||
Cache time.Duration
|
||||
}{
|
||||
Cache: interval,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cc.ClientID == "" && cc.ClientSecret == "" {
|
||||
return nil, errors.New("missing credentials")
|
||||
}
|
||||
|
||||
if cc.VIN == "" {
|
||||
return nil, errors.New("missing vin")
|
||||
}
|
||||
|
||||
var options []mercedes.IdentityOption
|
||||
|
||||
// TODO Load tokens from a persistence storage and use those during startup
|
||||
// e.g. persistence.Load("key")
|
||||
// if tokens != nil {
|
||||
// options = append(options, mercedes.WithToken(&oauth2.Token{
|
||||
// AccessToken: tokens.Access,
|
||||
// RefreshToken: tokens.Refresh,
|
||||
// Expiry: tokens.Expiry,
|
||||
// }))
|
||||
// }
|
||||
|
||||
log := util.NewLogger("mercedes")
|
||||
|
||||
// TODO session secret from config/persistence
|
||||
identity, err := mercedes.NewIdentity(log, cc.ClientID, cc.ClientSecret, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := mercedes.NewAPI(log, identity, cc.Sandbox)
|
||||
|
||||
v := &Mercedes{
|
||||
embed: &cc.embed,
|
||||
Provider: mercedes.NewProvider(api, strings.ToUpper(cc.VIN), cc.Cache),
|
||||
AuthProvider: identity, // expose the OAuth2 login
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
package mercedes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
// BaseURI is the Mercedes api base URI
|
||||
BaseURI = "https://api.mercedes-benz.com/vehicledata/v2"
|
||||
SandboxBaseURI = "https://api.mercedes-benz.com/vehicledata_tryout/v2"
|
||||
)
|
||||
|
||||
// API is the Mercedes api client
|
||||
type API struct {
|
||||
*request.Helper
|
||||
sandbox bool
|
||||
}
|
||||
|
||||
// NewAPI creates a new api client
|
||||
func NewAPI(log *util.Logger, identity *Identity, sandbox bool) *API {
|
||||
v := &API{
|
||||
Helper: request.NewHelper(log),
|
||||
}
|
||||
|
||||
// replace client transport with authenticated transport
|
||||
v.Client.Transport = &oauth2.Transport{
|
||||
Source: identity,
|
||||
Base: v.Client.Transport,
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *API) BaseURI() string {
|
||||
if v.sandbox {
|
||||
return SandboxBaseURI
|
||||
}
|
||||
return BaseURI
|
||||
}
|
||||
|
||||
// Soc implements the /soc response
|
||||
func (v *API) Soc(vin string) (EVResponse, error) {
|
||||
var res EVResponse
|
||||
|
||||
uri := fmt.Sprintf("%s/vehicles/%s/resources/soc", v.BaseURI(), vin)
|
||||
err := v.GetJSON(uri, &res)
|
||||
if err != nil {
|
||||
res, err = v.allinOne(vin)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Range implements the /rangeelectric response
|
||||
func (v *API) Range(vin string) (EVResponse, error) {
|
||||
var res EVResponse
|
||||
|
||||
uri := fmt.Sprintf("%s/vehicles/%s/resources/rangeelectric", v.BaseURI(), vin)
|
||||
err := v.GetJSON(uri, &res)
|
||||
if err != nil {
|
||||
res, err = v.allinOne(vin)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// allinOne is a 'fallback' to gather both metrics range and soc.
|
||||
// It is used in case for any reason the single endpoints return an error - which happend in the past.
|
||||
func (v *API) allinOne(vin string) (EVResponse, error) {
|
||||
var res []EVResponse
|
||||
|
||||
uri := fmt.Sprintf("%s/vehicles/%s/containers/electricvehicle", v.BaseURI(), vin)
|
||||
err := v.GetJSON(uri, &res)
|
||||
|
||||
evres := EVResponse{}
|
||||
|
||||
for _, r := range res {
|
||||
if r.Soc.Timestamp != 0 {
|
||||
evres.Soc = r.Soc
|
||||
continue
|
||||
}
|
||||
|
||||
if r.RangeElectric.Timestamp != 0 {
|
||||
evres.RangeElectric = r.RangeElectric
|
||||
}
|
||||
}
|
||||
|
||||
return evres, err
|
||||
}
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
package mercedes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/provider"
|
||||
"github.com/evcc-io/evcc/server/oauth2redirect"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// https://ssoalpha.dvb.corpinter.net/v1/.well-known/openid-configuration
|
||||
const OAuthURI = "https://ssoalpha.dvb.corpinter.net/v1"
|
||||
|
||||
type IdentityOption func(c *Identity) error
|
||||
|
||||
// WithToken provides an oauth2.Token to the client for auth.
|
||||
func WithToken(t *oauth2.Token) IdentityOption {
|
||||
return func(v *Identity) error {
|
||||
v.ReuseTokenSource.Apply(t)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
log *util.Logger
|
||||
*ReuseTokenSource
|
||||
oc *oauth2.Config
|
||||
baseURL string
|
||||
authC chan<- bool
|
||||
}
|
||||
|
||||
// TODO SessionSecret from config/persistence
|
||||
func NewIdentity(log *util.Logger, id, secret string, options ...IdentityOption) (*Identity, error) {
|
||||
provider, err := oidc.NewProvider(context.Background(), OAuthURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize OIDC provider: %s", err)
|
||||
}
|
||||
|
||||
oc := &oauth2.Config{
|
||||
ClientID: id,
|
||||
ClientSecret: secret,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: []string{
|
||||
oidc.ScopeOpenID,
|
||||
oidc.ScopeOfflineAccess,
|
||||
"mb:vehicle:mbdata:evstatus",
|
||||
},
|
||||
}
|
||||
|
||||
v := &Identity{
|
||||
log: log,
|
||||
oc: oc,
|
||||
}
|
||||
|
||||
ts := &ReuseTokenSource{
|
||||
oc: oc,
|
||||
cb: v.invalidToken,
|
||||
}
|
||||
ts.Apply(nil)
|
||||
|
||||
v.ReuseTokenSource = ts
|
||||
|
||||
for _, o := range options {
|
||||
if err := o(v); err != nil {
|
||||
return v, err
|
||||
}
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// invalidToken is the callback for the token source when token expires
|
||||
func (v *Identity) invalidToken() {
|
||||
if v.authC != nil {
|
||||
v.authC <- false
|
||||
}
|
||||
}
|
||||
|
||||
var _ api.AuthProvider = (*Identity)(nil)
|
||||
|
||||
func (v *Identity) SetCallbackParams(baseURL, redirectURL string, authC chan<- bool) {
|
||||
v.baseURL = baseURL
|
||||
v.oc.RedirectURL = redirectURL
|
||||
v.authC = authC
|
||||
}
|
||||
|
||||
func (v *Identity) LoginHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
state := oauth2redirect.Register(v.callbackHandler)
|
||||
|
||||
b, _ := json.Marshal(struct {
|
||||
LoginUri string `json:"loginUri"`
|
||||
}{
|
||||
LoginUri: v.oc.AuthCodeURL(state, oauth2.AccessTypeOffline,
|
||||
oauth2.SetAuthURLParam("prompt", "login consent"),
|
||||
),
|
||||
})
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Identity) LogoutHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
v.ReuseTokenSource.Apply(nil)
|
||||
v.authC <- false
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Identity) callbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
v.log.DEBUG.Println("callback request received")
|
||||
|
||||
data, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
fmt.Fprintln(w, "invalid response:", data)
|
||||
return
|
||||
}
|
||||
|
||||
codes, ok := data["code"]
|
||||
if !ok || len(codes) != 1 {
|
||||
fmt.Fprintln(w, "invalid response:", data)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := v.oc.Exchange(context.Background(), codes[0])
|
||||
if err != nil {
|
||||
fmt.Fprintln(w, "token error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if token.Valid() {
|
||||
v.log.TRACE.Println("sending login update...")
|
||||
v.ReuseTokenSource.Apply(token)
|
||||
v.authC <- true
|
||||
|
||||
provider.ResetCached()
|
||||
}
|
||||
|
||||
http.Redirect(w, r, v.baseURL, http.StatusFound)
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package mercedes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/provider"
|
||||
)
|
||||
|
||||
// Provider implements the vehicle api
|
||||
type Provider struct {
|
||||
chargerG func() (EVResponse, error)
|
||||
rangeG func() (EVResponse, error)
|
||||
}
|
||||
|
||||
// NewProvider creates a vehicle api provider
|
||||
func NewProvider(api *API, vin string, cache time.Duration) *Provider {
|
||||
impl := &Provider{
|
||||
chargerG: provider.Cached(func() (EVResponse, error) {
|
||||
return api.Soc(vin)
|
||||
}, cache),
|
||||
rangeG: provider.Cached(func() (EVResponse, error) {
|
||||
return api.Range(vin)
|
||||
}, cache),
|
||||
}
|
||||
return impl
|
||||
}
|
||||
|
||||
// Soc implements the api.Vehicle interface
|
||||
func (v *Provider) Soc() (float64, error) {
|
||||
res, err := v.chargerG()
|
||||
if err == nil {
|
||||
return float64(res.Soc.Value), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *Provider) Range() (rng int64, err error) {
|
||||
res, err := v.rangeG()
|
||||
if err == nil {
|
||||
return int64(res.RangeElectric.Value), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package mercedes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type ReuseTokenSource struct {
|
||||
mu sync.Mutex
|
||||
oc *oauth2.Config
|
||||
ts oauth2.TokenSource
|
||||
cb func()
|
||||
}
|
||||
|
||||
func (ts *ReuseTokenSource) Token() (*oauth2.Token, error) {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
|
||||
t, err := ts.ts.Token()
|
||||
if err != nil || !t.Valid() {
|
||||
// invalid token callback
|
||||
ts.cb()
|
||||
}
|
||||
|
||||
return t, err
|
||||
}
|
||||
|
||||
func (ts *ReuseTokenSource) Apply(t *oauth2.Token) {
|
||||
ts.mu.Lock()
|
||||
ts.ts = ts.oc.TokenSource(context.Background(), t)
|
||||
ts.mu.Unlock()
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package mercedes
|
||||
|
||||
type EVResponse struct {
|
||||
Soc struct {
|
||||
Value int64 `json:",string"`
|
||||
Timestamp int64
|
||||
}
|
||||
RangeElectric struct {
|
||||
Value int64 `json:",string"`
|
||||
Timestamp int64
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue