2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mybmw.internal.handler.backend;
15 import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.APP_VERSIONS;
17 import java.nio.charset.Charset;
18 import java.nio.charset.StandardCharsets;
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
25 import org.eclipse.jdt.annotation.NonNull;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.client.api.ContentResponse;
30 import org.eclipse.jetty.client.api.Request;
31 import org.eclipse.jetty.http.HttpHeader;
32 import org.eclipse.jetty.util.MultiMap;
33 import org.eclipse.jetty.util.UrlEncoded;
34 import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration;
35 import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer;
36 import org.openhab.binding.mybmw.internal.dto.charge.ChargingStatisticsContainer;
37 import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer;
38 import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
39 import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase;
40 import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer;
41 import org.openhab.binding.mybmw.internal.handler.auth.MyBMWTokenController;
42 import org.openhab.binding.mybmw.internal.handler.enums.RemoteService;
43 import org.openhab.binding.mybmw.internal.utils.BimmerConstants;
44 import org.openhab.binding.mybmw.internal.utils.Constants;
45 import org.openhab.binding.mybmw.internal.utils.Converter;
46 import org.openhab.binding.mybmw.internal.utils.HTTPConstants;
47 import org.openhab.binding.mybmw.internal.utils.ImageProperties;
48 import org.openhab.core.io.net.http.HttpClientFactory;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
53 * The {@link MyBMWHttpProxy} This class holds the important constants for the BMW Connected Drive Authorization.
54 * They are taken from the Bimmercode from github
55 * {@link https://github.com/bimmerconnected/bimmer_connected}
56 * File defining these constants
57 * {@link https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py}
58 * https://customer.bmwgroup.com/one/app/oauth.js
60 * @author Bernd Weymann - Initial contribution
61 * @author Norbert Truchsess - edit and send of charge profile
62 * @author Martin Grassl - refactoring
63 * @author Mark Herwege - extended log anonymization
66 public class MyBMWHttpProxy implements MyBMWProxy {
67 private final Logger logger = LoggerFactory.getLogger(MyBMWHttpProxy.class);
68 private final HttpClient httpClient;
69 private MyBMWBridgeConfiguration bridgeConfiguration;
70 private final MyBMWTokenController myBMWTokenHandler;
74 * https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py
76 private final String vehicleUrl;
77 private final String vehicleStateUrl;
78 private final String remoteCommandUrl;
79 private final String remoteStatusUrl;
81 public MyBMWHttpProxy(HttpClientFactory httpClientFactory, MyBMWBridgeConfiguration bridgeConfiguration) {
82 logger.trace("MyBMWHttpProxy - initialize");
83 httpClient = httpClientFactory.getCommonHttpClient();
85 myBMWTokenHandler = new MyBMWTokenController(bridgeConfiguration, httpClient);
87 this.bridgeConfiguration = bridgeConfiguration;
89 vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region)
90 + BimmerConstants.API_VEHICLES;
92 vehicleStateUrl = vehicleUrl + "/state";
94 remoteCommandUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region)
95 + BimmerConstants.API_REMOTE_SERVICE_BASE_URL;
96 remoteStatusUrl = remoteCommandUrl + "eventStatus";
97 logger.trace("MyBMWHttpProxy - ready");
101 public void setBridgeConfiguration(MyBMWBridgeConfiguration bridgeConfiguration) {
102 this.bridgeConfiguration = bridgeConfiguration;
106 * requests all vehicles
108 * @return list of vehicles
110 public List<@NonNull Vehicle> requestVehicles() throws NetworkException {
111 List<@NonNull Vehicle> vehicles = new ArrayList<>();
112 List<@NonNull VehicleBase> vehiclesBase = requestVehiclesBase();
114 for (VehicleBase vehicleBase : vehiclesBase) {
115 VehicleStateContainer vehicleState = requestVehicleState(vehicleBase.getVin(),
116 vehicleBase.getAttributes().getBrand());
118 Vehicle vehicle = new Vehicle();
119 vehicle.setVehicleBase(vehicleBase);
120 vehicle.setVehicleState(vehicleState);
121 vehicles.add(vehicle);
128 * request all vehicles for one specific brand and their state
131 * @return the vehicles of one brand
133 public List<VehicleBase> requestVehiclesBase(String brand) throws NetworkException {
134 String vehicleResponseString = requestVehiclesBaseJson(brand);
135 return JsonStringDeserializer.getVehicleBaseList(vehicleResponseString);
139 * request the raw JSON for the vehicle
142 * @return the base vehicle information as JSON string
144 public String requestVehiclesBaseJson(String brand) throws NetworkException {
145 byte[] vehicleResponse = get(vehicleUrl, brand, null, HTTPConstants.CONTENT_TYPE_JSON);
146 String vehicleResponseString = new String(vehicleResponse, Charset.defaultCharset());
147 return vehicleResponseString;
151 * request vehicles for all possible brands
153 * @return the list of vehicles
155 public List<VehicleBase> requestVehiclesBase() throws NetworkException {
156 List<VehicleBase> vehicles = new ArrayList<>();
158 for (String brand : BimmerConstants.REQUESTED_BRANDS) {
160 vehicles.addAll(requestVehiclesBase(brand));
163 } catch (Exception e) {
164 logger.warn("error retrieving the base vehicles for brand {}: {}", brand, e.getMessage());
172 * request the vehicle image
174 * @param vin the vin of the vehicle
175 * @param brand the brand of the vehicle
176 * @param props the image properties
177 * @return the image as a byte array
179 public byte[] requestImage(String vin, String brand, ImageProperties props) throws NetworkException {
180 final String localImageUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region)
181 + "/eadrax-ics/v3/presentation/vehicles/" + vin + "/images?carView=" + props.viewport;
182 return get(localImageUrl, brand, vin, HTTPConstants.CONTENT_TYPE_IMAGE);
186 * request the state for one specific vehicle
190 * @return the vehicle state
192 public VehicleStateContainer requestVehicleState(String vin, String brand) throws NetworkException {
193 String vehicleStateResponseString = requestVehicleStateJson(vin, brand);
194 return JsonStringDeserializer.getVehicleState(vehicleStateResponseString);
198 * request the raw state as JSON for one specific vehicle
202 * @return the vehicle state as string
204 public String requestVehicleStateJson(String vin, String brand) throws NetworkException {
205 byte[] vehicleStateResponse = get(vehicleStateUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON);
206 String vehicleStateResponseString = new String(vehicleStateResponse, Charset.defaultCharset());
207 return vehicleStateResponseString;
211 * request charge statistics for electric vehicles
215 * @return the charge statistics
217 public ChargingStatisticsContainer requestChargeStatistics(String vin, String brand) throws NetworkException {
218 String chargeStatisticsResponseString = requestChargeStatisticsJson(vin, brand);
219 return JsonStringDeserializer.getChargingStatistics(new String(chargeStatisticsResponseString));
223 * request charge statistics for electric vehicles as JSON
227 * @return the charge statistics as JSON string
229 public String requestChargeStatisticsJson(String vin, String brand) throws NetworkException {
230 MultiMap<@Nullable String> chargeStatisticsParams = new MultiMap<>();
231 chargeStatisticsParams.put("vin", vin);
232 chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
233 String params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
234 String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region)
235 + "/eadrax-chs/v1/charging-statistics?" + params;
236 byte[] chargeStatisticsResponse = get(chargeStatisticsUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON);
237 String chargeStatisticsResponseString = new String(chargeStatisticsResponse);
238 return chargeStatisticsResponseString;
242 * request charge sessions for electric vehicles
246 * @return the charge sessions
248 public ChargingSessionsContainer requestChargeSessions(String vin, String brand) throws NetworkException {
249 String chargeSessionsResponseString = requestChargeSessionsJson(vin, brand);
250 return JsonStringDeserializer.getChargingSessions(chargeSessionsResponseString);
254 * request charge sessions for electric vehicles as JSON string
258 * @return the charge sessions as JSON string
260 public String requestChargeSessionsJson(String vin, String brand) throws NetworkException {
261 MultiMap<@Nullable String> chargeSessionsParams = new MultiMap<>();
262 chargeSessionsParams.put("vin", vin);
263 chargeSessionsParams.put("maxResults", "40");
264 chargeSessionsParams.put("include_date_picker", "true");
265 String params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false);
266 String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region)
267 + "/eadrax-chs/v1/charging-sessions?" + params;
268 byte[] chargeSessionsResponse = get(chargeSessionsUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON);
269 String chargeSessionsResponseString = new String(chargeSessionsResponse);
270 return chargeSessionsResponseString;
274 * execute a remote service call
278 * @param service the service which should be executed
279 * @return the running service execution for status checks
281 public ExecutionStatusContainer executeRemoteServiceCall(String vin, String brand, RemoteService service)
282 throws NetworkException {
283 String executionUrl = remoteCommandUrl + vin + "/" + service.getCommand();
285 byte[] response = post(executionUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON, service.getBody());
287 return JsonStringDeserializer.getExecutionStatus(new String(response));
291 * check the status of a service call
294 * @param eventid the ID of the currently running service execution
295 * @return the running service execution for status checks
297 public ExecutionStatusContainer executeRemoteServiceStatusCall(String brand, String eventId)
298 throws NetworkException {
299 String executionUrl = remoteStatusUrl + Constants.QUESTION + "eventId=" + eventId;
301 byte[] response = post(executionUrl, brand, null, HTTPConstants.CONTENT_TYPE_JSON, null);
303 return JsonStringDeserializer.getExecutionStatus(new String(response));
307 * prepares a GET request to the backend
313 * @return byte array of the response body
315 private byte[] get(String url, final String brand, @Nullable String vin, String contentType)
316 throws NetworkException {
317 return call(url, false, brand, vin, contentType, null);
321 * prepares a POST request to the backend
328 * @return byte array of the response body
330 private byte[] post(String url, final String brand, @Nullable String vin, String contentType, @Nullable String body)
331 throws NetworkException {
332 return call(url, true, brand, vin, contentType, body);
336 * executes the real call to the backend
339 * @param post boolean value indicating if it is a post request
344 * @return byte array of the response body
346 private synchronized byte[] call(final String url, final boolean post, final String brand,
347 final @Nullable String vin, final String contentType, final @Nullable String body) throws NetworkException {
348 byte[] responseByteArray = "".getBytes();
350 // return in case of unknown brand
351 if (!BimmerConstants.REQUESTED_BRANDS.contains(brand.toLowerCase())) {
352 logger.warn("Unknown Brand {}", brand);
353 throw new NetworkException("Unknown Brand " + brand);
359 req = httpClient.POST(url);
361 req = httpClient.newRequest(url);
364 req.header(HttpHeader.AUTHORIZATION, myBMWTokenHandler.getToken().getBearerToken());
365 req.header(HTTPConstants.HEADER_X_USER_AGENT, String.format(BimmerConstants.X_USER_AGENT, brand.toLowerCase(),
366 APP_VERSIONS.get(bridgeConfiguration.region), bridgeConfiguration.region));
367 req.header(HttpHeader.ACCEPT_LANGUAGE, bridgeConfiguration.language);
368 req.header(HttpHeader.ACCEPT, contentType);
369 req.header(HTTPConstants.HEADER_BMW_VIN, vin);
372 ContentResponse response = req.timeout(HTTPConstants.HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send();
373 if (response.getStatus() >= 300) {
374 responseByteArray = "".getBytes();
375 NetworkException exception = new NetworkException(url, response.getStatus(),
376 ResponseContentAnonymizer.anonymizeResponseContent(response.getContentAsString()), body);
377 logResponse(ResponseContentAnonymizer.replaceVin(exception.getUrl(), vin), exception.getReason(),
378 ResponseContentAnonymizer.anonymizeResponseContent(body));
381 responseByteArray = response.getContent();
383 // don't print images
384 if (!HTTPConstants.CONTENT_TYPE_IMAGE.equals(contentType)) {
385 logResponse(ResponseContentAnonymizer.replaceVin(url, vin),
386 ResponseContentAnonymizer.anonymizeResponseContent(response.getContentAsString()),
387 ResponseContentAnonymizer.anonymizeResponseContent(body));
390 } catch (TimeoutException | ExecutionException e) {
391 logResponse(ResponseContentAnonymizer.replaceVin(url, vin), e.getMessage(),
392 ResponseContentAnonymizer.anonymizeResponseContent(vin));
393 throw new NetworkException(url, -1, null, body, e);
394 } catch (InterruptedException e) {
395 Thread.currentThread().interrupt();
396 logResponse(ResponseContentAnonymizer.replaceVin(url, vin), e.getMessage(),
397 ResponseContentAnonymizer.anonymizeResponseContent(vin));
398 throw new NetworkException(url, -1, null, body, e);
401 return responseByteArray;
404 private void logResponse(@Nullable String url, @Nullable String fingerprint, @Nullable String body) {
405 logger.debug("###### Request URL - BEGIN ######");
406 logger.debug("{}", url);
407 logger.debug("###### Request Body - BEGIN ######");
408 logger.debug("{}", body);
409 logger.debug("###### Response Data - BEGIN ######");
410 logger.debug("{}", fingerprint);
411 logger.debug("###### Response Data - END ######");