2 * Copyright (c) 2010-2023 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.TimeUnit;
17 import java.util.concurrent.TimeoutException;
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;
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;
46 * This is a Java version of the python renault-api project developed here:
47 * https://github.com/hacf-fr/renault-api
49 * @author Doug Culnane - Initial contribution
52 public class MyRenaultHttpSession {
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;
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;
68 private final Logger logger = LoggerFactory.getLogger(MyRenaultHttpSession.class);
70 public MyRenaultHttpSession(RenaultConfiguration config, HttpClient httpClient) {
72 this.httpClient = httpClient;
73 this.constants = new Constants(config.locale);
76 public void initSesssion(Car car) throws RenaultException, RenaultForbiddenException, RenaultUpdateException,
77 RenaultNotImplementedException, InterruptedException, ExecutionException, TimeoutException {
83 final String imageURL = car.getImageURL();
84 if (imageURL == null) {
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());
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);
111 } catch (JsonParseException | ClassCastException | IllegalStateException e) {
112 throw new RenaultException("Login Error: cookie value not found in JSON response");
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");
119 logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
120 response.getContentAsString());
121 throw new RenaultException("Login Error: " + response.getReason());
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());
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);
148 } catch (JsonParseException | ClassCastException | IllegalStateException e) {
149 throw new RenaultException(
150 "Get Account Info Error: personId or gigyaDataCenter value not found in JSON response");
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());
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());
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);
180 } catch (JsonParseException | ClassCastException | IllegalStateException e) {
181 throw new RenaultException("Get JWT Error: jwt value not found in JSON response");
184 logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
185 response.getContentAsString());
186 throw new RenaultException("Get JWT Error: " + response.getReason());
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();
203 if (kamereonaccountId == null) {
204 throw new RenaultException("Can not get Kamereon " + config.accountType + " Account ID!");
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);
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);
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);
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);
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);
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);
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 + "\"}}}");
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 + "\"}}}");
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 + "\"}}}");
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"));
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());
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);
315 ContentResponse response = request.send();
316 logKamereonCall(request, response);
317 if (HttpStatus.OK_200 == response.getStatus()) {
318 return JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
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());
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());
337 logger.debug("Kamereon Request: {} Response: [{}] {}\n{}", request.getURI().toString(),
338 response.getStatus(), response.getReason(), response.getContentAsString());
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");
359 private String getCountry(RenaultConfiguration config) {
360 String country = "XX";
361 if (config.locale.length() == 5) {
362 country = config.locale.substring(3);