From dc1ac7c6dddc5a30b311c4769e940797589ef626 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 3 Aug 2026 14:19:41 +0200 Subject: [PATCH] Vaillant: share token source across chargers on the same account (#32471) --- charger/vaillant.go | 6 ++--- charger/vaillant/identity.go | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 charger/vaillant/identity.go diff --git a/charger/vaillant.go b/charger/vaillant.go index e41d2efff..555fbe284 100644 --- a/charger/vaillant.go +++ b/charger/vaillant.go @@ -33,7 +33,6 @@ import ( "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/samber/lo" - "golang.org/x/oauth2" ) func init() { @@ -79,15 +78,14 @@ func NewVaillantFromConfig(ctx context.Context, other map[string]any) (api.Charg } log := util.NewLogger("vaillant").Redact(cc.User, cc.Password) - logCtx := context.WithValue(ctx, oauth2.HTTPClient, request.NewClient(log)) oc := sensonet.Oauth2ConfigForRealm(cc.Realm) - token, err := vaillant.Login(logCtx, log, oc, cc.User, cc.Password) + ts, err := vaillant.Identity(log, oc, cc.Realm, cc.User, cc.Password) if err != nil { return nil, err } - conn, err := sensonet.NewConnection(oc.TokenSource(logCtx, token), sensonet.WithHttpClient(request.NewClient(log))) + conn, err := sensonet.NewConnection(ts, sensonet.WithHttpClient(request.NewClient(log))) if err != nil { return nil, err } diff --git a/charger/vaillant/identity.go b/charger/vaillant/identity.go new file mode 100644 index 000000000..2868aa388 --- /dev/null +++ b/charger/vaillant/identity.go @@ -0,0 +1,47 @@ +package vaillant + +import ( + "context" + "strings" + "sync" + + "github.com/WulfgarW/sensonet" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "golang.org/x/oauth2" +) + +var ( + mu sync.Mutex + identities = make(map[string]oauth2.TokenSource) +) + +// Identity returns an oauth2 token source shared by all Vaillant chargers on the +// same myVaillant account. The login is performed once while holding the lock, +// which serialises concurrent startups so the parallel login flows can no longer +// clobber each other's session (#30625). +func Identity(log *util.Logger, oc *sensonet.Oauth2Config, realm, user, password string) (oauth2.TokenSource, error) { + // serialise instance handling + mu.Lock() + defer mu.Unlock() + + // reuse identity instance + subject := "vaillant." + strings.ToLower(realm) + "." + strings.ToLower(user) + if ts, ok := identities[subject]; ok { + return ts, nil + } + + // decoupled from the calling charger's context, which is bounded by a timeout + // during device creation and would otherwise cancel the shared token refresh + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log)) + + token, err := Login(ctx, log, oc, user, password) + if err != nil { + return nil, err + } + + ts := oc.TokenSource(ctx, token) + identities[subject] = ts + + return ts, nil +}