diff --git a/bundles/org.openhab.binding.mybmw/README.md b/bundles/org.openhab.binding.mybmw/README.md index f5d381b830c..3f9ebf0c58b 100644 --- a/bundles/org.openhab.binding.mybmw/README.md +++ b/bundles/org.openhab.binding.mybmw/README.md @@ -78,11 +78,12 @@ Properties will be attached to predefined vehicles if the VIN is matching. ### Bridge Configuration -| Parameter | Type | Description | -|-----------------|---------|--------------------------------------------------------------------| -| userName | text | MyBMW Username | -| password | text | MyBMW Password | -| region | text | Select region in order to connect to the appropriate BMW server. | +| Parameter | Type | Description | +|-----------------|---------|--------------------------------------------------------------------------------------------------------| +| userName | text | MyBMW Username | +| password | text | MyBMW Password | +| hcaptchatoken | text | HCaptcha-Token for initial login (see https://bimmer-connected.readthedocs.io/en/latest/captcha.html) | +| region | text | Select region in order to connect to the appropriate BMW server. | The region Configuration has 3 different options @@ -849,4 +850,4 @@ sitemap BMW label="BMW" { ## Credits -This work is based on the project of [Bimmer Connected](https://github.com/bimmerconnected/bimmer_connected). +This work is based on the great work of the project of [Bimmer Connected](https://github.com/bimmerconnected/bimmer_connected). diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWBridgeConfiguration.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWBridgeConfiguration.java index 1ef5cd168ed..7e82d0750b2 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWBridgeConfiguration.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWBridgeConfiguration.java @@ -19,7 +19,7 @@ import org.openhab.binding.mybmw.internal.utils.Constants; * The {@link MyBMWBridgeConfiguration} class contains fields mapping thing configuration parameters. * * @author Bernd Weymann - Initial contribution - * @author Martin Grassl - renamed + * @author Martin Grassl - renamed and added hcaptchastring */ @NonNullByDefault public class MyBMWBridgeConfiguration { @@ -27,20 +27,71 @@ public class MyBMWBridgeConfiguration { /** * Depending on the location the correct server needs to be called */ - public String region = Constants.EMPTY; + private String region = Constants.EMPTY; /** * MyBMW App Username */ - public String userName = Constants.EMPTY; + private String userName = Constants.EMPTY; /** * MyBMW App Password */ - public String password = Constants.EMPTY; + private String password = Constants.EMPTY; /** * Preferred Locale language */ - public String language = Constants.LANGUAGE_AUTODETECT; + private String language = Constants.LANGUAGE_AUTODETECT; + + /** + * the hCaptcha string + */ + private String hcaptchatoken = Constants.EMPTY; + + public String getRegion() { + return region; + } + + public void setRegion(String region) { + this.region = region; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + public String getHcaptchatoken() { + return hcaptchatoken; + } + + public void setHcaptchatoken(String hcaptchatoken) { + this.hcaptchatoken = hcaptchatoken; + } + + @Override + public String toString() { + return "MyBMWBridgeConfiguration [region=" + region + ", userName=" + userName + ", password=" + password + + ", language=" + language + ", hcaptchatoken=" + hcaptchatoken + "]"; + } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java index b3567bebe4d..da2ad414bd9 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java @@ -37,7 +37,6 @@ import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase; @@ -140,7 +139,7 @@ public class MyBMWCommandExtension extends AbstractConsoleCommandExtension imple String accountPath = path + File.separator + "Account-" + String.valueOf(accountNdx); handler.getMyBmwProxy().ifPresentOrElse(prox -> { // get list of vehicles - List<@NonNull VehicleBase> vehicles = null; + List vehicles = null; try { vehicles = prox.requestVehiclesBase(); @@ -314,7 +313,8 @@ public class MyBMWCommandExtension extends AbstractConsoleCommandExtension imple .filter(t -> THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(t.getThingTypeUID()) && args[1].equals(t.getConfiguration().get("userName"))) .map(t -> t.getHandler()).findAny().get(); - List vehicles = handler.getMyBmwProxy().get().requestVehiclesBase(); + List vehicles = handler != null ? handler.getMyBmwProxy().get().requestVehiclesBase() + : List.of(); return new StringsCompleter( vehicles.stream().map(v -> v.getVin()).filter(Objects::nonNull).collect(Collectors.toList()), false).complete(args, cursorArgumentIndex, cursorPosition, candidates); diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java index 384b6b60a68..6c75204612b 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java @@ -17,7 +17,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.mybmw.internal.MyBMWConstants; import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; @@ -80,7 +79,7 @@ public class VehicleDiscovery extends AbstractThingHandlerDiscoveryService> vehicleList = myBMWProxy.map(prox -> { + Optional> vehicleList = myBMWProxy.map(prox -> { try { return prox.requestVehicles(); } catch (NetworkException e) { @@ -88,8 +87,13 @@ public class VehicleDiscovery extends AbstractThingHandlerDiscoveryService { - thingHandler.vehicleDiscoverySuccess(); - processVehicles(vehicles); + if (vehicles.size() > 0) { + thingHandler.vehicleDiscoverySuccess(); + processVehicles(vehicles); + } else { + logger.warn("no vehicle found, maybe because of network error"); + thingHandler.vehicleDiscoveryError(); + } }, () -> thingHandler.vehicleDiscoveryError()); } catch (IllegalStateException ex) { thingHandler.vehicleDiscoveryError(); diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthResponse.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthResponse.java index 168c7a1f11a..34a8a3272ed 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthResponse.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthResponse.java @@ -20,17 +20,27 @@ import com.google.gson.annotations.SerializedName; * The {@link AuthResponse} Data Transfer Object * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - extracted from myBmwProxy */ public class AuthResponse { @SerializedName("access_token") public String accessToken = Constants.EMPTY; + + @SerializedName("refresh_token") + public String refreshToken = Constants.EMPTY; + @SerializedName("token_type") public String tokenType = Constants.EMPTY; + + @SerializedName("gcid") + public String gcid = Constants.EMPTY; + @SerializedName("expires_in") public int expiresIn = -1; @Override public String toString() { - return "Token " + accessToken + " type " + tokenType + " expires in " + expiresIn; + return "AuthResponse [accessToken=" + accessToken + ", refreshToken=" + refreshToken + ", tokenType=" + + tokenType + ", gcid=" + gcid + ", expiresIn=" + expiresIn + "]"; } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java index d51d065497e..e2bc7da34f4 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java @@ -60,6 +60,7 @@ public class MyBMWBridgeHandler extends BaseBridgeHandler { private Optional> initializerJob = Optional.empty(); private Optional vehicleDiscovery = Optional.empty(); private LocaleProvider localeProvider; + private Optional bmwBridgeConfiguration = Optional.empty(); public MyBMWBridgeHandler(Bridge bridge, HttpClientFactory hcf, LocaleProvider localeProvider) { super(bridge); @@ -82,11 +83,23 @@ public class MyBMWBridgeHandler extends BaseBridgeHandler { public void initialize() { logger.trace("MyBMWBridgeHandler.initialize"); updateStatus(ThingStatus.UNKNOWN); - MyBMWBridgeConfiguration config = getConfigAs(MyBMWBridgeConfiguration.class); - if (config.language.equals(Constants.LANGUAGE_AUTODETECT)) { - config.language = localeProvider.getLocale().getLanguage().toLowerCase(); + + this.bmwBridgeConfiguration = Optional.of(getConfigAs(MyBMWBridgeConfiguration.class)); + + MyBMWBridgeConfiguration localBridgeConfiguration; + + if (bmwBridgeConfiguration.isPresent()) { + localBridgeConfiguration = bmwBridgeConfiguration.get(); + } else { + logger.warn("the bridge configuration could not be retrieved"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR); + return; } - if (!MyBMWConfigurationChecker.checkConfiguration(config)) { + + if (localBridgeConfiguration.getLanguage().equals(Constants.LANGUAGE_AUTODETECT)) { + localBridgeConfiguration.setLanguage(localeProvider.getLocale().getLanguage().toLowerCase()); + } + if (!MyBMWConfigurationChecker.checkInitialConfiguration(localBridgeConfiguration)) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR); } else { // there is no risk in this functionality as several steps have to happen to get the file proxy working: @@ -100,19 +113,26 @@ public class MyBMWBridgeHandler extends BaseBridgeHandler { environment = ""; } - createMyBmwProxy(config, environment); + // this access has to be synchronized as the vehicleHandler as well as the bridge itself request the + // instance + Optional localProxy = getMyBmwProxy(); + localProxy.ifPresent(proxy -> proxy.setBridgeConfiguration(localBridgeConfiguration)); + initializerJob = Optional.of(scheduler.schedule(this::discoverVehicles, 2, TimeUnit.SECONDS)); } } private synchronized void createMyBmwProxy(MyBMWBridgeConfiguration config, String environment) { if (!myBmwProxy.isPresent()) { - if (!(TEST.equals(environment) && TESTUSER.equals(config.userName))) { + if (!(TEST.equals(environment) && TESTUSER.equals(config.getUserName()))) { myBmwProxy = Optional.of(new MyBMWHttpProxy(httpClientFactory, config)); } else { myBmwProxy = Optional.of(new MyBMWFileProxy(httpClientFactory, config)); } logger.trace("MyBMWBridgeHandler proxy set"); + } else { + myBmwProxy.get().setBridgeConfiguration(config); + logger.trace("MyBMWBridgeHandler update proxy with bridge configuration"); } } @@ -135,10 +155,6 @@ public class MyBMWBridgeHandler extends BaseBridgeHandler { private void discoverVehicles() { logger.trace("MyBMWBridgeHandler.requestVehicles"); - MyBMWBridgeConfiguration config = getConfigAs(MyBMWBridgeConfiguration.class); - - myBmwProxy.ifPresent(proxy -> proxy.setBridgeConfiguration(config)); - vehicleDiscovery.ifPresent(discovery -> discovery.discoverVehicles()); } @@ -148,9 +164,23 @@ public class MyBMWBridgeHandler extends BaseBridgeHandler { return List.of(VehicleDiscovery.class); } - public Optional getMyBmwProxy() { + public synchronized Optional getMyBmwProxy() { logger.trace("MyBMWBridgeHandler.getProxy"); - createMyBmwProxy(getConfigAs(MyBMWBridgeConfiguration.class), ENVIRONMENT); + + MyBMWBridgeConfiguration localBridgeConfiguration = null; + + if (bmwBridgeConfiguration.isPresent()) { + localBridgeConfiguration = bmwBridgeConfiguration.get(); + } else { + logger.warn("the bridge configuration could not be retrieved"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR); + throw new IllegalStateException("bridge handler - configuration is not available"); + } + + if (!myBmwProxy.isPresent()) { + createMyBmwProxy(localBridgeConfiguration, ENVIRONMENT); + } + return myBmwProxy; } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java index 90589a9e3ae..cadcd107e05 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java @@ -83,7 +83,7 @@ public class RemoteServiceExecutor { serviceExecuting.ifPresentOrElse(service -> { if (counter >= GIVEUP_COUNTER) { logger.warn("Giving up updating state for {} after {} times", service, GIVEUP_COUNTER); - handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), + handler.updateRemoteExecutionStatus(serviceExecuting.orElse(Constants.EMPTY), ExecutionState.TIMEOUT.name().toLowerCase()); reset(); // immediately refresh data @@ -107,7 +107,7 @@ public class RemoteServiceExecutor { private void handleRemoteServiceException(NetworkException e) { synchronized (this) { - handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), + handler.updateRemoteExecutionStatus(serviceExecuting.orElse(Constants.EMPTY), ExecutionState.ERROR.name().toLowerCase() + Constants.SPACE + Integer.toString(e.getStatus())); reset(); } @@ -117,12 +117,12 @@ public class RemoteServiceExecutor { if (!executionStatusContainer.getEventId().isEmpty()) { // service initiated - store event id for further MyBMW updates executingEventId = Optional.of(executionStatusContainer.getEventId()); - handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), + handler.updateRemoteExecutionStatus(serviceExecuting.orElse(Constants.EMPTY), ExecutionState.INITIATED.name().toLowerCase()); } else if (!executionStatusContainer.getEventStatus().isEmpty()) { // service status updated synchronized (this) { - handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), + handler.updateRemoteExecutionStatus(serviceExecuting.orElse(Constants.EMPTY), executionStatusContainer.getEventStatus().toLowerCase()); if (ExecutionState.EXECUTED.name().equalsIgnoreCase(executionStatusContainer.getEventStatus()) || ExecutionState.ERROR.name().equalsIgnoreCase(executionStatusContainer.getEventStatus())) { diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java index 900b7d99b23..e90861f97a3 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java @@ -23,6 +23,7 @@ import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.EADRAX_SE import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.LOGIN_NONCE; import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.OAUTH_ENDPOINT; import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.OCP_APIM_KEYS; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.REFRESH_TOKEN; import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.REGION_CHINA; import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.REGION_NORTH_AMERICA; import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.REGION_ROW; @@ -36,6 +37,7 @@ import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CODE_CHALLE import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CODE_VERIFIER; import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CONTENT_TYPE_URL_ENCODED; import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.GRANT_TYPE; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.HCAPTCHA_TOKEN; import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.HEADER_ACP_SUBSCRIPTION_KEY; import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.HEADER_BMW_CORRELATION_ID; import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.HEADER_X_CORRELATION_ID; @@ -57,6 +59,8 @@ import java.security.PublicKey; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; import javax.crypto.Cipher; @@ -86,7 +90,8 @@ import org.slf4j.LoggerFactory; * * requests the tokens for MyBMW API authorization * - * thanks to bimmer_connected https://github.com/bimmerconnected/bimmer_connected + * thanks to bimmer_connected + * https://github.com/bimmerconnected/bimmer_connected * * @author Bernd Weymann - Initial contribution * @author Martin Grassl - extracted from myBmwProxy @@ -97,14 +102,18 @@ public class MyBMWTokenController { private final Logger logger = LoggerFactory.getLogger(MyBMWTokenController.class); private Token token = new Token(); - private MyBMWBridgeConfiguration configuration; + private MyBMWBridgeConfiguration bridgeConfiguration; private HttpClient httpClient; public MyBMWTokenController(MyBMWBridgeConfiguration configuration, HttpClient httpClient) { - this.configuration = configuration; + this.bridgeConfiguration = configuration; this.httpClient = httpClient; } + public void setBridgeConfiguration(MyBMWBridgeConfiguration bridgeConfiguration) { + this.bridgeConfiguration = bridgeConfiguration; + } + /** * Gets new token if old one is expired or invalid. In case of error the token * remains. @@ -114,18 +123,31 @@ public class MyBMWTokenController { * @return token */ public Token getToken() { - if (!token.isValid()) { + if (!bridgeConfiguration.getHcaptchatoken().isBlank()) { + // if the hcaptchastring is available, then a new login is triggered + boolean tokenCreationSuccess = getInitialToken(); + + if (!tokenCreationSuccess) { + this.token = new Token(); + logger.warn( + "initial Authentication failed, maybe request a new captcha token, see https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html!"); + } + + // reset the token as it times out + bridgeConfiguration.setHcaptchatoken(Constants.EMPTY); + } else if (!token.isValid() && !Constants.EMPTY.equals(token.getRefreshToken())) { + // if the token is invalid, try to refresh the token boolean tokenUpdateSuccess = false; - switch (configuration.region) { + switch (bridgeConfiguration.getRegion()) { case REGION_CHINA: - tokenUpdateSuccess = updateTokenChina(); + tokenUpdateSuccess = getAndUpdateTokenChina(); break; case REGION_NORTH_AMERICA: case REGION_ROW: - tokenUpdateSuccess = updateToken(); + tokenUpdateSuccess = getUpdatedToken(); break; default: - logger.warn("Region {} not supported", configuration.region); + logger.warn("Region {} not supported", bridgeConfiguration.getRegion()); break; } if (!tokenUpdateSuccess) { @@ -143,32 +165,12 @@ public class MyBMWTokenController { * * @return true if the token was successfully updated */ - private synchronized boolean updateToken() { + private synchronized boolean getInitialToken() { try { /* * Step 1) Get basic values for further queries */ - String uuidString = UUID.randomUUID().toString(); - - String authValuesUrl = "https://" + EADRAX_SERVER_MAP.get(configuration.region) + API_OAUTH_CONFIG; - Request authValuesRequest = httpClient.newRequest(authValuesUrl); - authValuesRequest.header(HEADER_ACP_SUBSCRIPTION_KEY, OCP_APIM_KEYS.get(configuration.region)); - authValuesRequest.header(HEADER_X_USER_AGENT, String.format(X_USER_AGENT, BRAND_BMW, - APP_VERSIONS.get(configuration.region), configuration.region)); - authValuesRequest.header(HEADER_X_IDENTITY_PROVIDER, AUTH_PROVIDER); - authValuesRequest.header(HEADER_X_CORRELATION_ID, uuidString); - authValuesRequest.header(HEADER_BMW_CORRELATION_ID, uuidString); - - ContentResponse authValuesResponse = authValuesRequest.send(); - if (authValuesResponse.getStatus() != 200) { - throw new HttpResponseException("URL: " + authValuesRequest.getURI() + ", Error: " - + authValuesResponse.getStatus() + ", Message: " + authValuesResponse.getContentAsString(), - authValuesResponse); - } - AuthQueryResponse aqr = JsonStringDeserializer.deserializeString(authValuesResponse.getContentAsString(), - AuthQueryResponse.class); - - logger.trace("authQueryResponse: {}", aqr); + AuthQueryResponse aqr = getBasicAuthenticationValues(); /* * Step 2) Calculate values for oauth base parameters @@ -188,17 +190,19 @@ public class MyBMWTokenController { baseParams.put(CODE_CHALLENGE_METHOD, "S256"); /** - * Step 3) Authorization with username and password + * Step 3) Authentication with username and password */ String loginUrl = aqr.gcdmBaseUrl + OAUTH_ENDPOINT; Request loginRequest = httpClient.POST(loginUrl); loginRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + loginRequest.header(HCAPTCHA_TOKEN, bridgeConfiguration.getHcaptchatoken()); MultiMap<@Nullable String> loginParams = new MultiMap<>(baseParams); loginParams.put(GRANT_TYPE, AUTHORIZATION_CODE); - loginParams.put(USERNAME, configuration.userName); - loginParams.put(PASSWORD, configuration.password); + loginParams.put(USERNAME, bridgeConfiguration.getUserName()); + loginParams.put(PASSWORD, bridgeConfiguration.getPassword()); + logger.trace("loginParams {}", loginParams); loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8)); ContentResponse loginResponse = loginRequest.send(); @@ -207,10 +211,11 @@ public class MyBMWTokenController { + loginResponse.getStatus() + ", Message: " + loginResponse.getContentAsString(), loginResponse); } + String authCode = getAuthCode(loginResponse.getContentAsString()); /** - * Step 4) Authorize with code + * Step 4) Authenticate with code */ Request authRequest = httpClient.POST(loginUrl).followRedirects(false); MultiMap<@Nullable String> authParams = new MultiMap<>(baseParams); @@ -248,9 +253,13 @@ public class MyBMWTokenController { } AuthResponse ar = JsonStringDeserializer.deserializeString(codeResponse.getContentAsString(), AuthResponse.class); + token.setType(ar.tokenType); token.setToken(ar.accessToken); token.setExpiration(ar.expiresIn); + token.setRefreshToken(ar.refreshToken); + token.setGcid(ar.gcid); + return true; } catch (Exception e) { logger.warn("Authorization Exception: {}", e.getMessage()); @@ -258,6 +267,80 @@ public class MyBMWTokenController { return false; } + /** + * refresh the existing token + * + * @return true if token has successfully been refreshed + */ + private synchronized boolean getUpdatedToken() { + try { + /* + * Step 1) Get basic values for further queries + */ + AuthQueryResponse aqr = getBasicAuthenticationValues(); + + /** + * Step 2) Request token + */ + Request codeRequest = httpClient.POST(aqr.tokenEndpoint); + String basicAuth = "Basic " + + Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes()); + codeRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + codeRequest.header(AUTHORIZATION, basicAuth); + + MultiMap<@Nullable String> codeParams = new MultiMap<>(); + codeParams.put(SCOPE, String.join(Constants.SPACE, aqr.scopes)); + codeParams.put(REDIRECT_URI, aqr.returnUrl); + codeParams.put(GRANT_TYPE, REFRESH_TOKEN); + codeParams.put(REFRESH_TOKEN, token.getRefreshToken()); + codeRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, + UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8)); + ContentResponse codeResponse = codeRequest.send(); + if (codeResponse.getStatus() != 200) { + throw new HttpResponseException("URL: " + codeRequest.getURI() + ", Error: " + codeResponse.getStatus() + + ", Message: " + codeResponse.getContentAsString(), codeResponse); + } + AuthResponse ar = JsonStringDeserializer.deserializeString(codeResponse.getContentAsString(), + AuthResponse.class); + + token.setToken(ar.accessToken); + token.setExpiration(ar.expiresIn); + token.setRefreshToken(ar.refreshToken); + token.setGcid(ar.gcid); + + return true; + } catch (Exception e) { + logger.warn("Refresh Exception: {}", e.getMessage()); + } + return false; + } + + private AuthQueryResponse getBasicAuthenticationValues() + throws InterruptedException, TimeoutException, ExecutionException { + String uuidString = UUID.randomUUID().toString(); + + String authValuesUrl = "https://" + EADRAX_SERVER_MAP.get(bridgeConfiguration.getRegion()) + API_OAUTH_CONFIG; + Request authValuesRequest = httpClient.newRequest(authValuesUrl); + authValuesRequest.header(HEADER_ACP_SUBSCRIPTION_KEY, OCP_APIM_KEYS.get(bridgeConfiguration.getRegion())); + authValuesRequest.header(HEADER_X_USER_AGENT, String.format(X_USER_AGENT, BRAND_BMW, + APP_VERSIONS.get(bridgeConfiguration.getRegion()), bridgeConfiguration.getRegion())); + authValuesRequest.header(HEADER_X_IDENTITY_PROVIDER, AUTH_PROVIDER); + authValuesRequest.header(HEADER_X_CORRELATION_ID, uuidString); + authValuesRequest.header(HEADER_BMW_CORRELATION_ID, uuidString); + + ContentResponse authValuesResponse = authValuesRequest.send(); + if (authValuesResponse.getStatus() != 200) { + throw new HttpResponseException("URL: " + authValuesRequest.getURI() + ", Error: " + + authValuesResponse.getStatus() + ", Message: " + authValuesResponse.getContentAsString(), + authValuesResponse); + } + AuthQueryResponse aqr = JsonStringDeserializer.deserializeString(authValuesResponse.getContentAsString(), + AuthQueryResponse.class); + + logger.trace("authQueryResponse: {}", aqr); + return aqr; + } + private String generateState() { String stateBytes = StringUtils.getRandomAlphabetic(64).toLowerCase(); return Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes()); @@ -301,7 +384,7 @@ public class MyBMWTokenController { return codeFound.toString(); } - private synchronized boolean updateTokenChina() { + private synchronized boolean getAndUpdateTokenChina() { try { /** * Step 1) get public key @@ -310,7 +393,7 @@ public class MyBMWTokenController { Request oauthQueryRequest = httpClient.newRequest(publicKeyUrl); oauthQueryRequest.header(HttpHeader.USER_AGENT, USER_AGENT); oauthQueryRequest.header(HEADER_X_USER_AGENT, String.format(X_USER_AGENT, BRAND_BMW, - APP_VERSIONS.get(configuration.region), configuration.region)); + APP_VERSIONS.get(bridgeConfiguration.getRegion()), bridgeConfiguration.getRegion())); ContentResponse publicKeyResponse = oauthQueryRequest.send(); if (publicKeyResponse.getStatus() != 200) { throw new HttpResponseException("URL: " + oauthQueryRequest.getURI() + ", Error: " @@ -335,7 +418,7 @@ public class MyBMWTokenController { // https://www.thexcoders.net/java-ciphers-rsa/ Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); - byte[] encryptedBytes = cipher.doFinal(configuration.password.getBytes()); + byte[] encryptedBytes = cipher.doFinal(bridgeConfiguration.getPassword().getBytes()); String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes); /** @@ -344,9 +427,9 @@ public class MyBMWTokenController { String tokenUrl = "https://" + EADRAX_SERVER_MAP.get(REGION_CHINA) + CHINA_LOGIN; Request loginRequest = httpClient.POST(tokenUrl); loginRequest.header(HEADER_X_USER_AGENT, String.format(X_USER_AGENT, BRAND_BMW, - APP_VERSIONS.get(configuration.region), configuration.region)); - String jsonContent = "{ \"mobile\":\"" + configuration.userName + "\", \"password\":\"" + encodedPassword - + "\"}"; + APP_VERSIONS.get(bridgeConfiguration.getRegion()), bridgeConfiguration.getRegion())); + String jsonContent = "{ \"mobile\":\"" + bridgeConfiguration.getUserName() + "\", \"password\":\"" + + encodedPassword + "\"}"; loginRequest.content(new StringContentProvider(jsonContent)); ContentResponse tokenResponse = loginRequest.send(); if (tokenResponse.getStatus() != 200) { diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/Token.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/Token.java index 11e227729d2..4c362a95575 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/Token.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/Token.java @@ -19,15 +19,20 @@ import org.openhab.binding.mybmw.internal.utils.Constants; * The {@link Token} MyBMW Token storage * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - extracted to own class */ @NonNullByDefault public class Token { private String token = Constants.EMPTY; private String tokenType = Constants.EMPTY; + private String refreshToken = Constants.EMPTY; + private String gcid = Constants.EMPTY; + private long expiration = 0; public String getBearerToken() { - return new StringBuilder(tokenType).append(Constants.SPACE).append(token).toString(); + return token.equals(Constants.EMPTY) ? Constants.EMPTY + : new StringBuilder(tokenType).append(Constants.SPACE).append(token).toString(); } public void setToken(String token) { @@ -46,9 +51,25 @@ public class Token { tokenType = type; } + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public String getGcid() { + return gcid; + } + + public void setGcid(String gcid) { + this.gcid = gcid; + } + public boolean isValid() { return (!token.equals(Constants.EMPTY) && !tokenType.equals(Constants.EMPTY) - && (this.expiration - System.currentTimeMillis() / 1000) > 1); + && !refreshToken.equals(Constants.EMPTY) && (this.expiration - System.currentTimeMillis() / 1000) > 1); } @Override diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java index 220dd45c26d..c5c77d8e35f 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java @@ -19,7 +19,6 @@ import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer; @@ -60,17 +59,17 @@ public class MyBMWFileProxy implements MyBMWProxy { public MyBMWFileProxy(HttpClientFactory httpClientFactory, MyBMWBridgeConfiguration bridgeConfiguration) { logger.trace("MyBMWFileProxy - initialize"); - vehicleToBeTested = bridgeConfiguration.password; + vehicleToBeTested = bridgeConfiguration.getPassword(); } public void setBridgeConfiguration(MyBMWBridgeConfiguration bridgeConfiguration) { logger.trace("MyBMWFileProxy - update bridge"); - vehicleToBeTested = bridgeConfiguration.password; + vehicleToBeTested = bridgeConfiguration.getPassword(); } - public List<@NonNull Vehicle> requestVehicles() throws NetworkException { - List<@NonNull Vehicle> vehicles = new ArrayList<>(); - List<@NonNull VehicleBase> vehiclesBase = requestVehiclesBase(); + public List requestVehicles() throws NetworkException { + List vehicles = new ArrayList<>(); + List vehiclesBase = requestVehiclesBase(); for (VehicleBase vehicleBase : vehiclesBase) { VehicleStateContainer vehicleState = requestVehicleState(vehicleBase.getVin(), diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java index fbba7c14487..f6ad93518be 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java @@ -22,7 +22,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; @@ -67,7 +66,7 @@ public class MyBMWHttpProxy implements MyBMWProxy { private final Logger logger = LoggerFactory.getLogger(MyBMWHttpProxy.class); private final HttpClient httpClient; private MyBMWBridgeConfiguration bridgeConfiguration; - private final MyBMWTokenController myBMWTokenHandler; + MyBMWTokenController myBMWTokenController; /** * URLs taken from @@ -82,16 +81,16 @@ public class MyBMWHttpProxy implements MyBMWProxy { logger.trace("MyBMWHttpProxy - initialize"); httpClient = httpClientFactory.getCommonHttpClient(); - myBMWTokenHandler = new MyBMWTokenController(bridgeConfiguration, httpClient); + myBMWTokenController = new MyBMWTokenController(bridgeConfiguration, httpClient); this.bridgeConfiguration = bridgeConfiguration; - vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region) + vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.getRegion()) + BimmerConstants.API_VEHICLES; vehicleStateUrl = vehicleUrl + "/state"; - remoteCommandUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region) + remoteCommandUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.getRegion()) + BimmerConstants.API_REMOTE_SERVICE_BASE_URL; remoteStatusUrl = remoteCommandUrl + "eventStatus"; logger.trace("MyBMWHttpProxy - ready"); @@ -100,6 +99,7 @@ public class MyBMWHttpProxy implements MyBMWProxy { @Override public void setBridgeConfiguration(MyBMWBridgeConfiguration bridgeConfiguration) { this.bridgeConfiguration = bridgeConfiguration; + myBMWTokenController.setBridgeConfiguration(bridgeConfiguration); } /** @@ -107,9 +107,9 @@ public class MyBMWHttpProxy implements MyBMWProxy { * * @return list of vehicles */ - public List<@NonNull Vehicle> requestVehicles() throws NetworkException { - List<@NonNull Vehicle> vehicles = new ArrayList<>(); - List<@NonNull VehicleBase> vehiclesBase = requestVehiclesBase(); + public List requestVehicles() throws NetworkException { + List vehicles = new ArrayList<>(); + List vehiclesBase = requestVehiclesBase(); for (VehicleBase vehicleBase : vehiclesBase) { VehicleStateContainer vehicleState = requestVehicleState(vehicleBase.getVin(), @@ -177,7 +177,7 @@ public class MyBMWHttpProxy implements MyBMWProxy { * @return the image as a byte array */ public byte[] requestImage(String vin, String brand, ImageProperties props) throws NetworkException { - final String localImageUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region) + final String localImageUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.getRegion()) + "/eadrax-ics/v3/presentation/vehicles/" + vin + "/images?carView=" + props.viewport; return get(localImageUrl, brand, vin, HTTPConstants.CONTENT_TYPE_IMAGE); } @@ -231,7 +231,7 @@ public class MyBMWHttpProxy implements MyBMWProxy { chargeStatisticsParams.put("vin", vin); chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime()); String params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false); - String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region) + String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.getRegion()) + "/eadrax-chs/v1/charging-statistics?" + params; byte[] chargeStatisticsResponse = get(chargeStatisticsUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON); String chargeStatisticsResponseString = new String(chargeStatisticsResponse); @@ -263,7 +263,7 @@ public class MyBMWHttpProxy implements MyBMWProxy { chargeSessionsParams.put("maxResults", "40"); chargeSessionsParams.put("include_date_picker", "true"); String params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false); - String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region) + String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.getRegion()) + "/eadrax-chs/v1/charging-sessions?" + params; byte[] chargeSessionsResponse = get(chargeSessionsUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON); String chargeSessionsResponseString = new String(chargeSessionsResponse); @@ -353,6 +353,12 @@ public class MyBMWHttpProxy implements MyBMWProxy { throw new NetworkException("Unknown Brand " + brand); } + // if no token is available, no request can be triggered + if (Constants.EMPTY.equals(myBMWTokenController.getToken().getBearerToken())) { + logger.warn("The login failed, no token is available"); + throw new NetworkException("The login failed, no token is available"); + } + final Request req; if (post) { @@ -361,10 +367,10 @@ public class MyBMWHttpProxy implements MyBMWProxy { req = httpClient.newRequest(url); } - req.header(HttpHeader.AUTHORIZATION, myBMWTokenHandler.getToken().getBearerToken()); + req.header(HttpHeader.AUTHORIZATION, myBMWTokenController.getToken().getBearerToken()); req.header(HTTPConstants.HEADER_X_USER_AGENT, String.format(BimmerConstants.X_USER_AGENT, brand.toLowerCase(), - APP_VERSIONS.get(bridgeConfiguration.region), bridgeConfiguration.region)); - req.header(HttpHeader.ACCEPT_LANGUAGE, bridgeConfiguration.language); + APP_VERSIONS.get(bridgeConfiguration.getRegion()), bridgeConfiguration.getRegion())); + req.header(HttpHeader.ACCEPT_LANGUAGE, bridgeConfiguration.getLanguage()); req.header(HttpHeader.ACCEPT, contentType); req.header(HTTPConstants.HEADER_BMW_VIN, vin); diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxy.java index b151263aaf8..4726e4c4d56 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxy.java @@ -14,7 +14,6 @@ package org.openhab.binding.mybmw.internal.handler.backend; import java.util.List; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer; @@ -36,7 +35,7 @@ public interface MyBMWProxy { void setBridgeConfiguration(MyBMWBridgeConfiguration bridgeConfiguration); - List<@NonNull Vehicle> requestVehicles() throws NetworkException; + List requestVehicles() throws NetworkException; /** * request all vehicles for one specific brand and their state diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java index 8da8b65fe13..01e8b972169 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java @@ -59,17 +59,18 @@ public interface BimmerConstants { static final String CHINA_LOGIN = "/eadrax-coas/v2/login/pwd"; // Http variables - static final String APP_VERSION_NORTH_AMERICA = "2.12.0(19883)"; - static final String APP_VERSION_ROW = "2.12.0(19883)"; - static final String APP_VERSION_CHINA = "2.3.0(13603)"; + static final String APP_VERSION_NORTH_AMERICA = "4.9.2(36892)"; + static final String APP_VERSION_ROW = "4.9.2(36892)"; + static final String APP_VERSION_CHINA = "4.9.2(36892)"; static final Map APP_VERSIONS = Map.of(REGION_NORTH_AMERICA, APP_VERSION_NORTH_AMERICA, REGION_ROW, APP_VERSION_ROW, REGION_CHINA, APP_VERSION_CHINA); static final String USER_AGENT = "Dart/2.16 (dart:io)"; // see const.py of bimmer_constants: user-agent; brand; app_version; region - static final String X_USER_AGENT = "android(SP1A.210812.016.C1);%s;%s;%s"; + static final String X_USER_AGENT = "android(AP2A.240605.024);%s;%s;%s"; static final String LOGIN_NONCE = "login_nonce"; static final String AUTHORIZATION_CODE = "authorization_code"; + static final String REFRESH_TOKEN = "refresh_token"; // Parameters for API Requests static final String TIRE_GUARD_MODE = "tireGuardMode"; diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java index bc872f8932a..b8ec0a11c44 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java @@ -42,6 +42,7 @@ public interface HTTPConstants { static final String CREDENTIALS = "Credentials"; static final String USERNAME = "username"; static final String PASSWORD = "password"; + static final String HCAPTCHA_TOKEN = "hcaptchatoken"; static final String CONTENT_LENGTH = "Content-Length"; static final String CODE_CHALLENGE = "code_challenge"; static final String CODE_CHALLENGE_METHOD = "code_challenge_method"; diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java index 7cccf1545ec..3c8284150fc 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java @@ -24,11 +24,11 @@ import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; */ @NonNullByDefault public final class MyBMWConfigurationChecker { - public static boolean checkConfiguration(MyBMWBridgeConfiguration config) { - if (config.userName.isBlank() || config.password.isBlank()) { + public static boolean checkInitialConfiguration(MyBMWBridgeConfiguration config) { + if (config.getUserName().isBlank() || config.getPassword().isBlank() || config.getHcaptchatoken().isBlank()) { return false; } else { - return BimmerConstants.EADRAX_SERVER_MAP.containsKey(config.region); + return BimmerConstants.EADRAX_SERVER_MAP.containsKey(config.getRegion()); } } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/bridge-config.xml index 5decb677799..c198709b659 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/bridge-config.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/config/bridge-config.xml @@ -14,6 +14,11 @@ MyBMW Password password + + + Captcha-Token for login (see https://bimmer-connected.readthedocs.io/en/stable/captcha.html) + hcaptchatoken + Select Region in order to connect to the appropriate BMW Server diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties index e7409349577..8a501110b04 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties @@ -8,6 +8,8 @@ thing-type.config.mybmw.bridge.language.description = Channel data can be return thing-type.config.mybmw.bridge.language.label = Language Settings thing-type.config.mybmw.bridge.password.description = MyBMW Password thing-type.config.mybmw.bridge.password.label = Password +thing-type.config.mybmw.bridge.hcaptchatoken.description = Captcha-Token for the Login (see https://bimmer-connected.readthedocs.io/en/stable/captcha.html) +thing-type.config.mybmw.bridge.hcaptchatoken.label = Captcha-Token thing-type.config.mybmw.bridge.region.description = Select Region in order to connect to the appropriate BMW Server thing-type.config.mybmw.bridge.region.label = Region thing-type.config.mybmw.bridge.region.option.CHINA = China diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties index bffc3a83bf4..9c981660e81 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties @@ -22,6 +22,8 @@ thing-type.config.mybmw.bridge.language.label = Sprachauswahl thing-type.config.mybmw.bridge.language.description = Daten werden für die gewünschte Sprache angefordert (en, de, fr ...) thing-type.config.mybmw.bridge.password.label = Passwort thing-type.config.mybmw.bridge.password.description = Passwort für die MyBMW App +thing-type.config.mybmw.bridge.hcaptchatoken.label = Captcha-Token +thing-type.config.mybmw.bridge.hcaptchatoken.description = Captcha-Token für den Login (siehe https://bimmer-connected.readthedocs.io/en/stable/captcha.html) thing-type.config.mybmw.bridge.region.label = Region thing-type.config.mybmw.bridge.region.description = Auswahl Ihrer Region thing-type.config.mybmw.bridge.region.option.NORTH_AMERICA = Nordamerika diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/auth/AuthTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/auth/AuthTest.java index 166053b7849..4fd0bede7e6 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/auth/AuthTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/auth/AuthTest.java @@ -317,9 +317,9 @@ class AuthTest { HttpClientFactory mockHCF = mock(HttpClientFactory.class); when(mockHCF.getCommonHttpClient()).thenReturn(authHttpClient); MyBMWBridgeConfiguration config = new MyBMWBridgeConfiguration(); - config.region = BimmerConstants.REGION_CHINA; - config.userName = "Hello User"; - config.password = "Hello Password"; + config.setRegion(BimmerConstants.REGION_CHINA); + config.setUserName("Hello User"); + config.setPassword("Hello Password"); MyBMWTokenController tokenHandler = new MyBMWTokenController(config, authHttpClient); Token token = tokenHandler.getToken(); assertNotNull(token); diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java index 9390e0b3ffd..f08db7241b2 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java @@ -36,6 +36,8 @@ import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer; import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase; import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; +import org.openhab.binding.mybmw.internal.handler.auth.MyBMWTokenController; +import org.openhab.binding.mybmw.internal.handler.auth.Token; import org.openhab.binding.mybmw.internal.handler.enums.RemoteService; import org.openhab.binding.mybmw.internal.util.FileReader; import org.openhab.binding.mybmw.internal.utils.BimmerConstants; @@ -196,8 +198,13 @@ public class MyBMWHttpProxyTest { HttpClientFactory httpClientFactoryMock = Mockito.mock(HttpClientFactory.class); HttpClient httpClientMock = Mockito.mock(HttpClient.class); Request requestMock = Mockito.mock(Request.class); + MyBMWTokenController bmwTokenControllerMock = Mockito.mock(MyBMWTokenController.class); Mockito.when(httpClientMock.newRequest(Mockito.anyString())).thenReturn(requestMock); Mockito.when(httpClientMock.POST(Mockito.anyString())).thenReturn(requestMock); + + Token token = Mockito.mock(Token.class); + Mockito.when(token.getBearerToken()).thenReturn("blah"); + Mockito.when(bmwTokenControllerMock.getToken()).thenReturn(token); MyBMWBridgeConfiguration myBMWBridgeConfiguration = new MyBMWBridgeConfiguration(); Mockito.when(httpClientFactoryMock.getCommonHttpClient()).thenReturn(httpClientMock); @@ -216,6 +223,8 @@ public class MyBMWHttpProxyTest { logger.error(e1.getMessage(), e1); } - return new MyBMWHttpProxy(httpClientFactoryMock, myBMWBridgeConfiguration); + MyBMWHttpProxy proxy = new MyBMWHttpProxy(httpClientFactoryMock, myBMWBridgeConfiguration); + proxy.myBMWTokenController = bmwTokenControllerMock; + return proxy; } } diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java new file mode 100644 index 00000000000..345e10e0fa7 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java @@ -0,0 +1,327 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.backend; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingStatisticsContainer; +import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer; +import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; +import org.openhab.binding.mybmw.internal.handler.enums.ExecutionState; +import org.openhab.binding.mybmw.internal.handler.enums.RemoteService; +import org.openhab.binding.mybmw.internal.utils.BimmerConstants; +import org.openhab.binding.mybmw.internal.utils.ImageProperties; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +import ch.qos.logback.classic.Level; + +/** + * this integration test runs only if the connected account is set via environment variables + * CONNECTED_USER + * CONNECTED_PASSWORD + * HCAPTCHA_TOKEN + * + * if you want to execute the tests, please set the env variables and remove the disabled annotation + * + * @author Martin Grassl - initial contribution + */ +@NonNullByDefault +public class MyBMWProxyBackendIT { + + private final Logger logger = LoggerFactory.getLogger(MyBMWProxyBackendIT.class); + + public MyBMWProxy initializeProxy() { + String connectedUser = System.getenv("CONNECTED_USER"); + String connectedPassword = System.getenv("CONNECTED_PASSWORD"); + String hCaptchaString = System.getenv("HCAPTCHA_TOKEN"); + assertNotNull(connectedUser); + assertNotNull(connectedPassword); + assertNotNull(hCaptchaString); + + MyBMWBridgeConfiguration configuration = new MyBMWBridgeConfiguration(); + configuration.setLanguage("de-DE"); + configuration.setRegion(BimmerConstants.REGION_ROW); + configuration.setUserName(connectedUser); + configuration.setPassword(connectedPassword); + configuration.setHcaptchatoken(hCaptchaString); + + return new MyBMWHttpProxy(new MyHttpClientFactory(), configuration); + } + + @BeforeEach + public void setupLogger() { + Logger root = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + + ((ch.qos.logback.classic.Logger) root).setLevel(Level.TRACE); + + logger.trace("tracing enabled"); + logger.debug("debugging enabled"); + logger.info("info enabled"); + } + + @Test + public void testSequence() { + MyBMWProxy myBMWProxy = initializeProxy(); + + // get list of vehicles + List vehicles = null; + try { + vehicles = myBMWProxy.requestVehiclesBase(); + } catch (NetworkException e) { + fail(e.getReason(), e); + } + + assertNotNull(vehicles); + assertEquals(2, vehicles.size()); + + for (VehicleBase vehicleBase : vehicles) { + assertNotNull(vehicleBase.getVin()); + assertNotNull(vehicleBase.getAttributes().getBrand()); + + // get image + try { + byte[] bmwImage = myBMWProxy.requestImage(vehicleBase.getVin(), vehicleBase.getAttributes().getBrand(), + new ImageProperties()); + + assertNotNull(bmwImage); + } catch (NetworkException e) { + fail(e.getReason(), e); + } + + // get state + VehicleStateContainer vehicleState = null; + try { + vehicleState = myBMWProxy.requestVehicleState(vehicleBase.getVin(), + vehicleBase.getAttributes().getBrand()); + } catch (NetworkException e) { + fail(e.getReason(), e); + } + assertNotNull(vehicleState); + + // get charge statistics -> only successful for electric vehicles + ChargingStatisticsContainer chargeStatisticsContainer = null; + try { + chargeStatisticsContainer = myBMWProxy.requestChargeStatistics(vehicleBase.getVin(), + vehicleBase.getAttributes().getBrand()); + assertNotNull(chargeStatisticsContainer); + } catch (NetworkException e) { + logger.trace("error: {}", e.toString()); + } + + ChargingSessionsContainer chargeSessionsContainer = null; + try { + chargeSessionsContainer = myBMWProxy.requestChargeSessions(vehicleBase.getVin(), + vehicleBase.getAttributes().getBrand()); + assertNotNull(chargeSessionsContainer); + } catch (NetworkException e) { + logger.trace("error: {}", e.toString()); + } + + ExecutionStatusContainer remoteExecutionResponse = null; + try { + remoteExecutionResponse = myBMWProxy.executeRemoteServiceCall(vehicleBase.getVin(), + vehicleBase.getAttributes().getBrand(), RemoteService.LIGHT_FLASH); + } catch (NetworkException e) { + fail(e.getReason(), e); + } + + assertNotNull(remoteExecutionResponse); + logger.warn("{}", remoteExecutionResponse.toString()); + + ExecutionStatusContainer remoteExecutionStatusResponse = null; + try { + remoteExecutionStatusResponse = myBMWProxy.executeRemoteServiceStatusCall( + vehicleBase.getAttributes().getBrand(), remoteExecutionResponse.getEventId()); + + assertNotNull(remoteExecutionStatusResponse); + logger.warn("{}", remoteExecutionStatusResponse.toString()); + + int counter = 0; + while (!ExecutionState.EXECUTED.toString().equals(remoteExecutionStatusResponse.getEventStatus()) + && counter++ < 10) { + remoteExecutionStatusResponse = myBMWProxy.executeRemoteServiceStatusCall( + vehicleBase.getAttributes().getBrand(), remoteExecutionResponse.getEventId()); + logger.warn("{}", remoteExecutionStatusResponse.toString()); + + Thread.sleep(5000); + } + } catch (NetworkException e) { + fail(e.getReason(), e); + } catch (InterruptedException e) { + fail(e.getMessage(), e); + } + } + } + + @Test + public void testGetImages() { + MyBMWProxy myBMWProxy = initializeProxy(); + + ImageProperties imageProperties = new ImageProperties(); + + try { + imageProperties.viewport = "VehicleStatus"; + byte[] bmwImage = myBMWProxy.requestImage("please_set_here_your_vin", "bmw", imageProperties); + Files.write(new File("./" + imageProperties.viewport + ".jpg").toPath(), bmwImage); + assertNotNull(bmwImage); + } catch (NetworkException | IOException e) { + logger.error("error retrieving image", e); + } + + try { + imageProperties.viewport = "SideViewLeft"; + byte[] bmwImage = myBMWProxy.requestImage("please_set_here_your_vin", "bmw", imageProperties); + Files.write(new File("./" + imageProperties.viewport + ".jpg").toPath(), bmwImage); + assertNotNull(bmwImage); + } catch (NetworkException | IOException e) { + logger.error("error retrieving image", e); + } + + try { + imageProperties.viewport = "AngleSideViewForty"; + byte[] bmwImage = myBMWProxy.requestImage("please_set_here_your_vin", "bmw", imageProperties); + Files.write(new File("./" + imageProperties.viewport + ".jpg").toPath(), bmwImage); + assertNotNull(bmwImage); + } catch (NetworkException | IOException e) { + logger.error("error retrieving image", e); + } + + try { + imageProperties.viewport = "FrontView"; + byte[] bmwImage = myBMWProxy.requestImage("please_set_here_your_vin", "bmw", imageProperties); + Files.write(new File("./" + imageProperties.viewport + ".jpg").toPath(), bmwImage); + assertNotNull(bmwImage); + } catch (NetworkException | IOException e) { + logger.error("error retrieving image", e); + } + + try { + imageProperties.viewport = "FrontLeft"; + byte[] bmwImage = myBMWProxy.requestImage("please_set_here_your_vin", "bmw", imageProperties); + Files.write(new File("./" + imageProperties.viewport + ".jpg").toPath(), bmwImage); + assertNotNull(bmwImage); + } catch (NetworkException | IOException e) { + logger.error("error retrieving image", e); + } + + try { + imageProperties.viewport = "FrontRight"; + byte[] bmwImage = myBMWProxy.requestImage("please_set_here_your_vin", "bmw", imageProperties); + Files.write(new File("./" + imageProperties.viewport + ".jpg").toPath(), bmwImage); + assertNotNull(bmwImage); + } catch (NetworkException | IOException e) { + logger.error("error retrieving image", e); + } + + try { + imageProperties.viewport = "RearView"; + byte[] bmwImage = myBMWProxy.requestImage("please_set_here_your_vin", "bmw", imageProperties); + Files.write(new File("./" + imageProperties.viewport + ".jpg").toPath(), bmwImage); + assertNotNull(bmwImage); + } catch (NetworkException | IOException e) { + logger.error("error retrieving image", e); + } + } + + @Test + @Disabled + public void testGetVehicles() { + MyBMWProxy myBMWProxy = initializeProxy(); + + try { + List vehicles = myBMWProxy.requestVehicles(); + + logger.warn(ResponseContentAnonymizer.anonymizeResponseContent(new Gson().toJson(vehicles))); + assertNotNull(vehicles); + assertEquals(2, vehicles.size()); + } catch (NetworkException e) { + fail(e.getReason(), e); + } + } +} + +/** + * @author Martin Grassl - initial contribution + */ +@NonNullByDefault +class MyHttpClientFactory implements HttpClientFactory { + + private final Logger logger = LoggerFactory.getLogger(MyHttpClientFactory.class); + + @Override + public HttpClient createHttpClient(String consumerName) { + // Instantiate and configure the SslContextFactory + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + // Instantiate HttpClient with the SslContextFactory + HttpClient httpClient = new HttpClient(sslContextFactory); + + // Configure HttpClient, for example: + httpClient.setFollowRedirects(false); + + // Start HttpClient + try { + httpClient.start(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + + return httpClient; + } + + @Override + public HttpClient getCommonHttpClient() { + return createHttpClient("test"); + } + + @Override + public HTTP2Client createHttp2Client(String arg0) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'createHttp2Client'"); + } + + @Override + public HTTP2Client createHttp2Client(String arg0, @Nullable SslContextFactory arg1) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'createHttp2Client'"); + } + + @Override + public HttpClient createHttpClient(String arg0, @Nullable SslContextFactory arg1) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'createHttpClient'"); + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationCheckerTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationCheckerTest.java index 6b2f3e0dfb4..39d97af2047 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationCheckerTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationCheckerTest.java @@ -30,19 +30,21 @@ import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; public class MyBMWConfigurationCheckerTest { @Test void testCheckConfiguration() { - MyBMWBridgeConfiguration cdc = new MyBMWBridgeConfiguration(); - assertFalse(MyBMWConfigurationChecker.checkConfiguration(cdc)); - cdc.userName = "a"; - assertFalse(MyBMWConfigurationChecker.checkConfiguration(cdc)); - cdc.password = "b"; - assertFalse(MyBMWConfigurationChecker.checkConfiguration(cdc)); - cdc.region = "c"; - assertFalse(MyBMWConfigurationChecker.checkConfiguration(cdc)); - cdc.region = BimmerConstants.REGION_NORTH_AMERICA; - assertTrue(MyBMWConfigurationChecker.checkConfiguration(cdc)); - cdc.region = BimmerConstants.REGION_ROW; - assertTrue(MyBMWConfigurationChecker.checkConfiguration(cdc)); - cdc.region = BimmerConstants.REGION_CHINA; - assertTrue(MyBMWConfigurationChecker.checkConfiguration(cdc)); + MyBMWBridgeConfiguration myBMWBridgeConfiguration = new MyBMWBridgeConfiguration(); + assertFalse(MyBMWConfigurationChecker.checkInitialConfiguration(myBMWBridgeConfiguration)); + myBMWBridgeConfiguration.setUserName("a"); + assertFalse(MyBMWConfigurationChecker.checkInitialConfiguration(myBMWBridgeConfiguration)); + myBMWBridgeConfiguration.setPassword("b"); + assertFalse(MyBMWConfigurationChecker.checkInitialConfiguration(myBMWBridgeConfiguration)); + myBMWBridgeConfiguration.setHcaptchatoken("d"); + assertFalse(MyBMWConfigurationChecker.checkInitialConfiguration(myBMWBridgeConfiguration)); + myBMWBridgeConfiguration.setRegion("c"); + assertFalse(MyBMWConfigurationChecker.checkInitialConfiguration(myBMWBridgeConfiguration)); + myBMWBridgeConfiguration.setRegion(BimmerConstants.REGION_NORTH_AMERICA); + assertTrue(MyBMWConfigurationChecker.checkInitialConfiguration(myBMWBridgeConfiguration)); + myBMWBridgeConfiguration.setRegion(BimmerConstants.REGION_ROW); + assertTrue(MyBMWConfigurationChecker.checkInitialConfiguration(myBMWBridgeConfiguration)); + myBMWBridgeConfiguration.setRegion(BimmerConstants.REGION_CHINA); + assertTrue(MyBMWConfigurationChecker.checkInitialConfiguration(myBMWBridgeConfiguration)); } }