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

@ -209,9 +209,9 @@ type Tariff interface {
// AuthProvider is the ability to provide OAuth authentication through the ui
type AuthProvider interface {
Login(state string) string
Login(state string) (string, error)
Logout() error
HandleCallback(responseValues url.Values) error
HandleCallback(params url.Values) error
Authenticated() bool
DisplayName() string
}

View file

@ -14,6 +14,9 @@ var ErrSponsorRequired = errors.New("sponsorship required, see https://docs.evcc
// ErrMissingCredentials indicates that user/password are missing
var ErrMissingCredentials = errors.New("missing user/password credentials")
// ErrLoginRequired indicates that retrieving tokens credentials waits for login
var ErrLoginRequired = errors.New("login required")
// ErrMissingToken indicates that access/refresh tokens are missing
var ErrMissingToken = errors.New("missing token credentials")

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

View file

@ -69,7 +69,7 @@ func (a *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
provider, ok := a.providers[id]
if !ok {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "invalid id")
fmt.Fprintln(w, "invalid id")
return
}
@ -85,11 +85,18 @@ func (a *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
delete(a.states, encryptedState)
})
uri, err := provider.Login(encryptedState)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "error: %v", err)
return
}
// return authorization URL
res := struct {
LoginUri string `json:"loginUri"`
}{
LoginUri: provider.Login(encryptedState),
LoginUri: uri,
}
if err := json.NewEncoder(w).Encode(res); err != nil {
@ -109,7 +116,7 @@ func (a *Handler) handleLogout(w http.ResponseWriter, r *http.Request) {
provider, ok := a.providers[id]
if !ok {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "invalid id")
fmt.Fprintln(w, "invalid id")
return
}
@ -161,9 +168,9 @@ func (a *Handler) handleCallback(w http.ResponseWriter, r *http.Request) {
// Handle the callback
if err := provider.HandleCallback(r.URL.Query()); err != nil {
a.log.ERROR.Printf("callback handling for provider %s failed: %v", id, err)
a.log.ERROR.Printf("callback for provider %s failed: %v", id, err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "callback handling failed")
fmt.Fprintln(w, "callback failed")
return
}

View file

@ -0,0 +1,26 @@
template: cardata
products:
- brand: BMW
description:
generic: CarData (EU Data Act)
- brand: Mini
description:
generic: CarData (EU Data Act)
requirements:
description:
de: |
Benötigt CarData Einrichtung im BMW/Mini portal.
en: |
Requires CarData activation in BMW/Mini portal.
params:
- preset: vehicle-common
- name: vin
example: WBMW...
- name: clientid
- preset: vehicle-features
render: |
type: cardata
vin: {{ .vin }}
clientid: {{ .clientid }}
{{ include "vehicle-common" . }}
{{ include "vehicle-features" . }}

View file

@ -5,7 +5,7 @@ import (
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/vehicle/bmw"
bmw "github.com/evcc-io/evcc/vehicle/bmw/connected"
)
// BMW is an api.Vehicle implementation for BMW and Mini cars

150
vehicle/bmw/cardata/api.go Normal file
View file

@ -0,0 +1,150 @@
package cardata
import (
"fmt"
"net/http"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
"github.com/samber/lo"
"golang.org/x/oauth2"
)
const ApiURL = "https://api-cardata.bmwgroup.com"
var Config = oauth2.Config{
Scopes: []string{"authenticate_user", "openid", "cardata:streaming:read", "cardata:api:read"},
Endpoint: oauth2.Endpoint{
DeviceAuthURL: "https://customer.bmwgroup.com/gcdm/oauth/device/code",
TokenURL: "https://customer.bmwgroup.com/gcdm/oauth/token",
},
}
// requiredKeys are the necessary data dictionary entities according to
// https://mybmwweb-utilities.api.bmw/de-de/utilities/bmw/api/cd/catalogue/file
var requiredKeys = []string{
"vehicle.body.chargingPort.status",
"vehicle.cabin.hvac.preconditioning.status.comfortState",
"vehicle.drivetrain.batteryManagement.header",
"vehicle.drivetrain.electricEngine.charging.hvStatus",
"vehicle.drivetrain.electricEngine.charging.level",
"vehicle.drivetrain.electricEngine.charging.timeToFullyCharged",
"vehicle.drivetrain.electricEngine.kombiRemainingElectricRange",
"vehicle.powertrain.electric.battery.stateOfCharge.target",
"vehicle.vehicle.travelledDistance",
}
type API struct {
*request.Helper
}
func NewAPI(log *util.Logger, ts oauth2.TokenSource) *API {
v := &API{
Helper: request.NewHelper(log),
}
v.Transport = &oauth2.Transport{
Source: ts,
Base: &transport.Decorator{
Decorator: transport.DecorateHeaders(map[string]string{
"x-version": "v1",
}),
Base: v.Transport,
},
}
return v
}
func (v *API) Vehicles() ([]string, error) {
var res []VehicleMapping
err := v.GetJSON(ApiURL+"/customers/vehicles/mappings", &res)
return lo.Map(
lo.Filter(res, func(v VehicleMapping, _ int) bool {
return v.MappingType == "PRIMARY"
}), func(v VehicleMapping, _ int) string {
return v.Vin
},
), err
}
func (v *API) GetContainers() ([]Container, error) {
var res struct {
Containers []Container
}
if err := v.GetJSON(ApiURL+"/customers/containers", &res); err != nil {
return nil, err
}
return lo.Filter(res.Containers, func(c Container, _ int) bool {
return c.Name == "evcc.io"
}), nil
}
func (v *API) CreateContainer() error {
data := CreateContainer{
Name: "evcc.io",
Purpose: "evcc.io",
TechnicalDescriptors: requiredKeys,
}
var res any
req, _ := request.New(http.MethodPost, ApiURL+"/customers/containers", request.MarshalJSON(data))
return v.DoJSON(req, &res)
}
// func (v *API) DeleteContainer() error {
// if *deleteContainer && len(containers) == 1 {
// req, _ := request.New(http.MethodDelete, apiUrl+"/customers/containers/"+containers[0].ContainerId, nil)
// var res any
// if err := client.DoJSON(req, &res); err != nil {
// return err
// }
// containers = nil
// }
// }
func (v *API) EnsureContainer() (string, error) {
containers, err := v.GetContainers()
if err != nil {
return "", err
}
if len(containers) > 0 {
return containers[0].ContainerId, nil
}
data := CreateContainer{
Name: "evcc.io",
Purpose: "evcc.io",
TechnicalDescriptors: []string{
// https://mybmwweb-utilities.api.bmw/de-de/utilities/bmw/api/cd/catalogue/file
"vehicle.body.chargingPort.status",
"vehicle.cabin.hvac.preconditioning.status.comfortState",
"vehicle.drivetrain.batteryManagement.header",
"vehicle.drivetrain.electricEngine.charging.hvStatus",
"vehicle.drivetrain.electricEngine.charging.level",
"vehicle.drivetrain.electricEngine.charging.timeToFullyCharged",
"vehicle.drivetrain.electricEngine.kombiRemainingElectricRange",
"vehicle.powertrain.electric.battery.stateOfCharge.target",
"vehicle.vehicle.travelledDistance",
},
}
var res Container
req, _ := request.New(http.MethodPost, ApiURL+"/customers/containers", request.MarshalJSON(data))
err = v.DoJSON(req, &res)
return res.ContainerId, err
}
func (v *API) GetTelematics(vin, container string) (TelematicData, error) {
var res TelematicData
uri := fmt.Sprintf(ApiURL+"/customers/vehicles/%s/telematicData?containerId=%s", vin, container)
err := v.GetJSON(uri, &res)
return res, err
}

View file

@ -0,0 +1,246 @@
package cardata
import (
"encoding/json"
"errors"
"fmt"
"maps"
"slices"
"sync"
"time"
"github.com/cenkalti/backoff/v4"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/spf13/cast"
"golang.org/x/oauth2"
)
const StreamingURL = "tls://customer.streaming-cardata.bmwgroup.com:9000"
// Provider implements the vehicle api
type Provider struct {
mu sync.Mutex
log *util.Logger
api *API
ts oauth2.TokenSource
vin string
initial map[string]TelematicDataPoint
streaming map[string]StreamingData
}
// NewProvider creates a vehicle api provider
func NewProvider(log *util.Logger, api *API, ts oauth2.TokenSource, vin string) *Provider {
v := &Provider{
log: log,
api: api,
ts: ts,
vin: vin,
streaming: make(map[string]StreamingData),
}
go func() {
bo := backoff.NewExponentialBackOff(backoff.WithMaxInterval(time.Minute))
for {
token, err := ts.Token()
if err != nil {
if !tokenError(err) {
v.log.ERROR.Println(err)
}
time.Sleep(bo.NextBackOff())
}
bo.Reset()
if err := v.runMqtt(vin, token); err != nil {
v.log.ERROR.Println(err)
}
}
}()
return v
}
func (v *Provider) runMqtt(vin string, token *oauth2.Token) error {
gcid := TokenExtra(token, "gcid")
idToken := TokenExtra(token, "id_token")
paho := mqtt.NewClient(
mqtt.NewClientOptions().
AddBroker(StreamingURL).
SetAutoReconnect(true).
SetUsername(gcid).
SetPassword(idToken))
timeout := 30 * time.Second
if t := paho.Connect(); !t.WaitTimeout(timeout) {
return errors.New("connect timeout")
} else if err := t.Error(); err != nil {
return fmt.Errorf("connect: %w", err)
}
defer paho.Disconnect(0)
topic := fmt.Sprintf("%s/%s", gcid, vin)
if t := paho.Subscribe(topic, 0, v.handler); !t.WaitTimeout(timeout) {
return errors.New("subcribe timeout")
} else if err := t.Error(); err != nil {
return fmt.Errorf("subscribe: %w", err)
}
time.Sleep(time.Until(token.Expiry))
return nil
}
func (v *Provider) handler(c mqtt.Client, m mqtt.Message) {
var res StreamingMessage
if err := json.Unmarshal(m.Payload(), &res); err != nil {
v.log.ERROR.Println(m.Topic(), string(m.Payload()), err)
return
}
v.mu.Lock()
defer v.mu.Unlock()
maps.Copy(v.streaming, res.Data)
}
func (v *Provider) any(key string) (any, error) {
v.mu.Lock()
defer v.mu.Unlock()
if a, ok := v.streaming[key]; ok {
return a, nil
}
if v.initial == nil {
// don't try as long as there's no token
if _, err := v.ts.Token(); err != nil {
return nil, api.ErrNotAvailable
}
defer func() {
if v.initial == nil {
v.initial = make(map[string]TelematicDataPoint)
}
}()
container, err := v.api.EnsureContainer()
if err != nil {
v.log.ERROR.Printf("get container: %v", err)
return nil, api.ErrNotAvailable
}
if res, err := v.api.GetTelematics(v.vin, container); err == nil {
v.initial = res.TelematicData
} else {
v.log.ERROR.Printf("get telematics: %v", err)
return nil, api.ErrNotAvailable
}
}
if el, ok := v.initial[key]; ok {
return el.Value, nil
}
return nil, api.ErrNotAvailable
}
func (v *Provider) String(key string) (string, error) {
res, err := v.any(key)
if err != nil {
return "", err
}
return cast.ToStringE(res)
}
func (v *Provider) Int(key string) (int64, error) {
res, err := v.any(key)
if err != nil {
return 0, err
}
return cast.ToInt64E(res)
}
func (v *Provider) Float(key string) (float64, error) {
res, err := v.any(key)
if err != nil {
return 0, err
}
return cast.ToFloat64E(res)
}
var _ api.Battery = (*Provider)(nil)
// Soc implements the api.Vehicle interface
func (v *Provider) Soc() (float64, error) {
return v.Float("vehicle.drivetrain.electricEngine.charging.level")
}
var _ api.ChargeState = (*Provider)(nil)
// Status implements the api.ChargeState interface
func (v *Provider) Status() (api.ChargeStatus, error) {
port, err := v.String("vehicle.body.chargingPort.status")
if err != nil {
return api.StatusNone, err
}
status := api.StatusA // disconnected
if port == "CONNECTED" {
status = api.StatusB
}
hv, err := v.String("vehicle.drivetrain.electricEngine.charging.hvStatus")
if hv == "CHARGING" {
status = api.StatusC
}
return status, err
}
var _ api.VehicleFinishTimer = (*Provider)(nil)
// FinishTime implements the api.VehicleFinishTimer interface
func (v *Provider) FinishTime() (time.Time, error) {
res, err := v.Int("vehicle.drivetrain.electricEngine.charging.timeToFullyCharged")
return time.Now().Add(time.Duration(res) * time.Minute), err
}
var _ api.VehicleRange = (*Provider)(nil)
// Range implements the api.VehicleRange interface
func (v *Provider) Range() (int64, error) {
return v.Int("vehicle.drivetrain.electricEngine.kombiRemainingElectricRange")
}
var _ api.VehicleOdometer = (*Provider)(nil)
// Odometer implements the api.VehicleOdometer interface
func (v *Provider) Odometer() (float64, error) {
return v.Float("vehicle.vehicle.travelledDistance")
}
var _ api.SocLimiter = (*Provider)(nil)
// GetLimitSoc implements the api.SocLimiter interface
func (v *Provider) GetLimitSoc() (int64, error) {
return v.Int("vehicle.powertrain.electric.battery.stateOfCharge.target")
}
var _ api.VehicleClimater = (*Provider)(nil)
// Climater implements the api.VehicleClimater interface
func (v *Provider) Climater() (bool, error) {
res, err := v.String("vehicle.cabin.hvac.preconditioning.status.comfortState")
return slices.Contains([]string{"COMFORT_HEATING", "COMFORT_COOLING", "COMFORT_VENTILATION", "DEFROST"}, res), err
}

View file

@ -0,0 +1,33 @@
package cardata
import (
"errors"
"github.com/evcc-io/evcc/api"
"golang.org/x/oauth2"
)
type Token struct {
*oauth2.Token
IdToken string `json:"id_token"`
Gcid string `json:"gcid"`
}
func (t *Token) TokenEx() *oauth2.Token {
return t.Token.WithExtra(map[string]any{
"id_token": t.IdToken,
"gcid": t.Gcid,
})
}
// TokenExtra returns extra string properties of the oauth2.Token
func TokenExtra(t *oauth2.Token, key string) string {
if v := t.Extra(key); v != nil {
return v.(string)
}
return ""
}
func tokenError(err error) bool {
return errors.Is(err, api.ErrLoginRequired) || errors.Is(err, api.ErrMissingToken)
}

View file

@ -0,0 +1,46 @@
package cardata
import "time"
type VehicleMapping struct {
Vin string
MappedSince time.Time
MappingType string
}
type Container struct {
Name string `json:"name"`
Purpose string `json:"purpose"`
ContainerId string `json:"containerId"`
Created time.Time `json:"created"`
}
type CreateContainer struct {
Name string `json:"name"`
Purpose string `json:"purpose"`
TechnicalDescriptors []string `json:"technicalDescriptors"`
}
type TelematicDataPoint struct {
Timestamp time.Time
Unit string
Value string
}
type TelematicData struct {
TelematicData map[string]TelematicDataPoint
}
type StreamingMessage struct {
Vin string
EntityId string
Topic string
TimeStamp time.Time
Data map[string]StreamingData
}
type StreamingData struct {
TimeStamp time.Time
Value any
Unit string
}

93
vehicle/cardata.go Normal file
View file

@ -0,0 +1,93 @@
package vehicle
import (
"context"
"encoding/json"
"errors"
"net/http"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/plugin/auth"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/vehicle/bmw/cardata"
"golang.org/x/oauth2"
)
// Cardata is an api.Vehicle implementation for BMW and Mini cars
type Cardata struct {
*embed
*cardata.Provider // provides the api implementations
}
func init() {
registry.AddCtx("cardata", NewCardataFromConfig)
}
// NewCardataFromConfig creates a new BMW/Mini CarData vehicle
func NewCardataFromConfig(ctx context.Context, other map[string]interface{}) (api.Vehicle, error) {
cc := struct {
embed `mapstructure:",squash"`
ClientID, VIN string
Cache time.Duration
}{
Cache: 30 * time.Minute, // 50 requests per day
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if cc.ClientID == "" {
return nil, api.ErrMissingCredentials
}
v := &Cardata{
embed: &cc.embed,
}
oc := cardata.Config
oc.ClientID = cc.ClientID
log := util.NewLogger("cardata").Redact(cc.ClientID)
ts, err := auth.NewOauth(ctx, "cardata", &oc,
auth.WithOauthDeviceFlowOption(),
auth.WithTokenRetrieverOption(func(data string, res *oauth2.Token) error {
var token cardata.Token
if err := json.Unmarshal([]byte(data), &token); err != nil {
return err
}
*res = *token.TokenEx()
return nil
}),
auth.WithTokenStorerOption(func(token *oauth2.Token) any {
return cardata.Token{
Token: token,
IdToken: cardata.TokenExtra(token, "id_token"),
Gcid: cardata.TokenExtra(token, "gcid"),
}
}))
if err != nil {
return nil, err
}
api := cardata.NewAPI(log, ts)
is429 := func(err error) bool {
se := new(request.StatusError)
return errors.As(err, &se) && se.StatusCode() == http.StatusTooManyRequests
}
vehicle, err := ensureVehicle(
cc.VIN, api.Vehicles,
)
if err != nil && (cc.VIN == "" || !is429(err)) {
return nil, err
}
v.Provider = cardata.NewProvider(log, api, ts, vehicle)
return v, nil
}

View file

@ -45,7 +45,7 @@ func NewVolvoConnectedFromConfig(ctx context.Context, other map[string]interface
oc := connected.Oauth2Config(cc.Credentials.ID, cc.Credentials.Secret, cc.RedirectUri)
ctx = context.WithValue(ctx, oauth2.HTTPClient, request.NewClient(log))
ts, err := auth.NewOauth(ctx, oc, cc.embed.GetTitle())
ts, err := auth.NewOauth(ctx, cc.embed.GetTitle(), oc)
if err != nil {
return nil, err
}