]> git.basschouten.com Git - openhab-addons.git/commitdiff
[BMWConnectedDrive] Authorization fix + channel enhancements (#11213) (#11263) (...
authorBernd Weymann <bernd.weymann@gmail.com>
Sun, 3 Oct 2021 06:15:07 +0000 (08:15 +0200)
committerGitHub <noreply@github.com>
Sun, 3 Oct 2021 06:15:07 +0000 (08:15 +0200)
28 files changed:
bundles/org.openhab.binding.bmwconnecteddrive/README.md
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConfiguration.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConstants.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/auth/AuthResponse.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/navigation/NavigationContainer.java [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatusContainer.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/Converter.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/HTTPConstants.java
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/config/bridge-config.xml
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-range-channel-group.xml
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml
bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-group.xml
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/StatusWrapper.java
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java [deleted file]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleTests.java
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/auth_response.json [new file with mode: 0644]
bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/tokenResponse.json [new file with mode: 0644]

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