[mybmw] Fix hcaptchatoken issue (#17862) (#17896)

* [mybmw] add stop charging command
* [mybmw] fix hcaptcha issue (#17862)

Signed-off-by: Martin Grassl <martin.grassl@digital-filestore.de>
This commit is contained in:
Martin Grassl 2024-12-15 11:36:49 +01:00 committed by GitHub
parent 7fc3d3b685
commit fe624dd6c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 677 additions and 124 deletions

View File

@ -79,9 +79,10 @@ 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 |
| 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).

View File

@ -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 + "]";
}
}

View File

@ -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<VehicleBase> 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<VehicleBase> vehicles = handler.getMyBmwProxy().get().requestVehiclesBase();
List<VehicleBase> 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);

View File

@ -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<MyBMW
myBMWProxy = thingHandler.getMyBmwProxy();
try {
Optional<List<@NonNull Vehicle>> vehicleList = myBMWProxy.map(prox -> {
Optional<List<Vehicle>> vehicleList = myBMWProxy.map(prox -> {
try {
return prox.requestVehicles();
} catch (NetworkException e) {
@ -88,8 +87,13 @@ public class VehicleDiscovery extends AbstractThingHandlerDiscoveryService<MyBMW
}
});
vehicleList.ifPresentOrElse(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();

View File

@ -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 + "]";
}
}

View File

@ -60,6 +60,7 @@ public class MyBMWBridgeHandler extends BaseBridgeHandler {
private Optional<ScheduledFuture<?>> initializerJob = Optional.empty();
private Optional<VehicleDiscovery> vehicleDiscovery = Optional.empty();
private LocaleProvider localeProvider;
private Optional<MyBMWBridgeConfiguration> 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<MyBMWProxy> 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<MyBMWProxy> getMyBmwProxy() {
public synchronized Optional<MyBMWProxy> 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;
}
}

View File

@ -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())) {

View File

@ -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) {

View File

@ -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

View File

@ -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<Vehicle> requestVehicles() throws NetworkException {
List<Vehicle> vehicles = new ArrayList<>();
List<VehicleBase> vehiclesBase = requestVehiclesBase();
for (VehicleBase vehicleBase : vehiclesBase) {
VehicleStateContainer vehicleState = requestVehicleState(vehicleBase.getVin(),

View File

@ -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<Vehicle> requestVehicles() throws NetworkException {
List<Vehicle> vehicles = new ArrayList<>();
List<VehicleBase> 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);

View File

@ -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<Vehicle> requestVehicles() throws NetworkException;
/**
* request all vehicles for one specific brand and their state

View File

@ -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<String, String> 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";

View File

@ -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";

View File

@ -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());
}
}
}

View File

@ -14,6 +14,11 @@
<description>MyBMW Password</description>
<context>password</context>
</parameter>
<parameter name="hcaptchatoken" type="text" required="true">
<label>Captcha-Token</label>
<description>Captcha-Token for login (see https://bimmer-connected.readthedocs.io/en/stable/captcha.html)</description>
<context>hcaptchatoken</context>
</parameter>
<parameter name="region" type="text" required="true">
<label>Region</label>
<description>Select Region in order to connect to the appropriate BMW Server</description>

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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<VehicleBase> 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<Vehicle> 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'");
}
}

View File

@ -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));
}
}