]> git.basschouten.com Git - openhab-addons.git/blob
debad84e2ba226f20af04ed6e46c82754afdf643
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mybmw.internal.handler.backend;
14
15 import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.APP_VERSIONS;
16
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;
24
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;
51
52 /**
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
59  *
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
64  */
65 @NonNullByDefault
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;
71
72     /**
73      * URLs taken from
74      * https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py
75      */
76     private final String vehicleUrl;
77     private final String vehicleStateUrl;
78     private final String remoteCommandUrl;
79     private final String remoteStatusUrl;
80
81     public MyBMWHttpProxy(HttpClientFactory httpClientFactory, MyBMWBridgeConfiguration bridgeConfiguration) {
82         logger.trace("MyBMWHttpProxy - initialize");
83         httpClient = httpClientFactory.getCommonHttpClient();
84
85         myBMWTokenHandler = new MyBMWTokenController(bridgeConfiguration, httpClient);
86
87         this.bridgeConfiguration = bridgeConfiguration;
88
89         vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region)
90                 + BimmerConstants.API_VEHICLES;
91
92         vehicleStateUrl = vehicleUrl + "/state";
93
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");
98     }
99
100     @Override
101     public void setBridgeConfiguration(MyBMWBridgeConfiguration bridgeConfiguration) {
102         this.bridgeConfiguration = bridgeConfiguration;
103     }
104
105     /**
106      * requests all vehicles
107      * 
108      * @return list of vehicles
109      */
110     public List<@NonNull Vehicle> requestVehicles() throws NetworkException {
111         List<@NonNull Vehicle> vehicles = new ArrayList<>();
112         List<@NonNull VehicleBase> vehiclesBase = requestVehiclesBase();
113
114         for (VehicleBase vehicleBase : vehiclesBase) {
115             VehicleStateContainer vehicleState = requestVehicleState(vehicleBase.getVin(),
116                     vehicleBase.getAttributes().getBrand());
117
118             Vehicle vehicle = new Vehicle();
119             vehicle.setVehicleBase(vehicleBase);
120             vehicle.setVehicleState(vehicleState);
121             vehicles.add(vehicle);
122         }
123
124         return vehicles;
125     }
126
127     /**
128      * request all vehicles for one specific brand and their state
129      *
130      * @param brand
131      * @return the vehicles of one brand
132      */
133     public List<VehicleBase> requestVehiclesBase(String brand) throws NetworkException {
134         String vehicleResponseString = requestVehiclesBaseJson(brand);
135         return JsonStringDeserializer.getVehicleBaseList(vehicleResponseString);
136     }
137
138     /**
139      * request the raw JSON for the vehicle
140      *
141      * @param brand
142      * @return the base vehicle information as JSON string
143      */
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;
148     }
149
150     /**
151      * request vehicles for all possible brands
152      *
153      * @return the list of vehicles
154      */
155     public List<VehicleBase> requestVehiclesBase() throws NetworkException {
156         List<VehicleBase> vehicles = new ArrayList<>();
157
158         for (String brand : BimmerConstants.REQUESTED_BRANDS) {
159             try {
160                 vehicles.addAll(requestVehiclesBase(brand));
161
162                 Thread.sleep(10000);
163             } catch (Exception e) {
164                 logger.warn("error retrieving the base vehicles for brand {}: {}", brand, e.getMessage());
165             }
166         }
167
168         return vehicles;
169     }
170
171     /**
172      * request the vehicle image
173      *
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
178      */
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);
183     }
184
185     /**
186      * request the state for one specific vehicle
187      *
188      * @param vin
189      * @param brand
190      * @return the vehicle state
191      */
192     public VehicleStateContainer requestVehicleState(String vin, String brand) throws NetworkException {
193         String vehicleStateResponseString = requestVehicleStateJson(vin, brand);
194         return JsonStringDeserializer.getVehicleState(vehicleStateResponseString);
195     }
196
197     /**
198      * request the raw state as JSON for one specific vehicle
199      *
200      * @param vin
201      * @param brand
202      * @return the vehicle state as string
203      */
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;
208     }
209
210     /**
211      * request charge statistics for electric vehicles
212      * 
213      * @param vin
214      * @param brand
215      * @return the charge statistics
216      */
217     public ChargingStatisticsContainer requestChargeStatistics(String vin, String brand) throws NetworkException {
218         String chargeStatisticsResponseString = requestChargeStatisticsJson(vin, brand);
219         return JsonStringDeserializer.getChargingStatistics(new String(chargeStatisticsResponseString));
220     }
221
222     /**
223      * request charge statistics for electric vehicles as JSON
224      * 
225      * @param vin
226      * @param brand
227      * @return the charge statistics as JSON string
228      */
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;
239     }
240
241     /**
242      * request charge sessions for electric vehicles
243      *
244      * @param vin
245      * @param brand
246      * @return the charge sessions
247      */
248     public ChargingSessionsContainer requestChargeSessions(String vin, String brand) throws NetworkException {
249         String chargeSessionsResponseString = requestChargeSessionsJson(vin, brand);
250         return JsonStringDeserializer.getChargingSessions(chargeSessionsResponseString);
251     }
252
253     /**
254      * request charge sessions for electric vehicles as JSON string
255      *
256      * @param vin
257      * @param brand
258      * @return the charge sessions as JSON string
259      */
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;
271     }
272
273     /**
274      * execute a remote service call
275      *
276      * @param vin
277      * @param brand
278      * @param service the service which should be executed
279      * @return the running service execution for status checks
280      */
281     public ExecutionStatusContainer executeRemoteServiceCall(String vin, String brand, RemoteService service)
282             throws NetworkException {
283         String executionUrl = remoteCommandUrl + vin + "/" + service.getCommand();
284
285         byte[] response = post(executionUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON, service.getBody());
286
287         return JsonStringDeserializer.getExecutionStatus(new String(response));
288     }
289
290     /**
291      * check the status of a service call
292      *
293      * @param brand
294      * @param eventid the ID of the currently running service execution
295      * @return the running service execution for status checks
296      */
297     public ExecutionStatusContainer executeRemoteServiceStatusCall(String brand, String eventId)
298             throws NetworkException {
299         String executionUrl = remoteStatusUrl + Constants.QUESTION + "eventId=" + eventId;
300
301         byte[] response = post(executionUrl, brand, null, HTTPConstants.CONTENT_TYPE_JSON, null);
302
303         return JsonStringDeserializer.getExecutionStatus(new String(response));
304     }
305
306     /**
307      * prepares a GET request to the backend
308      *
309      * @param url
310      * @param brand
311      * @param vin
312      * @param contentType
313      * @return byte array of the response body
314      */
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);
318     }
319
320     /**
321      * prepares a POST request to the backend
322      *
323      * @param url
324      * @param brand
325      * @param vin
326      * @param contentType
327      * @param body
328      * @return byte array of the response body
329      */
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);
333     }
334
335     /**
336      * executes the real call to the backend
337      *
338      * @param url
339      * @param post boolean value indicating if it is a post request
340      * @param brand
341      * @param vin
342      * @param contentType
343      * @param body
344      * @return byte array of the response body
345      */
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();
349
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);
354         }
355
356         final Request req;
357
358         if (post) {
359             req = httpClient.POST(url);
360         } else {
361             req = httpClient.newRequest(url);
362         }
363
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);
370
371         try {
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));
379                 throw exception;
380             } else {
381                 responseByteArray = response.getContent();
382
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));
388                 }
389             }
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);
399         }
400
401         return responseByteArray;
402     }
403
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 ######");
412     }
413 }