]> git.basschouten.com Git - openhab-addons.git/commitdiff
[renault] Add new channels 'batterystatusupdated' and 'locked' (#14076)
authorDoug Culnane <32482395+dougculnane@users.noreply.github.com>
Thu, 5 Jan 2023 22:55:38 +0000 (23:55 +0100)
committerGitHub <noreply@github.com>
Thu, 5 Jan 2023 22:55:38 +0000 (23:55 +0100)
Signed-off-by: Doug Culnane <doug@culnane.net>
bundles/org.openhab.binding.renault/README.md
bundles/org.openhab.binding.renault/doc/sitemap.png [deleted file]
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/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/api/exceptions/RenaultActionException.java [new file with mode: 0644]
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 4175ece633727c0dd8b9bcffe7af6b9251621672..0f4eb18adc76fd571c49988edfa5fb612b9a199c 100644 (file)
@@ -19,15 +19,15 @@ No discovery
 
 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
 
@@ -35,6 +35,7 @@ You require your MyRenault credential, locale and VIN for your MyRenault registe
 |------------------------|--------------------|-------------------------------------------------|-----------|
 | 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       |
@@ -47,6 +48,7 @@ You require your MyRenault credential, locale and VIN for your MyRenault registe
 | 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
 
@@ -69,24 +71,24 @@ 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
+        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
     }
 }
 ```
 
-![Sitemap](doc/sitemap.png)
-
 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.
diff --git a/bundles/org.openhab.binding.renault/doc/sitemap.png b/bundles/org.openhab.binding.renault/doc/sitemap.png
deleted file mode 100644 (file)
index 44425d0..0000000
Binary files a/bundles/org.openhab.binding.renault/doc/sitemap.png and /dev/null differ
index 877d00bc0082ffb60eab762bad78a553f546eca5..4323727ebfcf14f303bf6c43f24e3db1744dc833 100644 (file)
@@ -32,6 +32,7 @@ public class RenaultBindingConstants {
     // 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";
@@ -42,6 +43,7 @@ public class RenaultBindingConstants {
     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";
 }
index bec8189ac5ca6dd43b34a8482a2ae9ea19212d2d..cbcc7caeace4c123aabb643b5c66009ae9a66e7d 100644 (file)
@@ -12,6 +12,9 @@
  */
 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;
@@ -40,6 +43,7 @@ public class Car {
     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;
@@ -47,12 +51,14 @@ public class Car {
     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;
@@ -63,14 +69,6 @@ public class Car {
         ALWAYS_CHARGING
     }
 
-    public enum PlugStatus {
-        UNPLUGGED,
-        PLUGGED,
-        PLUG_ERROR,
-        PLUG_UNKNOWN,
-        UNKNOWN
-    }
-
     public enum ChargingStatus {
         NOT_IN_CHARGE,
         WAITING_FOR_A_PLANNED_CHARGE,
@@ -83,6 +81,20 @@ public class Car {
         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);
@@ -105,6 +117,14 @@ public class Car {
                 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);
@@ -153,7 +173,25 @@ public class Car {
                     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) {
@@ -212,6 +250,10 @@ public class Car {
         return batteryLevel;
     }
 
+    public @Nullable ZonedDateTime getBatteryStatusUpdated() {
+        return batteryStatusUpdated;
+    }
+
     public @Nullable Boolean getHvacstatus() {
         return hvacstatus;
     }
@@ -232,7 +274,7 @@ public class Car {
         return gpsLongitude;
     }
 
-    public @Nullable String getLocationUpdated() {
+    public @Nullable ZonedDateTime getLocationUpdated() {
         return locationUpdated;
     }
 
@@ -312,6 +354,17 @@ public class Car {
         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) {
@@ -351,4 +404,16 @@ public class Car {
                 return ChargingStatus.UNKNOWN;
         }
     }
+
+    public LockStatus getLockStatus() {
+        return lockStatus;
+    }
+
+    public boolean isDisableLockStatus() {
+        return disableLockStatus;
+    }
+
+    public void setDisableLockStatus(boolean disableLockStatus) {
+        this.disableLockStatus = disableLockStatus;
+    }
 }
index fa8f0c06977cdb22a10d9673893122360722003e..3cb36b4912e7d26dea099444cbb99fa72e3fee14 100644 (file)
@@ -26,6 +26,7 @@ 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.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;
@@ -87,9 +88,13 @@ public class MyRenaultHttpSession {
         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");
@@ -104,11 +109,11 @@ public class MyRenaultHttpSession {
                 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());
         }
@@ -118,9 +123,13 @@ public class MyRenaultHttpSession {
         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");
@@ -138,7 +147,7 @@ public class MyRenaultHttpSession {
                         "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());
         }
@@ -151,20 +160,25 @@ public class MyRenaultHttpSession {
         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());
         }
@@ -233,108 +247,101 @@ 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.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) {
diff --git a/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultActionException.java b/bundles/org.openhab.binding.renault/src/main/java/org/openhab/binding/renault/internal/api/exceptions/RenaultActionException.java
new file mode 100644 (file)
index 0000000..aa15385
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * 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);
+    }
+}
index fae1d60e9e0be0e058e6ce697667ca30a5f14b87..2c793b26d729c537d200a1148fd5234a31d43c56 100644 (file)
@@ -18,6 +18,7 @@ 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.time.ZonedDateTime;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -36,12 +37,14 @@ 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.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;
@@ -53,6 +56,7 @@ 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.openhab.core.types.UnDefType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -110,19 +114,15 @@ public class RenaultHandler extends BaseThingHandler {
             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()) {
@@ -146,32 +146,39 @@ public class RenaultHandler extends BaseThingHandler {
                 }
                 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)) {
@@ -185,8 +192,9 @@ public class RenaultHandler extends BaseThingHandler {
                                 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());
                             }
@@ -196,7 +204,11 @@ public class RenaultHandler extends BaseThingHandler {
                         return;
                     }
                 }
+                break;
             default:
+                if (command instanceof RefreshType) {
+                    reschedulePollingJob();
+                }
                 break;
         }
     }
@@ -223,16 +235,15 @@ public class RenaultHandler extends BaseThingHandler {
             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) {
@@ -253,8 +264,10 @@ public class RenaultHandler extends BaseThingHandler {
                             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);
             }
         }
     }
@@ -269,13 +282,15 @@ public class RenaultHandler extends BaseThingHandler {
                     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);
             }
         }
     }
@@ -289,8 +304,10 @@ public class RenaultHandler extends BaseThingHandler {
                     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);
             }
         }
     }
@@ -320,10 +337,49 @@ public class RenaultHandler extends BaseThingHandler {
                     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);
+    }
 }
index 8dab36b39aa8b9c98081b5a4fb9501bbf26cf4d3..4faaca2d4788988c05fe8c9e45ae64720011026d 100644 (file)
@@ -55,6 +55,9 @@ thing-type.config.renault.car.vin.description = Vehicle Identification Number
 # 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
@@ -85,6 +88,8 @@ 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.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
index cd2eb522df7f595bd99a0748628bc6e785863fbd..3e517231be0bbb8431459965b0e9dea23c766150 100644 (file)
@@ -12,6 +12,7 @@
                <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"/>
@@ -24,6 +25,7 @@
                        <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>