diff --git a/templates/definition/vehicle/bmw.yaml b/templates/definition/vehicle/bmw.yaml index 05efccb4c..aba4fe34a 100644 --- a/templates/definition/vehicle/bmw.yaml +++ b/templates/definition/vehicle/bmw.yaml @@ -6,7 +6,18 @@ params: - preset: vehicle-identify - name: vin example: WBMW... + - name: region + description: + de: Region + en: Region + values: + - NA + - EU + default: EU render: | type: bmw {{ include "vehicle-base" . }} {{ include "vehicle-identify" . }} + {{- if ne .region "EU" }} + region: {{ .region }} + {{- end }} diff --git a/templates/definition/vehicle/mini.yaml b/templates/definition/vehicle/mini.yaml index fb1056633..e9577399f 100644 --- a/templates/definition/vehicle/mini.yaml +++ b/templates/definition/vehicle/mini.yaml @@ -6,7 +6,18 @@ params: - preset: vehicle-identify - name: vin example: WBMW... + - name: region + description: + de: Region + en: Region + values: + - NA + - EU + default: EU render: | type: mini {{ include "vehicle-base" . }} {{ include "vehicle-identify" . }} + {{- if ne .region "EU" }} + region: {{ .region }} + {{- end }} diff --git a/vehicle/bmw.go b/vehicle/bmw.go index bbeedc855..c051971a0 100644 --- a/vehicle/bmw.go +++ b/vehicle/bmw.go @@ -34,9 +34,11 @@ func NewBMWMiniFromConfig(brand string, other map[string]interface{}) (api.Vehic cc := struct { embed `mapstructure:",squash"` User, Password, VIN string + Region string Cache time.Duration }{ - Cache: interval, + Region: "EU", + Cache: interval, } if err := util.DecodeOther(other, &cc); err != nil { @@ -52,14 +54,14 @@ 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) + identity := bmw.NewIdentity(log, cc.Region) ts, err := identity.Login(cc.User, cc.Password) if err != nil { return nil, err } - api := bmw.NewAPI(log, brand, ts) + api := bmw.NewAPI(log, brand, cc.Region, ts) cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles) diff --git a/vehicle/bmw/api.go b/vehicle/bmw/api.go index 6eec9bc23..0da2a03b7 100644 --- a/vehicle/bmw/api.go +++ b/vehicle/bmw/api.go @@ -3,6 +3,7 @@ package bmw import ( "fmt" "net/http" + "strings" "time" "github.com/evcc-io/evcc/util" @@ -13,21 +14,19 @@ import ( // https://github.com/bimmerconnected/bimmer_connected // https://github.com/TA2k/ioBroker.bmw -const ( - CocoApiURI = "https://cocoapi.bmwgroup.com" -) - // API is an api.Vehicle implementation for BMW cars type API struct { *request.Helper xUserAgent string + region string } // NewAPI creates a new vehicle -func NewAPI(log *util.Logger, brand string, identity oauth2.TokenSource) *API { +func NewAPI(log *util.Logger, brand, region string, identity oauth2.TokenSource) *API { v := &API{ Helper: request.NewHelper(log), xUserAgent: fmt.Sprintf("android(SP1A.210812.016.C1);%s;99.0.0(99999);row", brand), + region: strings.ToUpper(region), } // replace client transport with authenticated transport @@ -42,7 +41,7 @@ func NewAPI(log *util.Logger, brand string, identity oauth2.TokenSource) *API { // Vehicles implements returns the /user/vehicles api func (v *API) Vehicles() ([]string, error) { var res []Vehicle - uri := fmt.Sprintf("%s/eadrax-vcs/v4/vehicles?apptimezone=120&appDateTime=%d", CocoApiURI, time.Now().UnixMilli()) + uri := fmt.Sprintf("%s/eadrax-vcs/v4/vehicles?apptimezone=120&appDateTime=%d", regions[v.region].CocoApiURI, time.Now().UnixMilli()) req, err := request.New(http.MethodGet, uri, nil, map[string]string{ "Content-Type": request.JSONContent, @@ -67,7 +66,7 @@ func (v *API) Vehicles() ([]string, error) { // Status implements the /user/vehicles//status api func (v *API) Status(vin string) (VehicleStatus, error) { var res VehicleStatus - uri := fmt.Sprintf("%s/eadrax-vcs/v4/vehicles/state?apptimezone=120&appDateTime=%d", CocoApiURI, time.Now().UnixMilli()) + uri := fmt.Sprintf("%s/eadrax-vcs/v4/vehicles/state?apptimezone=120&appDateTime=%d", regions[v.region].CocoApiURI, time.Now().UnixMilli()) req, err := request.New(http.MethodGet, uri, nil, map[string]string{ "Content-Type": request.JSONContent, diff --git a/vehicle/bmw/identity.go b/vehicle/bmw/identity.go index e31db2e72..e9b0d349f 100644 --- a/vehicle/bmw/identity.go +++ b/vehicle/bmw/identity.go @@ -18,18 +18,19 @@ import ( ) const ( - AuthURI = "https://customer.bmwgroup.com/gcdm/oauth" RedirectURI = "com.bmw.connected://oauth" ) type Identity struct { *request.Helper + region string } // NewIdentity creates BMW identity -func NewIdentity(log *util.Logger) *Identity { +func NewIdentity(log *util.Logger, region string) *Identity { v := &Identity{ Helper: request.NewHelper(log), + region: strings.ToUpper(region), } return v @@ -52,10 +53,10 @@ func (v *Identity) Login(user, password string) (oauth2.TokenSource, error) { } data := url.Values{ - "client_id": {"31c357a0-7a1d-4590-aa99-33b97244d048"}, + "client_id": {regions[v.region].ClientID}, "response_type": {"code"}, "redirect_uri": {RedirectURI}, - "state": {"cwU-gIE27j67poy2UcL3KQ"}, + "state": {regions[v.region].State}, "scope": {"openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user"}, "nonce": {"login_nonce"}, "code_challenge_method": {"S256"}, @@ -65,7 +66,7 @@ func (v *Identity) Login(user, password string) (oauth2.TokenSource, error) { "grant_type": {"authorization_code"}, } - uri := fmt.Sprintf("%s/authenticate", AuthURI) + uri := fmt.Sprintf("%s/oauth/authenticate", regions[v.region].AuthURI) req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), request.URLEncoding) if err != nil { return nil, err @@ -133,10 +134,10 @@ func (v *Identity) Login(user, password string) (oauth2.TokenSource, error) { } func (v *Identity) retrieveToken(data url.Values) (*oauth2.Token, error) { - uri := fmt.Sprintf("%s/token", AuthURI) + uri := fmt.Sprintf("%s/oauth/token", regions[v.region].AuthURI) req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), map[string]string{ "Content-Type": request.FormContent, - "Authorization": "Basic MzFjMzU3YTAtN2ExZC00NTkwLWFhOTktMzNiOTcyNDRkMDQ4OmMwZTMzOTNkLTcwYTItNGY2Zi05ZDNjLTg1MzBhZjY0ZDU1Mg==", + "Authorization": regions[v.region].Authorization, }) var tok oauth.Token diff --git a/vehicle/bmw/param.go b/vehicle/bmw/param.go new file mode 100644 index 000000000..8101157e3 --- /dev/null +++ b/vehicle/bmw/param.go @@ -0,0 +1,42 @@ +package bmw + +type ( + Region struct { + AuthURI, CocoApiURI string + Token + Authenticate + } + Token struct { + Authorization string + } + Authenticate struct { + ClientID, State string + } +) + +var regions = map[string]Region{ + "NA": { + "https://login.bmwusa.com/gcdm", + "https://cocoapi.bmwgroup.us", + Token{ + Authorization: "Basic NTQzOTRhNGItYjZjMS00NWZlLWI3YjItOGZkM2FhOTI1M2FhOmQ5MmYzMWMwLWY1NzktNDRmNS1hNzdkLTk2NmY4ZjAwZTM1MQ==", + // Verifier: "KDarcVUpgymBDCgHDH0PwwMfzycDxu1joeklioOhwXA", + }, + Authenticate{ + ClientID: "54394a4b-b6c1-45fe-b7b2-8fd3aa9253aa", + State: "rgastJbZsMtup49-Lp0FMQ", + }, + }, + "EU": { + "https://customer.bmwgroup.com/gcdm", + "https://cocoapi.bmwgroup.com", + Token{ + Authorization: "Basic MzFjMzU3YTAtN2ExZC00NTkwLWFhOTktMzNiOTcyNDRkMDQ4OmMwZTMzOTNkLTcwYTItNGY2Zi05ZDNjLTg1MzBhZjY0ZDU1Mg==", + // Verifier: "7PsmfPS5MpaNt0jEcPpi-B7M7u0gs1Nzw6ex0Y9pa-0", + }, + Authenticate{ + ClientID: "31c357a0-7a1d-4590-aa99-33b97244d048", + State: "cEG9eLAIi6Nv-aaCAniziE_B6FPoobva3qr5gukilYw", + }, + }, +}