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