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.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.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;
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;
45 * This is a Java version of the python renault-api project developed here:
46 * https://github.com/hacf-fr/renault-api
48 * @author Doug Culnane - Initial contribution
51 public class MyRenaultHttpSession {
53 private static final String CHARGING_MODE_SCHEDULE = "schedule_mode";
54 private static final String CHARGING_MODE_ALWAYS = "always_charging";
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;
66 private final Logger logger = LoggerFactory.getLogger(MyRenaultHttpSession.class);
68 public MyRenaultHttpSession(RenaultConfiguration config, HttpClient httpClient) {
70 this.httpClient = httpClient;
71 this.constants = new Constants(config.locale);
74 public void initSesssion(Car car) throws RenaultException, RenaultForbiddenException, RenaultUpdateException,
75 RenaultNotImplementedException, InterruptedException, ExecutionException, TimeoutException {
81 final String imageURL = car.getImageURL();
82 if (imageURL == null) {
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());
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);
109 } catch (JsonParseException | ClassCastException | IllegalStateException e) {
110 throw new RenaultException("Login Error: cookie value not found in JSON response");
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");
117 logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
118 response.getContentAsString());
119 throw new RenaultException("Login Error: " + response.getReason());
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());
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);
146 } catch (JsonParseException | ClassCastException | IllegalStateException e) {
147 throw new RenaultException(
148 "Get Account Info Error: personId or gigyaDataCenter value not found in JSON response");
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());
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());
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);
178 } catch (JsonParseException | ClassCastException | IllegalStateException e) {
179 throw new RenaultException("Get JWT Error: jwt value not found in JSON response");
182 logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
183 response.getContentAsString());
184 throw new RenaultException("Get JWT Error: " + response.getReason());
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("MYRENAULT")) {
196 kamereonaccountId = accounts.get(i).getAsJsonObject().get("accountId").getAsString();
201 if (kamereonaccountId == null) {
202 throw new RenaultException("Can not get Kamereon MyRenault Account ID!");
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);
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);
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);
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);
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);
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);
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 + "\"}}}");
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 + "\"}}}");
278 public void actionPause(boolean mode) throws RenaultForbiddenException, RenaultNotImplementedException,
279 RenaultActionException, RenaultAPIGatewayException {
281 final String apiMode = mode ? "pause" : "resume";
282 final String path = "/commerce/v1/accounts/" + kamereonaccountId + "/kamereon/kcm/v1/vehicles/" + config.vin
283 + "/charge/pause-resume?country=" + getCountry(config);
284 postKamereonRequest(path,
285 "{\"data\":{\"type\":\"ChargePauseResume\",\"attributes\":{\"action\":\"" + apiMode + "\"}}}");
288 private void postKamereonRequest(final String path, final String content) throws RenaultForbiddenException,
289 RenaultNotImplementedException, RenaultActionException, RenaultAPIGatewayException {
290 Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.POST)
291 .header("Content-type", "application/vnd.api+json").header("apikey", this.config.kamereonApiKey)
292 .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt)
293 .content(new StringContentProvider(content, "utf-8"));
295 ContentResponse response = request.send();
296 logKamereonCall(request, response);
297 checkResponse(response);
298 } catch (InterruptedException e) {
299 logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
300 Thread.currentThread().interrupt();
301 } catch (JsonParseException | TimeoutException | ExecutionException e) {
302 throw new RenaultActionException(e.toString());
306 private @Nullable JsonObject getKamereonResponse(String path) throws RenaultForbiddenException,
307 RenaultNotImplementedException, RenaultUpdateException, RenaultAPIGatewayException {
308 Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.GET)
309 .header("Content-type", "application/vnd.api+json").header("apikey", this.config.kamereonApiKey)
310 .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
312 ContentResponse response = request.send();
313 logKamereonCall(request, response);
314 if (HttpStatus.OK_200 == response.getStatus()) {
315 return JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
317 checkResponse(response);
318 } catch (InterruptedException e) {
319 logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
320 Thread.currentThread().interrupt();
321 } catch (JsonParseException | TimeoutException | ExecutionException e) {
322 throw new RenaultUpdateException(e.toString());
327 private void logKamereonCall(Request request, ContentResponse response) {
328 if (HttpStatus.OK_200 == response.getStatus()) {
329 if (logger.isTraceEnabled()) {
330 logger.trace("Kamereon Request: {} Response: [{}] {}\n{}", request.getURI().toString(),
331 response.getStatus(), response.getReason(), response.getContentAsString());
334 logger.warn("Kamereon Request: {} Response: [{}] {}\n{}", request.getURI().toString(), response.getStatus(),
335 response.getReason(), response.getContentAsString());
339 private void checkResponse(ContentResponse response)
340 throws RenaultForbiddenException, RenaultNotImplementedException, RenaultAPIGatewayException {
341 switch (response.getStatus()) {
342 case HttpStatus.FORBIDDEN_403:
343 throw new RenaultForbiddenException(
344 "Kamereon request forbidden! Ensure the car is paired in your MyRenault App.");
345 case HttpStatus.NOT_FOUND_404:
346 throw new RenaultNotImplementedException("Kamereon service not found");
347 case HttpStatus.NOT_IMPLEMENTED_501:
348 throw new RenaultNotImplementedException("Kamereon request not implemented");
349 case HttpStatus.BAD_GATEWAY_502:
350 throw new RenaultAPIGatewayException("Kamereon request failed");
356 private String getCountry(RenaultConfiguration config) {
357 String country = "XX";
358 if (config.locale.length() == 5) {
359 country = config.locale.substring(3);