diff --git a/templates/definition/vehicle/hyundai.yaml b/templates/definition/vehicle/hyundai.yaml index 1467f6c51..5c983114d 100644 --- a/templates/definition/vehicle/hyundai.yaml +++ b/templates/definition/vehicle/hyundai.yaml @@ -5,8 +5,14 @@ products: generic: Bluelink requirements: description: - en: Some models (e.g. Kona) switch internally to 2 phases at low charging currents (< 8A). In cases where the wallbox also measures the phase currents, this leads to undesirable fluctuations in the charging power. The remedy here is to set the minimum charging current to 8A. - de: Manche Modelle (z.B. Kona) schalten bei geringen Ladeströmen (< 8A) intern auf 2 Phasen um. In den Fällen, in denen die Wallbox auch die Phasenströme misst, führt das zu unerwünschten Schwankungen der Ladeleistung. Abhilfe schafft hier, den Mindestladestrom auf 8A zu setzen. + en: | + Instead of your account's password, the password field needs to be filled with a `refresh_token` ([instructions](https://github.com/evcc-io/evcc/wiki/Hyundai-Kia:-Refresh%E2%80%90Token-ermitteln#english-version)). + + Some models (e.g. Kona) switch internally to 2 phases at low charging currents (< 8A). In cases where the wallbox also measures the phase currents, this leads to undesirable fluctuations in the charging power. The remedy here is to set the minimum charging current to 8A. + de: | + Anstelle des Passworts muss in das Passwort-Feld ein `refresh_token` eingetragen werden ([Anleitung](https://github.com/evcc-io/evcc/wiki/Hyundai-Kia:-Refresh%E2%80%90Token-ermitteln)). + + Manche Modelle (z.B. Kona) schalten bei geringen Ladeströmen (< 8A) intern auf 2 Phasen um. In den Fällen, in denen die Wallbox auch die Phasenströme misst, führt das zu unerwünschten Schwankungen der Ladeleistung. Abhilfe schafft hier, den Mindestladestrom auf 8A zu setzen. params: - preset: vehicle-base - preset: vehicle-language diff --git a/templates/definition/vehicle/kia.yaml b/templates/definition/vehicle/kia.yaml index d56ed1f1c..ecc2c9df3 100644 --- a/templates/definition/vehicle/kia.yaml +++ b/templates/definition/vehicle/kia.yaml @@ -6,11 +6,11 @@ products: requirements: description: en: | - Instead of your account's password, the password field needs to be filled with a `refresh_token` ([instructions](https://github.com/evcc-io/evcc/wiki/Kia:-Refresh%E2%80%90Token-ermitteln#english-version)). + Instead of your account's password, the password field needs to be filled with a `refresh_token` ([instructions](https://github.com/evcc-io/evcc/wiki/Hyundai-Kia:-Refresh%E2%80%90Token-ermitteln#english-version)). Some models (e.g. Niro EV) switch internally to 2 phases at low charging currents (< 8A). In cases where the wallbox also measures the phase currents, this leads to undesirable fluctuations in the charging power. The remedy here is to set the minimum charging current to 8A. de: | - Anstelle des Passworts muss in das Passwort-Feld ein `refresh_token` eingetragen werden ([Anleitung](https://github.com/evcc-io/evcc/wiki/Kia:-Refresh%E2%80%90Token-ermitteln)). + Anstelle des Passworts muss in das Passwort-Feld ein `refresh_token` eingetragen werden ([Anleitung](https://github.com/evcc-io/evcc/wiki/Hyundai-Kia:-Refresh%E2%80%90Token-ermitteln)). Manche Modelle (z.B. Niro EV) schalten bei geringen Ladeströmen (< 8A) intern auf 2 Phasen um. In den Fällen, in denen die Wallbox auch die Phasenströme misst, führt das zu unerwünschten Schwankungen der Ladeleistung. Abhilfe schafft hier, den Mindestladestrom auf 8A zu setzen. params: diff --git a/vehicle/bluelink.go b/vehicle/bluelink.go index 4ed3c2fed..473691626 100644 --- a/vehicle/bluelink.go +++ b/vehicle/bluelink.go @@ -26,18 +26,15 @@ func init() { func NewHyundaiFromConfig(other map[string]interface{}) (api.Vehicle, error) { settings := bluelink.Config{ URI: "https://prd.eu-ccapi.hyundai.com:8080", - BasicToken: "NmQ0NzdjMzgtM2NhNC00Y2YzLTk1NTctMmExOTI5YTk0NjU0OktVeTQ5WHhQekxwTHVvSzB4aEJDNzdXNlZYaG10UVI5aVFobUlGampvWTRJcHhzVg==", + BasicToken: "KUy49XxPzLpLuoK0xhBC77W6VXhmtQR9iQhmIFjjoY4IpxsV", CCSPServiceID: "6d477c38-3ca4-4cf3-9557-2a1929a94654", CCSPApplicationID: bluelink.HyundaiAppID, AuthClientID: "64621b96-0f0d-11ec-82a8-0242ac130003", - BrandAuthUrl: "https://eu-account.hyundai.com/auth/realms/euhyundaiidm/protocol/openid-connect/auth?client_id=%s&scope=openid+profile+email+phone&response_type=code&hkid_session_reset=true&redirect_uri=%s/api/v1/user/integration/redirect/login&ui_locales=%s&state=%s:%s", PushType: "GCM", Cfb: "RFtoRq/vDXJmRndoZaZQyfOot7OrIqGVFj96iY2WL3yyH5Z/pUvlUhqmCxD2t+D65SQ=", Brand: "hyundai", - // for oauth2?? - // LoginFormHost: "https://idpconnect-eu.hyundai.com", - // AuthClientID: "6d477c38-3ca4-4cf3-9557-2a1929a94654", - // BrandAuthUrl: "%s/auth/api/v2/user/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s/api/v1/user/oauth2/redirect&lang=%s&state=ccsp", + LoginFormHost: "https://idpconnect-eu.hyundai.com", + BrandAuthUrl: "%s/auth/api/v2/user/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s/api/v1/user/oauth2/redirect&lang=%s&state=ccsp", } return newBluelinkFromConfig("hyundai", other, settings) diff --git a/vehicle/bluelink/identity.go b/vehicle/bluelink/identity.go index 180518147..ee30c7440 100644 --- a/vehicle/bluelink/identity.go +++ b/vehicle/bluelink/identity.go @@ -5,20 +5,17 @@ import ( "errors" "fmt" "net/http" - "net/http/cookiejar" "net/url" "strconv" "strings" "time" - "github.com/PuerkitoBio/goquery" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/oauth" "github.com/evcc-io/evcc/util/request" "github.com/google/uuid" "github.com/samber/lo" - "golang.org/x/net/publicsuffix" "golang.org/x/oauth2" ) @@ -106,289 +103,6 @@ func (v *Identity) getDeviceID() (string, error) { return res.ResMsg.DeviceID, err } -func (v *Identity) getCookies() (cookieClient *request.Helper, err error) { - cookieClient = request.NewHelper(v.log) - cookieClient.Client.Jar, _ = cookiejar.New(&cookiejar.Options{ - PublicSuffixList: publicsuffix.List, - }) - - // TODO: check whether &lang= is necessary - uri := fmt.Sprintf( - "%s/api/v1/user/oauth2/authorize?response_type=code&state=test&client_id=%s&redirect_uri=%s/api/v1/user/oauth2/redirect", - v.config.URI, - v.config.CCSPServiceID, - v.config.URI, - ) - - resp, err := cookieClient.Get(uri) - if err == nil { - resp.Body.Close() - } - - return cookieClient, err -} - -func (v *Identity) setLanguage(cookieClient *request.Helper, language string) error { - data := map[string]interface{}{ - "lang": language, - } - - req, err := request.New(http.MethodPost, v.config.URI+LanguageURL, request.MarshalJSON(data), request.JSONEncoding) - if err == nil { - var resp *http.Response - if resp, err = cookieClient.Do(req); err == nil { - resp.Body.Close() - } - } - - return err -} - -func (v *Identity) brandLoginHyundaiEU(cookieClient *request.Helper, user, password string) (string, error) { - req, err := request.New(http.MethodGet, v.config.URI+IntegrationInfoURL, nil, request.JSONEncoding) - - var info struct { - UserId string `json:"userId"` - ServiceId string `json:"serviceId"` - } - - if err == nil { - err = cookieClient.DoJSON(req, &info) - } - - var action string - var resp *http.Response - - if err == nil { - uri := fmt.Sprintf(v.config.BrandAuthUrl, v.config.AuthClientID, v.config.URI, "en", info.ServiceId, info.UserId) - - req, err = request.New(http.MethodGet, uri, nil) - if err == nil { - if resp, err = cookieClient.Do(req); err == nil { - defer resp.Body.Close() - - var doc *goquery.Document - if doc, err = goquery.NewDocumentFromReader(resp.Body); err == nil { - err = errors.New("form not found") - - if form := doc.Find("form"); form != nil && form.Length() == 1 { - var ok bool - if action, ok = form.Attr("action"); ok { - err = nil - } - } - } - } - } - } - - if err == nil { - data := url.Values{ - "username": {user}, - "password": {password}, - "credentialId": {""}, - "rememberMe": {"on"}, - } - - req, err = request.New(http.MethodPost, action, strings.NewReader(data.Encode()), request.URLEncoding) - if err == nil { - cookieClient.CheckRedirect = request.DontFollow - if resp, err = cookieClient.Do(req); err == nil { - defer resp.Body.Close() - - // need 302 - if resp.StatusCode != http.StatusFound { - err = errors.New("missing redirect") - - if doc, err2 := goquery.NewDocumentFromReader(resp.Body); err2 == nil { - if span := doc.Find("span[class=kc-feedback-text]"); span != nil && span.Length() == 1 { - err = errors.New(span.Text()) - } - } - } - } - - cookieClient.CheckRedirect = nil - } - } - - if err == nil { - resp, err = cookieClient.Get(resp.Header.Get("Location")) - if err == nil { - defer resp.Body.Close() - } - } - - var code string - if err == nil { - data := map[string]string{ - "intUserId": "", - } - - req, err = request.New(http.MethodPost, v.config.URI+SilentSigninURL, request.MarshalJSON(data), request.JSONEncoding) - if err == nil { - req.Header.Set("ccsp-service-id", v.config.CCSPServiceID) - cookieClient.CheckRedirect = request.DontFollow - - var res struct { - RedirectUrl string `json:"redirectUrl"` - } - - if err = cookieClient.DoJSON(req, &res); err == nil { - var uri *url.URL - if uri, err = url.Parse(res.RedirectUrl); err == nil { - if code = uri.Query().Get("code"); len(code) == 0 { - err = errors.New("code not found") - } - } - } - } - } - - return code, err -} - -/* Unused for now -func (v *Identity) brandLoginKiaEU(user, password string) (string, error) { - cookieClient := request.NewHelper(v.log) - cookieClient.Client.Jar, _ = cookiejar.New(&cookiejar.Options{ - PublicSuffixList: publicsuffix.List, - }) - - headers := map[string]string{ - "content-type": "application/x-www-form-urlencoded", - "User-Agent": "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19_CCS_APP_AOS", - // "User-Agent": "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19", - } - - data := url.Values{ - "client_id": {"peukiaidm-online-sales"}, - "encryptedPassword": {"false"}, - "password": {password}, - "redirect_uri": {"https://www.kia.com/api/bin/oneid/login"}, - "state": {"aHR0cHM6Ly93d3cua2lhLmNvbTo0NDMvZGUvP3ZlZD0yYWhVS0V3akI2ZFc3dDQtUEF4WFBSZkVESGNDQ0J4UVFnVTk2QkFnY0VBZyZfdG09MTc1NTg1NTY2ODE2Mg==_default"}, - "username": {user}, - "remember_me": {"false"}, - } - - req, _ := request.New(http.MethodPost, "https://idpconnect-eu.kia.com/auth/account/signin", strings.NewReader(data.Encode()), headers) - - if _, err := cookieClient.Do(req); err != nil { - return "", err - } - - v.deviceID, _ = v.getDeviceID() - - // get the connector_session_key - uri := fmt.Sprintf(v.config.BrandAuthUrl, v.config.LoginFormHost, v.config.CCSPServiceID, v.config.URI, "en") - headers = map[string]string{ - "ccsp-application-id": v.config.CCSPApplicationID, - "ccsp-device-id": v.deviceID, - "ccsp-service-id": v.config.CCSPServiceID, - "User-Agent": "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19", - } - req, _ = request.New(http.MethodGet, uri, nil, headers) - resp, err := cookieClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - // get redirect URL from request - nextUri := resp.Request.URL.Query().Get("next_uri") - if nextUri == "" { - return "", errors.New("empty redirect url on connector session key request") - } - - // create a client that doesn't honor redirects so we receive the original response - // no idea how to do that with the internal request.New(...) function - sc := http.Client{ - Jar: cookieClient.Client.Jar, - Transport: request.NewTripper(v.log, http.DefaultTransport), - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - } - - req, err = request.New(http.MethodGet, nextUri, nil, headers) - if err != nil { - return "", err - } - - resp, err = sc.Do(req) - if err != nil { - return "", err - } - - location := resp.Header.Get("Location") - if location == "" { - return "", errors.New("missing location header") - } - - locationUrl, err := url.Parse(location) - if err != nil { - return "", err - } - - code := locationUrl.Query().Get("code") - if code == "" { - return "", errors.New("missing code") - } - - return code, nil -} -*/ - -func (v *Identity) bluelinkLogin(cookieClient *request.Helper, user, password string) (string, error) { - data := map[string]interface{}{ - "email": user, - "password": password, - } - - req, err := request.New(http.MethodPost, v.config.URI+LoginURL, request.MarshalJSON(data), request.JSONEncoding) - if err != nil { - return "", err - } - - var res struct { - RedirectURL string `json:"redirectUrl"` - ErrCode string `json:"errCode"` - ErrMsg string `json:"errMsg"` - } - - var accCode string - if err = cookieClient.DoJSON(req, &res); err == nil { - if parsed, err := url.Parse(res.RedirectURL); err == nil { - accCode = parsed.Query().Get("code") - } - } else if res.ErrCode != "" { - err = fmt.Errorf("%w: %s (%s)", err, res.ErrMsg, res.ErrCode) - } - - return accCode, err -} - -func (v *Identity) exchangeCodeHyundaiEU(accCode string) (*oauth2.Token, error) { - headers := map[string]string{ - "Authorization": "Basic " + v.config.BasicToken, - "Content-type": "application/x-www-form-urlencoded", - "User-Agent": "okhttp/3.10.0", - } - - data := url.Values{ - "grant_type": {"authorization_code"}, - "redirect_uri": {v.config.URI + "/api/v1/user/oauth2/redirect"}, - "code": {accCode}, - } - - var token oauth2.Token - - req, _ := request.New(http.MethodPost, v.config.URI+TokenURL, strings.NewReader(data.Encode()), headers) - err := v.DoJSON(req, &token) - - return util.TokenWithExpiry(&token), err -} - func (v *Identity) exchangeCodeKiaEURefreshToken(accCode string) (*oauth2.Token, error) { uri := v.config.LoginFormHost + "/auth/api/v2/user/oauth2/token" headers := map[string]string{ @@ -413,30 +127,6 @@ func (v *Identity) exchangeCodeKiaEURefreshToken(accCode string) (*oauth2.Token, return util.TokenWithExpiry(&token), err } -/* Unused for now -func (v *Identity) exchangeCodeKiaEU(accCode string) (*oauth2.Token, error) { - uri := v.config.LoginFormHost + "/auth/api/v2/user/oauth2/token" - headers := map[string]string{ - "Content-type": "application/x-www-form-urlencoded", - "User-Agent": "okhttp/3.10.0", - } - data := url.Values{ - "grant_type": {"authorization_code"}, - "code": {accCode}, - "redirect_uri": {v.config.URI + "/api/v1/user/oauth2/redirect"}, - "client_id": {v.config.CCSPServiceID}, - "client_secret": {"secret"}, - } - - var token oauth2.Token - - req, _ := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), headers) - err := v.DoJSON(req, &token) - - return util.TokenWithExpiry(&token), err -} -*/ - // RefreshToken implements oauth.TokenRefresher func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { var res oauth2.Token @@ -450,8 +140,7 @@ func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { headers = map[string]string{ "Authorization": "Basic " + v.config.BasicToken, "Content-type": "application/x-www-form-urlencoded", - // "User-Agent": "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19_CCS_APP_AOS", - "User-Agent": "okhttp/3.10.0", + "User-Agent": "okhttp/3.10.0", } data = url.Values{ @@ -473,7 +162,7 @@ func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { "client_secret": {"secret"}, } default: - err = errors.New("Unsupported brand") + err = errors.New("unsupported brand") } // request token only if we didn't run unto the default branch @@ -499,43 +188,25 @@ func (v *Identity) Login(user, password, language, brand string) (err error) { if user == "" || password == "" { return api.ErrMissingCredentials } - var code string + // var code string switch brand { case "kia": // the "password" now is the refresh token ... - // code, err = v.brandLoginKiaEU(user, password) - // if err == nil { var token *oauth2.Token token, err = v.exchangeCodeKiaEURefreshToken(password) if err == nil { v.TokenSource = oauth.RefreshTokenSource(token, v) v.deviceID, err = v.getDeviceID() } - // } + case "hyundai": - v.deviceID, err = v.getDeviceID() - - var cookieClient *request.Helper + var token *oauth2.Token + token, err = v.exchangeCodeKiaEURefreshToken(password) if err == nil { - cookieClient, err = v.getCookies() + v.TokenSource = oauth.RefreshTokenSource(token, v) + v.deviceID, err = v.getDeviceID() } - if err == nil { - err = v.setLanguage(cookieClient, language) - } - - if err == nil { - // try new login first, then fallback - if code, err = v.brandLoginHyundaiEU(cookieClient, user, password); err != nil { - code, err = v.bluelinkLogin(cookieClient, user, password) - } - if err == nil { - var token *oauth2.Token - if token, err = v.exchangeCodeHyundaiEU(code); err == nil { - v.TokenSource = oauth.RefreshTokenSource(token, v) - } - } - } default: err = fmt.Errorf("unknown brand (%s)", brand) }