Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BMW/Mini hcaptcha integration #17445

Merged
merged 9 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions templates/definition/vehicle/bmw.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,6 +25,8 @@ params:
advanced: true
- name: welcomecharge
advanced: true
- name: hcaptcha
required: true
render: |
type: bmw
{{ include "vehicle-base" . }}
Expand All @@ -35,3 +43,4 @@ render: |
- welcomecharge
{{- end }}
{{- end }}
hcaptcha: {{ .hcaptcha }}
9 changes: 9 additions & 0 deletions templates/definition/vehicle/mini.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,6 +23,8 @@ params:
advanced: true
- name: welcomecharge
advanced: true
- name: hcaptcha
required: true
render: |
type: mini
{{ include "vehicle-base" . }}
Expand All @@ -27,3 +35,4 @@ render: |
{{- if eq .welcomecharge "true" }}
features: ["welcomecharge"]
{{- end }}
hcaptcha: {{ .hcaptcha }}
5 changes: 3 additions & 2 deletions vehicle/bmw.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}{
Expand All @@ -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
}

Expand All @@ -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
}
Expand Down
40 changes: 37 additions & 3 deletions vehicle/bmw/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -23,22 +24,41 @@ const (
type Identity struct {
*request.Helper
region Region
log *util.Logger
user string
}

// NewIdentity creates BMW identity
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{
Expand All @@ -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
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}