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