]> git.basschouten.com Git - openhab-addons.git/blob
fa8f0c06977cdb22a10d9673893122360722003e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.renault.internal.api;
14
15 import java.util.concurrent.ExecutionException;
16 import java.util.concurrent.TimeoutException;
17
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.eclipse.jetty.client.HttpClient;
21 import org.eclipse.jetty.client.api.ContentResponse;
22 import org.eclipse.jetty.client.api.Request;
23 import org.eclipse.jetty.client.util.StringContentProvider;
24 import org.eclipse.jetty.http.HttpMethod;
25 import org.eclipse.jetty.http.HttpStatus;
26 import org.eclipse.jetty.util.Fields;
27 import org.openhab.binding.renault.internal.RenaultConfiguration;
28 import org.openhab.binding.renault.internal.api.Car.ChargingMode;
29 import org.openhab.binding.renault.internal.api.exceptions.RenaultException;
30 import org.openhab.binding.renault.internal.api.exceptions.RenaultForbiddenException;
31 import org.openhab.binding.renault.internal.api.exceptions.RenaultNotImplementedException;
32 import org.openhab.binding.renault.internal.api.exceptions.RenaultUpdateException;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 import com.google.gson.JsonArray;
37 import com.google.gson.JsonElement;
38 import com.google.gson.JsonObject;
39 import com.google.gson.JsonParseException;
40 import com.google.gson.JsonParser;
41
42 /**
43  * This is a Java version of the python renault-api project developed here:
44  * https://github.com/hacf-fr/renault-api
45  *
46  * @author Doug Culnane - Initial contribution
47  */
48 @NonNullByDefault
49 public class MyRenaultHttpSession {
50
51     private static final String CHARGING_MODE_SCHEDULE = "schedule_mode";
52     private static final String CHARGING_MODE_ALWAYS = "always_charging";
53
54     private RenaultConfiguration config;
55     private HttpClient httpClient;
56     private Constants constants;
57     private @Nullable String kamereonToken;
58     private @Nullable String kamereonaccountId;
59     private @Nullable String cookieValue;
60     private @Nullable String personId;
61     private @Nullable String gigyaDataCenter;
62     private @Nullable String jwt;
63
64     private final Logger logger = LoggerFactory.getLogger(MyRenaultHttpSession.class);
65
66     public MyRenaultHttpSession(RenaultConfiguration config, HttpClient httpClient) {
67         this.config = config;
68         this.httpClient = httpClient;
69         this.constants = new Constants(config.locale);
70     }
71
72     public void initSesssion(Car car) throws RenaultException, RenaultForbiddenException, RenaultUpdateException,
73             RenaultNotImplementedException, InterruptedException, ExecutionException, TimeoutException {
74         login();
75         getAccountInfo();
76         getJWT();
77         getAccountID();
78
79         final String imageURL = car.getImageURL();
80         if (imageURL == null) {
81             getVehicle(car);
82         }
83     }
84
85     private void login() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
86         Fields fields = new Fields();
87         fields.add("ApiKey", this.constants.getGigyaApiKey());
88         fields.add("loginID", config.myRenaultUsername);
89         fields.add("password", config.myRenaultPassword);
90         logger.debug("URL: {}/accounts.login", this.constants.getGigyaRootUrl());
91         ContentResponse response = httpClient.FORM(this.constants.getGigyaRootUrl() + "/accounts.login", fields);
92         if (HttpStatus.OK_200 == response.getStatus()) {
93             try {
94                 JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
95                 JsonObject sessionInfoJson = responseJson.getAsJsonObject("sessionInfo");
96                 if (sessionInfoJson != null) {
97                     JsonElement element = sessionInfoJson.get("cookieValue");
98                     if (element != null) {
99                         cookieValue = element.getAsString();
100                         logger.debug("Cookie: {}", cookieValue);
101                     }
102                 }
103             } catch (JsonParseException | ClassCastException | IllegalStateException e) {
104                 throw new RenaultException("Login Error: cookie value not found in JSON response");
105             }
106             if (cookieValue == null) {
107                 logger.warn("Login Error: cookie value not found! Response: [{}] {}\n{}", response.getStatus(),
108                         response.getReason(), response.getContentAsString());
109             }
110         } else {
111             logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(),
112                     response.getContentAsString());
113             throw new RenaultException("Login Error: " + response.getReason());
114         }
115     }
116
117     private void getAccountInfo() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
118         Fields fields = new Fields();
119         fields.add("ApiKey", this.constants.getGigyaApiKey());
120         fields.add("login_token", cookieValue);
121         ContentResponse response = httpClient.FORM(this.constants.getGigyaRootUrl() + "/accounts.getAccountInfo",
122                 fields);
123         if (HttpStatus.OK_200 == response.getStatus()) {
124             try {
125                 JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
126                 JsonObject dataJson = responseJson.getAsJsonObject("data");
127                 if (dataJson != null) {
128                     JsonElement element1 = dataJson.get("personId");
129                     JsonElement element2 = dataJson.get("gigyaDataCenter");
130                     if (element1 != null && element2 != null) {
131                         personId = element1.getAsString();
132                         gigyaDataCenter = element2.getAsString();
133                         logger.debug("personId ID: {} gigyaDataCenter: {}", personId, gigyaDataCenter);
134                     }
135                 }
136             } catch (JsonParseException | ClassCastException | IllegalStateException e) {
137                 throw new RenaultException(
138                         "Get Account Info Error: personId or gigyaDataCenter value not found in JSON response");
139             }
140         } else {
141             logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(),
142                     response.getContentAsString());
143             throw new RenaultException("Get Account Info Error: " + response.getReason());
144         }
145     }
146
147     private void getJWT() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
148         Fields fields = new Fields();
149         fields.add("ApiKey", this.constants.getGigyaApiKey());
150         fields.add("login_token", cookieValue);
151         fields.add("fields", "data.personId,data.gigyaDataCenter");
152         fields.add("personId", personId);
153         fields.add("gigyaDataCenter", gigyaDataCenter);
154         ContentResponse response = this.httpClient.FORM(this.constants.getGigyaRootUrl() + "/accounts.getJWT", fields);
155         if (HttpStatus.OK_200 == response.getStatus()) {
156             try {
157                 JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
158                 JsonElement element = responseJson.get("id_token");
159                 if (element != null) {
160                     jwt = element.getAsString();
161                     logger.debug("jwt: {} ", jwt);
162                 }
163             } catch (JsonParseException | ClassCastException | IllegalStateException e) {
164                 throw new RenaultException("Get JWT Error: jwt value not found in JSON response");
165             }
166         } else {
167             logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(),
168                     response.getContentAsString());
169             throw new RenaultException("Get JWT Error: " + response.getReason());
170         }
171     }
172
173     private void getAccountID()
174             throws RenaultException, RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
175         JsonObject responseJson = getKamereonResponse(
176                 "/commerce/v1/persons/" + personId + "?country=" + getCountry(config));
177         if (responseJson != null) {
178             JsonArray accounts = responseJson.getAsJsonArray("accounts");
179             for (int i = 0; i < accounts.size(); i++) {
180                 if (accounts.get(i).getAsJsonObject().get("accountType").getAsString().equals("MYRENAULT")) {
181                     kamereonaccountId = accounts.get(i).getAsJsonObject().get("accountId").getAsString();
182                     break;
183                 }
184             }
185         }
186         if (kamereonaccountId == null) {
187             throw new RenaultException("Can not get Kamereon MyRenault Account ID!");
188         }
189     }
190
191     public void getVehicle(Car car)
192             throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
193         JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId + "/vehicles/"
194                 + config.vin + "/details?country=" + getCountry(config));
195         if (responseJson != null) {
196             car.setDetails(responseJson);
197         }
198     }
199
200     public void getBatteryStatus(Car car)
201             throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
202         JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
203                 + "/kamereon/kca/car-adapter/v2/cars/" + config.vin + "/battery-status?country=" + getCountry(config));
204         if (responseJson != null) {
205             car.setBatteryStatus(responseJson);
206         }
207     }
208
209     public void getHvacStatus(Car car)
210             throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
211         JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
212                 + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/hvac-status?country=" + getCountry(config));
213         if (responseJson != null) {
214             car.setHVACStatus(responseJson);
215         }
216     }
217
218     public void getCockpit(Car car)
219             throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
220         JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
221                 + "/kamereon/kca/car-adapter/v2/cars/" + config.vin + "/cockpit?country=" + getCountry(config));
222         if (responseJson != null) {
223             car.setCockpit(responseJson);
224         }
225     }
226
227     public void getLocation(Car car)
228             throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
229         JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
230                 + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/location?country=" + getCountry(config));
231         if (responseJson != null) {
232             car.setLocation(responseJson);
233         }
234     }
235
236     public void actionHvacOn(double hvacTargetTemperature)
237             throws RenaultForbiddenException, RenaultNotImplementedException {
238         Request request = httpClient
239                 .newRequest(this.constants.getKamereonRootUrl() + "/commerce/v1/accounts/" + kamereonaccountId
240                         + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/actions/hvac-start?country="
241                         + getCountry(config))
242                 .method(HttpMethod.POST).header("Content-type", "application/vnd.api+json")
243                 .header("apikey", this.config.kamereonApiKey)
244                 .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
245         request.content(new StringContentProvider(
246                 "{\"data\":{\"type\":\"HvacStart\",\"attributes\":{\"action\":\"start\",\"targetTemperature\":\""
247                         + hvacTargetTemperature + "\"}}}",
248                 "utf-8"));
249         try {
250             ContentResponse response = request.send();
251             logger.debug("Kamereon Response HVAC ON: {}", response.getContentAsString());
252             if (HttpStatus.OK_200 != response.getStatus()) {
253                 logger.warn("Kamereon Response: [{}] {} {}", response.getStatus(), response.getReason(),
254                         response.getContentAsString());
255                 if (HttpStatus.FORBIDDEN_403 == response.getStatus()) {
256                     throw new RenaultForbiddenException(
257                             "Kamereon Response Forbidden! Ensure the car is paired in your MyRenault App.");
258                 } else if (HttpStatus.NOT_IMPLEMENTED_501 == response.getStatus()) {
259                     throw new RenaultNotImplementedException(
260                             "Kamereon Service Not Implemented: [" + response.getStatus() + "] " + response.getReason());
261                 }
262             }
263         } catch (InterruptedException e) {
264             logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
265             Thread.currentThread().interrupt();
266         } catch (JsonParseException | TimeoutException | ExecutionException e) {
267             logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
268         }
269     }
270
271     public void actionChargeMode(ChargingMode mode) throws RenaultForbiddenException, RenaultNotImplementedException {
272         Request request = httpClient
273                 .newRequest(this.constants.getKamereonRootUrl() + "/commerce/v1/accounts/" + kamereonaccountId
274                         + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/actions/charge-mode?country="
275                         + getCountry(config))
276                 .method(HttpMethod.POST).header("Content-type", "application/vnd.api+json")
277                 .header("apikey", this.config.kamereonApiKey)
278                 .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
279
280         final String apiMode = ChargingMode.SCHEDULE_MODE.equals(mode) ? CHARGING_MODE_SCHEDULE : CHARGING_MODE_ALWAYS;
281         request.content(new StringContentProvider(
282                 "{\"data\":{\"type\":\"ChargeMode\",\"attributes\":{\"action\":\"" + apiMode + "\"}}}", "utf-8"));
283         try {
284             ContentResponse response = request.send();
285             logger.debug("Kamereon Response set ChargeMode: {}", response.getContentAsString());
286             if (HttpStatus.OK_200 != response.getStatus()) {
287                 logger.warn("Kamereon Response: [{}] {} {}", response.getStatus(), response.getReason(),
288                         response.getContentAsString());
289                 if (HttpStatus.FORBIDDEN_403 == response.getStatus()) {
290                     throw new RenaultForbiddenException(
291                             "Kamereon Response Forbidden! Ensure the car is paired in your MyRenault App.");
292                 } else if (HttpStatus.NOT_IMPLEMENTED_501 == response.getStatus()) {
293                     throw new RenaultNotImplementedException(
294                             "Kamereon Service Not Implemented: [" + response.getStatus() + "] " + response.getReason());
295                 }
296             }
297         } catch (InterruptedException e) {
298             logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
299             Thread.currentThread().interrupt();
300         } catch (JsonParseException | TimeoutException | ExecutionException e) {
301             logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
302         }
303     }
304
305     private @Nullable JsonObject getKamereonResponse(String path)
306             throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
307         Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.GET)
308                 .header("Content-type", "application/vnd.api+json").header("apikey", this.config.kamereonApiKey)
309                 .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
310         try {
311             ContentResponse response = request.send();
312             if (HttpStatus.OK_200 == response.getStatus()) {
313                 logger.debug("Kamereon Response: {}", response.getContentAsString());
314                 return JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
315             } else {
316                 logger.warn("Kamereon Response: [{}] {} {}", response.getStatus(), response.getReason(),
317                         response.getContentAsString());
318                 if (HttpStatus.FORBIDDEN_403 == response.getStatus()) {
319                     throw new RenaultForbiddenException(
320                             "Kamereon Response Forbidden! Ensure the car is paired in your MyRenault App.");
321                 } else if (HttpStatus.NOT_IMPLEMENTED_501 == response.getStatus()) {
322                     throw new RenaultNotImplementedException(
323                             "Kamereon Service Not Implemented: [" + response.getStatus() + "] " + response.getReason());
324                 } else {
325                     throw new RenaultUpdateException(
326                             "Kamereon Response Failed! Error: [" + response.getStatus() + "] " + response.getReason());
327                 }
328             }
329         } catch (InterruptedException e) {
330             logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
331             Thread.currentThread().interrupt();
332         } catch (JsonParseException | TimeoutException | ExecutionException e) {
333             logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
334         }
335         return null;
336     }
337
338     private String getCountry(RenaultConfiguration config) {
339         String country = "XX";
340         if (config.locale.length() == 5) {
341             country = config.locale.substring(3);
342         }
343         return country;
344     }
345 }