2 * Copyright (c) 2010-2022 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.renault.internal.api;
15 import java.util.concurrent.ExecutionException;
16 import java.util.concurrent.TimeoutException;
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.RenaultActionException;
30 import org.openhab.binding.renault.internal.api.exceptions.RenaultException;
31 import org.openhab.binding.renault.internal.api.exceptions.RenaultForbiddenException;
32 import org.openhab.binding.renault.internal.api.exceptions.RenaultNotImplementedException;
33 import org.openhab.binding.renault.internal.api.exceptions.RenaultUpdateException;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
37 import com.google.gson.JsonArray;
38 import com.google.gson.JsonElement;
39 import com.google.gson.JsonObject;
40 import com.google.gson.JsonParseException;
41 import com.google.gson.JsonParser;
44 * This is a Java version of the python renault-api project developed here:
45 * https://github.com/hacf-fr/renault-api
47 * @author Doug Culnane - Initial contribution
50 public class MyRenaultHttpSession {
52 private static final String CHARGING_MODE_SCHEDULE = "schedule_mode";
53 private static final String CHARGING_MODE_ALWAYS = "always_charging";
55 private RenaultConfiguration config;
56 private HttpClient httpClient;
57 private Constants constants;
58 private @Nullable String kamereonToken;
59 private @Nullable String kamereonaccountId;
60 private @Nullable String cookieValue;
61 private @Nullable String personId;
62 private @Nullable String gigyaDataCenter;
63 private @Nullable String jwt;
65 private final Logger logger = LoggerFactory.getLogger(MyRenaultHttpSession.class);
67 public MyRenaultHttpSession(RenaultConfiguration config, HttpClient httpClient) {
69 this.httpClient = httpClient;
70 this.constants = new Constants(config.locale);
73 public void initSesssion(Car car) throws RenaultException, RenaultForbiddenException, RenaultUpdateException,
74 RenaultNotImplementedException, InterruptedException, ExecutionException, TimeoutException {
80 final String imageURL = car.getImageURL();
81 if (imageURL == null) {
86 private void login() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
87 Fields fields = new Fields();
88 fields.add("ApiKey", this.constants.getGigyaApiKey());
89 fields.add("loginID", config.myRenaultUsername);
90 fields.add("password", config.myRenaultPassword);
91 final String url = this.constants.getGigyaRootUrl() + "/accounts.login";
92 ContentResponse response = httpClient.FORM(url, fields);
93 if (HttpStatus.OK_200 == response.getStatus()) {
94 if (logger.isTraceEnabled()) {
95 logger.trace("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(),
96 response.getReason(), response.getContentAsString());
99 JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
100 JsonObject sessionInfoJson = responseJson.getAsJsonObject("sessionInfo");
101 if (sessionInfoJson != null) {
102 JsonElement element = sessionInfoJson.get("cookieValue");
103 if (element != null) {
104 cookieValue = element.getAsString();
105 logger.debug("Cookie: {}", cookieValue);
108 } catch (JsonParseException | ClassCastException | IllegalStateException e) {
109 throw new RenaultException("Login Error: cookie value not found in JSON response");
111 if (cookieValue == null) {
112 logger.warn("Login Error: cookie value not found! Response: {}", response.getContentAsString());
113 throw new RenaultException("Login Error: cookie value not found in JSON response");
116 logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
117 response.getContentAsString());
118 throw new RenaultException("Login Error: " + response.getReason());
122 private void getAccountInfo() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
123 Fields fields = new Fields();
124 fields.add("ApiKey", this.constants.getGigyaApiKey());
125 fields.add("login_token", cookieValue);
126 final String url = this.constants.getGigyaRootUrl() + "/accounts.getAccountInfo";
127 ContentResponse response = httpClient.FORM(url, fields);
128 if (HttpStatus.OK_200 == response.getStatus()) {
129 if (logger.isTraceEnabled()) {
130 logger.trace("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(),
131 response.getReason(), response.getContentAsString());
134 JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
135 JsonObject dataJson = responseJson.getAsJsonObject("data");
136 if (dataJson != null) {
137 JsonElement element1 = dataJson.get("personId");
138 JsonElement element2 = dataJson.get("gigyaDataCenter");
139 if (element1 != null && element2 != null) {
140 personId = element1.getAsString();
141 gigyaDataCenter = element2.getAsString();
142 logger.debug("personId ID: {} gigyaDataCenter: {}", personId, gigyaDataCenter);
145 } catch (JsonParseException | ClassCastException | IllegalStateException e) {
146 throw new RenaultException(
147 "Get Account Info Error: personId or gigyaDataCenter value not found in JSON response");
150 logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
151 response.getContentAsString());
152 throw new RenaultException("Get Account Info Error: " + response.getReason());
156 private void getJWT() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
157 Fields fields = new Fields();
158 fields.add("ApiKey", this.constants.getGigyaApiKey());
159 fields.add("login_token", cookieValue);
160 fields.add("fields", "data.personId,data.gigyaDataCenter");
161 fields.add("personId", personId);
162 fields.add("gigyaDataCenter", gigyaDataCenter);
163 final String url = this.constants.getGigyaRootUrl() + "/accounts.getJWT";
164 ContentResponse response = this.httpClient.FORM(url, fields);
165 if (HttpStatus.OK_200 == response.getStatus()) {
166 if (logger.isTraceEnabled()) {
167 logger.trace("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(),
168 response.getReason(), response.getContentAsString());
171 JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
172 JsonElement element = responseJson.get("id_token");
173 if (element != null) {
174 jwt = element.getAsString();
175 logger.debug("GigyaApi jwt: {} ", jwt);
177 } catch (JsonParseException | ClassCastException | IllegalStateException e) {
178 throw new RenaultException("Get JWT Error: jwt value not found in JSON response");
181 logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
182 response.getContentAsString());
183 throw new RenaultException("Get JWT Error: " + response.getReason());
187 private void getAccountID()
188 throws RenaultException, RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
189 JsonObject responseJson = getKamereonResponse(
190 "/commerce/v1/persons/" + personId + "?country=" + getCountry(config));
191 if (responseJson != null) {
192 JsonArray accounts = responseJson.getAsJsonArray("accounts");
193 for (int i = 0; i < accounts.size(); i++) {
194 if (accounts.get(i).getAsJsonObject().get("accountType").getAsString().equals("MYRENAULT")) {
195 kamereonaccountId = accounts.get(i).getAsJsonObject().get("accountId").getAsString();
200 if (kamereonaccountId == null) {
201 throw new RenaultException("Can not get Kamereon MyRenault Account ID!");
205 public void getVehicle(Car car)
206 throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
207 JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId + "/vehicles/"
208 + config.vin + "/details?country=" + getCountry(config));
209 if (responseJson != null) {
210 car.setDetails(responseJson);
214 public void getBatteryStatus(Car car)
215 throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
216 JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
217 + "/kamereon/kca/car-adapter/v2/cars/" + config.vin + "/battery-status?country=" + getCountry(config));
218 if (responseJson != null) {
219 car.setBatteryStatus(responseJson);
223 public void getHvacStatus(Car car)
224 throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
225 JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
226 + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/hvac-status?country=" + getCountry(config));
227 if (responseJson != null) {
228 car.setHVACStatus(responseJson);
232 public void getCockpit(Car car)
233 throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
234 JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
235 + "/kamereon/kca/car-adapter/v2/cars/" + config.vin + "/cockpit?country=" + getCountry(config));
236 if (responseJson != null) {
237 car.setCockpit(responseJson);
241 public void getLocation(Car car)
242 throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
243 JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
244 + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/location?country=" + getCountry(config));
245 if (responseJson != null) {
246 car.setLocation(responseJson);
250 public void getLockStatus(Car car)
251 throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
252 JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
253 + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/lock-status?country=" + getCountry(config));
254 if (responseJson != null) {
255 car.setLockStatus(responseJson);
259 public void actionHvacOn(double hvacTargetTemperature)
260 throws RenaultForbiddenException, RenaultNotImplementedException, RenaultActionException {
261 final String path = "/commerce/v1/accounts/" + kamereonaccountId + "/kamereon/kca/car-adapter/v1/cars/"
262 + config.vin + "/actions/hvac-start?country=" + getCountry(config);
263 postKamereonRequest(path,
264 "{\"data\":{\"type\":\"HvacStart\",\"attributes\":{\"action\":\"start\",\"targetTemperature\":\""
265 + hvacTargetTemperature + "\"}}}");
268 public void actionChargeMode(ChargingMode mode)
269 throws RenaultForbiddenException, RenaultNotImplementedException, RenaultActionException {
270 final String apiMode = ChargingMode.SCHEDULE_MODE.equals(mode) ? CHARGING_MODE_SCHEDULE : CHARGING_MODE_ALWAYS;
271 final String path = "/commerce/v1/accounts/" + kamereonaccountId + "/kamereon/kca/car-adapter/v1/cars/"
272 + config.vin + "/actions/charge-mode?country=" + getCountry(config);
273 postKamereonRequest(path,
274 "{\"data\":{\"type\":\"ChargeMode\",\"attributes\":{\"action\":\"" + apiMode + "\"}}}");
277 private void postKamereonRequest(final String path, final String content)
278 throws RenaultForbiddenException, RenaultNotImplementedException, RenaultActionException {
279 Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.POST)
280 .header("Content-type", "application/vnd.api+json").header("apikey", this.config.kamereonApiKey)
281 .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt)
282 .content(new StringContentProvider(content, "utf-8"));
284 ContentResponse response = request.send();
285 logKamereonCall(request, response);
286 checkResponse(response);
287 } catch (InterruptedException e) {
288 logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
289 Thread.currentThread().interrupt();
290 } catch (JsonParseException | TimeoutException | ExecutionException e) {
291 throw new RenaultActionException(e.toString());
295 private @Nullable JsonObject getKamereonResponse(String path)
296 throws RenaultForbiddenException, RenaultNotImplementedException, RenaultUpdateException {
297 Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.GET)
298 .header("Content-type", "application/vnd.api+json").header("apikey", this.config.kamereonApiKey)
299 .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
301 ContentResponse response = request.send();
302 logKamereonCall(request, response);
303 if (HttpStatus.OK_200 == response.getStatus()) {
304 return JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
306 checkResponse(response);
307 } catch (InterruptedException e) {
308 logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
309 Thread.currentThread().interrupt();
310 } catch (JsonParseException | TimeoutException | ExecutionException e) {
311 throw new RenaultUpdateException(e.toString());
316 private void logKamereonCall(Request request, ContentResponse response) {
317 if (HttpStatus.OK_200 == response.getStatus()) {
318 if (logger.isTraceEnabled()) {
319 logger.trace("Kamereon Request: {} Response: [{}] {}\n{}", request.getURI().toString(),
320 response.getStatus(), response.getReason(), response.getContentAsString());
323 logger.warn("Kamereon Request: {} Response: [{}] {}\n{}", request.getURI().toString(), response.getStatus(),
324 response.getReason(), response.getContentAsString());
328 private void checkResponse(ContentResponse response)
329 throws RenaultForbiddenException, RenaultNotImplementedException {
330 switch (response.getStatus()) {
331 case HttpStatus.FORBIDDEN_403:
332 throw new RenaultForbiddenException(
333 "Kamereon request forbidden! Ensure the car is paired in your MyRenault App.");
334 case HttpStatus.NOT_FOUND_404:
335 throw new RenaultNotImplementedException("Kamereon service not found");
336 case HttpStatus.NOT_IMPLEMENTED_501:
337 throw new RenaultNotImplementedException("Kamereon request not implemented");
338 case HttpStatus.BAD_GATEWAY_502:
339 throw new RenaultNotImplementedException("Kamereon request failed");
345 private String getCountry(RenaultConfiguration config) {
346 String country = "XX";
347 if (config.locale.length() == 5) {
348 country = config.locale.substring(3);