From 3fd38808a588e26cb9f012edf896cdf4f64ac81f Mon Sep 17 00:00:00 2001 From: stefan Date: Mon, 20 Oct 2025 14:25:55 +0200 Subject: [PATCH] Hyundai/Kia: fix token refresh (#24580) --- templates/definition/vehicle/hyundai.yaml | 4 +- templates/definition/vehicle/kia.yaml | 4 +- vehicle/bluelink.go | 20 +++-- vehicle/bluelink/identity.go | 98 +++++------------------ vehicle/bluelink/params.go | 6 -- 5 files changed, 32 insertions(+), 100 deletions(-) delete mode 100644 vehicle/bluelink/params.go diff --git a/templates/definition/vehicle/hyundai.yaml b/templates/definition/vehicle/hyundai.yaml index 5c983114d..fce409de6 100644 --- a/templates/definition/vehicle/hyundai.yaml +++ b/templates/definition/vehicle/hyundai.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/Hyundai-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-Token)). 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)). + Anstelle des Passworts muss in das Passwort-Feld ein `refresh_token` eingetragen werden ([Anleitung](https://github.com/evcc-io/evcc/wiki/Hyundai-Kia:-Refresh-Token)). 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: diff --git a/templates/definition/vehicle/kia.yaml b/templates/definition/vehicle/kia.yaml index ecc2c9df3..c866ae4dd 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/Hyundai-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-Token)). 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/Hyundai-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-Token)). 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 473691626..2d267af38 100644 --- a/vehicle/bluelink.go +++ b/vehicle/bluelink.go @@ -26,15 +26,14 @@ func init() { func NewHyundaiFromConfig(other map[string]interface{}) (api.Vehicle, error) { settings := bluelink.Config{ URI: "https://prd.eu-ccapi.hyundai.com:8080", - BasicToken: "KUy49XxPzLpLuoK0xhBC77W6VXhmtQR9iQhmIFjjoY4IpxsV", CCSPServiceID: "6d477c38-3ca4-4cf3-9557-2a1929a94654", - CCSPApplicationID: bluelink.HyundaiAppID, - AuthClientID: "64621b96-0f0d-11ec-82a8-0242ac130003", - PushType: "GCM", + CCSPServiceSecret: "KUy49XxPzLpLuoK0xhBC77W6VXhmtQR9iQhmIFjjoY4IpxsV", + CCSPApplicationID: "014d2225-8495-4735-812d-2616334fd15d", Cfb: "RFtoRq/vDXJmRndoZaZQyfOot7OrIqGVFj96iY2WL3yyH5Z/pUvlUhqmCxD2t+D65SQ=", - Brand: "hyundai", + BasicToken: "NmQ0NzdjMzgtM2NhNC00Y2YzLTk1NTctMmExOTI5YTk0NjU0OktVeTQ5WHhQekxwTHVvSzB4aEJDNzdXNlZYaG10UVI5aVFobUlGampvWTRJcHhzVg==", + PushType: "GCM", 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", + Brand: "hyundai", } return newBluelinkFromConfig("hyundai", other, settings) @@ -44,14 +43,13 @@ func NewHyundaiFromConfig(other map[string]interface{}) (api.Vehicle, error) { func NewKiaFromConfig(other map[string]interface{}) (api.Vehicle, error) { settings := bluelink.Config{ URI: "https://prd.eu-ccapi.kia.com:8080", - BasicToken: "ZmRjODVjMDAtMGEyZi00YzY0LWJjYjQtMmNmYjE1MDA3MzBhOnNlY3JldA==", CCSPServiceID: "fdc85c00-0a2f-4c64-bcb4-2cfb1500730a", - CCSPApplicationID: bluelink.KiaAppID, - AuthClientID: "fdc85c00-0a2f-4c64-bcb4-2cfb1500730a", - 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", - PushType: "APNS", + CCSPServiceSecret: "secret", + CCSPApplicationID: "a2b8469b-30a3-4361-8e13-6fceea8fbe74", Cfb: "wLTVxwidmH8CfJYBWSnHD6E0huk0ozdiuygB4hLkM5XCgzAL1Dk5sE36d/bx5PFMbZs=", + BasicToken: "ZmRjODVjMDAtMGEyZi00YzY0LWJjYjQtMmNmYjE1MDA3MzBhOnNlY3JldA==", LoginFormHost: "https://idpconnect-eu.kia.com", + PushType: "APNS", Brand: "kia", } diff --git a/vehicle/bluelink/identity.go b/vehicle/bluelink/identity.go index ee30c7440..63522e6a4 100644 --- a/vehicle/bluelink/identity.go +++ b/vehicle/bluelink/identity.go @@ -25,17 +25,16 @@ const ( SilentSigninURL = "/api/v1/user/silentsignin" LanguageURL = "/api/v1/user/language" LoginURL = "/api/v1/user/signin" - TokenURL = "/api/v1/user/oauth2/token" + TokenURL = "/auth/api/v2/user/oauth2/token" ) // Config is the bluelink API configuration type Config struct { URI string - AuthClientID string // v2 - BrandAuthUrl string // v2 BasicToken string CCSPServiceID string CCSPApplicationID string + CCSPServiceSecret string PushType string Cfb string LoginFormHost string @@ -103,71 +102,20 @@ func (v *Identity) getDeviceID() (string, error) { return res.ResMsg.DeviceID, err } -func (v *Identity) exchangeCodeKiaEURefreshToken(accCode string) (*oauth2.Token, error) { - uri := v.config.LoginFormHost + "/auth/api/v2/user/oauth2/token" +// RefreshToken implements oauth.TokenRefresher +func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { + var res oauth2.Token + + uri := v.config.LoginFormHost + TokenURL 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", } data := url.Values{ "grant_type": {"refresh_token"}, - "refresh_token": {accCode}, + "refresh_token": {token.RefreshToken}, "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) - - // manually set the refresh token - token.RefreshToken = accCode - - return util.TokenWithExpiry(&token), err -} - -// RefreshToken implements oauth.TokenRefresher -func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { - var res oauth2.Token - var err error - var uri string - var headers map[string]string - var data url.Values - switch v.config.Brand { - case "hyundai": - uri = v.config.URI + TokenURL - 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": {"refresh_token"}, - "redirect_uri": {"https://www.getpostman.com/oauth2/callback"}, - "refresh_token": {token.RefreshToken}, - } - - case "kia": - uri = v.config.LoginFormHost + "/auth/api/v2/user/oauth2/token" - 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", - } - data = url.Values{ - "grant_type": {"refresh_token"}, - "refresh_token": {token.RefreshToken}, - "client_id": {v.config.CCSPServiceID}, - "client_secret": {"secret"}, - } - default: - err = errors.New("unsupported brand") - } - - // request token only if we didn't run unto the default branch - if err != nil { - return nil, err + "client_secret": {v.config.CCSPServiceSecret}, } req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), headers) @@ -188,31 +136,23 @@ func (v *Identity) Login(user, password, language, brand string) (err error) { if user == "" || password == "" { return api.ErrMissingCredentials } - // var code string + switch brand { case "kia": - // the "password" now is the refresh token ... - 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": - var token *oauth2.Token - token, err = v.exchangeCodeKiaEURefreshToken(password) - if err == nil { - v.TokenSource = oauth.RefreshTokenSource(token, v) - v.deviceID, err = v.getDeviceID() - } - default: - err = fmt.Errorf("unknown brand (%s)", brand) + return fmt.Errorf("unknown brand (%s)", brand) } + token, err := v.RefreshToken(&oauth2.Token{RefreshToken: password}) if err != nil { - err = fmt.Errorf("login failed: %w", err) + return fmt.Errorf("login failed: %w", err) + } + v.TokenSource = oauth.RefreshTokenSource(token, v) + + v.deviceID, err = v.getDeviceID() + if err != nil { + return fmt.Errorf("error getting device id: %w", err) } return err diff --git a/vehicle/bluelink/params.go b/vehicle/bluelink/params.go deleted file mode 100644 index 6f850bb44..000000000 --- a/vehicle/bluelink/params.go +++ /dev/null @@ -1,6 +0,0 @@ -package bluelink - -const ( - KiaAppID = "a2b8469b-30a3-4361-8e13-6fceea8fbe74" - HyundaiAppID = "014d2225-8495-4735-812d-2616334fd15d" -)