Kia (EU): use refresh_token as password (#23523)

This commit is contained in:
stefan 2025-09-14 14:51:47 +02:00 • committed by GitHub
parent a9ec457b57
commit fab9464cb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 99 additions and 25 deletions

View file

@ -5,8 +5,14 @@ products:
generic: Bluelink
requirements:
description:
en: 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: 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.
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)).
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)).
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:
- preset: vehicle-base
- preset: vehicle-language

View file

@ -33,6 +33,7 @@ func NewHyundaiFromConfig(other map[string]interface{}) (api.Vehicle, error) {
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",
@ -54,6 +55,7 @@ func NewKiaFromConfig(other map[string]interface{}) (api.Vehicle, error) {
PushType: "APNS",
Cfb: "wLTVxwidmH8CfJYBWSnHD6E0huk0ozdiuygB4hLkM5XCgzAL1Dk5sE36d/bx5PFMbZs=",
LoginFormHost: "https://idpconnect-eu.kia.com",
Brand: "kia",
}
return newBluelinkFromConfig("kia", other, settings)

View file

@ -42,6 +42,7 @@ type Config struct {
PushType string
Cfb string
LoginFormHost string
Brand string
}
// Identity implements the Kia/Hyundai bluelink identity.
@ -247,6 +248,7 @@ func (v *Identity) brandLoginHyundaiEU(cookieClient *request.Helper, user, passw
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{
@ -255,7 +257,8 @@ func (v *Identity) brandLoginKiaEU(user, password string) (string, error) {
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",
"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{
@ -334,6 +337,7 @@ func (v *Identity) brandLoginKiaEU(user, password string) (string, error) {
return code, nil
}
*/
func (v *Identity) bluelinkLogin(cookieClient *request.Helper, user, password string) (string, error) {
data := map[string]interface{}{
@ -385,6 +389,31 @@ func (v *Identity) exchangeCodeHyundaiEU(accCode string) (*oauth2.Token, error)
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{
"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},
"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
}
/* 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{
@ -406,26 +435,61 @@ func (v *Identity) exchangeCodeKiaEU(accCode string) (*oauth2.Token, error) {
return util.TokenWithExpiry(&token), err
}
*/
// RefreshToken implements oauth.TokenRefresher
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": "okhttp/3.10.0",
}
data := url.Values{
"grant_type": {"refresh_token"},
"redirect_uri": {"https://www.getpostman.com/oauth2/callback"},
"refresh_token": {token.RefreshToken},
}
req, err := request.New(http.MethodPost, v.config.URI+TokenURL, strings.NewReader(data.Encode()), headers)
var res oauth2.Token
if err == nil {
err = v.DoJSON(req, &res)
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": "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",
}
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
}
req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), headers)
if err != nil {
return nil, err
}
err = v.DoJSON(req, &res)
// carry over the old refresh token (if any and not populated already)
if res.RefreshToken == "" && token.RefreshToken != "" {
res.RefreshToken = token.RefreshToken
}
return util.TokenWithExpiry(&res), err
@ -438,13 +502,15 @@ func (v *Identity) Login(user, password, language, brand string) (err error) {
var code string
switch brand {
case "kia":
code, err = v.brandLoginKiaEU(user, password)
if err == nil {
var token *oauth2.Token
if token, err = v.exchangeCodeKiaEU(code); err == nil {
v.TokenSource = oauth.RefreshTokenSource(token, v)
}
// the "password" now is the refresh token ...
// code, err = v.brandLoginKiaEU(user, password)
// if err == nil {
var token *oauth2.Token
if token, err = v.exchangeCodeKiaEURefreshToken(password); err == nil {
v.TokenSource = oauth.RefreshTokenSource(token, v)
}
v.deviceID, err = v.getDeviceID()
// }
case "hyundai":
v.deviceID, err = v.getDeviceID()