* _CHINA_
* _ROW_ (Rest of World)
+
+#### Advanced Configuration
+
+| Parameter | Type | Description |
+|-----------------|---------|--------------------------------------------------------------------|
+| preferMyBmw | boolean | Prefer *MyBMW* API instead of *BMW Connected Drive* |
+
+
### Thing Configuration
Same configuration is needed for all things
| Next Service Date | service-date | DateTime | Date of upcoming service |
| Mileage till Next Service | service-mileage | Number:Length | Mileage till upcoming service |
| Check Control | check-control | String | Presence of active warning messages |
+| Plug Connection Status | plug-connection | String | Only available for phev, bev_rex and bev |
| Charging Status | charge | String | Only available for phev, bev_rex and bev |
| Last Status Timestamp | last-update | DateTime | Date and time of last status update |
+| Last Status Update Reason | last-update-reason | DateTime | Date and time of last status update |
Overall Door Status values
* _Charging Goal reached_
* _Waiting For Charging_
+Last update reasons
+
+* _CHARGING_DONE_
+* _CHARGING_INTERRUPED_
+* _CHARGING_PAUSED
+* _CHARGING_STARTED_
+* _CYCLIC_RECHARGING_
+* _DISCONNECTED_
+* _DOOR_STATE_CHANGED_
+* _NO_CYCLIC_RECHARGING_
+* _NO_LSC_TRIGGER_
+* _ON_DEMAND_
+* _PREDICTION_UPDATE_
+* _TEMPORARY_POWER_SUPPLY_FAILURE_
+* _UNKNOWN_
+* _VEHICLE_MOVING_
+* _VEHICLE_SECURED_
+* _VEHICLE_SHUTDOWN_
+* _VEHICLE_SHUTDOWN_SECURED_
+* _VEHICLE_UNSECURED_
+
#### Services
Group for all upcoming services with description, service date and/or service mileage.
* Availability according to table
* Read-only values
-| Channel Label | Channel ID | Type | conv | phev | bev_rex | bev |
-|-----------------------|-----------------------|----------------------|------|------|---------|-----|
-| Mileage | mileage | Number:Length | X | X | X | X |
-| Fuel Range | range-fuel | Number:Length | X | X | X | |
-| Battery Range | range-electric | Number:Length | | X | X | X |
-| Hybrid Range | range-hybrid | Number:Length | | X | X | |
-| Battery Charge Level | soc | Number:Dimensionless | | X | X | X |
-| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | |
-| Fuel Range Radius | range-radius-fuel | Number:Length | X | X | X | |
-| Electric Range Radius | range-radius-electric | Number:Length | | X | X | X |
-| Hybrid Range Radius | range-radius-hybrid | Number:Length | | X | X | |
+| Channel Label | Channel ID | Type | conv | phev | bev_rex | bev |
+|---------------------------|-------------------------|----------------------|------|------|---------|-----|
+| Mileage | mileage | Number:Length | X | X | X | X |
+| Fuel Range | range-fuel | Number:Length | X | X | X | |
+| Battery Range | range-electric | Number:Length | | X | X | X |
+| Max Battery Range | range-electric-max | Number:Length | | X | X | X |
+| Hybrid Range | range-hybrid | Number:Length | | X | X | |
+| Battery Charge Level | soc | Number:Dimensionless | | X | X | X |
+| Max Battery Capacity | soc-max | Number:Power | | | X | X | X |
+| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | |
+| Fuel Range Radius | range-radius-fuel | Number:Length | X | X | X | |
+| Electric Range Radius | range-radius-electric | Number:Length | | X | X | X |
+| Hybrid Range Radius | range-radius-hybrid | Number:Length | | X | X | |
+| Max Hybrid Range Radius | range-radius-hybrid-max | Number:Length | | X | X | |
#### Charge Profile
* BMW Connected Drive Password
*/
public String password = Constants.EMPTY;
+
+ /**
+ * Prefer MyBMW API instead of BMW Connected Drive
+ */
+ public boolean preferMyBmw = false;
}
public static final String SERVICE_DATE = "service-date";
public static final String SERVICE_MILEAGE = "service-mileage";
public static final String CHECK_CONTROL = "check-control";
+ public static final String PLUG_CONNECTION = "plug-connection";
public static final String CHARGE_STATUS = "charge";
public static final String CHARGE_END_REASON = "reason";
public static final String CHARGE_REMAINING = "remaining";
public static final String LAST_UPDATE = "last-update";
+ public static final String LAST_UPDATE_REASON = "last-update-reason";
// Door Details
public static final String DOOR_DRIVER_FRONT = "driver-front";
// Range
public static final String RANGE_HYBRID = "hybrid";
+ public static final String RANGE_HYBRID_MAX = "hybrid-max";
public static final String RANGE_ELECTRIC = "electric";
+ public static final String RANGE_ELECTRIC_MAX = "electric-max";
public static final String SOC = "soc";
+ public static final String SOC_MAX = "soc-max";
public static final String RANGE_FUEL = "fuel";
public static final String REMAINING_FUEL = "remaining-fuel";
public static final String RANGE_RADIUS_ELECTRIC = "radius-electric";
+ public static final String RANGE_RADIUS_ELECTRIC_MAX = "radius-electric-max";
public static final String RANGE_RADIUS_FUEL = "radius-fuel";
public static final String RANGE_RADIUS_HYBRID = "radius-hybrid";
+ public static final String RANGE_RADIUS_HYBRID_MAX = "radius-hybrid-max";
// Last Trip
public static final String DURATION = "duration";
public String tokenType;
@SerializedName("expires_in")
public int expiresIn;
+
+ @Override
+ public String toString() {
+ return "Token " + accessToken + " type " + tokenType + " expires in " + expiresIn;
+ }
}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.navigation;
+
+/**
+ * The {@link NavigationContainer} Data Transfer Object
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+public class NavigationContainer {
+ // "latitude": 56.789,
+ // "longitude": 8.765,
+ // "isoCountryCode": "DEU",
+ // "auxPowerRegular": 1.4,
+ // "auxPowerEcoPro": 1.2,
+ // "auxPowerEcoProPlus": 0.4,
+ // "soc": 25.952999114990234,
+ // "pendingUpdate": false,
+ // "vehicleTracking": true,
+ public double socmax;// ": 29.84
+}
*/
public class ExecutionStatusContainer {
public ExecutionStatus executionStatus;
+ public String eventId;
+ public String creationTime;
+ public String eventStatus;
}
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
troubleshootFingerprint = Optional.empty();
updateStatus(ThingStatus.UNKNOWN);
ConnectedDriveConfiguration config = getConfigAs(ConnectedDriveConfiguration.class);
+ logger.debug("Prefer MyBMW API {}", config.preferMyBmw);
if (!checkConfiguration(config)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
} else {
proxy = Optional.of(new ConnectedDriveProxy(httpClientFactory, config));
// give the system some time to create all predefined Vehicles
// check with API call if bridge is online
- initializerJob = Optional.of(scheduler.schedule(this::requestVehicles, 5, TimeUnit.SECONDS));
+ initializerJob = Optional.of(scheduler.schedule(this::requestVehicles, 2, TimeUnit.SECONDS));
+ Bridge b = super.getThing();
+ List<Thing> children = b.getThings();
+ logger.debug("Update {} things", children.size());
+ children.forEach(entry -> {
+ ThingHandler th = entry.getHandler();
+ if (th != null) {
+ th.dispose();
+ th.initialize();
+ } else {
+ logger.debug("Handler is null");
+ }
+ });
}
}
public static boolean checkConfiguration(ConnectedDriveConfiguration config) {
if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) {
return false;
- } else if (BimmerConstants.AUTH_SERVER_MAP.containsKey(config.region)) {
- return true;
} else {
- return false;
+ return BimmerConstants.AUTH_SERVER_MAP.containsKey(config.region);
}
}
proxy.ifPresent(prox -> prox.requestVehicles(this));
}
+ // https://www.bmw-connecteddrive.de/api/me/vehicles/v2?all=true&brand=BM
public String getDiscoveryFingerprint() {
return troubleshootFingerprint.map(fingerprint -> {
VehiclesContainer container = null;
});
return Converter.getGson().toJson(container);
}
+ } else {
+ logger.debug("container.vehicles is null");
}
}
} catch (JsonParseException jpe) {
}
});
}
- return Converter.getGson().toJson(container);
+ } else {
+ troubleshootFingerprint = Optional.of(Constants.EMPTY_JSON);
}
} catch (JsonParseException jpe) {
logger.debug("Fingerprint parse exception {}", jpe.getMessage());
import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
+import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.StringContentProvider;
-import org.eclipse.jetty.http.HttpField;
-import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.UrlEncoded;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.google.gson.JsonSyntaxException;
-
/**
* The {@link ConnectedDriveProxy} This class holds the important constants for the BMW Connected Drive Authorization.
* They
@NonNullByDefault
public class ConnectedDriveProxy {
private final Logger logger = LoggerFactory.getLogger(ConnectedDriveProxy.class);
+ private Optional<RemoteServiceHandler> remoteServiceHandler = Optional.empty();
private final Token token = new Token();
private final HttpClient httpClient;
private final HttpClient authHttpClient;
- private final String legacyAuthUri;
private final ConnectedDriveConfiguration configuration;
/**
final String baseUrl;
final String vehicleUrl;
final String legacyUrl;
+ final String remoteCommandUrl;
+ final String remoteStatusUrl;
+ final String navigationAPIUrl;
final String vehicleStatusAPI = "/status";
final String lastTripAPI = "/statistics/lastTrip";
final String allTripsAPI = "/statistics/allTrips";
final String rangeMapAPI = "/rangemap";
final String serviceExecutionAPI = "/executeService";
final String serviceExecutionStateAPI = "/serviceExecutionStatus";
+ public static final String REMOTE_SERVICE_EADRAX_BASE_URL = "/eadrax-vrccs/v2/presentation/remote-commands/"; // '/{vin}/{service_type}'
+ final String remoteServiceEADRXstatusUrl = REMOTE_SERVICE_EADRAX_BASE_URL + "eventStatus?eventId={event_id}";
+ final String vehicleEADRXPoiUrl = "/eadrax-dcs/v1/send-to-car/send-to-car";
public ConnectedDriveProxy(HttpClientFactory httpClientFactory, ConnectedDriveConfiguration config) {
httpClient = httpClientFactory.getCommonHttpClient();
authHttpClient = httpClientFactory.createHttpClient(AUTH_HTTP_CLIENT_NAME);
- authHttpClient.setFollowRedirects(false);
configuration = config;
- final StringBuilder legacyAuth = new StringBuilder();
- legacyAuth.append("https://");
- legacyAuth.append(BimmerConstants.AUTH_SERVER_MAP.get(configuration.region));
- legacyAuth.append(BimmerConstants.OAUTH_ENDPOINT);
- legacyAuthUri = legacyAuth.toString();
- vehicleUrl = "https://" + getRegionServer() + "/webapi/v1/user/vehicles";
+ vehicleUrl = "https://" + BimmerConstants.API_SERVER_MAP.get(configuration.region) + "/webapi/v1/user/vehicles";
baseUrl = vehicleUrl + "/";
- legacyUrl = "https://" + getRegionServer() + "/api/vehicle/dynamic/v1/";
+ legacyUrl = "https://" + BimmerConstants.API_SERVER_MAP.get(configuration.region) + "/api/vehicle/dynamic/v1/";
+ navigationAPIUrl = "https://" + BimmerConstants.API_SERVER_MAP.get(configuration.region)
+ + "/api/vehicle/navigation/v1/";
+ remoteCommandUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ + REMOTE_SERVICE_EADRAX_BASE_URL;
+ remoteStatusUrl = remoteCommandUrl + "eventStatus";
}
- private synchronized void call(final String url, final boolean post, final @Nullable MultiMap<String> params,
- final ResponseCallback callback) {
+ public synchronized void call(final String url, final boolean post, final @Nullable String encoding,
+ final @Nullable String params, final ResponseCallback callback) {
// only executed in "simulation mode"
// SimulationTest.testSimulationOff() assures Injector is off when releasing
if (Injector.isActive()) {
return;
}
final Request req;
- final String encoded = params == null || params.isEmpty() ? null
- : UrlEncoded.encode(params, StandardCharsets.UTF_8, false);
final String completeUrl;
if (post) {
completeUrl = url;
req = httpClient.POST(url);
- if (encoded != null) {
- req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, encoded, StandardCharsets.UTF_8));
+ if (encoding != null) {
+ if (CONTENT_TYPE_URL_ENCODED.equals(encoding)) {
+ req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
+ } else if (CONTENT_TYPE_JSON_ENCODED.equals(encoding)) {
+ req.header(HttpHeader.CONTENT_TYPE, encoding);
+ req.content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, params, StandardCharsets.UTF_8));
+ }
}
} else {
- completeUrl = encoded == null ? url : url + Constants.QUESTION + encoded;
+ completeUrl = params == null ? url : url + Constants.QUESTION + params;
req = httpClient.newRequest(completeUrl);
}
req.header(HttpHeader.AUTHORIZATION, getToken().getBearerToken());
- req.header(HttpHeader.REFERER, BimmerConstants.REFERER_URL);
+ req.header(HttpHeader.REFERER, BimmerConstants.LEGACY_REFERER_URL);
req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(new BufferingResponseListener() {
@NonNullByDefault({})
});
}
- public void get(String url, @Nullable MultiMap<String> params, ResponseCallback callback) {
- call(url, false, params, callback);
+ public void get(String url, @Nullable String coding, @Nullable String params, ResponseCallback callback) {
+ call(url, false, coding, params, callback);
}
- public void post(String url, @Nullable MultiMap<String> params, ResponseCallback callback) {
- call(url, true, params, callback);
+ public void post(String url, @Nullable String coding, @Nullable String params, ResponseCallback callback) {
+ call(url, true, coding, params, callback);
}
public void requestVehicles(StringResponseCallback callback) {
- get(vehicleUrl, null, callback);
+ get(vehicleUrl, null, null, callback);
}
public void requestVehcileStatus(VehicleConfiguration config, StringResponseCallback callback) {
- get(baseUrl + config.vin + vehicleStatusAPI, null, callback);
+ get(baseUrl + config.vin + vehicleStatusAPI, null, null, callback);
}
public void requestLegacyVehcileStatus(VehicleConfiguration config, StringResponseCallback callback) {
// see https://github.com/jupe76/bmwcdapi/search?q=dynamic%2Fv1
- get(legacyUrl + config.vin + "?offset=-60", null, callback);
+ get(legacyUrl + config.vin + "?offset=-60", null, null, callback);
+ }
+
+ public void requestLNavigation(VehicleConfiguration config, StringResponseCallback callback) {
+ // see https://github.com/jupe76/bmwcdapi/search?q=dynamic%2Fv1
+ get(navigationAPIUrl + config.vin, null, null, callback);
}
public void requestLastTrip(VehicleConfiguration config, StringResponseCallback callback) {
- get(baseUrl + config.vin + lastTripAPI, null, callback);
+ get(baseUrl + config.vin + lastTripAPI, null, null, callback);
}
public void requestAllTrips(VehicleConfiguration config, StringResponseCallback callback) {
- get(baseUrl + config.vin + allTripsAPI, null, callback);
+ get(baseUrl + config.vin + allTripsAPI, null, null, callback);
}
public void requestChargingProfile(VehicleConfiguration config, StringResponseCallback callback) {
- get(baseUrl + config.vin + chargeAPI, null, callback);
+ get(baseUrl + config.vin + chargeAPI, null, null, callback);
}
public void requestDestinations(VehicleConfiguration config, StringResponseCallback callback) {
- get(baseUrl + config.vin + destinationAPI, null, callback);
+ get(baseUrl + config.vin + destinationAPI, null, null, callback);
}
public void requestRangeMap(VehicleConfiguration config, @Nullable MultiMap<String> params,
StringResponseCallback callback) {
- get(baseUrl + config.vin + rangeMapAPI, params, callback);
+ get(baseUrl + config.vin + rangeMapAPI, CONTENT_TYPE_URL_ENCODED,
+ UrlEncoded.encode(params, StandardCharsets.UTF_8, false), callback);
}
public void requestImage(VehicleConfiguration config, ImageProperties props, ByteResponseCallback callback) {
dataMap.add("width", Integer.toString(props.size));
dataMap.add("height", Integer.toString(props.size));
dataMap.add("view", props.viewport);
- get(localImageUrl, dataMap, callback);
- }
-
- private String getRegionServer() {
- final String retVal = BimmerConstants.SERVER_MAP.get(configuration.region);
- return retVal == null ? Constants.INVALID : retVal;
- }
- private String getAuthorizationValue() {
- final String retVal = BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region);
- return retVal == null ? Constants.INVALID : retVal;
+ get(localImageUrl, CONTENT_TYPE_URL_ENCODED, UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false),
+ callback);
}
RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) {
- return new RemoteServiceHandler(vehicleHandler, this);
+ remoteServiceHandler = Optional.of(new RemoteServiceHandler(vehicleHandler, this));
+ return remoteServiceHandler.get();
}
// Token handling
* @return token
*/
public Token getToken() {
- if (token.isExpired() || !token.isValid()) {
- updateToken();
+ if (!token.isValid()) {
+ if (configuration.preferMyBmw) {
+ if (!updateToken()) {
+ if (!updateLegacyToken()) {
+ logger.debug("Authorization failed!");
+ }
+ }
+ } else {
+ if (!updateLegacyToken()) {
+ if (!updateToken()) {
+ logger.debug("Authorization failed!");
+ }
+ }
+ }
}
+ remoteServiceHandler.ifPresent(serviceHandler -> {
+ serviceHandler.setMyBmwApiUsage(token.isMyBmwApiUsage());
+ });
return token;
}
- /**
- * Authorize at BMW Connected Drive Portal and get Token
- *
- * @return
- */
- private synchronized void updateToken() {
+ public synchronized boolean updateToken() {
+ if (BimmerConstants.REGION_CHINA.equals(configuration.region)) {
+ // region China currently not supported for MyBMW API
+ logger.debug("Region {} not supported yet for MyBMW Login", BimmerConstants.REGION_CHINA);
+ return false;
+ }
+ if (!startAuthClient()) {
+ return false;
+ } // else continue
+ String authUri = "https://" + BimmerConstants.AUTH_SERVER_MAP.get(configuration.region)
+ + BimmerConstants.OAUTH_ENDPOINT;
+
+ Request authRequest = authHttpClient.POST(authUri);
+ authRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
+
+ MultiMap<String> authChallenge = getTokenBaseValues();
+ authChallenge.addAllValues(getTokenAuthValues());
+ String authEncoded = UrlEncoded.encode(authChallenge, Charset.defaultCharset(), false);
+ authRequest.content(new StringContentProvider(authEncoded));
+ try {
+ ContentResponse authResponse = authRequest.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send();
+ String authResponseString = URLDecoder.decode(authResponse.getContentAsString(), Charset.defaultCharset());
+ String authCode = getAuthCode(authResponseString);
+ if (authCode != Constants.EMPTY) {
+ MultiMap<String> codeChallenge = getTokenBaseValues();
+ codeChallenge.put(AUTHORIZATION, authCode);
+
+ Request codeRequest = authHttpClient.POST(authUri).followRedirects(false);
+ codeRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
+ String codeEncoded = UrlEncoded.encode(codeChallenge, Charset.defaultCharset(), false);
+ codeRequest.content(new StringContentProvider(codeEncoded));
+ ContentResponse codeResponse = codeRequest.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send();
+ String code = ConnectedDriveProxy.codeFromUrl(codeResponse.getHeaders().get(HttpHeader.LOCATION));
+
+ // Get Token
+ String tokenUrl = "https://" + BimmerConstants.AUTH_SERVER_MAP.get(configuration.region)
+ + BimmerConstants.TOKEN_ENDPOINT;
+
+ Request tokenRequest = authHttpClient.POST(tokenUrl).followRedirects(false);
+ tokenRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
+ tokenRequest.header(HttpHeader.AUTHORIZATION,
+ BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region));
+ String tokenEncoded = UrlEncoded.encode(getTokenValues(code), Charset.defaultCharset(), false);
+ tokenRequest.content(new StringContentProvider(tokenEncoded));
+ ContentResponse tokenResponse = tokenRequest.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send();
+ AuthResponse authResponseJson = Converter.getGson().fromJson(tokenResponse.getContentAsString(),
+ AuthResponse.class);
+ token.setToken(authResponseJson.accessToken);
+ token.setType(authResponseJson.tokenType);
+ token.setExpiration(authResponseJson.expiresIn);
+ token.setMyBmwApiUsage(true);
+ return true;
+ }
+ } catch (InterruptedException | ExecutionException |
+
+ TimeoutException e) {
+ logger.debug("Authorization exception: {}", e.getMessage());
+ }
+ return false;
+ }
+
+ private boolean startAuthClient() {
if (!authHttpClient.isStarted()) {
try {
authHttpClient.start();
} catch (Exception e) {
- logger.warn("Auth Http Client cannot be started {}", e.getMessage());
- return;
+ logger.error("Auth HttpClient start failed!");
+ return false;
}
}
+ return true;
+ }
- final Request req = authHttpClient.POST(legacyAuthUri);
- req.header(HttpHeader.CONNECTION, KEEP_ALIVE);
- req.header(HttpHeader.HOST, getRegionServer());
- req.header(HttpHeader.AUTHORIZATION, getAuthorizationValue());
- req.header(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES);
- req.header(HttpHeader.REFERER, BimmerConstants.REFERER_URL);
+ private MultiMap<String> getTokenBaseValues() {
+ MultiMap<String> baseValues = new MultiMap<String>();
+ baseValues.add(CLIENT_ID, Constants.EMPTY + BimmerConstants.CLIENT_ID.get(configuration.region));
+ baseValues.add(RESPONSE_TYPE, CODE);
+ baseValues.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE);
+ baseValues.add("state", Constants.EMPTY + BimmerConstants.STATE.get(configuration.region));
+ baseValues.add("nonce", "login_nonce");
+ baseValues.add(SCOPE, BimmerConstants.SCOPE_VALUES);
+ return baseValues;
+ }
- final MultiMap<String> dataMap = new MultiMap<String>();
- dataMap.add("grant_type", "password");
- dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES);
- dataMap.add(USERNAME, configuration.userName);
- dataMap.add(PASSWORD, configuration.password);
- req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
- UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
+ private MultiMap<String> getTokenAuthValues() {
+ MultiMap<String> authValues = new MultiMap<String>();
+ authValues.add(GRANT_TYPE, "authorization_code");
+ authValues.add(USERNAME, configuration.userName);
+ authValues.add(PASSWORD, configuration.password);
+ return authValues;
+ }
+
+ private MultiMap<String> getTokenValues(String code) {
+ MultiMap<String> tokenValues = new MultiMap<String>();
+ tokenValues.put(CODE, code);
+ tokenValues.put("code_verifier", Constants.EMPTY + BimmerConstants.CODE_VERIFIER.get(configuration.region));
+ tokenValues.put(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE);
+ tokenValues.put(GRANT_TYPE, "authorization_code");
+ return tokenValues;
+ }
+
+ private String getAuthCode(String response) {
+ String[] keys = response.split("&");
+ for (int i = 0; i < keys.length; i++) {
+ if (keys[i].startsWith(AUTHORIZATION)) {
+ String authCode = keys[i].split("=")[1];
+ authCode = authCode.split("\"")[0];
+ return authCode;
+ }
+ }
+ return Constants.EMPTY;
+ }
+
+ public synchronized boolean updateLegacyToken() {
+ logger.debug("updateLegacyToken");
try {
- ContentResponse contentResponse = req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send();
- // Status needs to be 302 - Response is stored in Header
- if (contentResponse.getStatus() == 302) {
- final HttpFields fields = contentResponse.getHeaders();
- final HttpField field = fields.getField(HttpHeader.LOCATION);
- tokenFromUrl(field.getValue());
- } else if (contentResponse.getStatus() == 200) {
- final String stringContent = contentResponse.getContentAsString();
- if (stringContent != null && !stringContent.isEmpty()) {
- try {
- final AuthResponse authResponse = Converter.getGson().fromJson(stringContent,
- AuthResponse.class);
- if (authResponse != null) {
- token.setToken(authResponse.accessToken);
- token.setType(authResponse.tokenType);
- token.setExpiration(authResponse.expiresIn);
- } else {
- logger.debug("not an Authorization response: {}", stringContent);
- }
- } catch (JsonSyntaxException jse) {
- logger.debug("Authorization response unparsable: {}", stringContent);
- }
- } else {
- logger.debug("Authorization response has no content");
- }
- } else {
- logger.debug("Authorization status {} reason {}", contentResponse.getStatus(),
- contentResponse.getReason());
+ /**
+ * The authorization with Jetty HttpClient doens't work anymore
+ * When calling Jetty with same headers and content a ConcurrentExcpetion is thrown
+ * So fallback legacy authorization will stay on java.net handling
+ */
+ String authUri = "https://" + BimmerConstants.AUTH_SERVER_MAP.get(configuration.region)
+ + BimmerConstants.OAUTH_ENDPOINT;
+ URL url = new URL(authUri);
+ HttpURLConnection.setFollowRedirects(false);
+ HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ con.setRequestMethod("POST");
+ con.setRequestProperty(HttpHeader.CONTENT_TYPE.toString(), CONTENT_TYPE_URL_ENCODED);
+ con.setRequestProperty(HttpHeader.CONNECTION.toString(), KEEP_ALIVE);
+ con.setRequestProperty(HttpHeader.HOST.toString(),
+ BimmerConstants.API_SERVER_MAP.get(configuration.region));
+ con.setRequestProperty(HttpHeader.AUTHORIZATION.toString(),
+ BimmerConstants.LEGACY_AUTHORIZATION_VALUE_MAP.get(configuration.region));
+ con.setRequestProperty(CREDENTIALS, BimmerConstants.LEGACY_CREDENTIAL_VALUES);
+ con.setDoOutput(true);
+
+ OutputStream os = con.getOutputStream();
+ byte[] input = getAuthEncodedData().getBytes("utf-8");
+ os.write(input, 0, input.length);
+
+ BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8"));
+ StringBuilder response = new StringBuilder();
+ String responseLine = null;
+ while ((responseLine = br.readLine()) != null) {
+ response.append(responseLine.trim());
}
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- logger.debug("Authorization exception: {}", e.getMessage());
+ token.setMyBmwApiUsage(false);
+ return tokenFromUrl(con.getHeaderField(HttpHeader.LOCATION.toString()));
+ } catch (IOException e) {
+ logger.warn("{}", e.getMessage());
}
+ return false;
}
- void tokenFromUrl(String encodedUrl) {
+ public boolean tokenFromUrl(String encodedUrl) {
final MultiMap<String> tokenMap = new MultiMap<String>();
UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
tokenMap.forEach((key, value) -> {
}
}
});
+ logger.info("Token valid? {}", token.isValid());
+ return token.isValid();
+ }
+
+ public static String codeFromUrl(String encodedUrl) {
+ final MultiMap<String> tokenMap = new MultiMap<String>();
+ UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
+ final StringBuilder codeFound = new StringBuilder();
+ tokenMap.forEach((key, value) -> {
+ if (value.size() > 0) {
+ String val = value.get(0);
+ if (key.endsWith(CODE)) {
+ codeFound.append(val.toString());
+ }
+ }
+ });
+ return codeFound.toString();
+ }
+
+ private String getAuthEncodedData() {
+ MultiMap<String> dataMap = new MultiMap<String>();
+ dataMap.add(CLIENT_ID, BimmerConstants.LEGACY_CLIENT_ID);
+ dataMap.add(RESPONSE_TYPE, TOKEN);
+ dataMap.add(REDIRECT_URI, BimmerConstants.LEGACY_REDIRECT_URI_VALUE);
+ dataMap.add(SCOPE, BimmerConstants.LEGACY_SCOPE_VALUES);
+ dataMap.add(USERNAME, configuration.userName);
+ dataMap.add(PASSWORD, configuration.password);
+ return UrlEncoded.encode(dataMap, Charset.defaultCharset(), false);
}
}
package org.openhab.binding.bmwconnecteddrive.internal.handler;
import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
+import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*;
+import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.UrlEncoded;
import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration;
import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
import org.openhab.binding.bmwconnecteddrive.internal.dto.remote.ExecutionStatusContainer;
private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class);
private static final String SERVICE_TYPE = "serviceType";
+ private static final String EVENT_ID = "eventId";
private static final String DATA = "data";
// after 6 retries the state update will give up
private static final int GIVEUP_COUNTER = 6;
private final ConnectedDriveProxy proxy;
private final VehicleHandler handler;
+ private final String legacyServiceExecutionAPI;
+ private final String legacyServiceExecutionStateAPI;
private final String serviceExecutionAPI;
private final String serviceExecutionStateAPI;
private int counter = 0;
private Optional<ScheduledFuture<?>> stateJob = Optional.empty();
private Optional<String> serviceExecuting = Optional.empty();
+ private Optional<String> executingEventId = Optional.empty();
+ private boolean myBmwApiUsage = false;
public enum ExecutionState {
READY,
}
public enum RemoteService {
- LIGHT_FLASH(REMOTE_SERVICE_LIGHT_FLASH, "Flash Lights"),
- VEHICLE_FINDER(REMOTE_SERVICE_VEHICLE_FINDER, "Vehicle Finder"),
- DOOR_LOCK(REMOTE_SERVICE_DOOR_LOCK, "Door Lock"),
- DOOR_UNLOCK(REMOTE_SERVICE_DOOR_UNLOCK, "Door Unlock"),
- HORN_BLOW(REMOTE_SERVICE_HORN, "Horn Blow"),
- CLIMATE_NOW(REMOTE_SERVICE_AIR_CONDITIONING, "Climate Control"),
- CHARGE_NOW(REMOTE_SERVICE_CHARGE_NOW, "Start Charging"),
- CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile");
+ LIGHT_FLASH(REMOTE_SERVICE_LIGHT_FLASH, "Flash Lights", "light-flash"),
+ VEHICLE_FINDER(REMOTE_SERVICE_VEHICLE_FINDER, "Vehicle Finder", "vehicle-finder"),
+ DOOR_LOCK(REMOTE_SERVICE_DOOR_LOCK, "Door Lock", "door-lock"),
+ DOOR_UNLOCK(REMOTE_SERVICE_DOOR_UNLOCK, "Door Unlock", "door-unlock"),
+ HORN_BLOW(REMOTE_SERVICE_HORN, "Horn Blow", "horn-blow"),
+ CLIMATE_NOW(REMOTE_SERVICE_AIR_CONDITIONING, "Climate Control", "air-conditioning"),
+ CHARGE_NOW(REMOTE_SERVICE_CHARGE_NOW, "Start Charging", "charge-now"),
+ CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile", "charging-control");
private final String command;
private final String label;
+ private final String remoteCommand;
- RemoteService(final String command, final String label) {
+ RemoteService(final String command, final String label, final String remoteCommand) {
this.command = command;
this.label = label;
+ this.remoteCommand = remoteCommand;
}
public String getCommand() {
public String getLabel() {
return label;
}
+
+ public String getRemoteCommand() {
+ return remoteCommand;
+ }
}
public RemoteServiceHandler(VehicleHandler vehicleHandler, ConnectedDriveProxy connectedDriveProxy) {
handler = vehicleHandler;
proxy = connectedDriveProxy;
final VehicleConfiguration config = handler.getConfiguration().get();
- serviceExecutionAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionAPI;
- serviceExecutionStateAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionStateAPI;
+ legacyServiceExecutionAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionAPI;
+ legacyServiceExecutionStateAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionStateAPI;
+ serviceExecutionAPI = proxy.remoteCommandUrl + config.vin + "/";
+ serviceExecutionStateAPI = proxy.remoteStatusUrl;
}
boolean execute(RemoteService service, String... data) {
synchronized (this) {
if (serviceExecuting.isPresent()) {
+ logger.debug("Execution rejected - {} still pending", serviceExecuting.get());
// only one service executing
return false;
}
serviceExecuting = Optional.of(service.name());
}
- final MultiMap<String> dataMap = new MultiMap<String>();
- dataMap.add(SERVICE_TYPE, service.name());
- if (data.length > 0) {
- dataMap.add(DATA, data[0]);
+ if (myBmwApiUsage) {
+ final MultiMap<String> dataMap = new MultiMap<String>();
+ if (data.length > 0) {
+ dataMap.add(DATA, data[0]);
+ proxy.post(serviceExecutionAPI + service.getRemoteCommand(), CONTENT_TYPE_JSON_ENCODED,
+ "{CHARGING_PROFILE:" + data[0] + "}", this);
+ } else {
+ proxy.post(serviceExecutionAPI + service.getRemoteCommand(), null, null, this);
+ }
+ } else {
+ final MultiMap<String> dataMap = new MultiMap<String>();
+ dataMap.add(SERVICE_TYPE, service.name());
+ if (data.length > 0) {
+ dataMap.add(DATA, data[0]);
+ }
+ proxy.post(legacyServiceExecutionAPI, CONTENT_TYPE_URL_ENCODED,
+ UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), this);
}
- proxy.post(serviceExecutionAPI, dataMap, this);
return true;
}
handler.getData();
}
counter++;
- final MultiMap<String> dataMap = new MultiMap<String>();
- dataMap.add(SERVICE_TYPE, service);
- proxy.get(serviceExecutionStateAPI, dataMap, this);
+ if (myBmwApiUsage) {
+ final MultiMap<String> dataMap = new MultiMap<String>();
+ dataMap.add(EVENT_ID, executingEventId.get());
+ final String encoded = dataMap == null || dataMap.isEmpty() ? null
+ : UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false);
+
+ proxy.post(serviceExecutionStateAPI + Constants.QUESTION + encoded, null, null, this);
+ } else {
+ final MultiMap<String> dataMap = new MultiMap<String>();
+ dataMap.add(SERVICE_TYPE, service);
+ proxy.get(legacyServiceExecutionStateAPI, CONTENT_TYPE_URL_ENCODED,
+ UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), this);
+ }
}, () -> {
logger.warn("No Service executed to get state");
});
if (result != null) {
try {
ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class);
- if (esc != null && esc.executionStatus != null) {
- String status = esc.executionStatus.status;
- synchronized (this) {
- handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), status);
- if (ExecutionState.EXECUTED.name().equals(status)) {
- // refresh loop ends - update of status handled in the normal refreshInterval. Earlier
- // update doesn't show better results!
- reset();
- return;
+ if (esc != null) {
+ if (esc.executionStatus != null) {
+ // handling of BMW ConnectedDrive updates
+ String status = esc.executionStatus.status;
+ if (status != null) {
+ synchronized (this) {
+ handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), status);
+ if (ExecutionState.EXECUTED.name().equals(status)) {
+ // refresh loop ends - update of status handled in the normal refreshInterval.
+ // Earlier
+ // update doesn't show better results!
+ reset();
+ return;
+ }
+ }
+ }
+ } else if (esc.eventId != null) {
+ // store event id for further MyBMW updates
+ executingEventId = Optional.of(esc.eventId);
+ } else if (esc.eventStatus != null) {
+ // update status for MyBMW API
+ synchronized (this) {
+ handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), esc.eventStatus);
+ if (ExecutionState.EXECUTED.name().equals(esc.eventStatus)) {
+ // refresh loop ends - update of status handled in the normal refreshInterval.
+ // Earlier
+ // update doesn't show better results!
+ reset();
+ return;
+ }
}
}
}
private void reset() {
serviceExecuting = Optional.empty();
+ executingEventId = Optional.empty();
counter = 0;
}
});
}
}
+
+ public void setMyBmwApiUsage(boolean b) {
+ myBmwApiUsage = b;
+ }
}
private String token = Constants.EMPTY;
private String tokenType = Constants.EMPTY;
private long expiration = 0;
+ private boolean myBmwApiUsage = false;
+
+ public boolean isMyBmwApiUsage() {
+ return myBmwApiUsage;
+ }
+
+ public void setMyBmwApiUsage(boolean myBmwAppUsage) {
+ this.myBmwApiUsage = myBmwAppUsage;
+ }
public String getBearerToken() {
return new StringBuilder(tokenType).append(Constants.SPACE).append(token).toString();
this.expiration = System.currentTimeMillis() / 1000 + expiration;
}
- /**
- * @return true if Token expires in less than 1 second
- */
- public boolean isExpired() {
- return (expiration - System.currentTimeMillis() / 1000) < 1;
- }
-
public void setType(String type) {
tokenType = type;
}
public boolean isValid() {
- return (!token.equals(Constants.EMPTY) && !tokenType.equals(Constants.EMPTY) && expiration > 0);
+ return (!token.equals(Constants.EMPTY) && !tokenType.equals(Constants.EMPTY)
+ && (this.expiration - System.currentTimeMillis() / 1000) > 1);
+ }
+
+ @Override
+ public String toString() {
+ return tokenType + token;
}
}
}
protected void updateCheckControls(List<CCMMessage> ccl) {
- if (ccl.size() == 0) {
+ if (ccl.isEmpty()) {
// No Check Control available - show not active
CCMMessage ccm = new CCMMessage();
ccm.ccmDescriptionLong = Constants.NO_ENTRIES;
protected void updateServices(List<CBSMessage> sl) {
// if list is empty add "undefined" element
- if (sl.size() == 0) {
+ if (sl.isEmpty()) {
CBSMessage cbsm = new CBSMessage();
cbsm.cbsType = Constants.NO_ENTRIES;
cbsm.cbsDescription = Constants.NO_ENTRIES;
protected void updateDestinations(List<Destination> dl) {
// if list is empty add "undefined" element
- if (dl.size() == 0) {
+ if (dl.isEmpty()) {
Destination dest = new Destination();
dest.city = Constants.NO_ENTRIES;
dest.lat = -1;
// last update Time
updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE,
DateTimeType.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus))));
+ // last update reason
+ updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE_REASON,
+ StringType.valueOf(Converter.toTitleCase(vStatus.updateReason)));
Doors doorState = null;
try {
// Range values
// based on unit of length decide if range shall be reported in km or miles
- float totalRange = 0;
+ double totalRange = 0;
+ double maxTotalRange = 0;
if (isElectric) {
totalRange += vStatus.remainingRangeElectric;
QuantityType<Length> qtElectricRange = QuantityType.valueOf(vStatus.remainingRangeElectric,
imperial ? Converter.getMiles(qtElectricRange) : qtElectricRange);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC,
imperial ? Converter.getMiles(qtElectricRadius) : qtElectricRadius);
+
+ maxTotalRange += vStatus.maxRangeElectric;
+ QuantityType<Length> qtMaxElectricRange = QuantityType.valueOf(vStatus.maxRangeElectric,
+ Constants.KILOMETRE_UNIT);
+ QuantityType<Length> qtMaxElectricRadius = QuantityType
+ .valueOf(Converter.guessRangeRadius(vStatus.maxRangeElectric), Constants.KILOMETRE_UNIT);
+
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC_MAX,
+ imperial ? Converter.getMiles(qtMaxElectricRange) : qtMaxElectricRange);
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC_MAX,
+ imperial ? Converter.getMiles(qtMaxElectricRadius) : qtMaxElectricRadius);
}
if (hasFuel) {
totalRange += vStatus.remainingRangeFuel;
+ maxTotalRange += vStatus.remainingRangeFuel;
QuantityType<Length> qtFuelRange = QuantityType.valueOf(vStatus.remainingRangeFuel,
Constants.KILOMETRE_UNIT);
QuantityType<Length> qtFuelRadius = QuantityType
QuantityType<Length> qtHybridRange = QuantityType.valueOf(totalRange, Constants.KILOMETRE_UNIT);
QuantityType<Length> qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(totalRange),
Constants.KILOMETRE_UNIT);
+ QuantityType<Length> qtMaxHybridRange = QuantityType.valueOf(maxTotalRange, Constants.KILOMETRE_UNIT);
+ QuantityType<Length> qtMaxHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(maxTotalRange),
+ Constants.KILOMETRE_UNIT);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID,
imperial ? Converter.getMiles(qtHybridRange) : qtHybridRange);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID,
imperial ? Converter.getMiles(qtHybridRadius) : qtHybridRadius);
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID_MAX,
+ imperial ? Converter.getMiles(qtMaxHybridRange) : qtMaxHybridRange);
+ updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID_MAX,
+ imperial ? Converter.getMiles(qtMaxHybridRadius) : qtMaxHybridRadius);
}
updateChannel(CHANNEL_GROUP_RANGE, MILEAGE,
// Charge Values
if (isElectric) {
+ if (vStatus.connectionStatus != null) {
+ updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION,
+ StringType.valueOf(Converter.toTitleCase(vStatus.connectionStatus)));
+ } else {
+ updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION, UnDefType.NULL);
+ }
if (vStatus.chargingStatus != null) {
if (Constants.INVALID.equals(vStatus.chargingStatus)) {
updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
import org.openhab.binding.bmwconnecteddrive.internal.dto.DestinationContainer;
import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
+import org.openhab.binding.bmwconnecteddrive.internal.dto.navigation.NavigationContainer;
import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTrips;
import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTripsContainer;
import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
private ImageProperties imageProperties = new ImageProperties();
VehicleStatusCallback vehicleStatusCallback = new VehicleStatusCallback();
StringResponseCallback oldVehicleStatusCallback = new LegacyVehicleStatusCallback();
+ StringResponseCallback navigationCallback = new NavigationStatusCallback();
StringResponseCallback lastTripCallback = new LastTripCallback();
StringResponseCallback allTripsCallback = new AllTripsCallback();
StringResponseCallback chargeProfileCallback = new ChargeProfilesCallback();
prox.requestVehcileStatus(config, vehicleStatusCallback);
}
addCallback(vehicleStatusCallback);
+ prox.requestLNavigation(config, navigationCallback);
+ addCallback(navigationCallback);
if (isSupported(Constants.STATISTICS)) {
prox.requestLastTrip(config, lastTripCallback);
prox.requestAllTrips(config, allTripsCallback);
@Override
public void onError(NetworkError error) {
- logger.debug("{}", error.toString());
vehicleStatusCallback.onError(error);
}
}
+ public class NavigationStatusCallback implements StringResponseCallback {
+ @Override
+ public void onResponse(@Nullable String content) {
+ if (content != null) {
+ try {
+ NavigationContainer nav = Converter.getGson().fromJson(content, NavigationContainer.class);
+ updateChannel(CHANNEL_GROUP_RANGE, SOC_MAX, QuantityType.valueOf(nav.socmax, Units.KILOWATT_HOUR));
+ } catch (JsonSyntaxException jse) {
+ logger.debug("{}", jse.getMessage());
+ }
+ }
+ removeCallback(this);
+ }
+
+ @Override
+ public void onError(NetworkError error) {
+ logger.debug("{}", error.toString());
+ removeCallback(this);
+ }
+ }
+
private void handleChargeProfileCommand(ChannelUID channelUID, Command command) {
if (chargeProfileEdit.isEmpty()) {
chargeProfileEdit = getChargeProfileWrapper();
public static final String REGION_ROW = "ROW";
// https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/country_selector.py
- public static final String AUTH_SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us/gcdm";
- public static final String AUTH_SERVER_CHINA = "b2vapi.bmwgroup.cn/gcdm";
- public static final String AUTH_SERVER_ROW = "b2vapi.bmwgroup.com/gcdm";
- public static final Map<String, String> AUTH_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, AUTH_SERVER_NORTH_AMERICA,
- REGION_CHINA, AUTH_SERVER_CHINA, REGION_ROW, AUTH_SERVER_ROW);
+ public static final String LEGACY_AUTH_SERVER_NORTH_AMERICA = "login.bmwusa.com/gcdm";
+ public static final String LEGACY_AUTH_SERVER_CHINA = "customer.bmwgroup.cn/gcdm";
+ public static final String LEGACY_AUTH_SERVER_ROW = "customer.bmwgroup.com/gcdm";
+ public static final Map<String, String> LEGACY_AUTH_SERVER_MAP = Map.of(REGION_NORTH_AMERICA,
+ LEGACY_AUTH_SERVER_NORTH_AMERICA, REGION_CHINA, LEGACY_AUTH_SERVER_CHINA, REGION_ROW,
+ LEGACY_AUTH_SERVER_ROW);
+
+ public static final String OAUTH_ENDPOINT = "/oauth/authenticate";
+ public static final String TOKEN_ENDPOINT = "/oauth/token";
- public static final String OAUTH_ENDPOINT = "/oauth/token";
+ public static final String API_SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us";
+ public static final String API_SERVER_CHINA = "b2vapi.bmwgroup.cn:8592";
+ public static final String API_SERVER_ROW = "b2vapi.bmwgroup.com";
- public static final String SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us";
- public static final String SERVER_CHINA = "b2vapi.bmwgroup.cn:8592";
- public static final String SERVER_ROW = "b2vapi.bmwgroup.com";
- public static final Map<String, String> SERVER_MAP = Map.of(REGION_NORTH_AMERICA, SERVER_NORTH_AMERICA,
- REGION_CHINA, SERVER_CHINA, REGION_ROW, SERVER_ROW);
+ public static final String EADRAX_SERVER_NORTH_AMERICA = "cocoapi.bmwgroup.us";
+ public static final String EADRAX_SERVER_ROW = "cocoapi.bmwgroup.com";
+ public static final String EADRAX_SERVER_CHINA = Constants.EMPTY;
+ public static final Map<String, String> EADRAX_SERVER_MAP = Map.of(REGION_NORTH_AMERICA,
+ EADRAX_SERVER_NORTH_AMERICA, REGION_CHINA, EADRAX_SERVER_CHINA, REGION_ROW, EADRAX_SERVER_ROW);
+
+ public static final Map<String, String> API_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, API_SERVER_NORTH_AMERICA,
+ REGION_CHINA, API_SERVER_CHINA, REGION_ROW, API_SERVER_ROW);
// see https://github.com/bimmerconnected/bimmer_connected/pull/252/files
- public static final Map<String, String> AUTHORIZATION_VALUE_MAP = Map.of(REGION_NORTH_AMERICA,
+ public static final Map<String, String> LEGACY_AUTHORIZATION_VALUE_MAP = Map.of(REGION_NORTH_AMERICA,
"Basic ZDc2NmI1MzctYTY1NC00Y2JkLWEzZGMtMGNhNTY3MmQ3ZjhkOjE1ZjY5N2Y2LWE1ZDUtNGNhZC05OWQ5LTNhMTViYzdmMzk3Mw==",
REGION_CHINA,
"Basic blF2NkNxdHhKdVhXUDc0eGYzQ0p3VUVQOjF6REh4NnVuNGNEanliTEVOTjNreWZ1bVgya0VZaWdXUGNRcGR2RFJwSUJrN3JPSg==",
REGION_ROW,
"Basic ZDc2NmI1MzctYTY1NC00Y2JkLWEzZGMtMGNhNTY3MmQ3ZjhkOjE1ZjY5N2Y2LWE1ZDUtNGNhZC05OWQ5LTNhMTViYzdmMzk3Mw==");
- public static final String CREDENTIAL_VALUES = "nQv6CqtxJuXWP74xf3CJwUEP:1zDHx6un4cDjybLENN3kyfumX2kEYigWPcQpdvDRpIBk7rOJ";
- public static final String REDIRECT_URI_VALUE = "https://www.bmw-connecteddrive.com/app/static/external-dispatch.html";
- public static final String SCOPE_VALUES = "authenticate_user vehicle_data remote_services";
-
public static final String LEGACY_CREDENTIAL_VALUES = "nQv6CqtxJuXWP74xf3CJwUEP:1zDHx6un4cDjybLENN3kyfumX2kEYigWPcQpdvDRpIBk7rOJ";
- public static final String REFERER_URL = "https://www.bmw-connecteddrive.de/app/index.html";
+ public static final String LEGACY_REDIRECT_URI_VALUE = "https://www.bmw-connecteddrive.com/app/static/external-dispatch.html";
+ public static final String LEGACY_SCOPE_VALUES = "authenticate_user vehicle_data remote_services";
+ public static final String LEGACY_CLIENT_ID = "dbf0a542-ebd1-4ff0-a9a7-55172fbfce35";
+
+ public static final String LEGACY_REFERER_URL = "https://www.bmw-connecteddrive.de/app/index.html";
+
+ public static final String AUTH_SERVER_NORTH_AMERICA = "login.bmwusa.com/gcdm";
+ public static final String AUTH_SERVER_CHINA = "customer.bmwgroup.cn/gcdm";
+ public static final String AUTH_SERVER_ROW = "customer.bmwgroup.com/gcdm";
+ public static final Map<String, String> AUTH_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, AUTH_SERVER_NORTH_AMERICA,
+ REGION_CHINA, AUTH_SERVER_CHINA, REGION_ROW, AUTH_SERVER_ROW);
+
+ public static final Map<String, String> AUTHORIZATION_VALUE_MAP = Map.of(REGION_NORTH_AMERICA,
+ "Basic NTQzOTRhNGItYjZjMS00NWZlLWI3YjItOGZkM2FhOTI1M2FhOmQ5MmYzMWMwLWY1NzktNDRmNS1hNzdkLTk2NmY4ZjAwZTM1MQ==",
+ REGION_CHINA,
+ "Basic blF2NkNxdHhKdVhXUDc0eGYzQ0p3VUVQOjF6REh4NnVuNGNEanliTEVOTjNreWZ1bVgya0VZaWdXUGNRcGR2RFJwSUJrN3JPSg==",
+ REGION_ROW,
+ "Basic MzFjMzU3YTAtN2ExZC00NTkwLWFhOTktMzNiOTcyNDRkMDQ4OmMwZTMzOTNkLTcwYTItNGY2Zi05ZDNjLTg1MzBhZjY0ZDU1Mg==");
+
+ public static final Map<String, String> CODE_VERIFIER = Map.of(REGION_NORTH_AMERICA,
+ "BKDarcVUpgymBDCgHDH0PwwMfzycDxu1joeklioOhwXA", REGION_CHINA, Constants.EMPTY, REGION_ROW,
+ "7PsmfPS5MpaNt0jEcPpi-B7M7u0gs1Nzw6ex0Y9pa-0");
+
+ public static final Map<String, String> CLIENT_ID = Map.of(REGION_NORTH_AMERICA,
+ "54394a4b-b6c1-45fe-b7b2-8fd3aa9253aa", REGION_CHINA, Constants.EMPTY, REGION_ROW,
+ "31c357a0-7a1d-4590-aa99-33b97244d048");
+
+ public static final Map<String, String> STATE = Map.of(REGION_NORTH_AMERICA, "rgastJbZsMtup49-Lp0FMQ", REGION_CHINA,
+ Constants.EMPTY, REGION_ROW, "cEG9eLAIi6Nv-aaCAniziE_B6FPoobva3qr5gukilYw");
+
+ public static final String REDIRECT_URI_VALUE = "com.bmw.connected://oauth";
+ public static final String SCOPE_VALUES = "openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user";
}
vs.remainingRangeFuelMls = attributesMap.beRemainingRangeFuelMile;
vs.remainingFuel = attributesMap.remainingFuel;
vs.chargingLevelHv = attributesMap.chargingLevelHv;
+ vs.maxRangeElectric = attributesMap.beMaxRangeElectric;
+ vs.maxRangeElectricMls = attributesMap.beMaxRangeElectricMile;
vs.chargingStatus = attributesMap.chargingHVStatus;
+ vs.connectionStatus = attributesMap.connectorStatus;
vs.lastChargingEndReason = attributesMap.lastChargingEndReason;
vs.updateTime = attributesMap.updateTimeConverted;
+ vs.updateReason = attributesMap.lastUpdateReason;
Position p = new Position();
p.lat = attributesMap.gpsLat;
public static final String AUTH_HTTP_CLIENT_NAME = "AuthHttpClient";
public static final String CONTENT_TYPE_URL_ENCODED = "application/x-www-form-urlencoded";
- public static final String CONTENT_TYPE_JSON = "application/json";
+ public static final String CONTENT_TYPE_JSON_ENCODED = "application/json";
public static final String KEEP_ALIVE = "Keep-Alive";
public static final String CLIENT_ID = "client_id";
public static final String RESPONSE_TYPE = "response_type";
public static final String TOKEN = "token";
+ public static final String CODE = "code";
public static final String REDIRECT_URI = "redirect_uri";
+ public static final String AUTHORIZATION = "authorization";
+ public static final String GRANT_TYPE = "grant_type";
public static final String SCOPE = "scope";
public static final String CREDENTIALS = "Credentials";
public static final String USERNAME = "username";
</options>
<default>ROW</default>
</parameter>
+ <parameter name="preferMyBmw" type="boolean" required="false">
+ <label>Prefer MyBMW API</label>
+ <description>Prefer *MyBMW* API instead of *BMW Connected Drive*</description>
+ <advanced>true</advanced>
+ <default>false</default>
+ </parameter>
</config-description>
</config-description:config-descriptions>
# bridge types
thing-type.bmwconnecteddrive.account.label = BMW ConnectedDrive Benutzerkonto
thing-type.bmwconnecteddrive.account.description = Zugriff auf das BMW ConnectedDrive Portal für einen Benutzer
-thing-type.config.bmwconnecteddrive.account.userName = Benutzername für das ConnectedDrive Portal
-thing-type.config.bmwconnecteddrive.account.password = Passwort für das ConnectedDrive Portal
-thing-type.config.bmwconnecteddrive.account.region = Auswahl Ihrer Region zur Verbindung mit dem korrekten BMW Server
+thing-type.config.bmwconnecteddrive.account.userName.label = Benutzername
+thing-type.config.bmwconnecteddrive.account.userName.description = Benutzername für das ConnectedDrive Portal
+thing-type.config.bmwconnecteddrive.account.password.label = Passwort
+thing-type.config.bmwconnecteddrive.account.password.description = Passwort für das ConnectedDrive Portal
+thing-type.config.bmwconnecteddrive.account.region.label = Region
+thing-type.config.bmwconnecteddrive.account.region.description = Auswahl Ihrer Region zur Verbindung mit dem korrekten BMW Server
thing-type.config.bmwconnecteddrive.account.region.option.NORTH_AMERICA = Nordamerika
thing-type.config.bmwconnecteddrive.account.region.option.CHINA = China
thing-type.config.bmwconnecteddrive.account.region.option.ROW = Rest der Welt
+thing-type.config.bmwconnecteddrive.account.preferMyBmw.label = Benutze MyBMW API
+thing-type.config.bmwconnecteddrive.account.preferMyBmw.description = Benutzung des MyBMW API anstelle der BMW ConnectedDrive API
# thing types
thing-type.bmwconnecteddrive.bev_rex.label = Elektrofahrzeug mit REX
channel-type.bmwconnecteddrive.next-service-mileage-channel.label = Nächster Service in Kilometern
channel-type.bmwconnecteddrive.check-control-channel.label = Warnung Aktiv
channel-type.bmwconnecteddrive.charging-status-channel.label = Ladezustand
+channel-type.bmwconnecteddrive.plug-connection-channel.label = Ladestecker
channel-type.bmwconnecteddrive.charging-remaining-channel.label = Verbleibende Ladezeit
channel-type.bmwconnecteddrive.last-update-channel.label = Letzte Aktualisierung
+channel-type.bmwconnecteddrive.last-update-reason-channel.label = Grund der letzten Aktualisierung
channel-type.bmwconnecteddrive.driver-front-channel.label = Fahrertür
channel-type.bmwconnecteddrive.driver-rear-channel.label = Fahrertür Hinten
channel-type.bmwconnecteddrive.mileage-channel.label = Tachostand
channel-type.bmwconnecteddrive.range-hybrid-channel.label = Hybride Reichweite
+channel-type.bmwconnecteddrive.range-hybrid-max-channel.label = Hybride Reichweite bei voller Ladung
channel-type.bmwconnecteddrive.range-electric-channel.label = Elektrische Reichweite
+channel-type.bmwconnecteddrive.range-electric-max-channel.label = Elektrische Reichweite bei voller Ladung
channel-type.bmwconnecteddrive.soc-channel.label = Batterie Ladestand
+channel-type.bmwconnecteddrive.soc-max-channel.label = Maximale Batteriekapazität
channel-type.bmwconnecteddrive.range-fuel-channel.label = Verbrenner Reichweite
channel-type.bmwconnecteddrive.remaining-fuel-channel.label = Tankstand
channel-type.bmwconnecteddrive.range-radius-electric-channel.label = Elektrischer Reichweiten-Radius
+channel-type.bmwconnecteddrive.range-radius-electric-max-channel.label = Elektrischer Reichweiten-Radius bei voller Ladung
channel-type.bmwconnecteddrive.range-radius-fuel-channel.label = Verbrenner Reichweiten-Radius
channel-type.bmwconnecteddrive.range-radius-hybrid-channel.label = Hybrider Reichweiten-Radius
+channel-type.bmwconnecteddrive.range-radius-hybrid-max-channel.label = Hybrider Reichweiten-Radius bei voller Ladung
channel-type.bmwconnecteddrive.service-name-channel.label = Service
channel-type.bmwconnecteddrive.service-details-channel.label = Service Details
<channels>
<channel id="mileage" typeId="mileage-channel"/>
<channel id="electric" typeId="range-electric-channel"/>
- <channel id="soc" typeId="soc-channel"/>
<channel id="radius-electric" typeId="range-radius-electric-channel"/>
+ <channel id="electric-max" typeId="range-electric-max-channel"/>
+ <channel id="radius-electric-max" typeId="range-radius-electric-max-channel"/>
+ <channel id="soc" typeId="soc-channel"/>
+ <channel id="soc-max" typeId="soc-max-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>
<channel id="service-date" typeId="next-service-date-channel"/>
<channel id="service-mileage" typeId="next-service-mileage-channel"/>
<channel id="check-control" typeId="check-control-channel"/>
+ <channel id="plug-connection" typeId="plug-connection-channel"/>
<channel id="charge" typeId="charging-status-channel"/>
<channel id="remaining" typeId="charging-remaining-channel"/>
<channel id="last-update" typeId="last-update-channel"/>
+ <channel id="last-update-reason" typeId="last-update-reason-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>
<channels>
<channel id="mileage" typeId="mileage-channel"/>
<channel id="hybrid" typeId="range-hybrid-channel"/>
+ <channel id="hybrid-max" typeId="range-hybrid-max-channel"/>
<channel id="electric" typeId="range-electric-channel"/>
- <channel id="soc" typeId="soc-channel"/>
+ <channel id="radius-electric" typeId="range-radius-electric-channel"/>
+ <channel id="electric-max" typeId="range-electric-max-channel"/>
+ <channel id="radius-electric-max" typeId="range-radius-electric-max-channel"/>
<channel id="fuel" typeId="range-fuel-channel"/>
<channel id="remaining-fuel" typeId="remaining-fuel-channel"/>
- <channel id="radius-electric" typeId="range-radius-electric-channel"/>
<channel id="radius-hybrid" typeId="range-radius-hybrid-channel"/>
+ <channel id="radius-hybrid-max" typeId="range-radius-hybrid-max-channel"/>
+ <channel id="soc" typeId="soc-channel"/>
+ <channel id="soc-max" typeId="soc-max-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>
<label>Electric Range</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
+ <channel-type id="range-electric-max-channel">
+ <item-type>Number:Length</item-type>
+ <label>Electric Range if Fully Charged</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
<channel-type id="range-fuel-channel">
<item-type>Number:Length</item-type>
<label>Fuel Range</label>
<label>Hybrid Range</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
+ <channel-type id="range-hybrid-max-channel">
+ <item-type>Number:Length</item-type>
+ <label>Hybrid Range if Fully Charged</label>
+ <state pattern="%d %unit%" readOnly="true"/>
+ </channel-type>
<channel-type id="soc-channel">
<item-type>Number:Dimensionless</item-type>
<label>Battery Charge Level</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
+ <channel-type id="soc-max-channel">
+ <item-type>Number:Power</item-type>
+ <label>Max Battery Capacity</label>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
<channel-type id="remaining-fuel-channel">
<item-type>Number:Volume</item-type>
<label>Remaining Fuel</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="range-radius-electric-channel">
+ <item-type>Number:Length</item-type>
+ <label>Electric Range Radius if Fully Charged</label>
+ <state pattern="%.0f %unit%" readOnly="true"/>
+ </channel-type>
+ <channel-type id="range-radius-electric-max-channel">
<item-type>Number:Length</item-type>
<label>Electric Range Radius</label>
<state pattern="%.0f %unit%" readOnly="true"/>
<label>Hybrid Range Radius</label>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
+ <channel-type id="range-radius-hybrid-max-channel">
+ <item-type>Number:Length</item-type>
+ <label>Hybrid Range Radius if Fully Charged</label>
+ <state pattern="%.0f %unit%" readOnly="true"/>
+ </channel-type>
</thing:thing-descriptions>
<label>Charging Status</label>
<state readOnly="true"/>
</channel-type>
+ <channel-type id="plug-connection-channel">
+ <item-type>String</item-type>
+ <label>Plug Connection Status</label>
+ <state readOnly="true"/>
+ </channel-type>
<channel-type id="charging-remaining-channel">
<item-type>Number:Time</item-type>
<label>Remaining Charging Time</label>
<label>Last Status Timestamp</label>
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
</channel-type>
+ <channel-type id="last-update-reason-channel">
+ <item-type>String</item-type>
+ <label>Last Status Timestamp Reason</label>
+ </channel-type>
</thing:thing-descriptions>
<channel id="service-mileage" typeId="next-service-mileage-channel"/>
<channel id="check-control" typeId="check-control-channel"/>
<channel id="last-update" typeId="last-update-channel"/>
+ <channel id="last-update-reason" typeId="last-update-reason-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>
ALLOWED_KM_ROUND_DEVIATION, "Mileage");
}
break;
+ case RANGE_ELECTRIC_MAX:
+ assertTrue(isElectric, "Is Eelctric");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ if (imperial) {
+ assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
+ assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.maxRangeElectricMls),
+ ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
+ } else {
+ assertEquals(KILOMETRE, qt.getUnit(), "KM");
+ assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.maxRangeElectric),
+ ALLOWED_KM_ROUND_DEVIATION, "Mileage");
+ }
+ break;
case RANGE_FUEL:
assertTrue(hasFuel, "Has Fuel");
if (!(state instanceof UnDefType)) {
ALLOWED_KM_ROUND_DEVIATION, "Mileage");
}
break;
+ case RANGE_HYBRID_MAX:
+ assertTrue(isHybrid, "Is Hybrid");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ if (imperial) {
+ assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
+ assertEquals(Converter.round(qt.floatValue()),
+ Converter.round(vStatus.maxRangeElectricMls + vStatus.remainingRangeFuelMls),
+ ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
+ } else {
+ assertEquals(KILOMETRE, qt.getUnit(), "KM");
+ assertEquals(Converter.round(qt.floatValue()),
+ Converter.round(vStatus.maxRangeElectric + vStatus.remainingRangeFuel),
+ ALLOWED_KM_ROUND_DEVIATION, "Mileage");
+ }
+ break;
case REMAINING_FUEL:
assertTrue(hasFuel, "Has Fuel");
assertTrue(state instanceof QuantityType);
assertEquals(Converter.round(vStatus.chargingLevelHv), Converter.round(qt.floatValue()), 0.01,
"Charge Level");
break;
+ case SOC_MAX:
+ assertTrue(isElectric, "Is Eelctric");
+ assertTrue(state instanceof QuantityType);
+ qt = ((QuantityType) state);
+ assertEquals(Units.KILOWATT_HOUR, qt.getUnit(), "kw/h");
+ assertEquals(Converter.round(vStatus.chargingLevelHv), Converter.round(qt.floatValue()), 0.01,
+ "SOC Max");
+ break;
case LOCK:
assertTrue(state instanceof StringType);
st = (StringType) state;
assertEquals(Units.MINUTE, qtt.getUnit(), "Minutes");
}
break;
+ case PLUG_CONNECTION:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.connectionStatus));
+ assertEquals(wanted.toString(), st.toString(), "Plug Connection State");
+ break;
case LAST_UPDATE:
assertTrue(state instanceof DateTimeType);
dtt = (DateTimeType) state;
.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus)));
assertEquals(expected.toString(), dtt.toString(), "Last Update");
break;
+ case LAST_UPDATE_REASON:
+ assertTrue(state instanceof StringType);
+ st = (StringType) state;
+ wanted = StringType.valueOf(Converter.toTitleCase(vStatus.updateReason));
+ assertEquals(wanted.toString(), st.toString(), "Last Update");
+ break;
case GPS:
assertTrue(state instanceof PointType);
pt = (PointType) state;
"Range Radius Electric km");
}
break;
+ case RANGE_RADIUS_ELECTRIC_MAX:
+ assertTrue(state instanceof QuantityType);
+ assertTrue(isElectric);
+ qt = (QuantityType) state;
+ if (imperial) {
+ assertEquals(Converter.guessRangeRadius(vStatus.maxRangeElectricMls), qt.floatValue(), 1,
+ "Range Radius Electric mi");
+ } else {
+ assertEquals(Converter.guessRangeRadius(vStatus.maxRangeElectric), qt.floatValue(), 0.1,
+ "Range Radius Electric km");
+ }
+ break;
case RANGE_RADIUS_FUEL:
assertTrue(state instanceof QuantityType);
assertTrue(hasFuel);
qt.floatValue(), ALLOWED_KM_ROUND_DEVIATION, "Range Radius Hybrid km");
}
break;
+ case RANGE_RADIUS_HYBRID_MAX:
+ assertTrue(state instanceof QuantityType);
+ assertTrue(isHybrid);
+ qt = (QuantityType) state;
+ if (imperial) {
+ assertEquals(
+ Converter.guessRangeRadius(vStatus.maxRangeElectricMls + vStatus.remainingRangeFuelMls),
+ qt.floatValue(), ALLOWED_MILE_CONVERSION_DEVIATION, "Range Radius Hybrid Max mi");
+ } else {
+ assertEquals(Converter.guessRangeRadius(vStatus.maxRangeElectric + vStatus.remainingRangeFuel),
+ qt.floatValue(), ALLOWED_KM_ROUND_DEVIATION, "Range Radius Hybrid Max km");
+ }
+ break;
case DOOR_DRIVER_FRONT:
assertTrue(state instanceof StringType);
st = (StringType) state;
+++ /dev/null
-/**
- * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.*;
-
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jetty.client.HttpClient;
-import org.junit.jupiter.api.Test;
-import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration;
-import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants;
-import org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants;
-import org.openhab.core.io.net.http.HttpClientFactory;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link AuthTest} is responsible for handling commands, which are
- * sent to one of the channels.
- *
- * @author Bernd Weymann - Initial contribution
- */
-@NonNullByDefault
-public class AuthTest {
- private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
-
- @Test
- public void testAuthServerMap() {
- Map<String, String> authServers = BimmerConstants.AUTH_SERVER_MAP;
- assertEquals(3, authServers.size(), "Number of Servers");
- Map<String, String> api = BimmerConstants.SERVER_MAP;
- assertEquals(3, api.size(), "Number of Servers");
- }
-
- @Test
- public void testTokenDecoding() {
- String headerValue = "https://www.bmw-connecteddrive.com/app/static/external-dispatch.html#access_token=SfXKgkEXeeFJkVqdD4XMmfUU224MRuyh&token_type=Bearer&expires_in=7199";
- HttpClientFactory hcf = mock(HttpClientFactory.class);
- when(hcf.getCommonHttpClient()).thenReturn(mock(HttpClient.class));
- when(hcf.createHttpClient(HTTPConstants.AUTH_HTTP_CLIENT_NAME)).thenReturn(mock(HttpClient.class));
- ConnectedDriveConfiguration config = new ConnectedDriveConfiguration();
- config.region = BimmerConstants.REGION_ROW;
- ConnectedDriveProxy dcp = new ConnectedDriveProxy(hcf, config);
- dcp.tokenFromUrl(headerValue);
- Token t = dcp.getToken();
- assertEquals("Bearer SfXKgkEXeeFJkVqdD4XMmfUU224MRuyh", t.getBearerToken(), "Token");
- }
-
- public void testRealTokenUpdate() {
- ConnectedDriveConfiguration config = new ConnectedDriveConfiguration();
- config.region = BimmerConstants.REGION_ROW;
- config.userName = "bla";
- config.password = "blub";
- HttpClientFactory hcf = mock(HttpClientFactory.class);
- when(hcf.getCommonHttpClient()).thenReturn(mock(HttpClient.class));
- when(hcf.createHttpClient(HTTPConstants.AUTH_HTTP_CLIENT_NAME)).thenReturn(mock(HttpClient.class));
- ConnectedDriveProxy dcp = new ConnectedDriveProxy(hcf, config);
- Token t = dcp.getToken();
- logger.info("Token {}", t.getBearerToken());
- logger.info("Expires {}", t.isExpired());
- }
-}
public class VehicleTests {
private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
- private static final int STATUS_ELECTRIC = 9;
- private static final int STATUS_CONV = 7;
- private static final int RANGE_HYBRID = 9;
+ private static final int STATUS_ELECTRIC = 12;
+ private static final int STATUS_CONV = 8;
+ private static final int RANGE_HYBRID = 12;
private static final int RANGE_CONV = 4;
- private static final int RANGE_ELECTRIC = 4;
+ private static final int RANGE_ELECTRIC = 5;
private static final int DOORS = 12;
private static final int CHECK_EMPTY = 3;
private static final int CHECK_AVAILABLE = 3;
--- /dev/null
+{
+ "redirect_to": "redirect_uri=com.bmw.connected://oauth?client_id=31c357a0-7a1d-4590-aa99-33b97244d048&response_type=code&scope=openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user&state=cEG9eLAIi6Nv-aaCAniziE_B6FPoobva3qr5gukilYw&authorization=XaTJvSCZePkXsQ3zLMbPyG2XpRo.*AAJTSQACMDIAAlNLABw2TmhkS25qQTQzc1lqUHdOYzNjanFZK1pkU2M9AAR0eXBlAANDVFMAAlMxAAIwMQ..*"
+}
\ No newline at end of file
--- /dev/null
+{
+ "token_type": "Bearer",
+ "access_token": "Iw-U6XS5zSeArLauaI-Ec6WFs88",
+ "refresh_token": "V3OAHd_foseD2nzTFV5_SsaMzGU",
+ "scope": "smacc vehicle_data perseus dlm svds openid profile vsapi remote_services authenticate_user cesim offline_access email fupo",
+ "expires_in": 3599,
+ "id_token": "eyJ0eXAiOiJKV1QiLCJraWQiOiIydGFUMUlOdTJFVE1QZFd4UWpIR3UyV3Q2T0E9IiwiYWxnIjoiUlMyNTYifQ.eyJhdF9oYXNoIjoiTGh1ZGZhT0pUOTBvYlNjYVhuN2RQUSIsInN1YiI6Im1hcmlrYS53ZXltYW5uQGdtYWlsLmNvbSIsImF1ZGl0VHJhY2tpbmdJZCI6ImJlNjcxM2M3LTY4NjgtNGU4My04NjIyLTg4ODMyNjg2MmU1OC0yODg4MDcwNDYiLCJnY2lkIjoiZDdjNTU5NjctNzQ5ZC00NjNiLTlhN2UtMTQ3ZGEwMmZiMzQ0IiwiaXNzIjoiaHR0cHM6Ly9jdXN0b21lci5ibXdncm91cC5jb20vYW0vb2F1dGgyIiwidG9rZW5OYW1lIjoiaWRfdG9rZW4iLCJhY3IiOiIwIiwiYXpwIjoiMzFjMzU3YTAtN2ExZC00NTkwLWFhOTktMzNiOTcyNDRkMDQ4IiwiYXV0aF90aW1lIjoxNjMwODYxOTE3LCJleHAiOjE2MzA4NjU1MTcsImlhdCI6MTYzMDg2MTkxNywiZW1haWwiOiJtYXJpa2Eud2V5bWFubkBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6Ik1BSUxfQUNUSVZFIiwiaG9tZV9tYXJrZXQiOiJERSIsImdpdmVuX25hbWUiOiJNYXJpa2EiLCJub25jZSI6ImxvZ2luX25vbmNlIiwiYXVkIjoiMzFjMzU3YTAtN2ExZC00NTkwLWFhOTktMzNiOTcyNDRkMDQ4IiwiY19oYXNoIjoiZVU5MjYyRTZiLUFlNzFfWHd2eWkwdyIsIm9yZy5mb3JnZXJvY2sub3BlbmlkY29ubmVjdC5vcHMiOiJwLWFnaGZMdlh1S29IcnNReTd1Z05xVEQyVEkiLCJzX2hhc2giOiJwcXpwa0pfS09mQ2htTTg4dFVLcExRIiwicmVhbG0iOiIvY3VzdG9tZXIiLCJzYWx1dGF0aW9uIjoiU0FMX01TIiwidG9rZW5UeXBlIjoiSldUVG9rZW4iLCJmYW1pbHlfbmFtZSI6IldleW1hbm4ifQ.LJxHE4BeUNh0YxhMIyF_LUa8hsAaGZ2VZot15vp_5SQWQvfGoC0KMgjuHawc-7CK01yDppR5awX2FwCsec3DemSUVvKeyjSg_of785dvCNsvcx9kvio-7nwet_6Acrv0bUlmpOtvN6GZpxE6NZi-ZkbEnw8KzrZvS8t6AgAv7dEeqPgVneZNu9XDSUM81QhS1X21FFGbyPD-9RnLt401Ft5WeKi4kN1ViCP7OkvpSOfRU3p4lv3fbsdoAoWU11Lp80TBYir8nJL-kykA076UK6qnks8zTFx1TlpPV0Nou5NgmqyLOprFaWk-9AG3gjhEYC2yLBMzQLHb8t2UYgAfUQ"
+}
\ No newline at end of file