diff --git a/templates/definition/vehicle/hyundai.yaml b/templates/definition/vehicle/hyundai.yaml index fce409de6..d138e77ae 100644 --- a/templates/definition/vehicle/hyundai.yaml +++ b/templates/definition/vehicle/hyundai.yaml @@ -16,7 +16,14 @@ requirements: params: - preset: vehicle-base - preset: vehicle-language + - name: region + default: Europe + choice: ["Europe", "Australia", "New Zealand"] + description: + en: Region your Bluelink account is registered in + de: Region, in der dein Bluelink-Konto registriert ist render: | type: hyundai + region: {{ .region }} {{ include "vehicle-base" . }} {{ include "vehicle-language" . }} diff --git a/vehicle/bluelink.go b/vehicle/bluelink.go index 0548ace9c..423e52419 100644 --- a/vehicle/bluelink.go +++ b/vehicle/bluelink.go @@ -1,6 +1,8 @@ package vehicle import ( + "fmt" + "strings" "time" "github.com/evcc-io/evcc/api" @@ -25,16 +27,46 @@ func init() { // NewHyundaiFromConfig creates a new vehicle func NewHyundaiFromConfig(other map[string]any) (api.Vehicle, error) { - settings := bluelink.Config{ - URI: "https://prd.eu-ccapi.hyundai.com:8080", - CCSPServiceID: "6d477c38-3ca4-4cf3-9557-2a1929a94654", - CCSPServiceSecret: "KUy49XxPzLpLuoK0xhBC77W6VXhmtQR9iQhmIFjjoY4IpxsV", - CCSPApplicationID: "014d2225-8495-4735-812d-2616334fd15d", - Cfb: "RFtoRq/vDXJmRndoZaZQyfOot7OrIqGVFj96iY2WL3yyH5Z/pUvlUhqmCxD2t+D65SQ=", - BasicToken: "NmQ0NzdjMzgtM2NhNC00Y2YzLTk1NTctMmExOTI5YTk0NjU0OktVeTQ5WHhQekxwTHVvSzB4aEJDNzdXNlZYaG10UVI5aVFobUlGampvWTRJcHhzVg==", - PushType: "GCM", - LoginFormHost: "https://idpconnect-eu.hyundai.com", - Brand: "hyundai", + // Decode region early so we can select the right config + cc := struct { + Region string + }{ + Region: "Europe", + } + if region, ok := other["region"].(string); ok { + cc.Region = strings.TrimSpace(region) + } + + var settings bluelink.Config + switch strings.ToLower(cc.Region) { + case "europe", "": + settings = bluelink.Config{ + URI: "https://prd.eu-ccapi.hyundai.com:8080", + CCSPServiceID: "6d477c38-3ca4-4cf3-9557-2a1929a94654", + CCSPServiceSecret: "KUy49XxPzLpLuoK0xhBC77W6VXhmtQR9iQhmIFjjoY4IpxsV", + CCSPApplicationID: "014d2225-8495-4735-812d-2616334fd15d", + Cfb: "RFtoRq/vDXJmRndoZaZQyfOot7OrIqGVFj96iY2WL3yyH5Z/pUvlUhqmCxD2t+D65SQ=", + BasicToken: "NmQ0NzdjMzgtM2NhNC00Y2YzLTk1NTctMmExOTI5YTk0NjU0OktVeTQ5WHhQekxwTHVvSzB4aEJDNzdXNlZYaG10UVI5aVFobUlGampvWTRJcHhzVg==", + PushType: "GCM", + LoginFormHost: "https://idpconnect-eu.hyundai.com", + Brand: "hyundai", + } + case "australia", "new zealand": + settings = bluelink.Config{ + URI: bluelink.HyundaiAUBaseURI, + CCSPServiceID: bluelink.HyundaiAUCCSPServiceID, + CCSPServiceSecret: "e6fbwHM32YNbhQl0pviaPp3rf4t3S6k91eceA3MJLdbdThCO", + CCSPApplicationID: bluelink.HyundaiAUCCSPAppID, + Cfb: bluelink.HyundaiAUCfb, + BasicToken: bluelink.HyundaiAUBasicToken, + PushType: "GCM", + LoginFormHost: bluelink.HyundaiAUBaseURI, + TokenURL: "/api/v1/user/oauth2/token", + Brand: "hyundai", + UseBasicAuth: true, + } + default: + return nil, fmt.Errorf("unknown region: %s", cc.Region) } return newBluelinkFromConfig("hyundai", other, settings) @@ -82,6 +114,7 @@ func newBluelinkFromConfig(brand string, other map[string]any, settings bluelink User, Password string VIN string Language string + Region string Expiry time.Duration Cache time.Duration }{ diff --git a/vehicle/bluelink/identity.go b/vehicle/bluelink/identity.go index d7021b3b1..c798c53e4 100644 --- a/vehicle/bluelink/identity.go +++ b/vehicle/bluelink/identity.go @@ -20,12 +20,15 @@ import ( ) const ( - DeviceIdURL = "/api/v1/spa/notifications/register" - IntegrationInfoURL = "/api/v1/user/integrationinfo" - SilentSigninURL = "/api/v1/user/silentsignin" - LanguageURL = "/api/v1/user/language" - LoginURL = "/api/v1/user/signin" - TokenURL = "/auth/api/v2/user/oauth2/token" + LanguageURL = "/api/v1/user/language" + LoginURL = "/api/v1/user/signin" + TokenURL = "/auth/api/v2/user/oauth2/token" + // AU region constants (sourced from hyundai_kia_connect_api KiaUvoApiAU.py) + HyundaiAUBaseURI = "https://au-apigw.ccs.hyundai.com.au:8080" + HyundaiAUCCSPServiceID = "855c72df-dfd7-4230-ab03-67cbf902bb1c" + HyundaiAUCCSPAppID = "f9ccfdac-a48d-4c57-bd32-9116963c24ed" + HyundaiAUBasicToken = "ODU1YzcyZGYtZGZkNy00MjMwLWFiMDMtNjdjYmY5MDJiYjFjOmU2ZmJ3SE0zMllOYmhRbDBwdmlhUHAzcmY0dDNTNms5MWVjZUEzTUpMZGJkVGhDTw==" + HyundaiAUCfb = "nGDHng3k4Cg9gWV+C+A6Yk/ecDopUNTkGmDpr2qVKAQXx9bvY2/YLoHPfObliK32mZQ=" ) // Config is the bluelink API configuration @@ -39,6 +42,8 @@ type Config struct { Cfb string LoginFormHost string Brand string + TokenURL string + UseBasicAuth bool } // Identity implements the Kia/Hyundai bluelink identity. @@ -90,7 +95,7 @@ func (v *Identity) getDeviceID() (string, error) { } } - req, err := request.New(http.MethodPost, v.config.URI+DeviceIdURL, request.MarshalJSON(data), headers) + req, err := request.New(http.MethodPost, v.config.URI+"/api/v1/spa/notifications/register", request.MarshalJSON(data), headers) if err == nil { err = v.DoJSON(req, &res) } @@ -106,16 +111,34 @@ func (v *Identity) getDeviceID() (string, error) { func (v *Identity) refreshToken(token *oauth2.Token) (*oauth2.Token, error) { var res oauth2.Token - uri := v.config.LoginFormHost + TokenURL + tokenURL := TokenURL + if v.config.TokenURL != "" { + tokenURL = v.config.TokenURL + } + 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": {token.RefreshToken}, - "client_id": {v.config.CCSPServiceID}, - "client_secret": {v.config.CCSPServiceSecret}, + } + + // EU uses client_id/secret in body; AU uses Basic auth header + if v.config.UseBasicAuth { + stamp, err := v.stamp() + if err != nil { + return nil, err + } + headers["Authorization"] = "Basic " + v.config.BasicToken + headers["Stamp"] = stamp + headers["User-Agent"] = "okhttp/3.12.0" + } else { + data.Set("client_id", v.config.CCSPServiceID) + data.Set("client_secret", v.config.CCSPServiceSecret) } req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), headers)