diff --git a/bundles/org.openhab.binding.renault/README.md b/bundles/org.openhab.binding.renault/README.md index d6533bf7f..e2cc4102f 100644 --- a/bundles/org.openhab.binding.renault/README.md +++ b/bundles/org.openhab.binding.renault/README.md @@ -1,43 +1,132 @@ # Renault Binding -This binding allow MyRenault App. users to get battery status and other data from their cars. +This binding allows MyRenault App. users to get battery status and other data from their cars. +They can also heat their cars by turning ON the HVAC status and toggle the car's charging mode. -A binding that translates the [python based renault-api](https://renault-api.readthedocs.io/en/latest/) in an easy to use binding. +The binding translates the [python based renault-api](https://renault-api.readthedocs.io/en/latest/) in an easy to use openHAB java 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. +This binding can only retrieve information that is available in 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 | +| 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 | + ## 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 | +| Channel ID | Type | Description | Read Only | +|------------------------|--------------------|-------------------------------------------------|-----------| +| batteryavailableEnergy | Number:Energy | Battery Energy Available | Yes | +| batterylevel | Number | State of the battery in % | Yes | +| chargingmode | String | Charging mode. always_charging or schedule_mode | No | +| chargingstatus | String | Charging status | Yes | +| chargingremainingtime | Number:Time | Charging time remaining | Yes | +| plugstatus | String | Status of charging plug | Yes | +| estimatedrange | Number:Length | Estimated range of the car | Yes | +| odometer | Number:Length | Total distance travelled | Yes | +| hvacstatus | String | HVAC status HVAC Status (ON, OFF, PENDING) | No | +| hvactargettemperature | Number:Temperature | HVAC target temperature (19 to 21) | No | +| externaltemperature | Number:Temperature | Temperature outside of the car | 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 | +## Limitations + +Some channels may not work depending on your car and MyRenault account. + +The "externaltemperature" only works on a few cars. + +The "hvactargettemperature" is used by the hvacstatus ON command for pre-conditioning the car. +This seams to only allow values 19, 20 and 21 or else the pre-conditioning command will not work. + + +## Example + +renaultcar.sitemap: + +``` +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 + } +} +``` + +![Sitemap](doc/sitemap.png) + +If you do not have a smart charger and want to limit the charge of the battery you can set up an active 15 minute charge schedule in the MyRenault App. +Then create a Dimmer item "RenaultCar_ChargeLimit" and set it to 80% for example. This rule will change the RenaultCar_ChargingMode to schedule_mode when the limit is reached. +This stops charging after the battery level reaches the charge limit. + +ChargeRenaultCarLimit Code + +``` +configuration: {} +triggers: + - id: "1" + configuration: + itemName: RenaultCar_BatteryLevel + type: core.ItemStateUpdateTrigger + - id: "3" + configuration: + itemName: RenaultCar_ChargeLimit + type: core.ItemStateUpdateTrigger + - id: "4" + configuration: + itemName: RenaultCar_PlugStatus + type: core.ItemStateUpdateTrigger +conditions: [] +actions: + - inputs: {} + id: "2" + configuration: + type: application/vnd.openhab.dsl.rule + script: >- + if ( RenaultCar_PlugStatus.state.toString == 'PLUGGED' ) { + if ( RenaultCar_BatteryLevel.state as Number >= RenaultCar_ChargeLimit.state as Number ) { + if (RenaultCar_ChargingMode.state.toString == 'ALWAYS_CHARGING' ) { + RenaultCar_ChargingMode.sendCommand("SCHEDULE_MODE") + } + } else { + if (RenaultCar_ChargingMode.state.toString == 'SCHEDULE_MODE' ) { + RenaultCar_ChargingMode.sendCommand("ALWAYS_CHARGING") + } + } + } + type: script.ScriptAction + +``` diff --git a/bundles/org.openhab.binding.renault/doc/sitemap.png b/bundles/org.openhab.binding.renault/doc/sitemap.png new file mode 100644 index 000000000..44425d092 Binary files /dev/null and b/bundles/org.openhab.binding.renault/doc/sitemap.png differ 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 index 2bb08cef4..877d00bc0 100644 --- 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 @@ -30,9 +30,18 @@ public class RenaultBindingConstants { public static final ThingTypeUID THING_TYPE_CAR = new ThingTypeUID(BINDING_ID, "car"); // 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_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_ESTIMATED_RANGE = "estimatedrange"; + public static final String CHANNEL_EXTERNAL_TEMPERATURE = "externaltemperature"; public static final String CHANNEL_HVAC_STATUS = "hvacstatus"; + public static final String CHANNEL_HVAC_TARGET_TEMPERATURE = "hvactargettemperature"; 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_ODOMETER = "odometer"; + public static final String CHANNEL_PLUG_STATUS = "plugstatus"; } 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 index fa7feff2e..55e7656e1 100644 --- 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 @@ -27,4 +27,5 @@ public class RenaultConfiguration { public String locale = ""; public String vin = ""; public int refreshInterval = 10; + public int updateDelay = 30; } 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 index e2b5e3e1d..bec8189ac 100644 --- 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 @@ -30,6 +30,10 @@ import com.google.gson.JsonObject; @NonNullByDefault public class Car { + public static final String HVAC_STATUS_ON = "ON"; + public static final String HVAC_STATUS_OFF = "OFF"; + public static final String HVAC_STATUS_PENDING = "PENDING"; + private final Logger logger = LoggerFactory.getLogger(Car.class); private boolean disableLocation = false; @@ -37,29 +41,90 @@ public class Car { private boolean disableCockpit = false; private boolean disableHvac = false; + private ChargingStatus chargingStatus = ChargingStatus.UNKNOWN; + private ChargingMode chargingMode = ChargingMode.UNKNOWN; + private PlugStatus plugStatus = PlugStatus.UNKNOWN; + private double hvacTargetTemperature = 20.0; private @Nullable Double batteryLevel; + private @Nullable Double batteryAvailableEnergy; + 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 Double gpsLatitude; private @Nullable Double gpsLongitude; + private @Nullable Double externalTemperature; + + public enum ChargingMode { + UNKNOWN, + SCHEDULE_MODE, + ALWAYS_CHARGING + } + + public enum PlugStatus { + UNPLUGGED, + PLUGGED, + PLUG_ERROR, + PLUG_UNKNOWN, + UNKNOWN + } + + public enum ChargingStatus { + NOT_IN_CHARGE, + WAITING_FOR_A_PLANNED_CHARGE, + CHARGE_ENDED, + WAITING_FOR_CURRENT_CHARGE, + ENERGY_FLAP_OPENED, + CHARGE_IN_PROGRESS, + CHARGE_ERROR, + UNAVAILABLE, + UNKNOWN + } public void setBatteryStatus(JsonObject responseJson) { try { JsonObject attributes = getAttributes(responseJson); - if (attributes != null && attributes.get("batteryLevel") != null) { - batteryLevel = attributes.get("batteryLevel").getAsDouble(); + if (attributes != null) { + if (attributes.get("batteryLevel") != null) { + batteryLevel = attributes.get("batteryLevel").getAsDouble(); + } + if (attributes.get("batteryAutonomy") != null) { + estimatedRange = attributes.get("batteryAutonomy").getAsDouble(); + } + if (attributes.get("plugStatus") != null) { + plugStatus = mapPlugStatus(attributes.get("plugStatus").getAsString()); + } + if (attributes.get("chargingStatus") != null) { + chargingStatus = mapChargingStatus(attributes.get("chargingStatus").getAsString()); + } + if (attributes.get("batteryAvailableEnergy") != null) { + batteryAvailableEnergy = attributes.get("batteryAvailableEnergy").getAsDouble(); + } + if (attributes.get("chargingRemainingTime") != null) { + chargingRemainingTime = attributes.get("chargingRemainingTime").getAsInt(); + } } } catch (IllegalStateException | ClassCastException e) { logger.warn("Error {} parsing Battery Status: {}", e.getMessage(), responseJson); } } + public void resetHVACStatus() { + this.hvacstatus = null; + } + public void setHVACStatus(JsonObject responseJson) { try { JsonObject attributes = getAttributes(responseJson); - if (attributes != null && attributes.get("hvacStatus") != null) { - hvacstatus = attributes.get("hvacStatus").getAsString().equals("on"); + if (attributes != null) { + if (attributes.get("hvacStatus") != null) { + hvacstatus = attributes.get("hvacStatus").getAsString().equals("on"); + } + if (attributes.get("externalTemperature") != null) { + externalTemperature = attributes.get("externalTemperature").getAsDouble(); + } } } catch (IllegalStateException | ClassCastException e) { logger.warn("Error {} parsing HVAC Status: {}", e.getMessage(), responseJson); @@ -87,6 +152,9 @@ public class Car { if (attributes.get("gpsLongitude") != null) { gpsLongitude = attributes.get("gpsLongitude").getAsDouble(); } + if (attributes.get("lastUpdateTime") != null) { + locationUpdated = attributes.get("lastUpdateTime").getAsString(); + } } } catch (IllegalStateException | ClassCastException e) { logger.warn("Error {} parsing Location: {}", e.getMessage(), responseJson); @@ -128,80 +196,112 @@ public class Car { 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; + public @Nullable String getLocationUpdated() { + return locationUpdated; + } + + public @Nullable Double getExternalTemperature() { + return externalTemperature; + } + + public @Nullable Double getEstimatedRange() { + return estimatedRange; + } + + public PlugStatus getPlugStatus() { + return plugStatus; + } + + public ChargingStatus getChargingStatus() { + return chargingStatus; + } + + public ChargingMode getChargingMode() { + return chargingMode; + } + + public @Nullable Integer getChargingRemainingTime() { + return chargingRemainingTime; + } + + public @Nullable Double getBatteryAvailableEnergy() { + return batteryAvailableEnergy; + } + + public double getHvacTargetTemperature() { + return hvacTargetTemperature; + } + + public void setHvacTargetTemperature(double hvacTargetTemperature) { + this.hvacTargetTemperature = hvacTargetTemperature; + } + + public void setDisableLocation(boolean disableLocation) { + this.disableLocation = disableLocation; + } + + public void setDisableBattery(boolean disableBattery) { + this.disableBattery = disableBattery; + } + + public void setDisableCockpit(boolean disableCockpit) { + this.disableCockpit = disableCockpit; + } + + public void setDisableHvac(boolean disableHvac) { + this.disableHvac = disableHvac; + } + + /** + * Set the charging mode to a known mode. + * + * @param mode + */ + public void setChargeMode(ChargingMode mode) { + switch (mode) { + case SCHEDULE_MODE: + case ALWAYS_CHARGING: + chargingMode = mode; + break; + default: + break; + } } private @Nullable JsonObject getAttributes(JsonObject responseJson) @@ -211,4 +311,44 @@ public class Car { } return null; } + + private PlugStatus mapPlugStatus(final String apiPlugState) { + // https://github.com/hacf-fr/renault-api/blob/main/src/renault_api/kamereon/enums.py + switch (apiPlugState) { + case "0": + return PlugStatus.UNPLUGGED; + case "1": + return PlugStatus.PLUGGED; + case "-1": + return PlugStatus.PLUG_ERROR; + case "-2147483648": + return PlugStatus.PLUG_UNKNOWN; + default: + return PlugStatus.UNKNOWN; + } + } + + private ChargingStatus mapChargingStatus(final String apiChargeState) { + // https://github.com/hacf-fr/renault-api/blob/main/src/renault_api/kamereon/enums.py + switch (apiChargeState) { + case "0.0": + return ChargingStatus.NOT_IN_CHARGE; + case "0.1": + return ChargingStatus.WAITING_FOR_A_PLANNED_CHARGE; + case "0.2": + return ChargingStatus.CHARGE_ENDED; + case "0.3": + return ChargingStatus.WAITING_FOR_CURRENT_CHARGE; + case "0.4": + return ChargingStatus.ENERGY_FLAP_OPENED; + case "1.0": + return ChargingStatus.CHARGE_IN_PROGRESS; + case "-1.0": + return ChargingStatus.CHARGE_ERROR; + case "-1.1": + return ChargingStatus.UNAVAILABLE; + default: + return ChargingStatus.UNKNOWN; + } + } } 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 index 2a555f610..f2539c81f 100644 --- 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 @@ -20,10 +20,12 @@ 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.client.util.StringContentProvider; 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.Car.ChargingMode; 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; @@ -46,6 +48,9 @@ import com.google.gson.JsonParser; @NonNullByDefault public class MyRenaultHttpSession { + private static final String CHARGING_MODE_SCHEDULE = "schedule_mode"; + private static final String CHARGING_MODE_ALWAYS = "always_charging"; + private RenaultConfiguration config; private HttpClient httpClient; private Constants constants; @@ -98,6 +103,10 @@ public class MyRenaultHttpSession { } catch (JsonParseException | ClassCastException | IllegalStateException e) { 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()); + } } else { logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(), response.getContentAsString()); @@ -224,6 +233,75 @@ public class MyRenaultHttpSession { } } + 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.constants.getKamereonApiKey()) + .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 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.constants.getKamereonApiKey()) + .header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt); + + 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")); + 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()); + } + } + } 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()); + } + } + private @Nullable JsonObject getKamereonResponse(String path) throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException { Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.GET) @@ -248,7 +326,10 @@ public class MyRenaultHttpSession { "Kamereon Response Failed! Error: [" + response.getStatus() + "] " + response.getReason()); } } - } catch (JsonParseException | InterruptedException | TimeoutException | ExecutionException e) { + } 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()); } return null; 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 index 3bbf4e934..fae1d60e9 100644 --- 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 @@ -15,32 +15,44 @@ 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 static org.openhab.core.library.unit.Units.KILOWATT_HOUR; +import static org.openhab.core.library.unit.Units.MINUTE; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.measure.quantity.Energy; import javax.measure.quantity.Length; +import javax.measure.quantity.Temperature; +import javax.measure.quantity.Time; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.renault.internal.RenaultBindingConstants; import org.openhab.binding.renault.internal.RenaultConfiguration; 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.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.library.unit.SIUnits; 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.openhab.core.types.RefreshType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -69,11 +81,6 @@ public class RenaultHandler extends BaseThingHandler { 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 @@ -104,6 +111,9 @@ public class RenaultHandler extends BaseThingHandler { } updateStatus(ThingStatus.UNKNOWN); + updateState(CHANNEL_HVAC_TARGET_TEMPERATURE, + new QuantityType(car.getHvacTargetTemperature(), SIUnits.CELSIUS)); + // Background initialization: ScheduledFuture job = pollingJob; if (job == null || job.isCancelled()) { @@ -111,6 +121,86 @@ public class RenaultHandler extends BaseThingHandler { } } + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + switch (channelUID.getId()) { + case RenaultBindingConstants.CHANNEL_HVAC_TARGET_TEMPERATURE: + if (!car.isDisableHvac()) { + if (command instanceof RefreshType) { + updateState(CHANNEL_HVAC_TARGET_TEMPERATURE, + new QuantityType(car.getHvacTargetTemperature(), SIUnits.CELSIUS)); + } else if (command instanceof DecimalType) { + car.setHvacTargetTemperature(((DecimalType) command).doubleValue()); + updateState(CHANNEL_HVAC_TARGET_TEMPERATURE, + new QuantityType(car.getHvacTargetTemperature(), SIUnits.CELSIUS)); + } else if (command instanceof QuantityType) { + @Nullable + QuantityType celsius = ((QuantityType) command) + .toUnit(SIUnits.CELSIUS); + if (celsius != null) { + car.setHvacTargetTemperature(celsius.doubleValue()); + } + updateState(CHANNEL_HVAC_TARGET_TEMPERATURE, + new QuantityType(car.getHvacTargetTemperature(), SIUnits.CELSIUS)); + } + } + 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); + } + 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) { + try { + ChargingMode newMode = ChargingMode.valueOf(command.toString()); + if (!ChargingMode.UNKNOWN.equals(newMode)) { + MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient); + try { + httpSession.initSesssion(car); + httpSession.actionChargeMode(newMode); + car.setChargeMode(newMode); + updateState(CHANNEL_CHARGING_MODE, new StringType(newMode.toString())); + } 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()); + } + } + } catch (IllegalArgumentException e) { + logger.warn("Invalid ChargingMode {}.", command.toString()); + return; + } + } + default: + break; + } + } + @Override public void dispose() { ScheduledFuture job = pollingJob; @@ -126,8 +216,10 @@ public class RenaultHandler extends BaseThingHandler { try { httpSession.initSesssion(car); updateStatus(ThingStatus.ONLINE); + } catch (InterruptedException e) { + logger.warn("Error My Renault Http Session.", e); + Thread.currentThread().interrupt(); } catch (Exception e) { - httpSession = null; logger.warn("Error My Renault Http Session.", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } @@ -148,8 +240,17 @@ public class RenaultHandler extends BaseThingHandler { try { httpSession.getHvacStatus(car); Boolean hvacstatus = car.getHvacstatus(); - if (hvacstatus != null) { - updateState(CHANNEL_HVAC_STATUS, OnOffType.from(hvacstatus.booleanValue())); + if (hvacstatus == null) { + updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING)); + } else if (hvacstatus.booleanValue()) { + updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_ON)); + } else { + updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_OFF)); + } + Double externalTemperature = car.getExternalTemperature(); + if (externalTemperature != null) { + updateState(CHANNEL_EXTERNAL_TEMPERATURE, + new QuantityType(externalTemperature.doubleValue(), SIUnits.CELSIUS)); } } catch (RenaultNotImplementedException e) { car.setDisableHvac(true); @@ -168,9 +269,13 @@ public class RenaultHandler extends BaseThingHandler { updateState(CHANNEL_LOCATION, new PointType(new DecimalType(latitude.doubleValue()), new DecimalType(longitude.doubleValue()))); } + String locationUpdated = car.getLocationUpdated(); + if (locationUpdated != null) { + updateState(CHANNEL_LOCATION_UPDATED, new DateTimeType(locationUpdated)); + } } catch (RenaultNotImplementedException e) { car.setDisableLocation(true); - } catch (RenaultForbiddenException | RenaultUpdateException e) { + } catch (IllegalArgumentException | RenaultForbiddenException | RenaultUpdateException e) { } } } @@ -194,10 +299,27 @@ public class RenaultHandler extends BaseThingHandler { if (!car.isDisableBattery()) { try { httpSession.getBatteryStatus(car); + updateState(CHANNEL_PLUG_STATUS, new StringType(car.getPlugStatus().name())); + updateState(CHANNEL_CHARGING_STATUS, new StringType(car.getChargingStatus().name())); Double batteryLevel = car.getBatteryLevel(); if (batteryLevel != null) { updateState(CHANNEL_BATTERY_LEVEL, new DecimalType(batteryLevel.doubleValue())); } + Double estimatedRange = car.getEstimatedRange(); + if (estimatedRange != null) { + updateState(CHANNEL_ESTIMATED_RANGE, + new QuantityType(estimatedRange.doubleValue(), KILO(METRE))); + } + Double batteryAvailableEnergy = car.getBatteryAvailableEnergy(); + if (batteryAvailableEnergy != null) { + updateState(CHANNEL_BATTERY_AVAILABLE_ENERGY, + new QuantityType(batteryAvailableEnergy.doubleValue(), KILOWATT_HOUR)); + } + Integer chargingRemainingTime = car.getChargingRemainingTime(); + if (chargingRemainingTime != null) { + updateState(CHANNEL_CHARGING_REMAINING_TIME, + new QuantityType