From 9754bb91c89f81617d3f924fe1e3bb25812bfaba Mon Sep 17 00:00:00 2001 From: BrickTop87 <151788504+BrickTop87@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:13:06 +0100 Subject: [PATCH] BMW/Mini: add hcaptcha (#17445) --- templates/definition/vehicle/bmw.yaml | 9 ++++++ templates/definition/vehicle/mini.yaml | 9 ++++++ vehicle/bmw.go | 5 ++-- vehicle/bmw/identity.go | 40 ++++++++++++++++++++++++-- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/templates/definition/vehicle/bmw.yaml b/templates/definition/vehicle/bmw.yaml index 1a1391ad0..1e36a9187 100644 --- a/templates/definition/vehicle/bmw.yaml +++ b/templates/definition/vehicle/bmw.yaml @@ -1,6 +1,12 @@ template: bmw products: - brand: BMW +requirements: + description: + de: | + Benötigt `hcaptcha` Token. Dieses muss einmalig unter https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html generiert werden. Das Token ist nur für kurze Zeit gültig. Bitte möglichst schnell nach Generierung in die Konfiguration kopieren und evcc starten. + en: | + Requires `hcaptcha` token. This must be generated once at https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html. The token is only valid for a short time. Please copy it into the configuration and start evcc as soon as possible after generation. params: - preset: vehicle-base - preset: vehicle-identify @@ -19,6 +25,8 @@ params: advanced: true - name: welcomecharge advanced: true + - name: hcaptcha + required: true render: | type: bmw {{ include "vehicle-base" . }} @@ -35,3 +43,4 @@ render: | - welcomecharge {{- end }} {{- end }} + hcaptcha: {{ .hcaptcha }} diff --git a/templates/definition/vehicle/mini.yaml b/templates/definition/vehicle/mini.yaml index 6b1b7a12f..e60cbfe7f 100644 --- a/templates/definition/vehicle/mini.yaml +++ b/templates/definition/vehicle/mini.yaml @@ -1,6 +1,12 @@ template: mini products: - brand: Mini +requirements: + description: + de: | + Benötigt `hcaptcha` Token. Dieses muss einmalig unter https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html generiert werden. Das Token ist nur für kurze Zeit gültig. Bitte möglichst schnell nach Generierung in die Konfiguration kopieren und evcc starten. + en: | + Requires `hcaptcha` token. This must be generated once at https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html. The token is only valid for a short time. Please copy it into the configuration and start evcc as soon as possible after generation. params: - preset: vehicle-base - preset: vehicle-identify @@ -17,6 +23,8 @@ params: advanced: true - name: welcomecharge advanced: true + - name: hcaptcha + required: true render: | type: mini {{ include "vehicle-base" . }} @@ -27,3 +35,4 @@ render: | {{- if eq .welcomecharge "true" }} features: ["welcomecharge"] {{- end }} + hcaptcha: {{ .hcaptcha }} diff --git a/vehicle/bmw.go b/vehicle/bmw.go index bc7fa5bdb..fe6958f48 100644 --- a/vehicle/bmw.go +++ b/vehicle/bmw.go @@ -34,6 +34,7 @@ func NewBMWMiniFromConfig(brand string, other map[string]interface{}) (api.Vehic cc := struct { embed `mapstructure:",squash"` User, Password, VIN string + Hcaptcha string Region string Cache time.Duration }{ @@ -45,7 +46,7 @@ func NewBMWMiniFromConfig(brand string, other map[string]interface{}) (api.Vehic return nil, err } - if cc.User == "" || cc.Password == "" { + if cc.User == "" || cc.Password == "" || cc.Hcaptcha == "" { return nil, api.ErrMissingCredentials } @@ -56,7 +57,7 @@ func NewBMWMiniFromConfig(brand string, other map[string]interface{}) (api.Vehic log := util.NewLogger(brand).Redact(cc.User, cc.Password, cc.VIN) identity := bmw.NewIdentity(log, cc.Region) - ts, err := identity.Login(cc.User, cc.Password) + ts, err := identity.Login(cc.User, cc.Password, cc.Hcaptcha) if err != nil { return nil, err } diff --git a/vehicle/bmw/identity.go b/vehicle/bmw/identity.go index 03faf3f58..f4b4af1ec 100644 --- a/vehicle/bmw/identity.go +++ b/vehicle/bmw/identity.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/oauth" "github.com/evcc-io/evcc/util/request" @@ -23,6 +24,8 @@ const ( type Identity struct { *request.Helper region Region + log *util.Logger + user string } // NewIdentity creates BMW identity @@ -30,15 +33,32 @@ func NewIdentity(log *util.Logger, region string) *Identity { v := &Identity{ Helper: request.NewHelper(log), region: regions[strings.ToUpper(region)], + log: log, } return v } -func (v *Identity) Login(user, password string) (oauth2.TokenSource, error) { +func (v *Identity) Login(user, password, hcaptcha string) (oauth2.TokenSource, error) { v.Client.CheckRedirect = request.DontFollow defer func() { v.Client.CheckRedirect = nil }() + v.user = user + + // database token + var tok oauth2.Token + if err := settings.Json(v.settingsKey(), &tok); err == nil { + v.log.DEBUG.Println("identity.Login - database token found") + tok, err := v.RefreshToken(&tok) + if err == nil { + ts := oauth2.ReuseTokenSourceWithExpiry(tok, oauth.RefreshTokenSource(tok, v), 15*time.Minute) + return ts, nil + } + v.log.DEBUG.Println("identity.Login - database token invalid. Proceeding to login via user, password and captcha.") + } else { + v.log.DEBUG.Println("identity.Login - no database token found. Proceeding to login via user, password and captcha.") + } + cv := oauth2.GenerateVerifier() v.Jar, _ = cookiejar.New(&cookiejar.Options{ @@ -60,7 +80,11 @@ func (v *Identity) Login(user, password string) (oauth2.TokenSource, error) { } uri := fmt.Sprintf("%s/oauth/authenticate", v.region.AuthURI) - req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), request.URLEncoding) + headers := map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + "hcaptchatoken": hcaptcha, + } + req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), headers) if err != nil { return nil, err } @@ -141,9 +165,15 @@ func (v *Identity) retrieveToken(data url.Values) (*oauth2.Token, error) { var tok oauth2.Token if err == nil { err = v.DoJSON(req, &tok) + } else { + return nil, err } - return util.TokenWithExpiry(&tok), err + tokex := util.TokenWithExpiry(&tok) + + err = settings.SetJson(v.settingsKey(), tokex) + + return tokex, err } func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { @@ -155,3 +185,7 @@ func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { return v.retrieveToken(data) } + +func (v *Identity) settingsKey() string { + return fmt.Sprintf("bmw.%s", v.user) +}