diff --git a/vehicle/mercedes.go b/vehicle/mercedes.go deleted file mode 100644 index 148e7e0d19..0000000000 --- a/vehicle/mercedes.go +++ /dev/null @@ -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 -} diff --git a/vehicle/mercedes/api.go b/vehicle/mercedes/api.go deleted file mode 100644 index dab9bd47cb..0000000000 --- a/vehicle/mercedes/api.go +++ /dev/null @@ -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 -} diff --git a/vehicle/mercedes/identity.go b/vehicle/mercedes/identity.go deleted file mode 100644 index 5bf3cf16ec..0000000000 --- a/vehicle/mercedes/identity.go +++ /dev/null @@ -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) -} diff --git a/vehicle/mercedes/provider.go b/vehicle/mercedes/provider.go deleted file mode 100644 index a8f716f355..0000000000 --- a/vehicle/mercedes/provider.go +++ /dev/null @@ -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 -} diff --git a/vehicle/mercedes/tokensource.go b/vehicle/mercedes/tokensource.go deleted file mode 100644 index 003b837aa5..0000000000 --- a/vehicle/mercedes/tokensource.go +++ /dev/null @@ -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() -} diff --git a/vehicle/mercedes/types.go b/vehicle/mercedes/types.go deleted file mode 100644 index 9c25c0206d..0000000000 --- a/vehicle/mercedes/types.go +++ /dev/null @@ -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 - } -}