]> git.basschouten.com Git - openhab-addons.git/commitdiff
[renault] Renault more channels and HVAC ON / toggle charge mode (#12095)
authorDoug Culnane <32482395+dougculnane@users.noreply.github.com>
Sat, 12 Feb 2022 19:22:35 +0000 (20:22 +0100)
committerGitHub <noreply@github.com>
Sat, 12 Feb 2022 19:22:35 +0000 (20:22 +0100)
* [renault] Add more channels to Renault Binding.
* [renault] Improve example rule to reduce API use.

Signed-off-by: Culnane Douglas <douglas.culnane@extern.a1.at>
bundles/org.openhab.binding.renault/README.md
bundles/org.openhab.binding.renault/doc/sitemap.png [new file with mode: 0644]
bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultBindingConstants.java
bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/RenaultConfiguration.java
bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/Car.java
bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/MyRenaultHttpSession.java
bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/handler/RenaultHandler.java
bundles/org.openhab.binding.renault/src/main/resources/OH-INF/i18n/renault.properties
bundles/org.openhab.binding.renault/src/main/resources/OH-INF/thing/thing-types.xml

index d6533bf7f11fc5a99261b16afaec82467ede85ca..e2cc4102f4c1e3384800b556a5804da12fa12e9a 100644 (file)
 # 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       |
-
-## Channels
+| 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       |
 
-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        |
 
+## Channels
 
+| 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 (file)
index 0000000..44425d0
Binary files /dev/null and b/bundles/org.openhab.binding.renault/doc/sitemap.png differ
index 2bb08cef4df2bc057a134b566201936bd9a6ff2a..877d00bc0082ffb60eab762bad78a553f546eca5 100644 (file)
@@ -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";
 }
index fa7feff2ec0c02aa8ea50150b574df6751a466e3..55e7656e1c66a8707ba98e3a654163725ce8206f 100644 (file)
@@ -27,4 +27,5 @@ public class RenaultConfiguration {
     public String locale = "";
     public String vin = "";
     public int refreshInterval = 10;
+    public int updateDelay = 30;
 }
index e2b5e3e1daa786c4a07b08a28e1054350e7bdc68..bec8189ac5ca6dd43b34a8482a2ae9ea19212d2d 100644 (file)
@@ -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;
+        }
+    }
 }
index 2a555f6100bb3917bf5c1698e2ccffe320d7afec..f2539c81faf0bda429dda1fc567372cbff68dec1 100644 (file)
@@ -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;
index 3bbf4e934b946c901e7a94682d0dd739731f5163..fae1d60e9e0be0e058e6ce697667ca30a5f14b87 100644 (file)
@@ -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<Temperature>(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<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
+                    } else if (command instanceof DecimalType) {
+                        car.setHvacTargetTemperature(((DecimalType) command).doubleValue());
+                        updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
+                                new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
+                    } else if (command instanceof QuantityType) {
+                        @Nullable
+                        QuantityType<Temperature> celsius = ((QuantityType<Temperature>) command)
+                                .toUnit(SIUnits.CELSIUS);
+                        if (celsius != null) {
+                            car.setHvacTargetTemperature(celsius.doubleValue());
+                        }
+                        updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
+                                new QuantityType<Temperature>(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<Temperature>(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<Length>(estimatedRange.doubleValue(), KILO(METRE)));
+                }
+                Double batteryAvailableEnergy = car.getBatteryAvailableEnergy();
+                if (batteryAvailableEnergy != null) {
+                    updateState(CHANNEL_BATTERY_AVAILABLE_ENERGY,
+                            new QuantityType<Energy>(batteryAvailableEnergy.doubleValue(), KILOWATT_HOUR));
+                }
+                Integer chargingRemainingTime = car.getChargingRemainingTime();
+                if (chargingRemainingTime != null) {
+                    updateState(CHANNEL_CHARGING_REMAINING_TIME,
+                            new QuantityType<Time>(chargingRemainingTime.doubleValue(), MINUTE));
+                }
             } catch (RenaultNotImplementedException e) {
                 car.setDisableBattery(true);
             } catch (RenaultForbiddenException | RenaultUpdateException e) {
index 2269672aa0474ec6eaa9dbd88204451c041361be..7d900722f58b5f5d181cbcfab57632bd86add6a4 100644 (file)
@@ -45,13 +45,50 @@ thing-type.config.renault.car.myRenaultPassword.label = MyRenault Password
 thing-type.config.renault.car.myRenaultUsername.label = MyRenault Username
 thing-type.config.renault.car.refreshInterval.label = Refresh Interval
 thing-type.config.renault.car.refreshInterval.description = Interval the car is polled in minutes.
+thing-type.config.renault.car.updateDelay.label = Update Delay
+thing-type.config.renault.car.updateDelay.description = How long to wait for commands to reach car and update to server in seconds.
 thing-type.config.renault.car.vin.label = VIN
 thing-type.config.renault.car.vin.description = Vehicle Identification Number
 
 # channel types
 
-channel-type.renault.hvacstatus.label = HVAC Status
+channel-type.renault.batteryavailableenergy.label = Battery Energy Available
+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.chargingmode.state.option.ALWAYS_CHARGING = Instant charge
+channel-type.renault.chargingremainingtime.label = Charging Time Remaining
+channel-type.renault.chargingstatus.label = Charging Status
+channel-type.renault.chargingstatus.state.option.NOT_IN_CHARGE = Not charging
+channel-type.renault.chargingstatus.state.option.WAITING_FOR_A_PLANNED_CHARGE = Waiting for schedule
+channel-type.renault.chargingstatus.state.option.CHARGE_ENDED = Charge ended
+channel-type.renault.chargingstatus.state.option.WAITING_FOR_CURRENT_CHARGE = Waiting for charge
+channel-type.renault.chargingstatus.state.option.ENERGY_FLAP_OPENED = Plug flap opened
+channel-type.renault.chargingstatus.state.option.CHARGE_IN_PROGRESS = Charge in progress
+channel-type.renault.chargingstatus.state.option.CHARGE_ERROR = Charge error
+channel-type.renault.chargingstatus.state.option.UNAVAILABLE = Unavailable
+channel-type.renault.chargingstatus.state.option.UNKNOWN = Unknown
+channel-type.renault.estimatedrange.label = Estimated Range
+channel-type.renault.estimatedrange.description = Estimated range of the car.
+channel-type.renault.externaltemperature.label = External Temperature
+channel-type.renault.externaltemperature.description = Temperature outside of the car
+channel-type.renault.hvacstatus.label = HVAC Status (ON | OFF | PENDING)
+channel-type.renault.hvacstatus.state.option.ON = On
+channel-type.renault.hvacstatus.state.option.PENDING = Pending
+channel-type.renault.hvacstatus.state.option.OFF = Off
+channel-type.renault.hvactargettemperature.label = HVAC Target Temperature
+channel-type.renault.hvactargettemperature.description = HVAC target temperature (19 to 21)
 channel-type.renault.image.label = Image URL
 channel-type.renault.image.description = Image URL of MyRenault
+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.odometer.label = Odometer
 channel-type.renault.odometer.description = Total distance travelled
+channel-type.renault.plugstatus.label = Plug Status
+channel-type.renault.plugstatus.description = Status of charging plug.
+channel-type.renault.plugstatus.state.option.UNPLUGGED = Unplugged
+channel-type.renault.plugstatus.state.option.PLUGGED = Plugged
+channel-type.renault.plugstatus.state.option.PLUG_ERROR = Plug error
+channel-type.renault.plugstatus.state.option.PLUG_UNKNOWN = Plug unknown
+channel-type.renault.plugstatus.state.option.UNKNOWN = Unknown
index ee21b82bbcd7ad3e438144bcb762383f2ce656bb..bede45cbb38067e42e1052c3de3c3a64b508ffb6 100644 (file)
 
                <channels>
                        <channel id="batterylevel" typeId="system.battery-level"/>
+                       <channel id="batteryavailableenergy" typeId="batteryavailableenergy"/>
+                       <channel id="plugstatus" typeId="plugstatus"/>
+                       <channel id="chargingstatus" typeId="chargingstatus"/>
+                       <channel id="chargingmode" typeId="chargingmode"/>
+                       <channel id="chargingremainingtime" typeId="chargingremainingtime"/>
+                       <channel id="estimatedrange" typeId="estimatedrange"/>
+                       <channel id="odometer" typeId="odometer"/>
                        <channel id="hvacstatus" typeId="hvacstatus"/>
+                       <channel id="hvactargettemperature" typeId="hvactargettemperature"/>
+                       <channel id="externaltemperature" typeId="externaltemperature"/>
                        <channel id="image" typeId="image"/>
                        <channel id="location" typeId="system.location"/>
-                       <channel id="odometer" typeId="odometer"/>
+                       <channel id="locationupdated" typeId="locationupdated"/>
                </channels>
                <config-description>
 
                                <description>Interval the car is polled in minutes.</description>
                                <default>10</default>
                        </parameter>
+                       <parameter name="updateDelay" type="integer" unit="s" min="5" max="120">
+                               <label>Update Delay</label>
+                               <description>How long to wait for commands to reach car and update to server in seconds.</description>
+                               <default>30</default>
+                       </parameter>
                </config-description>
        </thing-type>
 
-       <!-- Sample Channel Type -->
-       <channel-type id="hvacstatus">
-               <item-type>Switch</item-type>
-               <label>HVAC Status</label>
-               <state readOnly="true"></state>
+       <channel-type id="batteryavailableenergy">
+               <item-type>Number:Energy</item-type>
+               <label>Battery Energy Available</label>
+               <state pattern="%.1f %unit%" readOnly="true"></state>
        </channel-type>
-       <channel-type id="image">
+       <channel-type id="plugstatus">
                <item-type>String</item-type>
-               <label>Image URL</label>
-               <description>Image URL of MyRenault</description>
-               <state readOnly="true"></state>
+               <label>Plug Status</label>
+               <description>Status of charging plug.</description>
+               <state readOnly="true">
+                       <options>
+                               <option value="UNPLUGGED">Unplugged</option>
+                               <option value="PLUGGED">Plugged</option>
+                               <option value="PLUG_ERROR">Plug error</option>
+                               <option value="PLUG_UNKNOWN">Plug unknown</option>
+                               <option value="UNKNOWN">Unknown</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="chargingstatus">
+               <item-type>String</item-type>
+               <label>Charging Status</label>
+               <state readOnly="true">
+                       <options>
+                               <option value="NOT_IN_CHARGE">Not charging</option>
+                               <option value="WAITING_FOR_A_PLANNED_CHARGE">Waiting for schedule</option>
+                               <option value="CHARGE_ENDED">Charge ended</option>
+                               <option value="WAITING_FOR_CURRENT_CHARGE">Waiting for charge</option>
+                               <option value="ENERGY_FLAP_OPENED">Plug flap opened</option>
+                               <option value="CHARGE_IN_PROGRESS">Charge in progress</option>
+                               <option value="CHARGE_ERROR">Charge error</option>
+                               <option value="UNAVAILABLE">Unavailable</option>
+                               <option value="UNKNOWN">Unknown</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="chargingmode">
+               <item-type>String</item-type>
+               <label>Charging Mode</label>
+               <state readOnly="false">
+                       <options>
+                               <option value="UNKNOWN">Unknown</option>
+                               <option value="SCHEDULE_MODE">Schedule mode</option>
+                               <option value="ALWAYS_CHARGING">Instant charge</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="chargingremainingtime">
+               <item-type>Number:Time</item-type>
+               <label>Charging Time Remaining</label>
+               <category>Time</category>
+               <state pattern="%d %unit%" readOnly="true"></state>
+       </channel-type>
+       <channel-type id="estimatedrange">
+               <item-type>Number:Length</item-type>
+               <label>Estimated Range</label>
+               <description>Estimated range of the car.</description>
+               <state pattern="%.1f %unit%" readOnly="true"></state>
        </channel-type>
        <channel-type id="odometer">
                <item-type>Number:Length</item-type>
                <description>Total distance travelled</description>
                <state pattern="%.1f %unit%" readOnly="true"></state>
        </channel-type>
+       <channel-type id="hvacstatus">
+               <item-type>String</item-type>
+               <label>HVAC Status (ON | OFF | PENDING)</label>
+               <state readOnly="false">
+                       <options>
+                               <option value="ON">On</option>
+                               <option value="PENDING">Pending</option>
+                               <option value="OFF">Off</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="hvactargettemperature" advanced="true">
+               <item-type>Number:Temperature</item-type>
+               <label>HVAC Target Temperature</label>
+               <description>HVAC target temperature (19 to 21)</description>
+               <category>Temperature</category>
+               <state pattern="%.1f %unit%"></state>
+       </channel-type>
+       <channel-type id="externaltemperature" advanced="true">
+               <item-type>Number:Temperature</item-type>
+               <label>External Temperature</label>
+               <description>Temperature outside of the car</description>
+               <category>Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"></state>
+       </channel-type>
+       <channel-type id="image">
+               <item-type>String</item-type>
+               <label>Image URL</label>
+               <description>Image URL of MyRenault</description>
+               <state readOnly="true"></state>
+       </channel-type>
+       <channel-type id="locationupdated">
+               <item-type>DateTime</item-type>
+               <label>Location Update</label>
+               <description>Timestamp of the last location update</description>
+               <state pattern="%1$tH:%1$tM %1$td.%1$tm.%1$tY" readOnly="true"></state>
+       </channel-type>
 
 </thing:thing-descriptions>