Mercedes: sunset BYOC api (#9923)

This commit is contained in:
andig 2023-09-17 13:36:28 +02:00 • committed by GitHub
parent cc1e21bea7
commit 9e0ceb91a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 0 additions and 413 deletions

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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
}
}