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 |
-| updateDelay | How long to wait for commands to reach car and update to server in seconds.| no |
-| kamereonApiKey | Kamereon API Key. | no |
+| Parameter | Description | Default |
+|-------------------|----------------------------------------------------------------------------|----------------------------------|
+| myRenaultUsername | MyRenault Username. | |
+| myRenaultPassword | MyRenault Password. | |
+| locale | MyRenault Location (language_country). | |
+| vin | Vehicle Identification Number. | |
+| refreshInterval | Interval the car is polled in minutes. | 10 |
+| updateDelay | How long to wait for commands to reach car and update to server in seconds.| 30 |
+| kamereonApiKey | Kamereon API Key. | VAX7XYKGfa92yMvXculCkEFyfZbuM7Ss |
## Channels
|------------------------|--------------------|-------------------------------------------------|-----------|
| batteryavailableEnergy | Number:Energy | Battery Energy Available | Yes |
| batterylevel | Number | State of the battery in % | Yes |
+| batterystatusupdated | DateTime | Timestamp of the last battery status update | Yes |
| chargingmode | String | Charging mode. always_charging or schedule_mode | No |
| chargingstatus | String | Charging status | Yes |
| chargingremainingtime | Number:Time | Charging time remaining | Yes |
| image | String | Image URL of MyRenault | Yes |
| location | Location | The GPS position of the vehicle | Yes |
| locationupdated | DateTime | Timestamp of the last location update | Yes |
+| locked | Switch | Locked status of the car | Yes |
## Limitations
sitemap renaultcar label="Renault Car" {
Frame {
Image item=RenaultCar_ImageURL
- Default item=RenaultCar_BatteryLevel icon="batterylevel"
- Default item=RenaultCar_BatteryEnergyAvailable icon="energy"
- Default item=RenaultCar_PlugStatus icon="poweroutlet"
- Default item=RenaultCar_ChargingStatus icon="switch"
- Selection item=RenaultCar_ChargingMode mappings=[SCHEDULE_MODE="Schedule mode",ALWAYS_CHARGING="Instant charge"] icon="switch"
- Default item=RenaultCar_ChargingTimeRemaining icon="time"
- Default item=RenaultCar_EstimatedRange
- Default item=RenaultCar_Odometer
- Selection item=RenaultCar_HVACStatus mappings=[ON="ON"] icon="switch"
- Setpoint item=RenaultCar_HVACTargetTemperature minValue=19 maxValue=21 step=1 icon="temperature"
- Default item=RenaultCar_LocationUpdate icon="time"
- Default item=RenaultCar_Location
+ Default icon="batterylevel" item=RenaultCar_BatteryLevel
+ Default item=RenaultCar_BatteryEnergyAvailable
+ Default item=RenaultCar_BatteryStatusUpdated
+ Default icon="poweroutlet" item=RenaultCar_PlugStatus
+ Default icon="switch" item=RenaultCar_ChargingStatus
+ Selection icon="switch" item=RenaultCar_ChargingMode mappings=[SCHEDULE_MODE="Schedule mode",ALWAYS_CHARGING="Instant charge"]
+ Default item=RenaultCar_ChargingTimeRemaining
+ Default icon="pressure" item=RenaultCar_EstimatedRange
+ Default icon="pressure" item=RenaultCar_Odometer
+ Selection icon="switch" item=RenaultCar_HVACStatus mappings=[ON="ON"]
+ Setpoint icon="temperature" item=RenaultCar_HVACTargetTemperature maxValue=21 minValue=19 step=1
+ Default icon="lock" item=RenaultCar_Locked
+ Default item=RenaultCar_LocationUpdate
+ Default icon="zoom" item=RenaultCar_Location
}
}
```
-
-
If you want to limit the charge of the car battery to less than 100%, this can be done as follows.
- Set up an active dummy charge schedule in the MyRenault App.
// List of all Channel ids
public static final String CHANNEL_BATTERY_AVAILABLE_ENERGY = "batteryavailableenergy";
public static final String CHANNEL_BATTERY_LEVEL = "batterylevel";
+ public static final String CHANNEL_BATTERY_STATUS_UPDATED = "batterystatusupdated";
public static final String CHANNEL_CHARGING_MODE = "chargingmode";
public static final String CHANNEL_CHARGING_STATUS = "chargingstatus";
public static final String CHANNEL_CHARGING_REMAINING_TIME = "chargingremainingtime";
public static final String CHANNEL_IMAGE = "image";
public static final String CHANNEL_LOCATION = "location";
public static final String CHANNEL_LOCATION_UPDATED = "locationupdated";
+ public static final String CHANNEL_LOCKED = "locked";
public static final String CHANNEL_ODOMETER = "odometer";
public static final String CHANNEL_PLUG_STATUS = "plugstatus";
}
*/
package org.openhab.binding.renault.internal.api;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
private boolean disableBattery = false;
private boolean disableCockpit = false;
private boolean disableHvac = false;
+ private boolean disableLockStatus = false;
private ChargingStatus chargingStatus = ChargingStatus.UNKNOWN;
private ChargingMode chargingMode = ChargingMode.UNKNOWN;
private double hvacTargetTemperature = 20.0;
private @Nullable Double batteryLevel;
private @Nullable Double batteryAvailableEnergy;
+ private @Nullable ZonedDateTime batteryStatusUpdated;
private @Nullable Integer chargingRemainingTime;
private @Nullable Boolean hvacstatus;
private @Nullable Double odometer;
private @Nullable Double estimatedRange;
private @Nullable String imageURL;
- private @Nullable String locationUpdated;
+ private @Nullable ZonedDateTime locationUpdated;
+ private LockStatus lockStatus = LockStatus.UNKNOWN;
private @Nullable Double gpsLatitude;
private @Nullable Double gpsLongitude;
private @Nullable Double externalTemperature;
ALWAYS_CHARGING
}
- public enum PlugStatus {
- UNPLUGGED,
- PLUGGED,
- PLUG_ERROR,
- PLUG_UNKNOWN,
- UNKNOWN
- }
-
public enum ChargingStatus {
NOT_IN_CHARGE,
WAITING_FOR_A_PLANNED_CHARGE,
UNKNOWN
}
+ public enum LockStatus {
+ LOCKED,
+ UNLOCKED,
+ UNKNOWN
+ }
+
+ public enum PlugStatus {
+ UNPLUGGED,
+ PLUGGED,
+ PLUG_ERROR,
+ PLUG_UNKNOWN,
+ UNKNOWN
+ }
+
public void setBatteryStatus(JsonObject responseJson) {
try {
JsonObject attributes = getAttributes(responseJson);
if (attributes.get("chargingRemainingTime") != null) {
chargingRemainingTime = attributes.get("chargingRemainingTime").getAsInt();
}
+ if (attributes.get("timestamp") != null) {
+ try {
+ batteryStatusUpdated = ZonedDateTime.parse(attributes.get("timestamp").getAsString());
+ } catch (DateTimeParseException e) {
+ batteryStatusUpdated = null;
+ logger.debug("Error updating battery status updated timestamp. {}", e.getMessage());
+ }
+ }
}
} catch (IllegalStateException | ClassCastException e) {
logger.warn("Error {} parsing Battery Status: {}", e.getMessage(), responseJson);
gpsLongitude = attributes.get("gpsLongitude").getAsDouble();
}
if (attributes.get("lastUpdateTime") != null) {
- locationUpdated = attributes.get("lastUpdateTime").getAsString();
+ try {
+ locationUpdated = ZonedDateTime.parse(attributes.get("lastUpdateTime").getAsString());
+ } catch (DateTimeParseException e) {
+ locationUpdated = null;
+ logger.debug("Error updating location updated timestamp. {}", e.getMessage());
+ }
+ }
+ }
+ } catch (IllegalStateException | ClassCastException e) {
+ logger.warn("Error {} parsing Location: {}", e.getMessage(), responseJson);
+ }
+ }
+
+ public void setLockStatus(JsonObject responseJson) {
+ try {
+ JsonObject attributes = getAttributes(responseJson);
+ if (attributes != null) {
+ if (attributes.get("lockStatus") != null) {
+ lockStatus = mapLockStatus(attributes.get("lockStatus").getAsString());
}
}
} catch (IllegalStateException | ClassCastException e) {
return batteryLevel;
}
+ public @Nullable ZonedDateTime getBatteryStatusUpdated() {
+ return batteryStatusUpdated;
+ }
+
public @Nullable Boolean getHvacstatus() {
return hvacstatus;
}
return gpsLongitude;
}
- public @Nullable String getLocationUpdated() {
+ public @Nullable ZonedDateTime getLocationUpdated() {
return locationUpdated;
}
return null;
}
+ private LockStatus mapLockStatus(final String apiLockStatus) {
+ switch (apiLockStatus) {
+ case "locked":
+ return LockStatus.LOCKED;
+ case "unlocked":
+ return LockStatus.UNLOCKED;
+ default:
+ return LockStatus.UNKNOWN;
+ }
+ }
+
private PlugStatus mapPlugStatus(final String apiPlugState) {
// https://github.com/hacf-fr/renault-api/blob/main/src/renault_api/kamereon/enums.py
switch (apiPlugState) {
return ChargingStatus.UNKNOWN;
}
}
+
+ public LockStatus getLockStatus() {
+ return lockStatus;
+ }
+
+ public boolean isDisableLockStatus() {
+ return disableLockStatus;
+ }
+
+ public void setDisableLockStatus(boolean disableLockStatus) {
+ this.disableLockStatus = disableLockStatus;
+ }
}
import org.eclipse.jetty.util.Fields;
import org.openhab.binding.renault.internal.RenaultConfiguration;
import org.openhab.binding.renault.internal.api.Car.ChargingMode;
+import org.openhab.binding.renault.internal.api.exceptions.RenaultActionException;
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;
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);
+ final String url = this.constants.getGigyaRootUrl() + "/accounts.login";
+ ContentResponse response = httpClient.FORM(url, fields);
if (HttpStatus.OK_200 == response.getStatus()) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(),
+ response.getReason(), response.getContentAsString());
+ }
try {
JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
JsonObject sessionInfoJson = responseJson.getAsJsonObject("sessionInfo");
throw new RenaultException("Login Error: cookie value not found in JSON response");
}
if (cookieValue == null) {
- logger.warn("Login Error: cookie value not found! Response: [{}] {}\n{}", response.getStatus(),
- response.getReason(), response.getContentAsString());
+ logger.warn("Login Error: cookie value not found! Response: {}", response.getContentAsString());
+ throw new RenaultException("Login Error: cookie value not found in JSON response");
}
} else {
- logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(),
+ logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
response.getContentAsString());
throw new RenaultException("Login Error: " + response.getReason());
}
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);
+ final String url = this.constants.getGigyaRootUrl() + "/accounts.getAccountInfo";
+ ContentResponse response = httpClient.FORM(url, fields);
if (HttpStatus.OK_200 == response.getStatus()) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(),
+ response.getReason(), response.getContentAsString());
+ }
try {
JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
JsonObject dataJson = responseJson.getAsJsonObject("data");
"Get Account Info Error: personId or gigyaDataCenter value not found in JSON response");
}
} else {
- logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(),
+ logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
response.getContentAsString());
throw new RenaultException("Get Account Info Error: " + response.getReason());
}
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);
+ final String url = this.constants.getGigyaRootUrl() + "/accounts.getJWT";
+ ContentResponse response = this.httpClient.FORM(url, fields);
if (HttpStatus.OK_200 == response.getStatus()) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(),
+ response.getReason(), response.getContentAsString());
+ }
try {
JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
JsonElement element = responseJson.get("id_token");
if (element != null) {
jwt = element.getAsString();
- logger.debug("jwt: {} ", jwt);
+ logger.debug("GigyaApi 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(),
+ logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
response.getContentAsString());
throw new RenaultException("Get JWT Error: " + response.getReason());
}
}
}
- public void actionHvacOn(double hvacTargetTemperature)
- throws RenaultForbiddenException, RenaultNotImplementedException {
- Request request = httpClient
- .newRequest(this.constants.getKamereonRootUrl() + "/commerce/v1/accounts/" + kamereonaccountId
- + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/actions/hvac-start?country="
- + getCountry(config))
- .method(HttpMethod.POST).header("Content-type", "application/vnd.api+json")
- .header("apikey", this.config.kamereonApiKey)
- .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
- request.content(new StringContentProvider(
- "{\"data\":{\"type\":\"HvacStart\",\"attributes\":{\"action\":\"start\",\"targetTemperature\":\""
- + hvacTargetTemperature + "\"}}}",
- "utf-8"));
- try {
- ContentResponse response = request.send();
- logger.debug("Kamereon Response HVAC ON: {}", response.getContentAsString());
- if (HttpStatus.OK_200 != response.getStatus()) {
- 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());
- }
- }
- } catch (InterruptedException e) {
- logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
- Thread.currentThread().interrupt();
- } catch (JsonParseException | TimeoutException | ExecutionException e) {
- logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
+ public void getLockStatus(Car car)
+ throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
+ JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
+ + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/lock-status?country=" + getCountry(config));
+ if (responseJson != null) {
+ car.setLockStatus(responseJson);
}
}
- public void actionChargeMode(ChargingMode mode) throws RenaultForbiddenException, RenaultNotImplementedException {
- Request request = httpClient
- .newRequest(this.constants.getKamereonRootUrl() + "/commerce/v1/accounts/" + kamereonaccountId
- + "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/actions/charge-mode?country="
- + getCountry(config))
- .method(HttpMethod.POST).header("Content-type", "application/vnd.api+json")
- .header("apikey", this.config.kamereonApiKey)
- .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
+ public void actionHvacOn(double hvacTargetTemperature)
+ throws RenaultForbiddenException, RenaultNotImplementedException, RenaultActionException {
+ final String path = "/commerce/v1/accounts/" + kamereonaccountId + "/kamereon/kca/car-adapter/v1/cars/"
+ + config.vin + "/actions/hvac-start?country=" + getCountry(config);
+ postKamereonRequest(path,
+ "{\"data\":{\"type\":\"HvacStart\",\"attributes\":{\"action\":\"start\",\"targetTemperature\":\""
+ + hvacTargetTemperature + "\"}}}");
+ }
+ public void actionChargeMode(ChargingMode mode)
+ throws RenaultForbiddenException, RenaultNotImplementedException, RenaultActionException {
final String apiMode = ChargingMode.SCHEDULE_MODE.equals(mode) ? CHARGING_MODE_SCHEDULE : CHARGING_MODE_ALWAYS;
- request.content(new StringContentProvider(
- "{\"data\":{\"type\":\"ChargeMode\",\"attributes\":{\"action\":\"" + apiMode + "\"}}}", "utf-8"));
+ final String path = "/commerce/v1/accounts/" + kamereonaccountId + "/kamereon/kca/car-adapter/v1/cars/"
+ + config.vin + "/actions/charge-mode?country=" + getCountry(config);
+ postKamereonRequest(path,
+ "{\"data\":{\"type\":\"ChargeMode\",\"attributes\":{\"action\":\"" + apiMode + "\"}}}");
+ }
+
+ private void postKamereonRequest(final String path, final String content)
+ throws RenaultForbiddenException, RenaultNotImplementedException, RenaultActionException {
+ Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.POST)
+ .header("Content-type", "application/vnd.api+json").header("apikey", this.config.kamereonApiKey)
+ .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt)
+ .content(new StringContentProvider(content, "utf-8"));
try {
ContentResponse response = request.send();
- logger.debug("Kamereon Response set ChargeMode: {}", response.getContentAsString());
- if (HttpStatus.OK_200 != response.getStatus()) {
- 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());
- }
- }
+ logKamereonCall(request, response);
+ checkResponse(response);
} catch (InterruptedException e) {
logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
Thread.currentThread().interrupt();
} catch (JsonParseException | TimeoutException | ExecutionException e) {
- logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
+ throw new RenaultActionException(e.toString());
}
}
private @Nullable JsonObject getKamereonResponse(String path)
- throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
+ throws RenaultForbiddenException, RenaultNotImplementedException, RenaultUpdateException {
Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.GET)
.header("Content-type", "application/vnd.api+json").header("apikey", this.config.kamereonApiKey)
.header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
try {
ContentResponse response = request.send();
+ logKamereonCall(request, response);
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());
- }
}
+ checkResponse(response);
} catch (InterruptedException e) {
logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
Thread.currentThread().interrupt();
} catch (JsonParseException | TimeoutException | ExecutionException e) {
- logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
+ throw new RenaultUpdateException(e.toString());
}
return null;
}
+ private void logKamereonCall(Request request, ContentResponse response) {
+ if (HttpStatus.OK_200 == response.getStatus()) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Kamereon Request: {} Response: [{}] {}\n{}", request.getURI().toString(),
+ response.getStatus(), response.getReason(), response.getContentAsString());
+ }
+ } else {
+ logger.warn("Kamereon Request: {} Response: [{}] {}\n{}", request.getURI().toString(), response.getStatus(),
+ response.getReason(), response.getContentAsString());
+ }
+ }
+
+ private void checkResponse(ContentResponse response)
+ throws RenaultForbiddenException, RenaultNotImplementedException {
+ switch (response.getStatus()) {
+ case HttpStatus.FORBIDDEN_403:
+ throw new RenaultForbiddenException(
+ "Kamereon request forbidden! Ensure the car is paired in your MyRenault App.");
+ case HttpStatus.NOT_FOUND_404:
+ throw new RenaultNotImplementedException("Kamereon service not found");
+ case HttpStatus.NOT_IMPLEMENTED_501:
+ throw new RenaultNotImplementedException("Kamereon request not implemented");
+ case HttpStatus.BAD_GATEWAY_502:
+ throw new RenaultNotImplementedException("Kamereon request failed");
+ default:
+ break;
+ }
+ }
+
private String getCountry(RenaultConfiguration config) {
String country = "XX";
if (config.locale.length() == 5) {
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 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 perform action on the My Renault car.
+ *
+ * @author Doug Culnane - Initial contribution
+ */
+@NonNullByDefault
+public class RenaultActionException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public RenaultActionException(String message) {
+ super(message);
+ }
+}
import static org.openhab.core.library.unit.Units.KILOWATT_HOUR;
import static org.openhab.core.library.unit.Units.MINUTE;
+import java.time.ZonedDateTime;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.renault.internal.api.Car;
import org.openhab.binding.renault.internal.api.Car.ChargingMode;
import org.openhab.binding.renault.internal.api.MyRenaultHttpSession;
+import org.openhab.binding.renault.internal.api.exceptions.RenaultActionException;
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.openhab.core.library.types.DateTimeType;
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.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
return;
}
updateStatus(ThingStatus.UNKNOWN);
-
updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
- // Background initialization:
- ScheduledFuture<?> job = pollingJob;
- if (job == null || job.isCancelled()) {
- pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);
- }
+ reschedulePollingJob();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
+
switch (channelUID.getId()) {
case RenaultBindingConstants.CHANNEL_HVAC_TARGET_TEMPERATURE:
if (!car.isDisableHvac()) {
}
break;
case RenaultBindingConstants.CHANNEL_HVAC_STATUS:
- // We can only trigger pre-conditioning of the car.
- if (command instanceof StringType && command.toString().equals(Car.HVAC_STATUS_ON)
- && !car.isDisableHvac()) {
- final MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
- try {
- updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
- car.resetHVACStatus();
- httpSession.initSesssion(car);
- httpSession.actionHvacOn(car.getHvacTargetTemperature());
- if (pollingJob != null) {
- pollingJob.cancel(true);
+ if (!car.isDisableHvac()) {
+ if (command instanceof RefreshType) {
+ reschedulePollingJob();
+ } else if (command instanceof StringType && command.toString().equals(Car.HVAC_STATUS_ON)) {
+ // We can only trigger pre-conditioning of the car.
+ final MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
+ try {
+ updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
+ car.resetHVACStatus();
+ httpSession.initSesssion(car);
+ httpSession.actionHvacOn(car.getHvacTargetTemperature());
+ ScheduledFuture<?> job = pollingJob;
+ if (job != null) {
+ job.cancel(true);
+ }
+ pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, config.updateDelay,
+ config.refreshInterval * 60, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ logger.warn("Error My Renault Http Session.", e);
+ Thread.currentThread().interrupt();
+ } catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
+ | RenaultActionException | RenaultNotImplementedException | ExecutionException
+ | TimeoutException e) {
+ logger.warn("Error during action HVAC on.", e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
- pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, config.updateDelay,
- config.refreshInterval * 60, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- logger.warn("Error My Renault Http Session.", e);
- Thread.currentThread().interrupt();
- } catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
- | RenaultNotImplementedException | ExecutionException | TimeoutException e) {
- logger.warn("Error My Renault Http Session.", e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
break;
case RenaultBindingConstants.CHANNEL_CHARGING_MODE:
- if (command instanceof StringType) {
+ if (command instanceof RefreshType) {
+ reschedulePollingJob();
+ } else if (command instanceof StringType) {
try {
ChargingMode newMode = ChargingMode.valueOf(command.toString());
if (!ChargingMode.UNKNOWN.equals(newMode)) {
logger.warn("Error My Renault Http Session.", e);
Thread.currentThread().interrupt();
} catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
- | RenaultNotImplementedException | ExecutionException | TimeoutException e) {
- logger.warn("Error My Renault Http Session.", e);
+ | RenaultActionException | RenaultNotImplementedException | ExecutionException
+ | TimeoutException e) {
+ logger.warn("Error during action set charge mode.", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
e.getMessage());
}
return;
}
}
+ break;
default:
+ if (command instanceof RefreshType) {
+ reschedulePollingJob();
+ }
break;
}
}
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);
+ String imageURL = car.getImageURL();
+ if (imageURL != null && !imageURL.isEmpty()) {
+ updateState(CHANNEL_IMAGE, new StringType(imageURL));
}
+ updateHvacStatus(httpSession);
+ updateCockpit(httpSession);
+ updateLocation(httpSession);
+ updateBattery(httpSession);
+ updateLockStatus(httpSession);
}
private void updateHvacStatus(MyRenaultHttpSession httpSession) {
new QuantityType<Temperature>(externalTemperature.doubleValue(), SIUnits.CELSIUS));
}
} catch (RenaultNotImplementedException e) {
+ logger.warn("Disabling unsupported HVAC status update.");
car.setDisableHvac(true);
} catch (RenaultForbiddenException | RenaultUpdateException e) {
+ logger.warn("Error updating HVAC status.", e);
}
}
}
updateState(CHANNEL_LOCATION, new PointType(new DecimalType(latitude.doubleValue()),
new DecimalType(longitude.doubleValue())));
}
- String locationUpdated = car.getLocationUpdated();
+ ZonedDateTime locationUpdated = car.getLocationUpdated();
if (locationUpdated != null) {
updateState(CHANNEL_LOCATION_UPDATED, new DateTimeType(locationUpdated));
}
} catch (RenaultNotImplementedException e) {
+ logger.warn("Disabling unsupported location update.");
car.setDisableLocation(true);
} catch (IllegalArgumentException | RenaultForbiddenException | RenaultUpdateException e) {
+ logger.warn("Error updating location.", e);
}
}
}
updateState(CHANNEL_ODOMETER, new QuantityType<Length>(odometer.doubleValue(), KILO(METRE)));
}
} catch (RenaultNotImplementedException e) {
+ logger.warn("Disabling unsupported cockpit status update.");
car.setDisableCockpit(true);
} catch (RenaultForbiddenException | RenaultUpdateException e) {
+ logger.warn("Error updating cockpit status.", e);
}
}
}
updateState(CHANNEL_CHARGING_REMAINING_TIME,
new QuantityType<Time>(chargingRemainingTime.doubleValue(), MINUTE));
}
+ ZonedDateTime batteryStatusUpdated = car.getBatteryStatusUpdated();
+ if (batteryStatusUpdated != null) {
+ updateState(CHANNEL_BATTERY_STATUS_UPDATED, new DateTimeType(batteryStatusUpdated));
+ }
} catch (RenaultNotImplementedException e) {
+ logger.warn("Disabling unsupported battery update.");
car.setDisableBattery(true);
} catch (RenaultForbiddenException | RenaultUpdateException e) {
+ logger.warn("Error updating battery status.", e);
}
}
}
+
+ private void updateLockStatus(MyRenaultHttpSession httpSession) {
+ if (!car.isDisableLockStatus()) {
+ try {
+ httpSession.getLockStatus(car);
+ switch (car.getLockStatus()) {
+ case LOCKED:
+ updateState(CHANNEL_LOCKED, OnOffType.ON);
+ break;
+ case UNLOCKED:
+ updateState(CHANNEL_LOCKED, OnOffType.OFF);
+ break;
+ default:
+ updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
+ break;
+ }
+ } catch (RenaultNotImplementedException e) {
+ updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
+ logger.warn("Disabling unsupported lock status update.");
+ car.setDisableLockStatus(true);
+ } catch (RenaultForbiddenException | RenaultUpdateException e) {
+ logger.warn("Error updating lock status.", e);
+ }
+ }
+ }
+
+ private void reschedulePollingJob() {
+ ScheduledFuture<?> job = pollingJob;
+ if (job != null) {
+ job.cancel(true);
+ }
+ pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);
+ }
}
# channel types
channel-type.renault.batteryavailableenergy.label = Battery Energy Available
+channel-type.renault.batterystatusupdated.label = Battery Status Updated
+channel-type.renault.batterystatusupdated.description = Timestamp of the last battery status update
+channel-type.renault.batterystatusupdated.state.pattern = %1$tH:%1$tM %1$td.%1$tm.%1$tY
channel-type.renault.chargingmode.label = Charging Mode
channel-type.renault.chargingmode.state.option.UNKNOWN = Unknown
channel-type.renault.chargingmode.state.option.SCHEDULE_MODE = Schedule mode
channel-type.renault.locationupdated.label = Location Update
channel-type.renault.locationupdated.description = Timestamp of the last location update
channel-type.renault.locationupdated.state.pattern = %1$tH:%1$tM %1$td.%1$tm.%1$tY
+channel-type.renault.locked.label = Locked
+channel-type.renault.locked.description = Locked status of the car
channel-type.renault.odometer.label = Odometer
channel-type.renault.odometer.description = Total distance travelled
channel-type.renault.plugstatus.label = Plug Status
<channels>
<channel id="batterylevel" typeId="system.battery-level"/>
<channel id="batteryavailableenergy" typeId="batteryavailableenergy"/>
+ <channel id="batterystatusupdated" typeId="batterystatusupdated"/>
<channel id="plugstatus" typeId="plugstatus"/>
<channel id="chargingstatus" typeId="chargingstatus"/>
<channel id="chargingmode" typeId="chargingmode"/>
<channel id="image" typeId="image"/>
<channel id="location" typeId="system.location"/>
<channel id="locationupdated" typeId="locationupdated"/>
+ <channel id="locked" typeId="locked"/>
</channels>
<config-description>
<channel-type id="batteryavailableenergy">
<item-type>Number:Energy</item-type>
<label>Battery Energy Available</label>
+ <category>Energy</category>
<state pattern="%.1f %unit%" readOnly="true"></state>
</channel-type>
+ <channel-type id="batterystatusupdated">
+ <item-type>DateTime</item-type>
+ <label>Battery Status Updated</label>
+ <description>Timestamp of the last battery status update</description>
+ <category>Time</category>
+ <state pattern="%1$tH:%1$tM %1$td.%1$tm.%1$tY" readOnly="true"></state>
+ </channel-type>
<channel-type id="plugstatus">
<item-type>String</item-type>
<label>Plug Status</label>
</channel-type>
<channel-type id="locationupdated">
<item-type>DateTime</item-type>
- <label>Location Update</label>
+ <label>Location Updated</label>
<description>Timestamp of the last location update</description>
+ <category>Time</category>
<state pattern="%1$tH:%1$tM %1$td.%1$tm.%1$tY" readOnly="true"></state>
</channel-type>
+ <channel-type id="locked">
+ <item-type>Switch</item-type>
+ <label>Locked</label>
+ <description>Locked status of the car</description>
+ <category>Lock</category>
+ <state readOnly="true"/>
+ </channel-type>
</thing:thing-descriptions>