mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 23:22:02 +01:00
This commit is contained in:
parent
437fb7f336
commit
af007f4369
@ -95,6 +95,14 @@ The region Configuration has 3 different options
|
||||
* _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
|
||||
@ -147,8 +155,10 @@ Reflects overall status of the vehicle.
|
||||
| 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
|
||||
|
||||
@ -180,6 +190,27 @@ Charging 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.
|
||||
@ -253,17 +284,20 @@ See description [Range vs Range Radius](#range-vs-range-radius) to get more info
|
||||
* 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
|
||||
|
@ -37,4 +37,9 @@ public class ConnectedDriveConfiguration {
|
||||
* BMW Connected Drive Password
|
||||
*/
|
||||
public String password = Constants.EMPTY;
|
||||
|
||||
/**
|
||||
* Prefer MyBMW API instead of BMW Connected Drive
|
||||
*/
|
||||
public boolean preferMyBmw = false;
|
||||
}
|
||||
|
@ -119,10 +119,12 @@ public class ConnectedDriveConstants {
|
||||
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";
|
||||
@ -161,13 +163,18 @@ public class ConnectedDriveConstants {
|
||||
|
||||
// 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";
|
||||
|
@ -26,4 +26,9 @@ public class AuthResponse {
|
||||
public String tokenType;
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Token " + accessToken + " type " + tokenType + " expires in " + expiresIn;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
@ -19,4 +19,7 @@ package org.openhab.binding.bmwconnecteddrive.internal.dto.remote;
|
||||
*/
|
||||
public class ExecutionStatusContainer {
|
||||
public ExecutionStatus executionStatus;
|
||||
public String eventId;
|
||||
public String creationTime;
|
||||
public String eventStatus;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import static org.openhab.binding.bmwconnecteddrive.internal.utils.Constants.ANO
|
||||
|
||||
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;
|
||||
@ -33,9 +34,11 @@ import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
|
||||
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;
|
||||
@ -73,23 +76,34 @@ public class ConnectedDriveBridgeHandler extends BaseBridgeHandler implements St
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,6 +116,7 @@ public class ConnectedDriveBridgeHandler extends BaseBridgeHandler implements St
|
||||
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;
|
||||
@ -127,6 +142,8 @@ public class ConnectedDriveBridgeHandler extends BaseBridgeHandler implements St
|
||||
});
|
||||
return Converter.getGson().toJson(container);
|
||||
}
|
||||
} else {
|
||||
logger.debug("container.vehicles is null");
|
||||
}
|
||||
}
|
||||
} catch (JsonParseException jpe) {
|
||||
@ -172,7 +189,8 @@ public class ConnectedDriveBridgeHandler extends BaseBridgeHandler implements St
|
||||
}
|
||||
});
|
||||
}
|
||||
return Converter.getGson().toJson(container);
|
||||
} else {
|
||||
troubleshootFingerprint = Optional.of(Constants.EMPTY_JSON);
|
||||
}
|
||||
} catch (JsonParseException jpe) {
|
||||
logger.debug("Fingerprint parse exception {}", jpe.getMessage());
|
||||
|
@ -14,7 +14,16 @@ package org.openhab.binding.bmwconnecteddrive.internal.handler;
|
||||
|
||||
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;
|
||||
@ -27,8 +36,6 @@ import org.eclipse.jetty.client.api.Request;
|
||||
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;
|
||||
@ -45,8 +52,6 @@ import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
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
|
||||
@ -61,10 +66,10 @@ import com.google.gson.JsonSyntaxException;
|
||||
@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;
|
||||
|
||||
/**
|
||||
@ -73,6 +78,9 @@ public class ConnectedDriveProxy {
|
||||
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";
|
||||
@ -82,25 +90,27 @@ public class ConnectedDriveProxy {
|
||||
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()) {
|
||||
@ -114,22 +124,25 @@ public class ConnectedDriveProxy {
|
||||
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({})
|
||||
@ -160,46 +173,52 @@ public class ConnectedDriveProxy {
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -208,21 +227,14 @@ public class ConnectedDriveProxy {
|
||||
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
|
||||
@ -235,77 +247,182 @@ public class ConnectedDriveProxy {
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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));
|
||||
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());
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
logger.debug("Authorization exception: {}", e.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void tokenFromUrl(String encodedUrl) {
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
token.setMyBmwApiUsage(false);
|
||||
return tokenFromUrl(con.getHeaderField(HttpHeader.LOCATION.toString()));
|
||||
} catch (IOException e) {
|
||||
logger.warn("{}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean tokenFromUrl(String encodedUrl) {
|
||||
final MultiMap<String> tokenMap = new MultiMap<String>();
|
||||
UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
|
||||
tokenMap.forEach((key, value) -> {
|
||||
@ -320,5 +437,33 @@ public class ConnectedDriveProxy {
|
||||
}
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,9 @@
|
||||
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;
|
||||
@ -21,6 +23,7 @@ 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;
|
||||
@ -45,6 +48,7 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
||||
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;
|
||||
@ -52,12 +56,16 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
||||
|
||||
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,
|
||||
@ -69,21 +77,23 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -93,30 +103,49 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -130,9 +159,19 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
||||
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");
|
||||
});
|
||||
@ -145,15 +184,36 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -183,6 +243,7 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
||||
|
||||
private void reset() {
|
||||
serviceExecuting = Optional.empty();
|
||||
executingEventId = Optional.empty();
|
||||
counter = 0;
|
||||
}
|
||||
|
||||
@ -196,4 +257,8 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void setMyBmwApiUsage(boolean b) {
|
||||
myBmwApiUsage = b;
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,15 @@ public class Token {
|
||||
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();
|
||||
@ -38,18 +47,17 @@ public class Token {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
||||
}
|
||||
|
||||
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;
|
||||
@ -169,7 +169,7 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
||||
|
||||
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;
|
||||
@ -225,7 +225,7 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
||||
|
||||
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;
|
||||
@ -417,6 +417,9 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
||||
// 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 {
|
||||
@ -442,7 +445,8 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
||||
|
||||
// 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,
|
||||
@ -454,9 +458,21 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
||||
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
|
||||
@ -470,10 +486,17 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
||||
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,
|
||||
@ -488,6 +511,12 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
||||
|
||||
// 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,
|
||||
|
@ -30,6 +30,7 @@ import org.openhab.binding.bmwconnecteddrive.internal.action.BMWConnectedDriveAc
|
||||
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;
|
||||
@ -50,8 +51,10 @@ import org.openhab.core.io.net.http.HttpUtil;
|
||||
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;
|
||||
@ -86,6 +89,7 @@ public class VehicleHandler extends VehicleChannelHandler {
|
||||
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();
|
||||
@ -275,6 +279,8 @@ public class VehicleHandler extends VehicleChannelHandler {
|
||||
prox.requestVehcileStatus(config, vehicleStatusCallback);
|
||||
}
|
||||
addCallback(vehicleStatusCallback);
|
||||
prox.requestLNavigation(config, navigationCallback);
|
||||
addCallback(navigationCallback);
|
||||
if (isSupported(Constants.STATISTICS)) {
|
||||
prox.requestLastTrip(config, lastTripCallback);
|
||||
prox.requestAllTrips(config, allTripsCallback);
|
||||
@ -677,11 +683,31 @@ public class VehicleHandler extends VehicleChannelHandler {
|
||||
|
||||
@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();
|
||||
|
@ -34,32 +34,68 @@ public class BimmerConstants {
|
||||
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/token";
|
||||
public static final String OAUTH_ENDPOINT = "/oauth/authenticate";
|
||||
public static final String TOKEN_ENDPOINT = "/oauth/token";
|
||||
|
||||
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 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 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";
|
||||
}
|
||||
|
@ -251,10 +251,14 @@ public class Converter {
|
||||
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;
|
||||
|
@ -25,12 +25,15 @@ public class HTTPConstants {
|
||||
|
||||
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";
|
||||
|
@ -24,5 +24,11 @@
|
||||
</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>
|
||||
|
@ -5,12 +5,17 @@ binding.bmwconnecteddrive.description = Zeigt die Fahrzeugdaten
|
||||
# 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
|
||||
@ -143,8 +148,10 @@ channel-type.bmwconnecteddrive.next-service-date-channel.label = N
|
||||
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
|
||||
@ -161,13 +168,18 @@ channel-type.bmwconnecteddrive.sunroof-channel.label = Schiebedach
|
||||
|
||||
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
|
||||
|
@ -9,8 +9,11 @@
|
||||
<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>
|
||||
|
@ -13,9 +13,11 @@
|
||||
<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>
|
||||
|
@ -9,12 +9,17 @@
|
||||
<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>
|
||||
|
@ -13,6 +13,11 @@
|
||||
<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>
|
||||
@ -23,17 +28,32 @@
|
||||
<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"/>
|
||||
@ -48,4 +68,9 @@
|
||||
<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>
|
||||
|
@ -38,6 +38,11 @@
|
||||
<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>
|
||||
@ -48,4 +53,8 @@
|
||||
<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>
|
||||
|
@ -14,6 +14,7 @@
|
||||
<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>
|
||||
|
@ -164,6 +164,20 @@ public class StatusWrapper {
|
||||
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)) {
|
||||
@ -196,6 +210,22 @@ public class StatusWrapper {
|
||||
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);
|
||||
@ -212,6 +242,14 @@ public class StatusWrapper {
|
||||
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;
|
||||
@ -274,6 +312,12 @@ public class StatusWrapper {
|
||||
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;
|
||||
@ -281,6 +325,12 @@ public class StatusWrapper {
|
||||
.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;
|
||||
@ -306,6 +356,18 @@ public class StatusWrapper {
|
||||
"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);
|
||||
@ -333,6 +395,19 @@ public class StatusWrapper {
|
||||
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;
|
||||
|
@ -1,75 +0,0 @@
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
@ -51,11 +51,11 @@ import org.slf4j.LoggerFactory;
|
||||
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;
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"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..*"
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"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"
|
||||
}
|
Loading…
Reference in New Issue
Block a user