mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
This commit is contained in:
parent
437fb7f336
commit
af007f4369
@ -95,6 +95,14 @@ The region Configuration has 3 different options
|
|||||||
* _CHINA_
|
* _CHINA_
|
||||||
* _ROW_ (Rest of World)
|
* _ROW_ (Rest of World)
|
||||||
|
|
||||||
|
|
||||||
|
#### Advanced Configuration
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------------|---------|--------------------------------------------------------------------|
|
||||||
|
| preferMyBmw | boolean | Prefer *MyBMW* API instead of *BMW Connected Drive* |
|
||||||
|
|
||||||
|
|
||||||
### Thing Configuration
|
### Thing Configuration
|
||||||
|
|
||||||
Same configuration is needed for all things
|
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 |
|
| Next Service Date | service-date | DateTime | Date of upcoming service |
|
||||||
| Mileage till Next Service | service-mileage | Number:Length | Mileage till upcoming service |
|
| Mileage till Next Service | service-mileage | Number:Length | Mileage till upcoming service |
|
||||||
| Check Control | check-control | String | Presence of active warning messages |
|
| 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 |
|
| 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 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
|
Overall Door Status values
|
||||||
|
|
||||||
@ -180,6 +190,27 @@ Charging Status values
|
|||||||
* _Charging Goal reached_
|
* _Charging Goal reached_
|
||||||
* _Waiting For Charging_
|
* _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
|
#### Services
|
||||||
|
|
||||||
Group for all upcoming services with description, service date and/or service mileage.
|
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
|
* Availability according to table
|
||||||
* Read-only values
|
* Read-only values
|
||||||
|
|
||||||
| Channel Label | Channel ID | Type | conv | phev | bev_rex | bev |
|
| Channel Label | Channel ID | Type | conv | phev | bev_rex | bev |
|
||||||
|-----------------------|-----------------------|----------------------|------|------|---------|-----|
|
|---------------------------|-------------------------|----------------------|------|------|---------|-----|
|
||||||
| Mileage | mileage | Number:Length | X | X | X | X |
|
| Mileage | mileage | Number:Length | X | X | X | X |
|
||||||
| Fuel Range | range-fuel | Number:Length | X | X | X | |
|
| Fuel Range | range-fuel | Number:Length | X | X | X | |
|
||||||
| Battery Range | range-electric | Number:Length | | X | X | X |
|
| Battery Range | range-electric | Number:Length | | X | X | X |
|
||||||
| Hybrid Range | range-hybrid | Number:Length | | X | X | |
|
| Max Battery Range | range-electric-max | Number:Length | | X | X | X |
|
||||||
| Battery Charge Level | soc | Number:Dimensionless | | X | X | X |
|
| Hybrid Range | range-hybrid | Number:Length | | X | X | |
|
||||||
| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | |
|
| Battery Charge Level | soc | Number:Dimensionless | | X | X | X |
|
||||||
| Fuel Range Radius | range-radius-fuel | Number:Length | X | X | X | |
|
| Max Battery Capacity | soc-max | Number:Power | | | X | X | X |
|
||||||
| Electric Range Radius | range-radius-electric | Number:Length | | X | X | X |
|
| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | |
|
||||||
| Hybrid Range Radius | range-radius-hybrid | Number:Length | | 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
|
#### Charge Profile
|
||||||
|
@ -37,4 +37,9 @@ public class ConnectedDriveConfiguration {
|
|||||||
* BMW Connected Drive Password
|
* BMW Connected Drive Password
|
||||||
*/
|
*/
|
||||||
public String password = Constants.EMPTY;
|
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_DATE = "service-date";
|
||||||
public static final String SERVICE_MILEAGE = "service-mileage";
|
public static final String SERVICE_MILEAGE = "service-mileage";
|
||||||
public static final String CHECK_CONTROL = "check-control";
|
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_STATUS = "charge";
|
||||||
public static final String CHARGE_END_REASON = "reason";
|
public static final String CHARGE_END_REASON = "reason";
|
||||||
public static final String CHARGE_REMAINING = "remaining";
|
public static final String CHARGE_REMAINING = "remaining";
|
||||||
public static final String LAST_UPDATE = "last-update";
|
public static final String LAST_UPDATE = "last-update";
|
||||||
|
public static final String LAST_UPDATE_REASON = "last-update-reason";
|
||||||
|
|
||||||
// Door Details
|
// Door Details
|
||||||
public static final String DOOR_DRIVER_FRONT = "driver-front";
|
public static final String DOOR_DRIVER_FRONT = "driver-front";
|
||||||
@ -161,13 +163,18 @@ public class ConnectedDriveConstants {
|
|||||||
|
|
||||||
// Range
|
// Range
|
||||||
public static final String RANGE_HYBRID = "hybrid";
|
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 = "electric";
|
||||||
|
public static final String RANGE_ELECTRIC_MAX = "electric-max";
|
||||||
public static final String SOC = "soc";
|
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 RANGE_FUEL = "fuel";
|
||||||
public static final String REMAINING_FUEL = "remaining-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 = "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_FUEL = "radius-fuel";
|
||||||
public static final String RANGE_RADIUS_HYBRID = "radius-hybrid";
|
public static final String RANGE_RADIUS_HYBRID = "radius-hybrid";
|
||||||
|
public static final String RANGE_RADIUS_HYBRID_MAX = "radius-hybrid-max";
|
||||||
|
|
||||||
// Last Trip
|
// Last Trip
|
||||||
public static final String DURATION = "duration";
|
public static final String DURATION = "duration";
|
||||||
|
@ -26,4 +26,9 @@ public class AuthResponse {
|
|||||||
public String tokenType;
|
public String tokenType;
|
||||||
@SerializedName("expires_in")
|
@SerializedName("expires_in")
|
||||||
public int expiresIn;
|
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 class ExecutionStatusContainer {
|
||||||
public ExecutionStatus executionStatus;
|
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.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
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.io.net.http.HttpClientFactory;
|
||||||
import org.openhab.core.thing.Bridge;
|
import org.openhab.core.thing.Bridge;
|
||||||
import org.openhab.core.thing.ChannelUID;
|
import org.openhab.core.thing.ChannelUID;
|
||||||
|
import org.openhab.core.thing.Thing;
|
||||||
import org.openhab.core.thing.ThingStatus;
|
import org.openhab.core.thing.ThingStatus;
|
||||||
import org.openhab.core.thing.ThingStatusDetail;
|
import org.openhab.core.thing.ThingStatusDetail;
|
||||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
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.thing.binding.ThingHandlerService;
|
||||||
import org.openhab.core.types.Command;
|
import org.openhab.core.types.Command;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -73,23 +76,34 @@ public class ConnectedDriveBridgeHandler extends BaseBridgeHandler implements St
|
|||||||
troubleshootFingerprint = Optional.empty();
|
troubleshootFingerprint = Optional.empty();
|
||||||
updateStatus(ThingStatus.UNKNOWN);
|
updateStatus(ThingStatus.UNKNOWN);
|
||||||
ConnectedDriveConfiguration config = getConfigAs(ConnectedDriveConfiguration.class);
|
ConnectedDriveConfiguration config = getConfigAs(ConnectedDriveConfiguration.class);
|
||||||
|
logger.debug("Prefer MyBMW API {}", config.preferMyBmw);
|
||||||
if (!checkConfiguration(config)) {
|
if (!checkConfiguration(config)) {
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
|
||||||
} else {
|
} else {
|
||||||
proxy = Optional.of(new ConnectedDriveProxy(httpClientFactory, config));
|
proxy = Optional.of(new ConnectedDriveProxy(httpClientFactory, config));
|
||||||
// give the system some time to create all predefined Vehicles
|
// give the system some time to create all predefined Vehicles
|
||||||
// check with API call if bridge is online
|
// 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) {
|
public static boolean checkConfiguration(ConnectedDriveConfiguration config) {
|
||||||
if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) {
|
if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) {
|
||||||
return false;
|
return false;
|
||||||
} else if (BimmerConstants.AUTH_SERVER_MAP.containsKey(config.region)) {
|
|
||||||
return true;
|
|
||||||
} else {
|
} 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));
|
proxy.ifPresent(prox -> prox.requestVehicles(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://www.bmw-connecteddrive.de/api/me/vehicles/v2?all=true&brand=BM
|
||||||
public String getDiscoveryFingerprint() {
|
public String getDiscoveryFingerprint() {
|
||||||
return troubleshootFingerprint.map(fingerprint -> {
|
return troubleshootFingerprint.map(fingerprint -> {
|
||||||
VehiclesContainer container = null;
|
VehiclesContainer container = null;
|
||||||
@ -127,6 +142,8 @@ public class ConnectedDriveBridgeHandler extends BaseBridgeHandler implements St
|
|||||||
});
|
});
|
||||||
return Converter.getGson().toJson(container);
|
return Converter.getGson().toJson(container);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug("container.vehicles is null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (JsonParseException jpe) {
|
} 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) {
|
} catch (JsonParseException jpe) {
|
||||||
logger.debug("Fingerprint parse exception {}", jpe.getMessage());
|
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 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.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
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.api.Result;
|
||||||
import org.eclipse.jetty.client.util.BufferingResponseListener;
|
import org.eclipse.jetty.client.util.BufferingResponseListener;
|
||||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
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.http.HttpHeader;
|
||||||
import org.eclipse.jetty.util.MultiMap;
|
import org.eclipse.jetty.util.MultiMap;
|
||||||
import org.eclipse.jetty.util.UrlEncoded;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.google.gson.JsonSyntaxException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link ConnectedDriveProxy} This class holds the important constants for the BMW Connected Drive Authorization.
|
* The {@link ConnectedDriveProxy} This class holds the important constants for the BMW Connected Drive Authorization.
|
||||||
* They
|
* They
|
||||||
@ -61,10 +66,10 @@ import com.google.gson.JsonSyntaxException;
|
|||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class ConnectedDriveProxy {
|
public class ConnectedDriveProxy {
|
||||||
private final Logger logger = LoggerFactory.getLogger(ConnectedDriveProxy.class);
|
private final Logger logger = LoggerFactory.getLogger(ConnectedDriveProxy.class);
|
||||||
|
private Optional<RemoteServiceHandler> remoteServiceHandler = Optional.empty();
|
||||||
private final Token token = new Token();
|
private final Token token = new Token();
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
private final HttpClient authHttpClient;
|
private final HttpClient authHttpClient;
|
||||||
private final String legacyAuthUri;
|
|
||||||
private final ConnectedDriveConfiguration configuration;
|
private final ConnectedDriveConfiguration configuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,6 +78,9 @@ public class ConnectedDriveProxy {
|
|||||||
final String baseUrl;
|
final String baseUrl;
|
||||||
final String vehicleUrl;
|
final String vehicleUrl;
|
||||||
final String legacyUrl;
|
final String legacyUrl;
|
||||||
|
final String remoteCommandUrl;
|
||||||
|
final String remoteStatusUrl;
|
||||||
|
final String navigationAPIUrl;
|
||||||
final String vehicleStatusAPI = "/status";
|
final String vehicleStatusAPI = "/status";
|
||||||
final String lastTripAPI = "/statistics/lastTrip";
|
final String lastTripAPI = "/statistics/lastTrip";
|
||||||
final String allTripsAPI = "/statistics/allTrips";
|
final String allTripsAPI = "/statistics/allTrips";
|
||||||
@ -82,25 +90,27 @@ public class ConnectedDriveProxy {
|
|||||||
final String rangeMapAPI = "/rangemap";
|
final String rangeMapAPI = "/rangemap";
|
||||||
final String serviceExecutionAPI = "/executeService";
|
final String serviceExecutionAPI = "/executeService";
|
||||||
final String serviceExecutionStateAPI = "/serviceExecutionStatus";
|
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) {
|
public ConnectedDriveProxy(HttpClientFactory httpClientFactory, ConnectedDriveConfiguration config) {
|
||||||
httpClient = httpClientFactory.getCommonHttpClient();
|
httpClient = httpClientFactory.getCommonHttpClient();
|
||||||
authHttpClient = httpClientFactory.createHttpClient(AUTH_HTTP_CLIENT_NAME);
|
authHttpClient = httpClientFactory.createHttpClient(AUTH_HTTP_CLIENT_NAME);
|
||||||
authHttpClient.setFollowRedirects(false);
|
|
||||||
configuration = config;
|
configuration = config;
|
||||||
|
|
||||||
final StringBuilder legacyAuth = new StringBuilder();
|
vehicleUrl = "https://" + BimmerConstants.API_SERVER_MAP.get(configuration.region) + "/webapi/v1/user/vehicles";
|
||||||
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";
|
|
||||||
baseUrl = vehicleUrl + "/";
|
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,
|
public synchronized void call(final String url, final boolean post, final @Nullable String encoding,
|
||||||
final ResponseCallback callback) {
|
final @Nullable String params, final ResponseCallback callback) {
|
||||||
// only executed in "simulation mode"
|
// only executed in "simulation mode"
|
||||||
// SimulationTest.testSimulationOff() assures Injector is off when releasing
|
// SimulationTest.testSimulationOff() assures Injector is off when releasing
|
||||||
if (Injector.isActive()) {
|
if (Injector.isActive()) {
|
||||||
@ -114,22 +124,25 @@ public class ConnectedDriveProxy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Request req;
|
final Request req;
|
||||||
final String encoded = params == null || params.isEmpty() ? null
|
|
||||||
: UrlEncoded.encode(params, StandardCharsets.UTF_8, false);
|
|
||||||
final String completeUrl;
|
final String completeUrl;
|
||||||
|
|
||||||
if (post) {
|
if (post) {
|
||||||
completeUrl = url;
|
completeUrl = url;
|
||||||
req = httpClient.POST(url);
|
req = httpClient.POST(url);
|
||||||
if (encoded != null) {
|
if (encoding != null) {
|
||||||
req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, encoded, StandardCharsets.UTF_8));
|
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 {
|
} else {
|
||||||
completeUrl = encoded == null ? url : url + Constants.QUESTION + encoded;
|
completeUrl = params == null ? url : url + Constants.QUESTION + params;
|
||||||
req = httpClient.newRequest(completeUrl);
|
req = httpClient.newRequest(completeUrl);
|
||||||
}
|
}
|
||||||
req.header(HttpHeader.AUTHORIZATION, getToken().getBearerToken());
|
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() {
|
req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(new BufferingResponseListener() {
|
||||||
@NonNullByDefault({})
|
@NonNullByDefault({})
|
||||||
@ -160,46 +173,52 @@ public class ConnectedDriveProxy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void get(String url, @Nullable MultiMap<String> params, ResponseCallback callback) {
|
public void get(String url, @Nullable String coding, @Nullable String params, ResponseCallback callback) {
|
||||||
call(url, false, params, callback);
|
call(url, false, coding, params, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void post(String url, @Nullable MultiMap<String> params, ResponseCallback callback) {
|
public void post(String url, @Nullable String coding, @Nullable String params, ResponseCallback callback) {
|
||||||
call(url, true, params, callback);
|
call(url, true, coding, params, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void requestVehicles(StringResponseCallback callback) {
|
public void requestVehicles(StringResponseCallback callback) {
|
||||||
get(vehicleUrl, null, callback);
|
get(vehicleUrl, null, null, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void requestVehcileStatus(VehicleConfiguration config, StringResponseCallback 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) {
|
public void requestLegacyVehcileStatus(VehicleConfiguration config, StringResponseCallback callback) {
|
||||||
// see https://github.com/jupe76/bmwcdapi/search?q=dynamic%2Fv1
|
// 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) {
|
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) {
|
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) {
|
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) {
|
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,
|
public void requestRangeMap(VehicleConfiguration config, @Nullable MultiMap<String> params,
|
||||||
StringResponseCallback callback) {
|
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) {
|
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("width", Integer.toString(props.size));
|
||||||
dataMap.add("height", Integer.toString(props.size));
|
dataMap.add("height", Integer.toString(props.size));
|
||||||
dataMap.add("view", props.viewport);
|
dataMap.add("view", props.viewport);
|
||||||
get(localImageUrl, dataMap, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getRegionServer() {
|
get(localImageUrl, CONTENT_TYPE_URL_ENCODED, UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false),
|
||||||
final String retVal = BimmerConstants.SERVER_MAP.get(configuration.region);
|
callback);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) {
|
RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) {
|
||||||
return new RemoteServiceHandler(vehicleHandler, this);
|
remoteServiceHandler = Optional.of(new RemoteServiceHandler(vehicleHandler, this));
|
||||||
|
return remoteServiceHandler.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token handling
|
// Token handling
|
||||||
@ -235,77 +247,182 @@ public class ConnectedDriveProxy {
|
|||||||
* @return token
|
* @return token
|
||||||
*/
|
*/
|
||||||
public Token getToken() {
|
public Token getToken() {
|
||||||
if (token.isExpired() || !token.isValid()) {
|
if (!token.isValid()) {
|
||||||
updateToken();
|
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;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public synchronized boolean updateToken() {
|
||||||
* Authorize at BMW Connected Drive Portal and get Token
|
if (BimmerConstants.REGION_CHINA.equals(configuration.region)) {
|
||||||
*
|
// region China currently not supported for MyBMW API
|
||||||
* @return
|
logger.debug("Region {} not supported yet for MyBMW Login", BimmerConstants.REGION_CHINA);
|
||||||
*/
|
return false;
|
||||||
private synchronized void updateToken() {
|
}
|
||||||
|
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()) {
|
if (!authHttpClient.isStarted()) {
|
||||||
try {
|
try {
|
||||||
authHttpClient.start();
|
authHttpClient.start();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Auth Http Client cannot be started {}", e.getMessage());
|
logger.error("Auth HttpClient start failed!");
|
||||||
return;
|
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);
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>();
|
final MultiMap<String> tokenMap = new MultiMap<String>();
|
||||||
UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
|
UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
|
||||||
tokenMap.forEach((key, value) -> {
|
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;
|
package org.openhab.binding.bmwconnecteddrive.internal.handler;
|
||||||
|
|
||||||
import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
|
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.Optional;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
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.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.eclipse.jetty.util.MultiMap;
|
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.VehicleConfiguration;
|
||||||
import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
|
import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
|
||||||
import org.openhab.binding.bmwconnecteddrive.internal.dto.remote.ExecutionStatusContainer;
|
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 final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class);
|
||||||
|
|
||||||
private static final String SERVICE_TYPE = "serviceType";
|
private static final String SERVICE_TYPE = "serviceType";
|
||||||
|
private static final String EVENT_ID = "eventId";
|
||||||
private static final String DATA = "data";
|
private static final String DATA = "data";
|
||||||
// after 6 retries the state update will give up
|
// after 6 retries the state update will give up
|
||||||
private static final int GIVEUP_COUNTER = 6;
|
private static final int GIVEUP_COUNTER = 6;
|
||||||
@ -52,12 +56,16 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
|||||||
|
|
||||||
private final ConnectedDriveProxy proxy;
|
private final ConnectedDriveProxy proxy;
|
||||||
private final VehicleHandler handler;
|
private final VehicleHandler handler;
|
||||||
|
private final String legacyServiceExecutionAPI;
|
||||||
|
private final String legacyServiceExecutionStateAPI;
|
||||||
private final String serviceExecutionAPI;
|
private final String serviceExecutionAPI;
|
||||||
private final String serviceExecutionStateAPI;
|
private final String serviceExecutionStateAPI;
|
||||||
|
|
||||||
private int counter = 0;
|
private int counter = 0;
|
||||||
private Optional<ScheduledFuture<?>> stateJob = Optional.empty();
|
private Optional<ScheduledFuture<?>> stateJob = Optional.empty();
|
||||||
private Optional<String> serviceExecuting = Optional.empty();
|
private Optional<String> serviceExecuting = Optional.empty();
|
||||||
|
private Optional<String> executingEventId = Optional.empty();
|
||||||
|
private boolean myBmwApiUsage = false;
|
||||||
|
|
||||||
public enum ExecutionState {
|
public enum ExecutionState {
|
||||||
READY,
|
READY,
|
||||||
@ -69,21 +77,23 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum RemoteService {
|
public enum RemoteService {
|
||||||
LIGHT_FLASH(REMOTE_SERVICE_LIGHT_FLASH, "Flash Lights"),
|
LIGHT_FLASH(REMOTE_SERVICE_LIGHT_FLASH, "Flash Lights", "light-flash"),
|
||||||
VEHICLE_FINDER(REMOTE_SERVICE_VEHICLE_FINDER, "Vehicle Finder"),
|
VEHICLE_FINDER(REMOTE_SERVICE_VEHICLE_FINDER, "Vehicle Finder", "vehicle-finder"),
|
||||||
DOOR_LOCK(REMOTE_SERVICE_DOOR_LOCK, "Door Lock"),
|
DOOR_LOCK(REMOTE_SERVICE_DOOR_LOCK, "Door Lock", "door-lock"),
|
||||||
DOOR_UNLOCK(REMOTE_SERVICE_DOOR_UNLOCK, "Door Unlock"),
|
DOOR_UNLOCK(REMOTE_SERVICE_DOOR_UNLOCK, "Door Unlock", "door-unlock"),
|
||||||
HORN_BLOW(REMOTE_SERVICE_HORN, "Horn Blow"),
|
HORN_BLOW(REMOTE_SERVICE_HORN, "Horn Blow", "horn-blow"),
|
||||||
CLIMATE_NOW(REMOTE_SERVICE_AIR_CONDITIONING, "Climate Control"),
|
CLIMATE_NOW(REMOTE_SERVICE_AIR_CONDITIONING, "Climate Control", "air-conditioning"),
|
||||||
CHARGE_NOW(REMOTE_SERVICE_CHARGE_NOW, "Start Charging"),
|
CHARGE_NOW(REMOTE_SERVICE_CHARGE_NOW, "Start Charging", "charge-now"),
|
||||||
CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile");
|
CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile", "charging-control");
|
||||||
|
|
||||||
private final String command;
|
private final String command;
|
||||||
private final String label;
|
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.command = command;
|
||||||
this.label = label;
|
this.label = label;
|
||||||
|
this.remoteCommand = remoteCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCommand() {
|
public String getCommand() {
|
||||||
@ -93,30 +103,49 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
|||||||
public String getLabel() {
|
public String getLabel() {
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRemoteCommand() {
|
||||||
|
return remoteCommand;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RemoteServiceHandler(VehicleHandler vehicleHandler, ConnectedDriveProxy connectedDriveProxy) {
|
public RemoteServiceHandler(VehicleHandler vehicleHandler, ConnectedDriveProxy connectedDriveProxy) {
|
||||||
handler = vehicleHandler;
|
handler = vehicleHandler;
|
||||||
proxy = connectedDriveProxy;
|
proxy = connectedDriveProxy;
|
||||||
final VehicleConfiguration config = handler.getConfiguration().get();
|
final VehicleConfiguration config = handler.getConfiguration().get();
|
||||||
serviceExecutionAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionAPI;
|
legacyServiceExecutionAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionAPI;
|
||||||
serviceExecutionStateAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionStateAPI;
|
legacyServiceExecutionStateAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionStateAPI;
|
||||||
|
serviceExecutionAPI = proxy.remoteCommandUrl + config.vin + "/";
|
||||||
|
serviceExecutionStateAPI = proxy.remoteStatusUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean execute(RemoteService service, String... data) {
|
boolean execute(RemoteService service, String... data) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
if (serviceExecuting.isPresent()) {
|
if (serviceExecuting.isPresent()) {
|
||||||
|
logger.debug("Execution rejected - {} still pending", serviceExecuting.get());
|
||||||
// only one service executing
|
// only one service executing
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
serviceExecuting = Optional.of(service.name());
|
serviceExecuting = Optional.of(service.name());
|
||||||
}
|
}
|
||||||
final MultiMap<String> dataMap = new MultiMap<String>();
|
if (myBmwApiUsage) {
|
||||||
dataMap.add(SERVICE_TYPE, service.name());
|
final MultiMap<String> dataMap = new MultiMap<String>();
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
dataMap.add(DATA, data[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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,9 +159,19 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
|||||||
handler.getData();
|
handler.getData();
|
||||||
}
|
}
|
||||||
counter++;
|
counter++;
|
||||||
final MultiMap<String> dataMap = new MultiMap<String>();
|
if (myBmwApiUsage) {
|
||||||
dataMap.add(SERVICE_TYPE, service);
|
final MultiMap<String> dataMap = new MultiMap<String>();
|
||||||
proxy.get(serviceExecutionStateAPI, dataMap, this);
|
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");
|
logger.warn("No Service executed to get state");
|
||||||
});
|
});
|
||||||
@ -145,15 +184,36 @@ public class RemoteServiceHandler implements StringResponseCallback {
|
|||||||
if (result != null) {
|
if (result != null) {
|
||||||
try {
|
try {
|
||||||
ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class);
|
ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class);
|
||||||
if (esc != null && esc.executionStatus != null) {
|
if (esc != null) {
|
||||||
String status = esc.executionStatus.status;
|
if (esc.executionStatus != null) {
|
||||||
synchronized (this) {
|
// handling of BMW ConnectedDrive updates
|
||||||
handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), status);
|
String status = esc.executionStatus.status;
|
||||||
if (ExecutionState.EXECUTED.name().equals(status)) {
|
if (status != null) {
|
||||||
// refresh loop ends - update of status handled in the normal refreshInterval. Earlier
|
synchronized (this) {
|
||||||
// update doesn't show better results!
|
handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), status);
|
||||||
reset();
|
if (ExecutionState.EXECUTED.name().equals(status)) {
|
||||||
return;
|
// 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() {
|
private void reset() {
|
||||||
serviceExecuting = Optional.empty();
|
serviceExecuting = Optional.empty();
|
||||||
|
executingEventId = Optional.empty();
|
||||||
counter = 0;
|
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 token = Constants.EMPTY;
|
||||||
private String tokenType = Constants.EMPTY;
|
private String tokenType = Constants.EMPTY;
|
||||||
private long expiration = 0;
|
private long expiration = 0;
|
||||||
|
private boolean myBmwApiUsage = false;
|
||||||
|
|
||||||
|
public boolean isMyBmwApiUsage() {
|
||||||
|
return myBmwApiUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMyBmwApiUsage(boolean myBmwAppUsage) {
|
||||||
|
this.myBmwApiUsage = myBmwAppUsage;
|
||||||
|
}
|
||||||
|
|
||||||
public String getBearerToken() {
|
public String getBearerToken() {
|
||||||
return new StringBuilder(tokenType).append(Constants.SPACE).append(token).toString();
|
return new StringBuilder(tokenType).append(Constants.SPACE).append(token).toString();
|
||||||
@ -38,18 +47,17 @@ public class Token {
|
|||||||
this.expiration = System.currentTimeMillis() / 1000 + expiration;
|
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) {
|
public void setType(String type) {
|
||||||
tokenType = type;
|
tokenType = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isValid() {
|
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) {
|
protected void updateCheckControls(List<CCMMessage> ccl) {
|
||||||
if (ccl.size() == 0) {
|
if (ccl.isEmpty()) {
|
||||||
// No Check Control available - show not active
|
// No Check Control available - show not active
|
||||||
CCMMessage ccm = new CCMMessage();
|
CCMMessage ccm = new CCMMessage();
|
||||||
ccm.ccmDescriptionLong = Constants.NO_ENTRIES;
|
ccm.ccmDescriptionLong = Constants.NO_ENTRIES;
|
||||||
@ -169,7 +169,7 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
|||||||
|
|
||||||
protected void updateServices(List<CBSMessage> sl) {
|
protected void updateServices(List<CBSMessage> sl) {
|
||||||
// if list is empty add "undefined" element
|
// if list is empty add "undefined" element
|
||||||
if (sl.size() == 0) {
|
if (sl.isEmpty()) {
|
||||||
CBSMessage cbsm = new CBSMessage();
|
CBSMessage cbsm = new CBSMessage();
|
||||||
cbsm.cbsType = Constants.NO_ENTRIES;
|
cbsm.cbsType = Constants.NO_ENTRIES;
|
||||||
cbsm.cbsDescription = Constants.NO_ENTRIES;
|
cbsm.cbsDescription = Constants.NO_ENTRIES;
|
||||||
@ -225,7 +225,7 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
|||||||
|
|
||||||
protected void updateDestinations(List<Destination> dl) {
|
protected void updateDestinations(List<Destination> dl) {
|
||||||
// if list is empty add "undefined" element
|
// if list is empty add "undefined" element
|
||||||
if (dl.size() == 0) {
|
if (dl.isEmpty()) {
|
||||||
Destination dest = new Destination();
|
Destination dest = new Destination();
|
||||||
dest.city = Constants.NO_ENTRIES;
|
dest.city = Constants.NO_ENTRIES;
|
||||||
dest.lat = -1;
|
dest.lat = -1;
|
||||||
@ -417,6 +417,9 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
|||||||
// last update Time
|
// last update Time
|
||||||
updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE,
|
updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE,
|
||||||
DateTimeType.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus))));
|
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;
|
Doors doorState = null;
|
||||||
try {
|
try {
|
||||||
@ -442,7 +445,8 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
|||||||
|
|
||||||
// Range values
|
// Range values
|
||||||
// based on unit of length decide if range shall be reported in km or miles
|
// 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) {
|
if (isElectric) {
|
||||||
totalRange += vStatus.remainingRangeElectric;
|
totalRange += vStatus.remainingRangeElectric;
|
||||||
QuantityType<Length> qtElectricRange = QuantityType.valueOf(vStatus.remainingRangeElectric,
|
QuantityType<Length> qtElectricRange = QuantityType.valueOf(vStatus.remainingRangeElectric,
|
||||||
@ -454,9 +458,21 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
|||||||
imperial ? Converter.getMiles(qtElectricRange) : qtElectricRange);
|
imperial ? Converter.getMiles(qtElectricRange) : qtElectricRange);
|
||||||
updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC,
|
updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC,
|
||||||
imperial ? Converter.getMiles(qtElectricRadius) : qtElectricRadius);
|
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) {
|
if (hasFuel) {
|
||||||
totalRange += vStatus.remainingRangeFuel;
|
totalRange += vStatus.remainingRangeFuel;
|
||||||
|
maxTotalRange += vStatus.remainingRangeFuel;
|
||||||
QuantityType<Length> qtFuelRange = QuantityType.valueOf(vStatus.remainingRangeFuel,
|
QuantityType<Length> qtFuelRange = QuantityType.valueOf(vStatus.remainingRangeFuel,
|
||||||
Constants.KILOMETRE_UNIT);
|
Constants.KILOMETRE_UNIT);
|
||||||
QuantityType<Length> qtFuelRadius = QuantityType
|
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> qtHybridRange = QuantityType.valueOf(totalRange, Constants.KILOMETRE_UNIT);
|
||||||
QuantityType<Length> qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(totalRange),
|
QuantityType<Length> qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(totalRange),
|
||||||
Constants.KILOMETRE_UNIT);
|
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,
|
updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID,
|
||||||
imperial ? Converter.getMiles(qtHybridRange) : qtHybridRange);
|
imperial ? Converter.getMiles(qtHybridRange) : qtHybridRange);
|
||||||
updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID,
|
updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID,
|
||||||
imperial ? Converter.getMiles(qtHybridRadius) : qtHybridRadius);
|
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,
|
updateChannel(CHANNEL_GROUP_RANGE, MILEAGE,
|
||||||
@ -488,6 +511,12 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
|
|||||||
|
|
||||||
// Charge Values
|
// Charge Values
|
||||||
if (isElectric) {
|
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 (vStatus.chargingStatus != null) {
|
||||||
if (Constants.INVALID.equals(vStatus.chargingStatus)) {
|
if (Constants.INVALID.equals(vStatus.chargingStatus)) {
|
||||||
updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
|
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.DestinationContainer;
|
||||||
import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
|
import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
|
||||||
import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
|
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.AllTrips;
|
||||||
import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTripsContainer;
|
import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTripsContainer;
|
||||||
import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
|
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.DateTimeType;
|
||||||
import org.openhab.core.library.types.DecimalType;
|
import org.openhab.core.library.types.DecimalType;
|
||||||
import org.openhab.core.library.types.OnOffType;
|
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.RawType;
|
||||||
import org.openhab.core.library.types.StringType;
|
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.Bridge;
|
||||||
import org.openhab.core.thing.ChannelUID;
|
import org.openhab.core.thing.ChannelUID;
|
||||||
import org.openhab.core.thing.Thing;
|
import org.openhab.core.thing.Thing;
|
||||||
@ -86,6 +89,7 @@ public class VehicleHandler extends VehicleChannelHandler {
|
|||||||
private ImageProperties imageProperties = new ImageProperties();
|
private ImageProperties imageProperties = new ImageProperties();
|
||||||
VehicleStatusCallback vehicleStatusCallback = new VehicleStatusCallback();
|
VehicleStatusCallback vehicleStatusCallback = new VehicleStatusCallback();
|
||||||
StringResponseCallback oldVehicleStatusCallback = new LegacyVehicleStatusCallback();
|
StringResponseCallback oldVehicleStatusCallback = new LegacyVehicleStatusCallback();
|
||||||
|
StringResponseCallback navigationCallback = new NavigationStatusCallback();
|
||||||
StringResponseCallback lastTripCallback = new LastTripCallback();
|
StringResponseCallback lastTripCallback = new LastTripCallback();
|
||||||
StringResponseCallback allTripsCallback = new AllTripsCallback();
|
StringResponseCallback allTripsCallback = new AllTripsCallback();
|
||||||
StringResponseCallback chargeProfileCallback = new ChargeProfilesCallback();
|
StringResponseCallback chargeProfileCallback = new ChargeProfilesCallback();
|
||||||
@ -275,6 +279,8 @@ public class VehicleHandler extends VehicleChannelHandler {
|
|||||||
prox.requestVehcileStatus(config, vehicleStatusCallback);
|
prox.requestVehcileStatus(config, vehicleStatusCallback);
|
||||||
}
|
}
|
||||||
addCallback(vehicleStatusCallback);
|
addCallback(vehicleStatusCallback);
|
||||||
|
prox.requestLNavigation(config, navigationCallback);
|
||||||
|
addCallback(navigationCallback);
|
||||||
if (isSupported(Constants.STATISTICS)) {
|
if (isSupported(Constants.STATISTICS)) {
|
||||||
prox.requestLastTrip(config, lastTripCallback);
|
prox.requestLastTrip(config, lastTripCallback);
|
||||||
prox.requestAllTrips(config, allTripsCallback);
|
prox.requestAllTrips(config, allTripsCallback);
|
||||||
@ -677,11 +683,31 @@ public class VehicleHandler extends VehicleChannelHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(NetworkError error) {
|
public void onError(NetworkError error) {
|
||||||
logger.debug("{}", error.toString());
|
|
||||||
vehicleStatusCallback.onError(error);
|
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) {
|
private void handleChargeProfileCommand(ChannelUID channelUID, Command command) {
|
||||||
if (chargeProfileEdit.isEmpty()) {
|
if (chargeProfileEdit.isEmpty()) {
|
||||||
chargeProfileEdit = getChargeProfileWrapper();
|
chargeProfileEdit = getChargeProfileWrapper();
|
||||||
|
@ -34,32 +34,68 @@ public class BimmerConstants {
|
|||||||
public static final String REGION_ROW = "ROW";
|
public static final String REGION_ROW = "ROW";
|
||||||
|
|
||||||
// https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/country_selector.py
|
// 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 LEGACY_AUTH_SERVER_NORTH_AMERICA = "login.bmwusa.com/gcdm";
|
||||||
public static final String AUTH_SERVER_CHINA = "b2vapi.bmwgroup.cn/gcdm";
|
public static final String LEGACY_AUTH_SERVER_CHINA = "customer.bmwgroup.cn/gcdm";
|
||||||
public static final String AUTH_SERVER_ROW = "b2vapi.bmwgroup.com/gcdm";
|
public static final String LEGACY_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,
|
public static final Map<String, String> LEGACY_AUTH_SERVER_MAP = Map.of(REGION_NORTH_AMERICA,
|
||||||
REGION_CHINA, AUTH_SERVER_CHINA, REGION_ROW, AUTH_SERVER_ROW);
|
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 API_SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us";
|
||||||
public static final String SERVER_CHINA = "b2vapi.bmwgroup.cn:8592";
|
public static final String API_SERVER_CHINA = "b2vapi.bmwgroup.cn:8592";
|
||||||
public static final String SERVER_ROW = "b2vapi.bmwgroup.com";
|
public static final String API_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
|
// 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==",
|
"Basic ZDc2NmI1MzctYTY1NC00Y2JkLWEzZGMtMGNhNTY3MmQ3ZjhkOjE1ZjY5N2Y2LWE1ZDUtNGNhZC05OWQ5LTNhMTViYzdmMzk3Mw==",
|
||||||
REGION_CHINA,
|
REGION_CHINA,
|
||||||
"Basic blF2NkNxdHhKdVhXUDc0eGYzQ0p3VUVQOjF6REh4NnVuNGNEanliTEVOTjNreWZ1bVgya0VZaWdXUGNRcGR2RFJwSUJrN3JPSg==",
|
"Basic blF2NkNxdHhKdVhXUDc0eGYzQ0p3VUVQOjF6REh4NnVuNGNEanliTEVOTjNreWZ1bVgya0VZaWdXUGNRcGR2RFJwSUJrN3JPSg==",
|
||||||
REGION_ROW,
|
REGION_ROW,
|
||||||
"Basic ZDc2NmI1MzctYTY1NC00Y2JkLWEzZGMtMGNhNTY3MmQ3ZjhkOjE1ZjY5N2Y2LWE1ZDUtNGNhZC05OWQ5LTNhMTViYzdmMzk3Mw==");
|
"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 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.remainingRangeFuelMls = attributesMap.beRemainingRangeFuelMile;
|
||||||
vs.remainingFuel = attributesMap.remainingFuel;
|
vs.remainingFuel = attributesMap.remainingFuel;
|
||||||
vs.chargingLevelHv = attributesMap.chargingLevelHv;
|
vs.chargingLevelHv = attributesMap.chargingLevelHv;
|
||||||
|
vs.maxRangeElectric = attributesMap.beMaxRangeElectric;
|
||||||
|
vs.maxRangeElectricMls = attributesMap.beMaxRangeElectricMile;
|
||||||
vs.chargingStatus = attributesMap.chargingHVStatus;
|
vs.chargingStatus = attributesMap.chargingHVStatus;
|
||||||
|
vs.connectionStatus = attributesMap.connectorStatus;
|
||||||
vs.lastChargingEndReason = attributesMap.lastChargingEndReason;
|
vs.lastChargingEndReason = attributesMap.lastChargingEndReason;
|
||||||
|
|
||||||
vs.updateTime = attributesMap.updateTimeConverted;
|
vs.updateTime = attributesMap.updateTimeConverted;
|
||||||
|
vs.updateReason = attributesMap.lastUpdateReason;
|
||||||
|
|
||||||
Position p = new Position();
|
Position p = new Position();
|
||||||
p.lat = attributesMap.gpsLat;
|
p.lat = attributesMap.gpsLat;
|
||||||
|
@ -25,12 +25,15 @@ public class HTTPConstants {
|
|||||||
|
|
||||||
public static final String AUTH_HTTP_CLIENT_NAME = "AuthHttpClient";
|
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_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 KEEP_ALIVE = "Keep-Alive";
|
||||||
public static final String CLIENT_ID = "client_id";
|
public static final String CLIENT_ID = "client_id";
|
||||||
public static final String RESPONSE_TYPE = "response_type";
|
public static final String RESPONSE_TYPE = "response_type";
|
||||||
public static final String TOKEN = "token";
|
public static final String TOKEN = "token";
|
||||||
|
public static final String CODE = "code";
|
||||||
public static final String REDIRECT_URI = "redirect_uri";
|
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 SCOPE = "scope";
|
||||||
public static final String CREDENTIALS = "Credentials";
|
public static final String CREDENTIALS = "Credentials";
|
||||||
public static final String USERNAME = "username";
|
public static final String USERNAME = "username";
|
||||||
|
@ -24,5 +24,11 @@
|
|||||||
</options>
|
</options>
|
||||||
<default>ROW</default>
|
<default>ROW</default>
|
||||||
</parameter>
|
</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-description:config-descriptions>
|
</config-description:config-descriptions>
|
||||||
|
@ -5,12 +5,17 @@ binding.bmwconnecteddrive.description = Zeigt die Fahrzeugdaten
|
|||||||
# bridge types
|
# bridge types
|
||||||
thing-type.bmwconnecteddrive.account.label = BMW ConnectedDrive Benutzerkonto
|
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.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.userName.label = Benutzername
|
||||||
thing-type.config.bmwconnecteddrive.account.password = Passwort für das ConnectedDrive Portal
|
thing-type.config.bmwconnecteddrive.account.userName.description = Benutzername 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.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.NORTH_AMERICA = Nordamerika
|
||||||
thing-type.config.bmwconnecteddrive.account.region.option.CHINA = China
|
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.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 types
|
||||||
thing-type.bmwconnecteddrive.bev_rex.label = Elektrofahrzeug mit REX
|
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.next-service-mileage-channel.label = Nächster Service in Kilometern
|
||||||
channel-type.bmwconnecteddrive.check-control-channel.label = Warnung Aktiv
|
channel-type.bmwconnecteddrive.check-control-channel.label = Warnung Aktiv
|
||||||
channel-type.bmwconnecteddrive.charging-status-channel.label = Ladezustand
|
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.charging-remaining-channel.label = Verbleibende Ladezeit
|
||||||
channel-type.bmwconnecteddrive.last-update-channel.label = Letzte Aktualisierung
|
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-front-channel.label = Fahrertür
|
||||||
channel-type.bmwconnecteddrive.driver-rear-channel.label = Fahrertür Hinten
|
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.mileage-channel.label = Tachostand
|
||||||
channel-type.bmwconnecteddrive.range-hybrid-channel.label = Hybride Reichweite
|
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-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-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.range-fuel-channel.label = Verbrenner Reichweite
|
||||||
channel-type.bmwconnecteddrive.remaining-fuel-channel.label = Tankstand
|
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-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-fuel-channel.label = Verbrenner Reichweiten-Radius
|
||||||
channel-type.bmwconnecteddrive.range-radius-hybrid-channel.label = Hybrider 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-name-channel.label = Service
|
||||||
channel-type.bmwconnecteddrive.service-details-channel.label = Service Details
|
channel-type.bmwconnecteddrive.service-details-channel.label = Service Details
|
||||||
|
@ -9,8 +9,11 @@
|
|||||||
<channels>
|
<channels>
|
||||||
<channel id="mileage" typeId="mileage-channel"/>
|
<channel id="mileage" typeId="mileage-channel"/>
|
||||||
<channel id="electric" typeId="range-electric-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="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>
|
</channels>
|
||||||
</channel-group-type>
|
</channel-group-type>
|
||||||
</thing:thing-descriptions>
|
</thing:thing-descriptions>
|
||||||
|
@ -13,9 +13,11 @@
|
|||||||
<channel id="service-date" typeId="next-service-date-channel"/>
|
<channel id="service-date" typeId="next-service-date-channel"/>
|
||||||
<channel id="service-mileage" typeId="next-service-mileage-channel"/>
|
<channel id="service-mileage" typeId="next-service-mileage-channel"/>
|
||||||
<channel id="check-control" typeId="check-control-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="charge" typeId="charging-status-channel"/>
|
||||||
<channel id="remaining" typeId="charging-remaining-channel"/>
|
<channel id="remaining" typeId="charging-remaining-channel"/>
|
||||||
<channel id="last-update" typeId="last-update-channel"/>
|
<channel id="last-update" typeId="last-update-channel"/>
|
||||||
|
<channel id="last-update-reason" typeId="last-update-reason-channel"/>
|
||||||
</channels>
|
</channels>
|
||||||
</channel-group-type>
|
</channel-group-type>
|
||||||
</thing:thing-descriptions>
|
</thing:thing-descriptions>
|
||||||
|
@ -9,12 +9,17 @@
|
|||||||
<channels>
|
<channels>
|
||||||
<channel id="mileage" typeId="mileage-channel"/>
|
<channel id="mileage" typeId="mileage-channel"/>
|
||||||
<channel id="hybrid" typeId="range-hybrid-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="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="fuel" typeId="range-fuel-channel"/>
|
||||||
<channel id="remaining-fuel" typeId="remaining-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" 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>
|
</channels>
|
||||||
</channel-group-type>
|
</channel-group-type>
|
||||||
</thing:thing-descriptions>
|
</thing:thing-descriptions>
|
||||||
|
@ -13,6 +13,11 @@
|
|||||||
<label>Electric Range</label>
|
<label>Electric Range</label>
|
||||||
<state pattern="%d %unit%" readOnly="true"/>
|
<state pattern="%d %unit%" readOnly="true"/>
|
||||||
</channel-type>
|
</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">
|
<channel-type id="range-fuel-channel">
|
||||||
<item-type>Number:Length</item-type>
|
<item-type>Number:Length</item-type>
|
||||||
<label>Fuel Range</label>
|
<label>Fuel Range</label>
|
||||||
@ -23,17 +28,32 @@
|
|||||||
<label>Hybrid Range</label>
|
<label>Hybrid Range</label>
|
||||||
<state pattern="%d %unit%" readOnly="true"/>
|
<state pattern="%d %unit%" readOnly="true"/>
|
||||||
</channel-type>
|
</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">
|
<channel-type id="soc-channel">
|
||||||
<item-type>Number:Dimensionless</item-type>
|
<item-type>Number:Dimensionless</item-type>
|
||||||
<label>Battery Charge Level</label>
|
<label>Battery Charge Level</label>
|
||||||
<state pattern="%d %unit%" readOnly="true"/>
|
<state pattern="%d %unit%" readOnly="true"/>
|
||||||
</channel-type>
|
</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">
|
<channel-type id="remaining-fuel-channel">
|
||||||
<item-type>Number:Volume</item-type>
|
<item-type>Number:Volume</item-type>
|
||||||
<label>Remaining Fuel</label>
|
<label>Remaining Fuel</label>
|
||||||
<state pattern="%d %unit%" readOnly="true"/>
|
<state pattern="%d %unit%" readOnly="true"/>
|
||||||
</channel-type>
|
</channel-type>
|
||||||
<channel-type id="range-radius-electric-channel">
|
<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>
|
<item-type>Number:Length</item-type>
|
||||||
<label>Electric Range Radius</label>
|
<label>Electric Range Radius</label>
|
||||||
<state pattern="%.0f %unit%" readOnly="true"/>
|
<state pattern="%.0f %unit%" readOnly="true"/>
|
||||||
@ -48,4 +68,9 @@
|
|||||||
<label>Hybrid Range Radius</label>
|
<label>Hybrid Range Radius</label>
|
||||||
<state pattern="%.0f %unit%" readOnly="true"/>
|
<state pattern="%.0f %unit%" readOnly="true"/>
|
||||||
</channel-type>
|
</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>
|
</thing:thing-descriptions>
|
||||||
|
@ -38,6 +38,11 @@
|
|||||||
<label>Charging Status</label>
|
<label>Charging Status</label>
|
||||||
<state readOnly="true"/>
|
<state readOnly="true"/>
|
||||||
</channel-type>
|
</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">
|
<channel-type id="charging-remaining-channel">
|
||||||
<item-type>Number:Time</item-type>
|
<item-type>Number:Time</item-type>
|
||||||
<label>Remaining Charging Time</label>
|
<label>Remaining Charging Time</label>
|
||||||
@ -48,4 +53,8 @@
|
|||||||
<label>Last Status Timestamp</label>
|
<label>Last Status Timestamp</label>
|
||||||
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
|
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
|
||||||
</channel-type>
|
</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>
|
</thing:thing-descriptions>
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
<channel id="service-mileage" typeId="next-service-mileage-channel"/>
|
<channel id="service-mileage" typeId="next-service-mileage-channel"/>
|
||||||
<channel id="check-control" typeId="check-control-channel"/>
|
<channel id="check-control" typeId="check-control-channel"/>
|
||||||
<channel id="last-update" typeId="last-update-channel"/>
|
<channel id="last-update" typeId="last-update-channel"/>
|
||||||
|
<channel id="last-update-reason" typeId="last-update-reason-channel"/>
|
||||||
</channels>
|
</channels>
|
||||||
</channel-group-type>
|
</channel-group-type>
|
||||||
</thing:thing-descriptions>
|
</thing:thing-descriptions>
|
||||||
|
@ -164,6 +164,20 @@ public class StatusWrapper {
|
|||||||
ALLOWED_KM_ROUND_DEVIATION, "Mileage");
|
ALLOWED_KM_ROUND_DEVIATION, "Mileage");
|
||||||
}
|
}
|
||||||
break;
|
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:
|
case RANGE_FUEL:
|
||||||
assertTrue(hasFuel, "Has Fuel");
|
assertTrue(hasFuel, "Has Fuel");
|
||||||
if (!(state instanceof UnDefType)) {
|
if (!(state instanceof UnDefType)) {
|
||||||
@ -196,6 +210,22 @@ public class StatusWrapper {
|
|||||||
ALLOWED_KM_ROUND_DEVIATION, "Mileage");
|
ALLOWED_KM_ROUND_DEVIATION, "Mileage");
|
||||||
}
|
}
|
||||||
break;
|
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:
|
case REMAINING_FUEL:
|
||||||
assertTrue(hasFuel, "Has Fuel");
|
assertTrue(hasFuel, "Has Fuel");
|
||||||
assertTrue(state instanceof QuantityType);
|
assertTrue(state instanceof QuantityType);
|
||||||
@ -212,6 +242,14 @@ public class StatusWrapper {
|
|||||||
assertEquals(Converter.round(vStatus.chargingLevelHv), Converter.round(qt.floatValue()), 0.01,
|
assertEquals(Converter.round(vStatus.chargingLevelHv), Converter.round(qt.floatValue()), 0.01,
|
||||||
"Charge Level");
|
"Charge Level");
|
||||||
break;
|
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:
|
case LOCK:
|
||||||
assertTrue(state instanceof StringType);
|
assertTrue(state instanceof StringType);
|
||||||
st = (StringType) state;
|
st = (StringType) state;
|
||||||
@ -274,6 +312,12 @@ public class StatusWrapper {
|
|||||||
assertEquals(Units.MINUTE, qtt.getUnit(), "Minutes");
|
assertEquals(Units.MINUTE, qtt.getUnit(), "Minutes");
|
||||||
}
|
}
|
||||||
break;
|
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:
|
case LAST_UPDATE:
|
||||||
assertTrue(state instanceof DateTimeType);
|
assertTrue(state instanceof DateTimeType);
|
||||||
dtt = (DateTimeType) state;
|
dtt = (DateTimeType) state;
|
||||||
@ -281,6 +325,12 @@ public class StatusWrapper {
|
|||||||
.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus)));
|
.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus)));
|
||||||
assertEquals(expected.toString(), dtt.toString(), "Last Update");
|
assertEquals(expected.toString(), dtt.toString(), "Last Update");
|
||||||
break;
|
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:
|
case GPS:
|
||||||
assertTrue(state instanceof PointType);
|
assertTrue(state instanceof PointType);
|
||||||
pt = (PointType) state;
|
pt = (PointType) state;
|
||||||
@ -306,6 +356,18 @@ public class StatusWrapper {
|
|||||||
"Range Radius Electric km");
|
"Range Radius Electric km");
|
||||||
}
|
}
|
||||||
break;
|
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:
|
case RANGE_RADIUS_FUEL:
|
||||||
assertTrue(state instanceof QuantityType);
|
assertTrue(state instanceof QuantityType);
|
||||||
assertTrue(hasFuel);
|
assertTrue(hasFuel);
|
||||||
@ -333,6 +395,19 @@ public class StatusWrapper {
|
|||||||
qt.floatValue(), ALLOWED_KM_ROUND_DEVIATION, "Range Radius Hybrid km");
|
qt.floatValue(), ALLOWED_KM_ROUND_DEVIATION, "Range Radius Hybrid km");
|
||||||
}
|
}
|
||||||
break;
|
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:
|
case DOOR_DRIVER_FRONT:
|
||||||
assertTrue(state instanceof StringType);
|
assertTrue(state instanceof StringType);
|
||||||
st = (StringType) state;
|
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 {
|
public class VehicleTests {
|
||||||
private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
|
private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
|
||||||
|
|
||||||
private static final int STATUS_ELECTRIC = 9;
|
private static final int STATUS_ELECTRIC = 12;
|
||||||
private static final int STATUS_CONV = 7;
|
private static final int STATUS_CONV = 8;
|
||||||
private static final int RANGE_HYBRID = 9;
|
private static final int RANGE_HYBRID = 12;
|
||||||
private static final int RANGE_CONV = 4;
|
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 DOORS = 12;
|
||||||
private static final int CHECK_EMPTY = 3;
|
private static final int CHECK_EMPTY = 3;
|
||||||
private static final int CHECK_AVAILABLE = 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