Add BMW CarData api (#23972)

This commit is contained in:
andig 2025-10-04 11:43:34 +02:00 • committed by GitHub
parent 3139499b0f
commit 9dd4599fc2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 731 additions and 37 deletions

View file

@ -9,6 +9,7 @@ import (
"net/url"
"strings"
"sync"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/server/db/settings"
@ -26,6 +27,30 @@ type OAuth struct {
subject string
cv string
ctx context.Context
deviceFlow bool
tokenRetriever func(string, *oauth2.Token) error
tokenStorer func(*oauth2.Token) any
}
type oauthOption func(*OAuth)
func WithOauthDeviceFlowOption() func(o *OAuth) {
return func(o *OAuth) {
o.deviceFlow = true
}
}
func WithTokenStorerOption(ts func(*oauth2.Token) any) func(o *OAuth) {
return func(o *OAuth) {
o.tokenStorer = ts
}
}
func WithTokenRetrieverOption(tr func(string, *oauth2.Token) error) func(o *OAuth) {
return func(o *OAuth) {
o.tokenRetriever = tr
}
}
var (
@ -59,20 +84,23 @@ func NewOauthFromConfig(ctx context.Context, other map[string]any) (oauth2.Token
return nil, err
}
return NewOauth(ctx, &cc.Config, cc.Name)
return NewOauth(ctx, cc.Name, &cc.Config)
}
func NewOauth(ctx context.Context, oc *oauth2.Config, instanceName string) (oauth2.TokenSource, error) {
log := util.NewLogger("oauth-generic")
var (
_ oauth.TokenRefresher = (*OAuth)(nil)
_ api.AuthProvider = (*OAuth)(nil)
)
if instanceName == "" {
func NewOauth(ctx context.Context, name string, oc *oauth2.Config, opts ...oauthOption) (oauth2.TokenSource, error) {
if name == "" {
return nil, errors.New("instance name must not be empty")
}
// hash oauth2 config
h := sha256.Sum256(fmt.Append(nil, oc))
hash := hex.EncodeToString(h[:])[:8]
subject := instanceName + " (" + hash + ")"
subject := name + " (" + hash + ")"
// reuse instance
if instance := getInstance(subject); instance != nil {
@ -83,21 +111,36 @@ func NewOauth(ctx context.Context, oc *oauth2.Config, instanceName string) (oaut
o := &OAuth{
subject: subject,
oc: oc,
log: log,
log: util.NewLogger("oauth"),
ctx: ctx,
}
for _, opt := range opts {
opt(o)
}
// load token from db
var tok oauth2.Token
var token oauth2.Token
if settings.Exists(o.subject) {
o.log.DEBUG.Printf("loading token for %s from database", o.subject)
if err := settings.Json(o.subject, &tok); err != nil {
return nil, err
if o.tokenRetriever != nil {
plain, err := settings.String(o.subject)
if err != nil {
return nil, err
}
if err := o.tokenRetriever(plain, &token); err != nil {
return nil, err
}
} else {
if err := settings.Json(o.subject, &token); err != nil {
return nil, err
}
}
}
o.TokenSource = oauth.RefreshTokenSource(&tok, o)
o.TokenSource = oauth.RefreshTokenSource(&token, o)
// add instance
addInstance(o.subject, o)
@ -108,69 +151,116 @@ func NewOauth(ctx context.Context, oc *oauth2.Config, instanceName string) (oaut
return o, nil
}
// RefreshToken implements oauth.RefreshTokenSource.
// RefreshToken implements oauth.TokenRefresher.
func (o *OAuth) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
if token.RefreshToken == "" {
return nil, api.ErrMissingToken
}
// log token before refresh
o.log.DEBUG.Printf("refreshing token for %s", o.subject)
// refresh token source
token, err := o.oc.TokenSource(o.ctx, token).Token()
if err != nil {
if strings.Contains(err.Error(), "invalid_grant") {
if settings.Exists(o.subject) {
settings.Delete(o.subject)
}
if strings.Contains(err.Error(), "invalid_grant") && settings.Exists(o.subject) {
settings.Delete(o.subject)
}
return nil, err
}
err = settings.SetJson(o.subject, token)
return token, err
}
// updateToken must only be called when lock is held
func (o *OAuth) updateToken(token *oauth2.Token) {
var store any = token
// tokenStorer allows persisting the token together with it's extra properties
if o.tokenStorer != nil {
store = o.tokenStorer(token)
}
if err := settings.SetJson(o.subject, store); err != nil {
o.log.ERROR.Printf("error saving token: %v", err)
}
o.TokenSource = oauth.RefreshTokenSource(token, o)
}
// HandleCallback implements api.AuthProvider.
func (o *OAuth) HandleCallback(responseValues url.Values) error {
code := responseValues.Get("code")
func (o *OAuth) HandleCallback(params url.Values) error {
code := params.Get("code")
o.mu.Lock()
defer o.mu.Unlock()
token, err := o.oc.Exchange(o.ctx, code, oauth2.VerifierOption(o.cv))
if err != nil {
o.log.ERROR.Printf("error during oauth exchange: %s", err)
return err
}
if err := settings.SetJson(o.subject, token); err != nil {
o.log.ERROR.Printf("error saving token: %s", err)
}
o.updateToken(token)
o.TokenSource = oauth.RefreshTokenSource(token, o)
return nil
}
// Login implements api.AuthProvider.
func (o *OAuth) Login(state string) string {
func (o *OAuth) Login(state string) (string, error) {
o.mu.Lock()
defer o.mu.Unlock()
o.cv = oauth2.GenerateVerifier()
return o.oc.AuthCodeURL(state, oauth2.S256ChallengeOption(o.cv))
if o.deviceFlow {
ctx := context.Background()
da, err := o.oc.DeviceAuth(ctx, oauth2.S256ChallengeOption(o.cv))
if err != nil {
return "", err
}
go func() {
ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
token, err := o.oc.DeviceAccessToken(ctx, da, oauth2.VerifierOption(o.cv))
if err != nil {
o.log.ERROR.Printf("error retrieving token: %v", err)
return
}
o.mu.Lock()
defer o.mu.Unlock()
o.updateToken(token)
}()
return da.VerificationURIComplete, nil
}
if o.oc.Endpoint.AuthURL == "" {
return "", errors.New("missing auth url")
}
return o.oc.AuthCodeURL(state, oauth2.S256ChallengeOption(o.cv)), nil
}
// Logout implements api.AuthProvider.
func (o *OAuth) Logout() error {
o.log.INFO.Printf("removing %s from database", o.subject)
o.log.DEBUG.Printf("removing %s from database", o.subject)
if settings.Exists(o.subject) {
settings.Delete(o.subject)
if err := settings.Delete(o.subject); err != nil {
o.log.ERROR.Println(err)
}
}
o.mu.Lock()
defer o.mu.Unlock()
o.TokenSource = oauth.RefreshTokenSource(nil, o)
return nil
}
@ -182,6 +272,6 @@ func (o *OAuth) DisplayName() string {
// Authenticated implements api.AuthProvider.
func (o *OAuth) Authenticated() bool {
token, err := o.TokenSource.Token()
token, err := o.Token()
return err == nil && token.Valid()
}