From ea5b52edc54a971a90d6f7c82318762f9d9da1fe Mon Sep 17 00:00:00 2001 From: Doug Culnane <32482395+dougculnane@users.noreply.github.com> Date: Sun, 5 Dec 2021 09:33:32 +0100 Subject: [PATCH] [renault] Initial Contribution (#11467) * #11465 Initial renault-api binding Signed-off-by: Doug Culnane Signed-off-by: Andras Uhrin --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.renault/NOTICE | 13 + bundles/org.openhab.binding.renault/README.md | 43 +++ bundles/org.openhab.binding.renault/pom.xml | 17 ++ .../src/main/feature/feature.xml | 9 + .../internal/RenaultBindingConstants.java | 38 +++ .../internal/RenaultConfiguration.java | 30 ++ .../internal/RenaultHandlerFactory.java | 67 +++++ .../binding/renault/internal/api/Car.java | 214 ++++++++++++++ .../renault/internal/api/Constants.java | 238 ++++++++++++++++ .../internal/api/MyRenaultHttpSession.java | 264 ++++++++++++++++++ .../api/exceptions/RenaultException.java | 30 ++ .../exceptions/RenaultForbiddenException.java | 31 ++ .../RenaultNotImplementedException.java | 31 ++ .../exceptions/RenaultUpdateException.java | 30 ++ .../internal/handler/RenaultHandler.java | 207 ++++++++++++++ .../main/resources/OH-INF/binding/binding.xml | 9 + .../resources/OH-INF/thing/thing-types.xml | 94 +++++++ bundles/pom.xml | 1 + 20 files changed, 1372 insertions(+) create mode 100644 bundles/org.openhab.binding.renault/NOTICE create mode 100644 bundles/org.openhab.binding.renault/README.md create mode 100644 bundles/org.openhab.binding.renault/pom.xml create mode 100644 bundles/org.openhab.binding.renault/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultBindingConstants.java create mode 100644 bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultConfiguration.java create mode 100644 bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultHandlerFactory.java create mode 100644 bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/Car.java create mode 100644 bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/Constants.java create mode 100644 bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/MyRenaultHttpSession.java create mode 100644 bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultException.java create mode 100644 bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultForbiddenException.java create mode 100644 bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultNotImplementedException.java create mode 100644 bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultUpdateException.java create mode 100644 bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/handler/RenaultHandler.java create mode 100644 bundles/org.openhab.binding.renault/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.renault/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index 76c5453aed0de..356044cd42223 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -254,6 +254,7 @@ /bundles/org.openhab.binding.regoheatpump/ @crnjan /bundles/org.openhab.binding.revogi/ @andibraeu /bundles/org.openhab.binding.remoteopenhab/ @lolodomo +/bundles/org.openhab.binding.renault/ @dougculnane /bundles/org.openhab.binding.resol/ @ramack /bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila /bundles/org.openhab.binding.rme/ @kgoderis diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index fdaa5add0e736..f1d656775c9b3 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1256,6 +1256,11 @@ org.openhab.binding.remoteopenhab ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.renault + ${project.version} + org.openhab.addons.bundles org.openhab.binding.resol diff --git a/bundles/org.openhab.binding.renault/NOTICE b/bundles/org.openhab.binding.renault/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.renault/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.renault/README.md b/bundles/org.openhab.binding.renault/README.md new file mode 100644 index 0000000000000..d6533bf7f11fc --- /dev/null +++ b/bundles/org.openhab.binding.renault/README.md @@ -0,0 +1,43 @@ +# Renault Binding + +This binding allow MyRenault App. users to get battery status and other data from their cars. + +A binding that translates the [python based renault-api](https://renault-api.readthedocs.io/en/latest/) in an easy to use binding. + + +## Supported Things + +Supports MyRenault registered cars with an active Connected-Services account. + +This binding can only retrieve information that is available in the the MyRenault App. + + +## Discovery + +No discovery + +## Thing Configuration + +You require your MyRenault credential, locale and VIN for your MyRenault registered car. + +| Parameter | Description | Required | +|-------------------|----------------------------------------|----------| +| myRenaultUsername | MyRenault Username. | yes | +| myRenaultPassword | MyRenault Password. | yes | +| locale | MyRenault Location (language_country). | yes | +| vin | Vehicle Identification Number. | yes | +| refreshInterval | Interval the car is polled in minutes. | no | + +## Channels + +Currently all available channels are read only: + +| Channel ID | Type | Description | +|--------------|---------------|---------------------------------| +| batterylevel | Number | State of the battery in % | +| hvacstatus | Switch | HVAC status switch | +| image | String | Image URL of MyRenault | +| location | Location | The GPS position of the vehicle | +| odometer | Number:Length | Total distance travelled | + + diff --git a/bundles/org.openhab.binding.renault/pom.xml b/bundles/org.openhab.binding.renault/pom.xml new file mode 100644 index 0000000000000..bbd7abe02f806 --- /dev/null +++ b/bundles/org.openhab.binding.renault/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.2.0-SNAPSHOT + + + org.openhab.binding.renault + + openHAB Add-ons :: Bundles :: Renault Binding + + diff --git a/bundles/org.openhab.binding.renault/src/main/feature/feature.xml b/bundles/org.openhab.binding.renault/src/main/feature/feature.xml new file mode 100644 index 0000000000000..e443ed5c5c6cb --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.renault/${project.version} + + diff --git a/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultBindingConstants.java b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultBindingConstants.java new file mode 100644 index 0000000000000..fd22c3f88abe1 --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultBindingConstants.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.renault.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link RenaultBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Doug Culnane - Initial contribution + */ +@NonNullByDefault +public class RenaultBindingConstants { + + private static final String BINDING_ID = "renault"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_CAR = new ThingTypeUID(BINDING_ID, "car"); + + // List of all Channel ids + public static final String CHANNEL_BATTERY_LEVEL = "batterylevel"; + public static final String CHANNEL_HVAC_STATUS = "hvacstatus"; + public static final String CHANNEL_IMAGE = "image"; + public static final String CHANNEL_LOCATION = "location"; + public static final String CHANNEL_ODOMETER = "odometer"; +} diff --git a/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultConfiguration.java b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultConfiguration.java new file mode 100644 index 0000000000000..8040c4241658d --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultConfiguration.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.renault.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link RenaultConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Doug Culnane - Initial contribution + */ +@NonNullByDefault +public class RenaultConfiguration { + + public String myRenaultUsername = ""; + public String myRenaultPassword = ""; + public String locale = ""; + public String vin = ""; + public int refreshInterval = 10; +} diff --git a/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultHandlerFactory.java b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultHandlerFactory.java new file mode 100644 index 0000000000000..7561d811cb6e3 --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultHandlerFactory.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.renault.internal; + +import static org.openhab.binding.renault.internal.RenaultBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.renault.internal.handler.RenaultHandler; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link RenaultHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Doug Culnane - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.renault", service = ThingHandlerFactory.class) +public class RenaultHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_CAR); + + private final HttpClient httpClient; + + @Activate + public RenaultHandlerFactory(final @Reference HttpClientFactory httpClientFactory) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_CAR.equals(thingTypeUID)) { + return new RenaultHandler(thing, httpClient); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/Car.java b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/Car.java new file mode 100644 index 0000000000000..fec7c182f0ae7 --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/Car.java @@ -0,0 +1,214 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.renault.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * MyRenault registered car for parsing HTTP responses and collecting data and + * information. + * + * @author Doug Culnane - Initial contribution + */ +@NonNullByDefault +public class Car { + + private final Logger logger = LoggerFactory.getLogger(Car.class); + + private boolean disableLocation = false; + private boolean disableBattery = false; + private boolean disableCockpit = false; + private boolean disableHvac = false; + + private @Nullable Double batteryLevel; + private @Nullable Boolean hvacstatus; + private @Nullable Double odometer; + private @Nullable String imageURL; + private @Nullable Double gpsLatitude; + private @Nullable Double gpsLongitude; + + public void setBatteryStatus(JsonObject responseJson) { + try { + JsonObject attributes = getAttributes(responseJson); + if (attributes != null && attributes.get("batteryLevel") != null) { + batteryLevel = attributes.get("batteryLevel").getAsDouble(); + } + } catch (IllegalStateException | ClassCastException e) { + logger.warn("Error {} parsing Battery Status: {}", e.getMessage(), responseJson); + } + } + + public void setHVACStatus(JsonObject responseJson) { + try { + JsonObject attributes = getAttributes(responseJson); + if (attributes != null && attributes.get("hvacStatus") != null) { + hvacstatus = attributes.get("hvacStatus").getAsString().equals("on"); + } + } catch (IllegalStateException | ClassCastException e) { + logger.warn("Error {} parsing HVAC Status: {}", e.getMessage(), responseJson); + } + } + + public void setCockpit(JsonObject responseJson) { + try { + JsonObject attributes = getAttributes(responseJson); + if (attributes != null && attributes.get("totalMileage") != null) { + odometer = attributes.get("totalMileage").getAsDouble(); + } + } catch (IllegalStateException | ClassCastException e) { + logger.warn("Error {} parsing Cockpit: {}", e.getMessage(), responseJson); + } + } + + public void setLocation(JsonObject responseJson) { + try { + JsonObject attributes = getAttributes(responseJson); + if (attributes != null) { + if (attributes.get("gpsLatitude") != null) { + gpsLatitude = attributes.get("gpsLatitude").getAsDouble(); + } + if (attributes.get("gpsLongitude") != null) { + gpsLongitude = attributes.get("gpsLongitude").getAsDouble(); + } + } + } catch (IllegalStateException | ClassCastException e) { + logger.warn("Error {} parsing Location: {}", e.getMessage(), responseJson); + } + } + + public void setDetails(JsonObject responseJson) { + try { + if (responseJson.get("assets") != null) { + JsonArray assetsJson = responseJson.get("assets").getAsJsonArray(); + String url = null; + for (JsonElement asset : assetsJson) { + if (asset.getAsJsonObject().get("assetType") != null + && asset.getAsJsonObject().get("assetType").getAsString().equals("PICTURE")) { + if (asset.getAsJsonObject().get("renditions") != null) { + JsonArray renditions = asset.getAsJsonObject().get("renditions").getAsJsonArray(); + for (JsonElement rendition : renditions) { + if (rendition.getAsJsonObject().get("resolutionType") != null + && rendition.getAsJsonObject().get("resolutionType").getAsString() + .equals("ONE_MYRENAULT_SMALL")) { + url = rendition.getAsJsonObject().get("url").getAsString(); + break; + } + } + } + } + if (url != null && !url.isEmpty()) { + imageURL = url; + break; + } + } + } + } catch (IllegalStateException | ClassCastException e) { + logger.warn("Error {} parsing Details: {}", e.getMessage(), responseJson); + } + } + + public boolean isDisableLocation() { + return disableLocation; + } + + public void setDisableLocation(boolean disableLocation) { + this.disableLocation = disableLocation; + } + + public boolean isDisableBattery() { + return disableBattery; + } + + public void setDisableBattery(boolean disableBattery) { + this.disableBattery = disableBattery; + } + + public boolean isDisableCockpit() { + return disableCockpit; + } + + public void setDisableCockpit(boolean disableCockpit) { + this.disableCockpit = disableCockpit; + } + + public boolean isDisableHvac() { + return disableHvac; + } + + public void setDisableHvac(boolean disableHvac) { + this.disableHvac = disableHvac; + } + + public @Nullable Double getBatteryLevel() { + return batteryLevel; + } + + public void setBatteryLevel(Double batteryLevel) { + this.batteryLevel = batteryLevel; + } + + public @Nullable Boolean getHvacstatus() { + return hvacstatus; + } + + public void setHvacstatus(Boolean hvacstatus) { + this.hvacstatus = hvacstatus; + } + + public @Nullable Double getOdometer() { + return odometer; + } + + public void setOdometer(Double odometer) { + this.odometer = odometer; + } + + public @Nullable String getImageURL() { + return imageURL; + } + + public void setImageURL(String imageURL) { + this.imageURL = imageURL; + } + + public @Nullable Double getGpsLatitude() { + return gpsLatitude; + } + + public void setGpsLatitude(Double gpsLatitude) { + this.gpsLatitude = gpsLatitude; + } + + public @Nullable Double getGpsLongitude() { + return gpsLongitude; + } + + public void setGpsLongitude(Double gpsLongitude) { + this.gpsLongitude = gpsLongitude; + } + + private @Nullable JsonObject getAttributes(JsonObject responseJson) + throws IllegalStateException, ClassCastException { + if (responseJson.get("data") != null && responseJson.get("data").getAsJsonObject().get("attributes") != null) { + return responseJson.get("data").getAsJsonObject().get("attributes").getAsJsonObject(); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/Constants.java b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/Constants.java new file mode 100644 index 0000000000000..45170ba9522a3 --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/Constants.java @@ -0,0 +1,238 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.renault.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Constants for Renault API. + * + * https://github.com/hacf-fr/renault-api/blob/main/src/renault_api/const.py + * + * @author Doug Culnane - Initial contribution + */ +@NonNullByDefault +public class Constants { + + private static final String GIGYA_URL_EU = "https://accounts.eu1.gigya.com"; + private static final String GIGYA_URL_US = "https://accounts.us1.gigya.com"; + private static final String KAMEREON_APIKEY = "Ae9FDWugRxZQAGm3Sxgk7uJn6Q4CGEA2"; + private static final String KAMEREON_URL_EU = "https://api-wired-prod-1-euw1.wrd-aws.com"; + private static final String KAMEREON_URL_US = "https://api-wired-prod-1-usw2.wrd-aws.com"; + + private String gigyaApiKey = "gigya-api-key"; + private String gigyaRootUrl = "gigya-root-url"; + private String kamereonApiKey = "kamereon-api-key"; + private String kamereonRootUrl = "kamereon-root-url"; + + public Constants(final String locale) { + switch (locale) { + case "bg_BG": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3__3ER_6lFvXEXHTP_faLtq6eEdbKDXd9F5GoKwzRyZq37ZQ-db7mXcLzR1Jtls5sn"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "cs_CZ": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_oRlKr5PCVL_sPWUZdJ8c5NOl5Ej8nIZw7VKG7S9Rg36UkDszFzfHfxCaUAUU5or2"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "da_DK": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_5x-2C8b1R4MJPQXkwTPdIqgBpcw653Dakw_ZaEneQRkTBdg9UW9Qg_5G-tMNrTMc"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "de_DE": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_7PLksOyBRkHv126x5WhHb-5pqC1qFR8pQjxSeLB6nhAnPERTUlwnYoznHSxwX668"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "de_AT": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3__B4KghyeUb0GlpU62ZXKrjSfb7CPzwBS368wioftJUL5qXE0Z_sSy0rX69klXuHy"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "de_CH": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_UyiWZs_1UXYCUqK_1n7l7l44UiI_9N9hqwtREV0-UYA_5X7tOV-VKvnGxPBww4q2"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "en_GB": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_e8d4g4SE_Fo8ahyHwwP7ohLGZ79HKNN2T8NjQqoNnk6Epj6ilyYwKdHUyCw3wuxz"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "en_IE": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_Xn7tuOnT9raLEXuwSI1_sFFZNEJhSD0lv3gxkwFtGI-RY4AgiePBiJ9EODh8d9yo"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "es_ES": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_DyMiOwEaxLcPdBTu63Gv3hlhvLaLbW3ufvjHLeuU8U5bx3zx19t5rEKq7KMwk9f1"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "es_MX": + gigyaRootUrl = GIGYA_URL_US; + gigyaApiKey = "3_BFzR-2wfhMhUs5OCy3R8U8IiQcHS-81vF8bteSe8eFrboMTjEWzbf4pY1aHQ7cW0"; + kamereonRootUrl = KAMEREON_URL_US; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "fi_FI": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_xSRCLDYhk1SwSeYQLI3DmA8t-etfAfu5un51fws125ANOBZHgh8Lcc4ReWSwaqNY"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "fr_FR": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_4LKbCcMMcvjDm3X89LU4z4mNKYKdl_W0oD9w-Jvih21WqgJKtFZAnb9YdUgWT9_a"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "fr_BE": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_ZK9x38N8pzEvdiG7ojWHeOAAej43APkeJ5Av6VbTkeoOWR4sdkRc-wyF72HzUB8X"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "fr_CH": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_h3LOcrKZ9mTXxMI9clb2R1VGAWPke6jMNqMw4yYLz4N7PGjYyD0hqRgIFAIHusSn"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "fr_LU": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_zt44Wl_wT9mnqn-BHrR19PvXj3wYRPQKLcPbGWawlatFR837KdxSZZStbBTDaqnb"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "hr_HR": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_HcDC5GGZ89NMP1jORLhYNNCcXt7M3thhZ85eGrcQaM2pRwrgrzcIRWEYi_36cFj9"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "hu_HU": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_nGDWrkSGZovhnVFv5hdIxyuuCuJGZfNmlRGp7-5kEn9yb0bfIfJqoDa2opHOd3Mu"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "it_IT": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_js8th3jdmCWV86fKR3SXQWvXGKbHoWFv8NAgRbH7FnIBsi_XvCpN_rtLcI07uNuq"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "it_CH": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_gHkmHaGACxSLKXqD_uDDx415zdTw7w8HXAFyvh0qIP0WxnHPMF2B9K_nREJVSkGq"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "nl_NL": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_ZIOtjqmP0zaHdEnPK7h1xPuBYgtcOyUxbsTY8Gw31Fzy7i7Ltjfm-hhPh23fpHT5"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "nl_BE": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_yachztWczt6i1pIMhLIH9UA6DXK6vXXuCDmcsoA4PYR0g35RvLPDbp49YribFdpC"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "no_NO": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_QrPkEJr69l7rHkdCVls0owC80BB4CGz5xw_b0gBSNdn3pL04wzMBkcwtbeKdl1g9"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "pl_PL": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_2YBjydYRd1shr6bsZdrvA9z7owvSg3W5RHDYDp6AlatXw9hqx7nVoanRn8YGsBN8"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "pt_PT": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3__afxovspi2-Ip1E5kNsAgc4_35lpLAKCF6bq4_xXj2I2bFPjIWxAOAQJlIkreKTD"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "ro_RO": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_WlBp06vVHuHZhiDLIehF8gchqbfegDJADPQ2MtEsrc8dWVuESf2JCITRo5I2CIxs"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "ru_RU": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_N_ecy4iDyoRtX8v5xOxewwZLKXBjRgrEIv85XxI0KJk8AAdYhJIi17LWb086tGXR"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "sk_SK": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_e8d4g4SE_Fo8ahyHwwP7ohLGZ79HKNN2T8NjQqoNnk6Epj6ilyYwKdHUyCw3wuxz"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "sl_SI": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_QKt0ADYxIhgcje4F3fj9oVidHsx3JIIk-GThhdyMMQi8AJR0QoHdA62YArVjbZCt"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + case "sv_SE": + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3_EN5Hcnwanu9_Dqot1v1Aky1YelT5QqG4TxveO0EgKFWZYu03WkeB9FKuKKIWUXIS"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + default: + gigyaRootUrl = GIGYA_URL_EU; + gigyaApiKey = "3__B4KghyeUb0GlpU62ZXKrjSfb7CPzwBS368wioftJUL5qXE0Z_sSy0rX69klXuHy"; + kamereonRootUrl = KAMEREON_URL_EU; + kamereonApiKey = KAMEREON_APIKEY; + break; + } + } + + public String getGigyaApiKey() { + return gigyaApiKey; + } + + public String getGigyaRootUrl() { + return gigyaRootUrl; + } + + public String getKamereonApiKey() { + return kamereonApiKey; + } + + public String getKamereonRootUrl() { + return kamereonRootUrl; + } +} diff --git a/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/MyRenaultHttpSession.java b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/MyRenaultHttpSession.java new file mode 100644 index 0000000000000..b7ca956e2fefb --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/MyRenaultHttpSession.java @@ -0,0 +1,264 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.renault.internal.api; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.util.Fields; +import org.openhab.binding.renault.internal.RenaultConfiguration; +import org.openhab.binding.renault.internal.api.exceptions.RenaultException; +import org.openhab.binding.renault.internal.api.exceptions.RenaultForbiddenException; +import org.openhab.binding.renault.internal.api.exceptions.RenaultNotImplementedException; +import org.openhab.binding.renault.internal.api.exceptions.RenaultUpdateException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +/** + * This is a Java version of the python renault-api project developed here: + * https://github.com/hacf-fr/renault-api + * + * @author Doug Culnane - Initial contribution + */ +@NonNullByDefault +public class MyRenaultHttpSession { + + private RenaultConfiguration config; + private HttpClient httpClient; + private Constants constants; + private @Nullable String kamereonToken; + private @Nullable String kamereonaccountId; + private @Nullable String cookieValue; + private @Nullable String personId; + private @Nullable String gigyaDataCenter; + private @Nullable String jwt; + + private final Logger logger = LoggerFactory.getLogger(MyRenaultHttpSession.class); + + public MyRenaultHttpSession(RenaultConfiguration config, HttpClient httpClient) { + this.config = config; + this.httpClient = httpClient; + this.constants = new Constants(config.locale); + } + + public void initSesssion(Car car) throws RenaultException, RenaultForbiddenException, RenaultUpdateException, + RenaultNotImplementedException, InterruptedException, ExecutionException, TimeoutException { + login(); + getAccountInfo(); + getJWT(); + getAccountID(); + + final String imageURL = car.getImageURL(); + if (imageURL == null) { + getVehicle(car); + } + } + + private void login() throws RenaultException, InterruptedException, ExecutionException, TimeoutException { + Fields fields = new Fields(); + fields.add("ApiKey", this.constants.getGigyaApiKey()); + fields.add("loginID", config.myRenaultUsername); + fields.add("password", config.myRenaultPassword); + logger.debug("URL: {}/accounts.login", this.constants.getGigyaRootUrl()); + ContentResponse response = httpClient.FORM(this.constants.getGigyaRootUrl() + "/accounts.login", fields); + if (HttpStatus.OK_200 == response.getStatus()) { + try { + JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject(); + JsonObject sessionInfoJson = responseJson.getAsJsonObject("sessionInfo"); + if (sessionInfoJson != null) { + JsonElement element = sessionInfoJson.get("cookieValue"); + if (element != null) { + cookieValue = element.getAsString(); + logger.debug("Cookie: {}", cookieValue); + } + } + } catch (JsonParseException | ClassCastException | IllegalStateException e) { + throw new RenaultException("Login Error: cookie value not found in JSON response"); + } + } else { + logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(), + response.getContentAsString()); + throw new RenaultException("Login Error: " + response.getReason()); + } + } + + private void getAccountInfo() throws RenaultException, InterruptedException, ExecutionException, TimeoutException { + Fields fields = new Fields(); + fields.add("ApiKey", this.constants.getGigyaApiKey()); + fields.add("login_token", cookieValue); + ContentResponse response = httpClient.FORM(this.constants.getGigyaRootUrl() + "/accounts.getAccountInfo", + fields); + if (HttpStatus.OK_200 == response.getStatus()) { + try { + JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject(); + JsonObject dataJson = responseJson.getAsJsonObject("data"); + if (dataJson != null) { + JsonElement element1 = dataJson.get("personId"); + JsonElement element2 = dataJson.get("gigyaDataCenter"); + if (element1 != null && element2 != null) { + personId = element1.getAsString(); + gigyaDataCenter = element2.getAsString(); + logger.debug("personId ID: {} gigyaDataCenter: {}", personId, gigyaDataCenter); + } + } + } catch (JsonParseException | ClassCastException | IllegalStateException e) { + throw new RenaultException( + "Get Account Info Error: personId or gigyaDataCenter value not found in JSON response"); + } + } else { + logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(), + response.getContentAsString()); + throw new RenaultException("Get Account Info Error: " + response.getReason()); + } + } + + private void getJWT() throws RenaultException, InterruptedException, ExecutionException, TimeoutException { + Fields fields = new Fields(); + fields.add("ApiKey", this.constants.getGigyaApiKey()); + fields.add("login_token", cookieValue); + fields.add("fields", "data.personId,data.gigyaDataCenter"); + fields.add("personId", personId); + fields.add("gigyaDataCenter", gigyaDataCenter); + ContentResponse response = this.httpClient.FORM(this.constants.getGigyaRootUrl() + "/accounts.getJWT", fields); + if (HttpStatus.OK_200 == response.getStatus()) { + try { + JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject(); + JsonElement element = responseJson.get("id_token"); + if (element != null) { + jwt = element.getAsString(); + logger.debug("jwt: {} ", jwt); + } + } catch (JsonParseException | ClassCastException | IllegalStateException e) { + throw new RenaultException("Get JWT Error: jwt value not found in JSON response"); + } + } else { + logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(), + response.getContentAsString()); + throw new RenaultException("Get JWT Error: " + response.getReason()); + } + } + + private void getAccountID() + throws RenaultException, RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException { + JsonObject responseJson = getKamereonResponse( + "/commerce/v1/persons/" + personId + "?country=" + getCountry(config)); + if (responseJson != null) { + JsonArray accounts = responseJson.getAsJsonArray("accounts"); + for (int i = 0; i < accounts.size(); i++) { + if (accounts.get(i).getAsJsonObject().get("accountType").getAsString().equals("MYRENAULT")) { + kamereonaccountId = accounts.get(i).getAsJsonObject().get("accountId").getAsString(); + break; + } + } + } + if (kamereonaccountId == null) { + throw new RenaultException("Can not get Kamereon MyRenault Account ID!"); + } + } + + public void getVehicle(Car car) + throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException { + JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId + "/vehicles/" + + config.vin + "/details?country=" + getCountry(config)); + if (responseJson != null) { + car.setDetails(responseJson); + } + } + + public void getBatteryStatus(Car car) + throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException { + JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId + + "/kamereon/kca/car-adapter/v2/cars/" + config.vin + "/battery-status?country=" + getCountry(config)); + if (responseJson != null) { + car.setBatteryStatus(responseJson); + } + } + + public void getHvacStatus(Car car) + throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException { + JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId + + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/hvac-status?country=" + getCountry(config)); + if (responseJson != null) { + car.setHVACStatus(responseJson); + } + } + + public void getCockpit(Car car) + throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException { + JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId + + "/kamereon/kca/car-adapter/v2/cars/" + config.vin + "/cockpit?country=" + getCountry(config)); + if (responseJson != null) { + car.setCockpit(responseJson); + } + } + + public void getLocation(Car car) + throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException { + JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId + + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/location?country=" + getCountry(config)); + if (responseJson != null) { + car.setLocation(responseJson); + } + } + + private @Nullable JsonObject getKamereonResponse(String path) + throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException { + Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.GET) + .header("Content-type", "application/vnd.api+json").header("apikey", this.constants.getKamereonApiKey()) + .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt); + try { + ContentResponse response = request.send(); + if (HttpStatus.OK_200 == response.getStatus()) { + logger.debug("Kamereon Response: {}", response.getContentAsString()); + return JsonParser.parseString(response.getContentAsString()).getAsJsonObject(); + } else { + logger.warn("Kamereon Response: [{}] {} {}", response.getStatus(), response.getReason(), + response.getContentAsString()); + if (HttpStatus.FORBIDDEN_403 == response.getStatus()) { + throw new RenaultForbiddenException( + "Kamereon Response Forbidden! Ensure the car is paired in your MyRenault App."); + } else if (HttpStatus.NOT_IMPLEMENTED_501 == response.getStatus()) { + throw new RenaultNotImplementedException( + "Kamereon Service Not Implemented: [" + response.getStatus() + "] " + response.getReason()); + } else { + throw new RenaultUpdateException( + "Kamereon Response Failed! Error: [" + response.getStatus() + "] " + response.getReason()); + } + } + } catch (JsonParseException | InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage()); + } + return null; + } + + private String getCountry(RenaultConfiguration config) { + String country = "XX"; + if (config.locale.length() == 5) { + country = config.locale.substring(3); + } + return country; + } +} diff --git a/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultException.java b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultException.java new file mode 100644 index 0000000000000..bb8385a483313 --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultException.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.renault.internal.api.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown while trying to access the My Renault web services. + * + * @author Doug Culnane - Initial contribution + */ +@NonNullByDefault +public class RenaultException extends Exception { + + private static final long serialVersionUID = 1L; + + public RenaultException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultForbiddenException.java b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultForbiddenException.java new file mode 100644 index 0000000000000..f469daf6aec80 --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultForbiddenException.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.renault.internal.api.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown while trying to access the My Renault web services when HTTP + * 403 is returned. Normally means the car is not paired to the account. + * + * @author Doug Culnane - Initial contribution + */ +@NonNullByDefault +public class RenaultForbiddenException extends Exception { + + private static final long serialVersionUID = 1L; + + public RenaultForbiddenException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultNotImplementedException.java b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultNotImplementedException.java new file mode 100644 index 0000000000000..d948cbc7a44a1 --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultNotImplementedException.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.renault.internal.api.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown while trying to access the My Renault service for information + * that is not implemented. + * + * @author Doug Culnane - Initial contribution + */ +@NonNullByDefault +public class RenaultNotImplementedException extends Exception { + + private static final long serialVersionUID = 1L; + + public RenaultNotImplementedException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultUpdateException.java b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultUpdateException.java new file mode 100644 index 0000000000000..a7266f7304275 --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultUpdateException.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.renault.internal.api.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown while trying to update the My Renault car information. + * + * @author Doug Culnane - Initial contribution + */ +@NonNullByDefault +public class RenaultUpdateException extends Exception { + + private static final long serialVersionUID = 1L; + + public RenaultUpdateException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/handler/RenaultHandler.java b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/handler/RenaultHandler.java new file mode 100644 index 0000000000000..fedfab292baeb --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/handler/RenaultHandler.java @@ -0,0 +1,207 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.renault.internal.handler; + +import static org.openhab.binding.renault.internal.RenaultBindingConstants.*; +import static org.openhab.core.library.unit.MetricPrefix.KILO; +import static org.openhab.core.library.unit.SIUnits.METRE; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.measure.quantity.Length; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.renault.internal.RenaultConfiguration; +import org.openhab.binding.renault.internal.api.Car; +import org.openhab.binding.renault.internal.api.MyRenaultHttpSession; +import org.openhab.binding.renault.internal.api.exceptions.RenaultForbiddenException; +import org.openhab.binding.renault.internal.api.exceptions.RenaultNotImplementedException; +import org.openhab.binding.renault.internal.api.exceptions.RenaultUpdateException; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PointType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link RenaultHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Doug Culnane - Initial contribution + */ +@NonNullByDefault +public class RenaultHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(RenaultHandler.class); + + private RenaultConfiguration config = new RenaultConfiguration(); + + private @Nullable ScheduledFuture pollingJob; + + private HttpClient httpClient; + + private Car car; + + public RenaultHandler(Thing thing, HttpClient httpClient) { + super(thing); + this.car = new Car(); + this.httpClient = httpClient; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // This binding only polls status data automatically. + } + + @Override + public void initialize() { + // reset the car on initialize + this.car = new Car(); + this.config = getConfigAs(RenaultConfiguration.class); + + // Validate configuration + if (this.config.myRenaultUsername.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Username is empty!"); + return; + } + if (this.config.myRenaultPassword.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Password is empty!"); + return; + } + if (this.config.locale.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Location is empty!"); + return; + } + if (this.config.vin.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "VIN is empty!"); + return; + } + if (this.config.refreshInterval < 1) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "The refresh interval mush to be larger than 1"); + return; + } + updateStatus(ThingStatus.UNKNOWN); + + // Background initialization: + ScheduledFuture job = pollingJob; + if (job == null || job.isCancelled()) { + pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES); + } + } + + @Override + public void dispose() { + ScheduledFuture job = pollingJob; + if (job != null) { + job.cancel(true); + pollingJob = null; + } + super.dispose(); + } + + private void getStatus() { + MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient); + try { + httpSession.initSesssion(car); + updateStatus(ThingStatus.ONLINE); + } catch (Exception e) { + httpSession = null; + logger.warn("Error My Renault Http Session.", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + if (httpSession != null) { + String imageURL = car.getImageURL(); + if (imageURL != null && !imageURL.isEmpty()) { + updateState(CHANNEL_IMAGE, new StringType(imageURL)); + } + updateHvacStatus(httpSession); + updateCockpit(httpSession); + updateLocation(httpSession); + updateBattery(httpSession); + } + } + + private void updateHvacStatus(MyRenaultHttpSession httpSession) { + if (!car.isDisableHvac()) { + try { + httpSession.getHvacStatus(car); + Boolean hvacstatus = car.getHvacstatus(); + if (hvacstatus != null) { + updateState(CHANNEL_HVAC_STATUS, OnOffType.from(hvacstatus.booleanValue())); + } + } catch (RenaultNotImplementedException e) { + car.setDisableHvac(true); + } catch (RenaultForbiddenException | RenaultUpdateException e) { + } + } + } + + private void updateLocation(MyRenaultHttpSession httpSession) { + if (!car.isDisableLocation()) { + try { + httpSession.getLocation(car); + Double latitude = car.getGpsLatitude(); + Double longitude = car.getGpsLongitude(); + if (latitude != null && longitude != null) { + updateState(CHANNEL_LOCATION, new PointType(new DecimalType(latitude.doubleValue()), + new DecimalType(longitude.doubleValue()))); + } + } catch (RenaultNotImplementedException e) { + car.setDisableLocation(true); + } catch (RenaultForbiddenException | RenaultUpdateException e) { + } + } + } + + private void updateCockpit(MyRenaultHttpSession httpSession) { + if (!car.isDisableCockpit()) { + try { + httpSession.getCockpit(car); + Double odometer = car.getOdometer(); + if (odometer != null) { + updateState(CHANNEL_ODOMETER, new QuantityType(odometer.doubleValue(), KILO(METRE))); + } + } catch (RenaultNotImplementedException e) { + car.setDisableCockpit(true); + } catch (RenaultForbiddenException | RenaultUpdateException e) { + } + } + } + + private void updateBattery(MyRenaultHttpSession httpSession) { + if (!car.isDisableBattery()) { + try { + httpSession.getBatteryStatus(car); + Double batteryLevel = car.getBatteryLevel(); + if (batteryLevel != null) { + updateState(CHANNEL_BATTERY_LEVEL, new DecimalType(batteryLevel.doubleValue())); + } + } catch (RenaultNotImplementedException e) { + car.setDisableBattery(true); + } catch (RenaultForbiddenException | RenaultUpdateException e) { + } + } + } +} diff --git a/bundles/org.openhab.binding.renault/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.renault/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..6f8634020eda3 --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + Renault Binding + This is the binding for Renault electric cars. + + diff --git a/bundles/org.openhab.binding.renault/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.renault/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..ee21b82bbcd7a --- /dev/null +++ b/bundles/org.openhab.binding.renault/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,94 @@ + + + + + + + A MyRenault registered car. + + + + + + + + + + + + + + + password + + + + + The country (and language combination) that best fits with your MyRenault registered car. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vehicle Identification Number + + + + Interval the car is polled in minutes. + 10 + + + + + + + Switch + + + + + String + + Image URL of MyRenault + + + + Number:Length + + Total distance travelled + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 3c0f756994396..2bdfc6edaf628 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -286,6 +286,7 @@ org.openhab.binding.regoheatpump org.openhab.binding.revogi org.openhab.binding.remoteopenhab + org.openhab.binding.renault org.openhab.binding.resol org.openhab.binding.rfxcom org.openhab.binding.rme