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.RenaultException;
30 import org.openhab.binding.renault.internal.api.exceptions.RenaultForbiddenException;
31 import org.openhab.binding.renault.internal.api.exceptions.RenaultNotImplementedException;
32 import org.openhab.binding.renault.internal.api.exceptions.RenaultUpdateException;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
36 import com.google.gson.JsonArray;
37 import com.google.gson.JsonElement;
38 import com.google.gson.JsonObject;
39 import com.google.gson.JsonParseException;
40 import com.google.gson.JsonParser;
43 * This is a Java version of the python renault-api project developed here:
44 * https://github.com/hacf-fr/renault-api
46 * @author Doug Culnane - Initial contribution
49 public class MyRenaultHttpSession {
51 private static final String CHARGING_MODE_SCHEDULE = "schedule_mode";
52 private static final String CHARGING_MODE_ALWAYS = "always_charging";
54 private RenaultConfiguration config;
55 private HttpClient httpClient;
56 private Constants constants;
57 private @Nullable String kamereonToken;
58 private @Nullable String kamereonaccountId;
59 private @Nullable String cookieValue;
60 private @Nullable String personId;
61 private @Nullable String gigyaDataCenter;
62 private @Nullable String jwt;
64 private final Logger logger = LoggerFactory.getLogger(MyRenaultHttpSession.class);
66 public MyRenaultHttpSession(RenaultConfiguration config, HttpClient httpClient) {
68 this.httpClient = httpClient;
69 this.constants = new Constants(config.locale);
72 public void initSesssion(Car car) throws RenaultException, RenaultForbiddenException, RenaultUpdateException,
73 RenaultNotImplementedException, InterruptedException, ExecutionException, TimeoutException {
79 final String imageURL = car.getImageURL();
80 if (imageURL == null) {
85 private void login() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
86 Fields fields = new Fields();
87 fields.add("ApiKey", this.constants.getGigyaApiKey());
88 fields.add("loginID", config.myRenaultUsername);
89 fields.add("password", config.myRenaultPassword);
90 logger.debug("URL: {}/accounts.login", this.constants.getGigyaRootUrl());
91 ContentResponse response = httpClient.FORM(this.constants.getGigyaRootUrl() + "/accounts.login", fields);
92 if (HttpStatus.OK_200 == response.getStatus()) {
94 JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
95 JsonObject sessionInfoJson = responseJson.getAsJsonObject("sessionInfo");
96 if (sessionInfoJson != null) {
97 JsonElement element = sessionInfoJson.get("cookieValue");
98 if (element != null) {
99 cookieValue = element.getAsString();
100 logger.debug("Cookie: {}", cookieValue);
103 } catch (JsonParseException | ClassCastException | IllegalStateException e) {
104 throw new RenaultException("Login Error: cookie value not found in JSON response");
106 if (cookieValue == null) {
107 logger.warn("Login Error: cookie value not found! Response: [{}] {}\n{}", response.getStatus(),
108 response.getReason(), response.getContentAsString());
111 logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(),
112 response.getContentAsString());
113 throw new RenaultException("Login Error: " + response.getReason());
117 private void getAccountInfo() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
118 Fields fields = new Fields();
119 fields.add("ApiKey", this.constants.getGigyaApiKey());
120 fields.add("login_token", cookieValue);
121 ContentResponse response = httpClient.FORM(this.constants.getGigyaRootUrl() + "/accounts.getAccountInfo",
123 if (HttpStatus.OK_200 == response.getStatus()) {
125 JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
126 JsonObject dataJson = responseJson.getAsJsonObject("data");
127 if (dataJson != null) {
128 JsonElement element1 = dataJson.get("personId");
129 JsonElement element2 = dataJson.get("gigyaDataCenter");
130 if (element1 != null && element2 != null) {
131 personId = element1.getAsString();
132 gigyaDataCenter = element2.getAsString();
133 logger.debug("personId ID: {} gigyaDataCenter: {}", personId, gigyaDataCenter);
136 } catch (JsonParseException | ClassCastException | IllegalStateException e) {
137 throw new RenaultException(
138 "Get Account Info Error: personId or gigyaDataCenter value not found in JSON response");
141 logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(),
142 response.getContentAsString());
143 throw new RenaultException("Get Account Info Error: " + response.getReason());
147 private void getJWT() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
148 Fields fields = new Fields();
149 fields.add("ApiKey", this.constants.getGigyaApiKey());
150 fields.add("login_token", cookieValue);
151 fields.add("fields", "data.personId,data.gigyaDataCenter");
152 fields.add("personId", personId);
153 fields.add("gigyaDataCenter", gigyaDataCenter);
154 ContentResponse response = this.httpClient.FORM(this.constants.getGigyaRootUrl() + "/accounts.getJWT", fields);
155 if (HttpStatus.OK_200 == response.getStatus()) {
157 JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
158 JsonElement element = responseJson.get("id_token");
159 if (element != null) {
160 jwt = element.getAsString();
161 logger.debug("jwt: {} ", jwt);
163 } catch (JsonParseException | ClassCastException | IllegalStateException e) {
164 throw new RenaultException("Get JWT Error: jwt value not found in JSON response");
167 logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(),
168 response.getContentAsString());
169 throw new RenaultException("Get JWT Error: " + response.getReason());
173 private void getAccountID()
174 throws RenaultException, RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
175 JsonObject responseJson = getKamereonResponse(
176 "/commerce/v1/persons/" + personId + "?country=" + getCountry(config));
177 if (responseJson != null) {
178 JsonArray accounts = responseJson.getAsJsonArray("accounts");
179 for (int i = 0; i < accounts.size(); i++) {
180 if (accounts.get(i).getAsJsonObject().get("accountType").getAsString().equals("MYRENAULT")) {
181 kamereonaccountId = accounts.get(i).getAsJsonObject().get("accountId").getAsString();
186 if (kamereonaccountId == null) {
187 throw new RenaultException("Can not get Kamereon MyRenault Account ID!");
191 public void getVehicle(Car car)
192 throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
193 JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId + "/vehicles/"
194 + config.vin + "/details?country=" + getCountry(config));
195 if (responseJson != null) {
196 car.setDetails(responseJson);
200 public void getBatteryStatus(Car car)
201 throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
202 JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
203 + "/kamereon/kca/car-adapter/v2/cars/" + config.vin + "/battery-status?country=" + getCountry(config));
204 if (responseJson != null) {
205 car.setBatteryStatus(responseJson);
209 public void getHvacStatus(Car car)
210 throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
211 JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
212 + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/hvac-status?country=" + getCountry(config));
213 if (responseJson != null) {
214 car.setHVACStatus(responseJson);
218 public void getCockpit(Car car)
219 throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
220 JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
221 + "/kamereon/kca/car-adapter/v2/cars/" + config.vin + "/cockpit?country=" + getCountry(config));
222 if (responseJson != null) {
223 car.setCockpit(responseJson);
227 public void getLocation(Car car)
228 throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
229 JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
230 + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/location?country=" + getCountry(config));
231 if (responseJson != null) {
232 car.setLocation(responseJson);
236 public void actionHvacOn(double hvacTargetTemperature)
237 throws RenaultForbiddenException, RenaultNotImplementedException {
238 Request request = httpClient
239 .newRequest(this.constants.getKamereonRootUrl() + "/commerce/v1/accounts/" + kamereonaccountId
240 + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/actions/hvac-start?country="
241 + getCountry(config))
242 .method(HttpMethod.POST).header("Content-type", "application/vnd.api+json")
243 .header("apikey", this.config.kamereonApiKey)
244 .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
245 request.content(new StringContentProvider(
246 "{\"data\":{\"type\":\"HvacStart\",\"attributes\":{\"action\":\"start\",\"targetTemperature\":\""
247 + hvacTargetTemperature + "\"}}}",
250 ContentResponse response = request.send();
251 logger.debug("Kamereon Response HVAC ON: {}", response.getContentAsString());
252 if (HttpStatus.OK_200 != response.getStatus()) {
253 logger.warn("Kamereon Response: [{}] {} {}", response.getStatus(), response.getReason(),
254 response.getContentAsString());
255 if (HttpStatus.FORBIDDEN_403 == response.getStatus()) {
256 throw new RenaultForbiddenException(
257 "Kamereon Response Forbidden! Ensure the car is paired in your MyRenault App.");
258 } else if (HttpStatus.NOT_IMPLEMENTED_501 == response.getStatus()) {
259 throw new RenaultNotImplementedException(
260 "Kamereon Service Not Implemented: [" + response.getStatus() + "] " + response.getReason());
263 } catch (InterruptedException e) {
264 logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
265 Thread.currentThread().interrupt();
266 } catch (JsonParseException | TimeoutException | ExecutionException e) {
267 logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
271 public void actionChargeMode(ChargingMode mode) throws RenaultForbiddenException, RenaultNotImplementedException {
272 Request request = httpClient
273 .newRequest(this.constants.getKamereonRootUrl() + "/commerce/v1/accounts/" + kamereonaccountId
274 + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/actions/charge-mode?country="
275 + getCountry(config))
276 .method(HttpMethod.POST).header("Content-type", "application/vnd.api+json")
277 .header("apikey", this.config.kamereonApiKey)
278 .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
280 final String apiMode = ChargingMode.SCHEDULE_MODE.equals(mode) ? CHARGING_MODE_SCHEDULE : CHARGING_MODE_ALWAYS;
281 request.content(new StringContentProvider(
282 "{\"data\":{\"type\":\"ChargeMode\",\"attributes\":{\"action\":\"" + apiMode + "\"}}}", "utf-8"));
284 ContentResponse response = request.send();
285 logger.debug("Kamereon Response set ChargeMode: {}", response.getContentAsString());
286 if (HttpStatus.OK_200 != response.getStatus()) {
287 logger.warn("Kamereon Response: [{}] {} {}", response.getStatus(), response.getReason(),
288 response.getContentAsString());
289 if (HttpStatus.FORBIDDEN_403 == response.getStatus()) {
290 throw new RenaultForbiddenException(
291 "Kamereon Response Forbidden! Ensure the car is paired in your MyRenault App.");
292 } else if (HttpStatus.NOT_IMPLEMENTED_501 == response.getStatus()) {
293 throw new RenaultNotImplementedException(
294 "Kamereon Service Not Implemented: [" + response.getStatus() + "] " + response.getReason());
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 logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
305 private @Nullable JsonObject getKamereonResponse(String path)
306 throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
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);
311 ContentResponse response = request.send();
312 if (HttpStatus.OK_200 == response.getStatus()) {
313 logger.debug("Kamereon Response: {}", response.getContentAsString());
314 return JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
316 logger.warn("Kamereon Response: [{}] {} {}", response.getStatus(), response.getReason(),
317 response.getContentAsString());
318 if (HttpStatus.FORBIDDEN_403 == response.getStatus()) {
319 throw new RenaultForbiddenException(
320 "Kamereon Response Forbidden! Ensure the car is paired in your MyRenault App.");
321 } else if (HttpStatus.NOT_IMPLEMENTED_501 == response.getStatus()) {
322 throw new RenaultNotImplementedException(
323 "Kamereon Service Not Implemented: [" + response.getStatus() + "] " + response.getReason());
325 throw new RenaultUpdateException(
326 "Kamereon Response Failed! Error: [" + response.getStatus() + "] " + response.getReason());
329 } catch (InterruptedException e) {
330 logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
331 Thread.currentThread().interrupt();
332 } catch (JsonParseException | TimeoutException | ExecutionException e) {
333 logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
338 private String getCountry(RenaultConfiguration config) {
339 String country = "XX";
340 if (config.locale.length() == 5) {
341 country = config.locale.substring(3);