]> git.basschouten.com Git - openhab-addons.git/commitdiff
[VolvoOnCall] OH3 update (#8595)
authorGaël L'hopital <gael@lhopital.org>
Sat, 3 Oct 2020 18:59:14 +0000 (20:59 +0200)
committerGitHub <noreply@github.com>
Sat, 3 Oct 2020 18:59:14 +0000 (11:59 -0700)
* [VolvoOnCall] OH3 update

Solving issue #8554  and issue #8518
Some code corrections and enhancements.
Introduced new trigger channels.

* Doing spotless apply

* Some other code improvements

* Moving back to Jetty HttpClient
Introduced Expiring cache for request to avoid flooding voc servers
Reduced the number of requests emitted
Changed  user agent identification

* Correcting spotless
* Pleasing Travis
* Code review corrections
* Adressing cpmeister code review

Signed-off-by: clinique <gael@lhopital.org>
24 files changed:
bundles/org.openhab.binding.volvooncall/README.md
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/VolvoOnCallBindingConstants.java
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/VolvoOnCallException.java
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/VolvoOnCallHandlerFactory.java
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/action/IVolvoOnCallActions.java [deleted file]
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/action/VolvoOnCallActions.java
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/api/ActionResultController.java [new file with mode: 0644]
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/api/VocHttpApi.java [new file with mode: 0644]
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/config/ApiBridgeConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/config/VehicleConfiguration.java
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/config/VolvoOnCallBridgeConfiguration.java [deleted file]
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/discovery/VolvoOnCallDiscoveryService.java [deleted file]
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/discovery/VolvoVehicleDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/dto/CustomerAccounts.java
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/dto/HvBattery.java
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/dto/PostResponse.java
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/dto/Trip.java
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/dto/Trips.java
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/dto/TyrePressure.java
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/handler/VehicleHandler.java
bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/handler/VolvoOnCallBridgeHandler.java
bundles/org.openhab.binding.volvooncall/src/main/resources/OH-INF/binding/binding.xml
bundles/org.openhab.binding.volvooncall/src/main/resources/OH-INF/i18n/volvooncall_fr.properties [new file with mode: 0644]
bundles/org.openhab.binding.volvooncall/src/main/resources/OH-INF/thing/vehicle.xml

index c88336be9b40a70703485d0e088e65c4a94d8b98..f2ea6f4c3d78b6f03dcc0abe03c326d8dd1c57ff 100644 (file)
@@ -20,10 +20,10 @@ The binding has no configuration options itself, all configuration is done at 'T
 The 'VolvoOnCall API' bridge uses the owner's email address and password in order to access the VOC Remote API.
 This is the same email address and password as used in the VolvoOnCall smartphone app, that allows to remotely control your car(s).
 
-| Parameter | Description                                                              | Required |
-|-----------|------------------------------------------------------------------------- |--------- |
-| username  | Username from the VolvoOnCall app (email address)                        | yes      |
-| password  | Password from the VolvoOnCall app                                        | yes      |
+| Parameter       | Description                                          | Required |
+|-----------------|------------------------------------------------------|--------- |
+| username        | Username from the VolvoOnCall app (email address)    | yes      |
+| password        | Password from the VolvoOnCall app                    | yes      |
 
 Once the bridge created, you will be able to launch discovery of the vehicles attached to it.
 
@@ -35,7 +35,8 @@ The 'VolvoOnCall API' bridge uses the owner's email address and password in orde
 | Parameter       | Name             | Description                                             | Required |
 |-----------------|------------------|---------------------------------------------------------|----------|
 | vin             | Vin              | Vehicle Identification Number of the car                | yes      |
-| refreshinterval | Refresh interval | Interval in minutes to refresh the data (default=10)    | no       |
+| refreshinterval | Refresj Interval | Interval in minutes to refresh the data (default=10)    | yes      |
+
 
 
 
@@ -100,6 +101,15 @@ Following channels are currently available:
 | lasttrip#endPosition                          | Location             | Last trip end location                           |                                                 |
 
 
+## Events
+
+| Channel Type ID    | Options     | Description                                                    |
+|--------------------|-------------|----------------------------------------------------------------|
+| other#carEvent     |             |                                                                |
+|                    | CAR_STOPPED | Triggered when the car has finished a trip                     |
+|                    | CAR_MOVED   | Triggered if the car mileage has changed between two polls     |
+|                    | CAR_STARTED | Triggered when the engine of the car went on between two polls |
+
 ## Full Example
 
 demo.things:
index 771e7a46a7db99c8befc389b9f422be44df1f467..f8b9a53200d9fa885d60dcaba530b97872b736dd 100644 (file)
@@ -30,16 +30,6 @@ public class VolvoOnCallBindingConstants {
 
     public static final String BINDING_ID = "volvooncall";
 
-    // Vehicle properties
-    public static final String VIN = "vin";
-
-    // The URL to use to connect to VocAPI with.
-    // TODO : for North America and China syntax changes to vocapi-cn.xxx
-    public static final String SERVICE_URL = "https://vocapi.wirelesscar.net/customerapi/rest/v3.0/";
-
-    // The JSON content type used when talking to VocAPI.
-    public static final String JSON_CONTENT_TYPE = "application/json";
-
     // List of Thing Type UIDs
     public static final ThingTypeUID APIBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "vocapi");
     public static final ThingTypeUID VEHICLE_THING_TYPE = new ThingTypeUID(BINDING_ID, "vehicle");
@@ -49,6 +39,10 @@ public class VolvoOnCallBindingConstants {
     public static final String GROUP_WINDOWS = "windows";
     public static final String GROUP_TYRES = "tyrePressure";
     public static final String GROUP_BATTERY = "battery";
+    public static final String GROUP_OTHER = "other";
+    public static final String GROUP_POSITION = "position";
+    public static final String GROUP_ODOMETER = "odometer";
+    public static final String GROUP_TANK = "tank";
 
     // List of Channel id's
     public static final String TAILGATE = "tailgate";
@@ -90,6 +84,11 @@ public class VolvoOnCallBindingConstants {
     public static final String CHARGING_END = "chargingEnd";
     public static final String BULB_FAILURE = "bulbFailure";
 
+    // Car Events
+    public static final String CAR_EVENT = "carEvent";
+    public static final String EVENT_CAR_STOPPED = "CAR_STOPPED";
+    public static final String EVENT_CAR_MOVED = "CAR_MOVED";
+    public static final String EVENT_CAR_STARTED = "CAR_STARTED";
     // Last Trip Channel Id's
     public static final String LAST_TRIP_GROUP = "lasttrip";
     public static final String TRIP_CONSUMPTION = "tripConsumption";
index 54ad35670f9a1fea7d73e1bf947c415a16b6f210..146e3ed648e980459d215c0098ec2b78e84e9b76 100644 (file)
@@ -34,7 +34,9 @@ public class VolvoOnCallException extends Exception {
     public static enum ErrorType {
         UNKNOWN,
         SERVICE_UNAVAILABLE,
+        SERVICE_UNABLE_TO_START,
         IOEXCEPTION,
+        INTERRUPTED,
         JSON_SYNTAX;
     }
 
@@ -44,6 +46,8 @@ public class VolvoOnCallException extends Exception {
         super(label);
         if ("FoundationServicesUnavailable".equalsIgnoreCase(label)) {
             cause = ErrorType.SERVICE_UNAVAILABLE;
+        } else if ("ServiceUnableToStart".equalsIgnoreCase(label)) {
+            cause = ErrorType.SERVICE_UNABLE_TO_START;
         } else {
             cause = ErrorType.UNKNOWN;
             logger.warn("Unhandled VoC error : {} : {}", label, description);
@@ -56,6 +60,8 @@ public class VolvoOnCallException extends Exception {
             cause = ErrorType.IOEXCEPTION;
         } else if (e instanceof JsonSyntaxException) {
             cause = ErrorType.JSON_SYNTAX;
+        } else if (e instanceof InterruptedException) {
+            cause = ErrorType.INTERRUPTED;
         } else {
             cause = ErrorType.UNKNOWN;
             logger.warn("Unhandled VoC error : {}", e.getMessage());
index 43e36336cbcea0044ab450ac700b13a342a505f0..4c29c70c84eaf3fb316fb3d24918959c38591448 100644 (file)
@@ -14,31 +14,34 @@ package org.openhab.binding.volvooncall.internal;
 
 import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
 
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.Map;
+import java.time.ZonedDateTime;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.volvooncall.internal.discovery.VolvoOnCallDiscoveryService;
+import org.eclipse.jetty.client.HttpClient;
 import org.openhab.binding.volvooncall.internal.handler.VehicleHandler;
 import org.openhab.binding.volvooncall.internal.handler.VehicleStateDescriptionProvider;
 import org.openhab.binding.volvooncall.internal.handler.VolvoOnCallBridgeHandler;
-import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.library.types.StringType;
 import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingTypeUID;
-import org.openhab.core.thing.ThingUID;
 import org.openhab.core.thing.binding.BaseThingHandlerFactory;
 import org.openhab.core.thing.binding.ThingHandler;
 import org.openhab.core.thing.binding.ThingHandlerFactory;
-import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializer;
+
 /**
  * The {@link VolvoOnCallHandlerFactory} is responsible for creating things and thing
  * handlers.
@@ -48,13 +51,31 @@ import org.slf4j.LoggerFactory;
 @NonNullByDefault
 @Component(configurationPid = "binding.volvooncall", service = ThingHandlerFactory.class)
 public class VolvoOnCallHandlerFactory extends BaseThingHandlerFactory {
+
     private final Logger logger = LoggerFactory.getLogger(VolvoOnCallHandlerFactory.class);
-    private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
     private final VehicleStateDescriptionProvider stateDescriptionProvider;
+    private final Gson gson;
+    private final HttpClient httpClient;
 
     @Activate
-    public VolvoOnCallHandlerFactory(@Reference VehicleStateDescriptionProvider provider) {
+    public VolvoOnCallHandlerFactory(@Reference VehicleStateDescriptionProvider provider,
+            @Reference HttpClientFactory httpClientFactory) {
         this.stateDescriptionProvider = provider;
+        this.httpClient = httpClientFactory.createHttpClient(BINDING_ID);
+        this.gson = new GsonBuilder()
+                .registerTypeAdapter(ZonedDateTime.class,
+                        (JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
+                                .parse(json.getAsJsonPrimitive().getAsString().replaceAll("\\+0000", "Z")))
+                .registerTypeAdapter(OpenClosedType.class,
+                        (JsonDeserializer<OpenClosedType>) (json, type,
+                                jsonDeserializationContext) -> json.getAsBoolean() ? OpenClosedType.OPEN
+                                        : OpenClosedType.CLOSED)
+                .registerTypeAdapter(OnOffType.class,
+                        (JsonDeserializer<OnOffType>) (json, type,
+                                jsonDeserializationContext) -> json.getAsBoolean() ? OnOffType.ON : OnOffType.OFF)
+                .registerTypeAdapter(StringType.class, (JsonDeserializer<StringType>) (json, type,
+                        jsonDeserializationContext) -> StringType.valueOf(json.getAsString()))
+                .create();
     }
 
     @Override
@@ -66,36 +87,11 @@ public class VolvoOnCallHandlerFactory extends BaseThingHandlerFactory {
     protected @Nullable ThingHandler createHandler(Thing thing) {
         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
         if (APIBRIDGE_THING_TYPE.equals(thingTypeUID)) {
-            VolvoOnCallBridgeHandler bridgeHandler = new VolvoOnCallBridgeHandler((Bridge) thing);
-            registerDeviceDiscoveryService(bridgeHandler);
-            return bridgeHandler;
+            return new VolvoOnCallBridgeHandler((Bridge) thing, gson, httpClient);
         } else if (VEHICLE_THING_TYPE.equals(thingTypeUID)) {
             return new VehicleHandler(thing, stateDescriptionProvider);
         }
         logger.warn("ThingHandler not found for {}", thing.getThingTypeUID());
         return null;
     }
-
-    @Override
-    protected void removeHandler(ThingHandler thingHandler) {
-        if (thingHandler instanceof VolvoOnCallBridgeHandler) {
-            ThingUID thingUID = thingHandler.getThing().getUID();
-            unregisterDeviceDiscoveryService(thingUID);
-        }
-        super.removeHandler(thingHandler);
-    }
-
-    private void registerDeviceDiscoveryService(VolvoOnCallBridgeHandler bridgeHandler) {
-        VolvoOnCallDiscoveryService discoveryService = new VolvoOnCallDiscoveryService(bridgeHandler);
-        discoveryServiceRegs.put(bridgeHandler.getThing().getUID(),
-                bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
-    }
-
-    private void unregisterDeviceDiscoveryService(ThingUID thingUID) {
-        if (discoveryServiceRegs.containsKey(thingUID)) {
-            ServiceRegistration<?> serviceReg = discoveryServiceRegs.get(thingUID);
-            serviceReg.unregister();
-            discoveryServiceRegs.remove(thingUID);
-        }
-    }
 }
diff --git a/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/action/IVolvoOnCallActions.java b/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/action/IVolvoOnCallActions.java
deleted file mode 100644 (file)
index a8f4f71..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Copyright (c) 2010-2020 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.volvooncall.internal.action;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-
-/**
- * The {@link IVolvoOnCallActions} defines the interface for all thing actions supported by the binding.
- *
- * @author Gaël L'hopital - Initial contribution
- */
-@NonNullByDefault
-public interface IVolvoOnCallActions {
-    public void honkBlinkCommand(Boolean honk, Boolean blink);
-
-    public void preclimatizationStopCommand();
-
-    public void heaterStopCommand();
-
-    public void heaterStartCommand();
-
-    public void preclimatizationStartCommand();
-
-    public void engineStartCommand(@Nullable Integer runtime);
-
-    public void openCarCommand();
-
-    public void closeCarCommand();
-}
index 2d4e72cae7b776598f5374c1f72c0e1f956ad2db..d15fd3438c3ae82bd5f9221bb677e707c2e1bb0b 100644 (file)
  */
 package org.openhab.binding.volvooncall.internal.action;
 
-import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
+import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.volvooncall.internal.handler.VehicleHandler;
 import org.openhab.core.automation.annotation.ActionInput;
 import org.openhab.core.automation.annotation.RuleAction;
+import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.thing.binding.ThingActions;
 import org.openhab.core.thing.binding.ThingActionsScope;
 import org.openhab.core.thing.binding.ThingHandler;
@@ -34,7 +34,7 @@ import org.slf4j.LoggerFactory;
  */
 @ThingActionsScope(name = "volvooncall")
 @NonNullByDefault
-public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
+public class VolvoOnCallActions implements ThingActions {
 
     private final Logger logger = LoggerFactory.getLogger(VolvoOnCallActions.class);
 
@@ -56,40 +56,29 @@ public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
         return this.handler;
     }
 
-    @Override
-    @RuleAction(label = "Volvo On Call : Close", description = "Closes the car")
+    @RuleAction(label = "close the car", description = "Closes the car")
     public void closeCarCommand() {
         logger.debug("closeCarCommand called");
         VehicleHandler handler = this.handler;
         if (handler != null) {
-            handler.actionClose();
+            handler.actionOpenClose(LOCK, OnOffType.ON);
         } else {
             logger.warn("VolvoOnCall Action service ThingHandler is null!");
         }
     }
 
-    public static void closeCarCommand(@Nullable ThingActions actions) {
-        invokeMethodOf(actions).closeCarCommand();
-    }
-
-    @Override
-    @RuleAction(label = "Volvo On Call : Open", description = "Opens the car")
+    @RuleAction(label = "open the car", description = "Opens the car")
     public void openCarCommand() {
         logger.debug("openCarCommand called");
         VehicleHandler handler = this.handler;
         if (handler != null) {
-            handler.actionOpen();
+            handler.actionOpenClose(UNLOCK, OnOffType.OFF);
         } else {
             logger.warn("VolvoOnCall Action service ThingHandler is null!");
         }
     }
 
-    public static void openCarCommand(@Nullable ThingActions actions) {
-        invokeMethodOf(actions).openCarCommand();
-    }
-
-    @Override
-    @RuleAction(label = "Volvo On Call : Start Engine", description = "Starts the engine")
+    @RuleAction(label = "start the engine", description = "Starts the engine")
     public void engineStartCommand(@ActionInput(name = "runtime", label = "Runtime") @Nullable Integer runtime) {
         logger.debug("engineStartCommand called");
         VehicleHandler handler = this.handler;
@@ -100,76 +89,51 @@ public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
         }
     }
 
-    public static void engineStartCommand(@Nullable ThingActions actions, @Nullable Integer runtime) {
-        invokeMethodOf(actions).engineStartCommand(runtime);
-    }
-
-    @Override
-    @RuleAction(label = "Volvo On Call : Heater Start", description = "Starts car heater")
+    @RuleAction(label = "start the heater", description = "Starts car heater")
     public void heaterStartCommand() {
         logger.debug("heaterStartCommand called");
         VehicleHandler handler = this.handler;
         if (handler != null) {
-            handler.actionHeater(true);
+            handler.actionHeater(REMOTE_HEATER, true);
         } else {
             logger.warn("VolvoOnCall Action service ThingHandler is null!");
         }
     }
 
-    public static void heaterStartCommand(@Nullable ThingActions actions) {
-        invokeMethodOf(actions).heaterStartCommand();
-    }
-
-    @Override
-    @RuleAction(label = "Volvo On Call : Preclimatization Start", description = "Starts car heater")
+    @RuleAction(label = "start preclimatization", description = "Starts the car heater")
     public void preclimatizationStartCommand() {
         logger.debug("preclimatizationStartCommand called");
         VehicleHandler handler = this.handler;
         if (handler != null) {
-            handler.actionPreclimatization(true);
+            handler.actionHeater(PRECLIMATIZATION, true);
         } else {
             logger.warn("VolvoOnCall Action service ThingHandler is null!");
         }
     }
 
-    public static void preclimatizationStartCommand(@Nullable ThingActions actions) {
-        invokeMethodOf(actions).preclimatizationStartCommand();
-    }
-
-    @Override
-    @RuleAction(label = "Volvo On Call : Heater Stop", description = "Stops car heater")
+    @RuleAction(label = "stop the heater", description = "Stops car heater")
     public void heaterStopCommand() {
         logger.debug("heaterStopCommand called");
         VehicleHandler handler = this.handler;
         if (handler != null) {
-            handler.actionHeater(false);
+            handler.actionHeater(REMOTE_HEATER, false);
         } else {
             logger.warn("VolvoOnCall Action service ThingHandler is null!");
         }
     }
 
-    public static void heaterStopCommand(@Nullable ThingActions actions) {
-        invokeMethodOf(actions).heaterStopCommand();
-    }
-
-    @Override
-    @RuleAction(label = "Volvo On Call : Preclimatization Stop", description = "Stops car heater")
+    @RuleAction(label = "stop preclimatization", description = "Stops the car heater")
     public void preclimatizationStopCommand() {
         logger.debug("preclimatizationStopCommand called");
         VehicleHandler handler = this.handler;
         if (handler != null) {
-            handler.actionPreclimatization(false);
+            handler.actionHeater(PRECLIMATIZATION, false);
         } else {
             logger.warn("VolvoOnCall Action service ThingHandler is null!");
         }
     }
 
-    public static void preclimatizationStopCommand(@Nullable ThingActions actions) {
-        invokeMethodOf(actions).preclimatizationStopCommand();
-    }
-
-    @Override
-    @RuleAction(label = "Volvo On Call : Honk-blink", description = "Activates the horn and or lights of the car")
+    @RuleAction(label = "honk-blink", description = "Activates the horn and or lights of the car")
     public void honkBlinkCommand(@ActionInput(name = "honk", label = "Honk") Boolean honk,
             @ActionInput(name = "blink", label = "Blink") Boolean blink) {
         logger.debug("honkBlinkCommand called");
@@ -180,27 +144,4 @@ public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
             logger.warn("VolvoOnCall Action service ThingHandler is null!");
         }
     }
-
-    public static void honkBlinkCommand(@Nullable ThingActions actions, Boolean honk, Boolean blink) {
-        invokeMethodOf(actions).honkBlinkCommand(honk, blink);
-    }
-
-    private static IVolvoOnCallActions invokeMethodOf(@Nullable ThingActions actions) {
-        if (actions == null) {
-            throw new IllegalArgumentException("actions cannot be null");
-        }
-        if (actions.getClass().getName().equals(VolvoOnCallActions.class.getName())) {
-            if (actions instanceof IVolvoOnCallActions) {
-                return (IVolvoOnCallActions) actions;
-            } else {
-                return (IVolvoOnCallActions) Proxy.newProxyInstance(IVolvoOnCallActions.class.getClassLoader(),
-                        new Class[] { IVolvoOnCallActions.class }, (Object proxy, Method method, Object[] args) -> {
-                            Method m = actions.getClass().getDeclaredMethod(method.getName(),
-                                    method.getParameterTypes());
-                            return m.invoke(actions, args);
-                        });
-            }
-        }
-        throw new IllegalArgumentException("Actions is not an instance of VolvoOnCallActions");
-    }
 }
diff --git a/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/api/ActionResultController.java b/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/api/ActionResultController.java
new file mode 100644 (file)
index 0000000..75e7ed6
--- /dev/null
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2010-2020 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.volvooncall.internal.api;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
+import org.openhab.binding.volvooncall.internal.VolvoOnCallException.ErrorType;
+import org.openhab.binding.volvooncall.internal.dto.PostResponse;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ActionResultController} is responsible for triggering information
+ * update after a post has been submitted to the webservice.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class ActionResultController implements Runnable {
+    private final Logger logger = LoggerFactory.getLogger(ActionResultController.class);
+
+    private final VocHttpApi service;
+    private final ScheduledExecutorService scheduler;
+    private final PostResponse postResponse;
+    private final ThingHandler vehicle;
+
+    public ActionResultController(VocHttpApi service, PostResponse postResponse, ScheduledExecutorService scheduler,
+            ThingHandler vehicle) {
+        this.postResponse = postResponse;
+        this.service = service;
+        this.scheduler = scheduler;
+        this.vehicle = vehicle;
+    }
+
+    @Override
+    public void run() {
+        switch (postResponse.status) {
+            case SUCCESSFULL:
+            case FAILED:
+                logger.debug("Action {} for vehicle {} resulted : {}.", postResponse.serviceType,
+                        postResponse.vehicleId, postResponse.status);
+                vehicle.handleCommand(vehicle.getThing().getChannels().get(0).getUID(), RefreshType.REFRESH);
+                break;
+            default:
+                try {
+                    scheduler.schedule(
+                            new ActionResultController(service,
+                                    service.getURL(postResponse.serviceURL, PostResponse.class), scheduler, vehicle),
+                            10000, TimeUnit.MILLISECONDS);
+                } catch (VolvoOnCallException e) {
+                    if (e.getType() == ErrorType.SERVICE_UNAVAILABLE) {
+                        scheduler.schedule(this, 10000, TimeUnit.MILLISECONDS);
+                    }
+                }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/api/VocHttpApi.java b/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/api/VocHttpApi.java
new file mode 100644 (file)
index 0000000..8d80377
--- /dev/null
@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2010-2020 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.volvooncall.internal.api;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentProvider;
+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.HttpField;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
+import org.openhab.binding.volvooncall.internal.VolvoOnCallException.ErrorType;
+import org.openhab.binding.volvooncall.internal.config.ApiBridgeConfiguration;
+import org.openhab.binding.volvooncall.internal.dto.PostResponse;
+import org.openhab.binding.volvooncall.internal.dto.VocAnswer;
+import org.openhab.core.cache.ExpiringCacheMap;
+import org.openhab.core.id.InstanceUUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * {@link VocHttpApi} wraps the VolvoOnCall REST API.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class VocHttpApi {
+    // The URL to use to connect to VocAPI.
+    // For North America and China syntax changes to vocapi-cn.xxx
+    private static final String SERVICE_URL = "https://vocapi.wirelesscar.net/customerapi/rest/v3.0/";
+    private static final int TIMEOUT_MS = 10000;
+    private static final String JSON_CONTENT_TYPE = "application/json";
+
+    private final Logger logger = LoggerFactory.getLogger(VocHttpApi.class);
+    private final Gson gson;
+    private final ExpiringCacheMap<String, @Nullable String> cache;
+    private final HttpClient httpClient;
+    private final ApiBridgeConfiguration configuration;
+
+    public VocHttpApi(ApiBridgeConfiguration configuration, Gson gson, HttpClient httpClient)
+            throws VolvoOnCallException {
+        this.gson = gson;
+        this.cache = new ExpiringCacheMap<>(120 * 1000);
+        this.configuration = configuration;
+        this.httpClient = httpClient;
+
+        httpClient.setUserAgentField(new HttpField(HttpHeader.USER_AGENT, "openhab/voc_binding/" + InstanceUUID.get()));
+        try {
+            httpClient.start();
+        } catch (Exception e) {
+            throw new VolvoOnCallException(new IOException("Unable to start Jetty HttpClient", e));
+        }
+    }
+
+    public void dispose() throws Exception {
+        httpClient.stop();
+    }
+
+    private @Nullable String getResponse(HttpMethod method, String url, @Nullable String body) {
+        try {
+            Request request = httpClient.newRequest(url).header(HttpHeader.CACHE_CONTROL, "no-cache")
+                    .header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE).header(HttpHeader.ACCEPT, "*/*")
+                    .header(HttpHeader.AUTHORIZATION, configuration.getAuthorization()).header("x-device-id", "Device")
+                    .header("x-originator-type", "App").header("x-os-type", "Android").header("x-os-version", "22")
+                    .timeout(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            if (body != null) {
+                ContentProvider content = new StringContentProvider(JSON_CONTENT_TYPE, body, StandardCharsets.UTF_8);
+                request = request.content(content);
+            }
+            ContentResponse contentResponse = request.method(method).send();
+            return contentResponse.getContentAsString();
+        } catch (InterruptedException | TimeoutException | ExecutionException e) {
+            return null;
+        }
+    }
+
+    private <T extends VocAnswer> T callUrl(HttpMethod method, String endpoint, Class<T> objectClass,
+            @Nullable String body) throws VolvoOnCallException {
+        try {
+            String url = endpoint.startsWith("http") ? endpoint : SERVICE_URL + endpoint;
+            String jsonResponse = method == HttpMethod.GET
+                    ? cache.putIfAbsentAndGet(endpoint, () -> getResponse(method, url, body))
+                    : getResponse(method, url, body);
+            if (jsonResponse == null) {
+                throw new IOException();
+            } else {
+                logger.debug("Request to `{}` answered : {}", url, jsonResponse);
+                T responseDTO = gson.fromJson(jsonResponse, objectClass);
+                String error = responseDTO.getErrorLabel();
+                if (error != null) {
+                    throw new VolvoOnCallException(error, responseDTO.getErrorDescription());
+                }
+                return responseDTO;
+            }
+        } catch (JsonSyntaxException | IOException e) {
+            throw new VolvoOnCallException(e);
+        }
+    }
+
+    public <T extends VocAnswer> T getURL(String endpoint, Class<T> objectClass) throws VolvoOnCallException {
+        return callUrl(HttpMethod.GET, endpoint, objectClass, null);
+    }
+
+    public @Nullable PostResponse postURL(String endpoint, @Nullable String body) throws VolvoOnCallException {
+        try {
+            return callUrl(HttpMethod.POST, endpoint, PostResponse.class, body);
+        } catch (VolvoOnCallException e) {
+            if (e.getType() == ErrorType.SERVICE_UNABLE_TO_START) {
+                logger.info("Unable to start service request sent to VoC");
+                return null;
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    public <T extends VocAnswer> T getURL(Class<T> objectClass, String vin) throws VolvoOnCallException {
+        String url = String.format("vehicles/%s/%s", vin, objectClass.getSimpleName().toLowerCase());
+        return getURL(url, objectClass);
+    }
+}
diff --git a/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/config/ApiBridgeConfiguration.java b/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/config/ApiBridgeConfiguration.java
new file mode 100644 (file)
index 0000000..5b86e7c
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2020 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.volvooncall.internal.config;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ApiBridgeConfiguration} is responsible for holding
+ * configuration informations needed to access VOC API
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class ApiBridgeConfiguration {
+    public String username = "";
+    public String password = "";
+
+    public String getAuthorization() {
+        byte[] authorization = Base64.getEncoder().encode((String.format("%s:%s", username, password)).getBytes());
+        return "Basic " + new String(authorization, StandardCharsets.UTF_8);
+    }
+}
index f6083b4b14bc297d984ce17c013199240d4f465b..a20812c32e69ca4662babf2ad09f35a6d6a165c9 100644 (file)
@@ -22,6 +22,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
  */
 @NonNullByDefault
 public class VehicleConfiguration {
+    public static String VIN = "vin";
+
     public String vin = "";
-    public Integer refresh = 5;
+    public int refresh = 10;
 }
diff --git a/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/config/VolvoOnCallBridgeConfiguration.java b/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/config/VolvoOnCallBridgeConfiguration.java
deleted file mode 100644 (file)
index 54e5074..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Copyright (c) 2010-2020 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.volvooncall.internal.config;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * The {@link VolvoOnCallBridgeConfiguration} is responsible for holding
- * configuration informations needed to access VOC API
- *
- * @author Gaël L'hopital - Initial contribution
- */
-@NonNullByDefault
-public class VolvoOnCallBridgeConfiguration {
-    public String username = "";
-    public String password = "";
-
-    public String getAuthorization() {
-        byte[] authorization = Base64.getEncoder().encode((String.format("%s:%s", username, password)).getBytes());
-        return "Basic " + new String(authorization, StandardCharsets.UTF_8);
-    }
-}
diff --git a/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/discovery/VolvoOnCallDiscoveryService.java b/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/discovery/VolvoOnCallDiscoveryService.java
deleted file mode 100644 (file)
index ab77e89..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Copyright (c) 2010-2020 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.volvooncall.internal.discovery;
-
-import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
-
-import java.util.Arrays;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
-import org.openhab.binding.volvooncall.internal.dto.AccountVehicleRelation;
-import org.openhab.binding.volvooncall.internal.dto.Attributes;
-import org.openhab.binding.volvooncall.internal.dto.Vehicles;
-import org.openhab.binding.volvooncall.internal.handler.VolvoOnCallBridgeHandler;
-import org.openhab.core.config.discovery.AbstractDiscoveryService;
-import org.openhab.core.config.discovery.DiscoveryResultBuilder;
-import org.openhab.core.thing.ThingUID;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link VolvoOnCallDiscoveryService} searches for available
- * cars discoverable through VocAPI
- *
- * @author Gaël L'hopital - Initial contribution
- */
-@NonNullByDefault
-public class VolvoOnCallDiscoveryService extends AbstractDiscoveryService {
-    private static final int SEARCH_TIME = 2;
-    private final Logger logger = LoggerFactory.getLogger(VolvoOnCallDiscoveryService.class);
-    private final VolvoOnCallBridgeHandler bridgeHandler;
-
-    public VolvoOnCallDiscoveryService(VolvoOnCallBridgeHandler bridgeHandler) {
-        super(SUPPORTED_THING_TYPES_UIDS, SEARCH_TIME);
-        this.bridgeHandler = bridgeHandler;
-    }
-
-    @Override
-    public void startScan() {
-        String[] relations = bridgeHandler.getVehiclesRelationsURL();
-        Arrays.stream(relations).forEach(relationURL -> {
-            try {
-                AccountVehicleRelation accountVehicle = bridgeHandler.getURL(relationURL, AccountVehicleRelation.class);
-                logger.debug("Found vehicle : {}", accountVehicle.vehicleId);
-
-                Vehicles vehicle = bridgeHandler.getURL(accountVehicle.vehicleURL, Vehicles.class);
-                Attributes attributes = bridgeHandler.getURL(Attributes.class, vehicle.vehicleId);
-
-                thingDiscovered(DiscoveryResultBuilder
-                        .create(new ThingUID(VEHICLE_THING_TYPE, bridgeHandler.getThing().getUID(),
-                                accountVehicle.vehicleId))
-                        .withLabel(attributes.vehicleType + " " + attributes.registrationNumber)
-                        .withBridge(bridgeHandler.getThing().getUID()).withProperty(VIN, attributes.vin)
-                        .withRepresentationProperty(accountVehicle.vehicleId).build());
-
-            } catch (VolvoOnCallException e) {
-                logger.warn("Error while discovering vehicle: {}", e.getMessage());
-            }
-        });
-
-        stopScan();
-    }
-}
diff --git a/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/discovery/VolvoVehicleDiscoveryService.java b/bundles/org.openhab.binding.volvooncall/src/main/java/org/openhab/binding/volvooncall/internal/discovery/VolvoVehicleDiscoveryService.java
new file mode 100644 (file)
index 0000000..fcb010d
--- /dev/null
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2010-2020 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.volvooncall.internal.discovery;
+
+import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
+import org.openhab.binding.volvooncall.internal.api.VocHttpApi;
+import org.openhab.binding.volvooncall.internal.config.VehicleConfiguration;
+import org.openhab.binding.volvooncall.internal.dto.AccountVehicleRelation;
+import org.openhab.binding.volvooncall.internal.dto.Attributes;
+import org.openhab.binding.volvooncall.internal.dto.CustomerAccounts;
+import org.openhab.binding.volvooncall.internal.dto.Vehicles;
+import org.openhab.binding.volvooncall.internal.handler.VolvoOnCallBridgeHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link VolvoVehicleDiscoveryService} searches for available
+ * cars discoverable through VocAPI
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class VolvoVehicleDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+    private static final int SEARCH_TIME = 2;
+    private final Logger logger = LoggerFactory.getLogger(VolvoVehicleDiscoveryService.class);
+    private @Nullable VolvoOnCallBridgeHandler handler;
+
+    public VolvoVehicleDiscoveryService() {
+        super(SUPPORTED_THING_TYPES_UIDS, SEARCH_TIME);
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        if (handler instanceof VolvoOnCallBridgeHandler) {
+            this.handler = (VolvoOnCallBridgeHandler) handler;
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return handler;
+    }
+
+    @Override
+    public void activate(@Nullable Map<String, @Nullable Object> configProperties) {
+        super.activate(configProperties);
+    }
+
+    @Override
+    public void deactivate() {
+        super.deactivate();
+    }
+
+    @Override
+    protected void startScan() {
+        VolvoOnCallBridgeHandler bridgeHandler = this.handler;
+        if (bridgeHandler != null) {
+            ThingUID bridgeUID = bridgeHandler.getThing().getUID();
+            VocHttpApi api = bridgeHandler.getApi();
+            if (api != null) {
+                try {
+                    CustomerAccounts account = api.getURL("customeraccounts/", CustomerAccounts.class);
+                    account.accountVehicleRelationsURL.forEach(relationURL -> {
+                        try {
+                            AccountVehicleRelation accountVehicle = api.getURL(relationURL,
+                                    AccountVehicleRelation.class);
+                            logger.debug("Found vehicle : {}", accountVehicle.vehicleId);
+
+                            Vehicles vehicle = api.getURL(accountVehicle.vehicleURL, Vehicles.class);
+                            Attributes attributes = api.getURL(Attributes.class, vehicle.vehicleId);
+
+                            thingDiscovered(DiscoveryResultBuilder
+                                    .create(new ThingUID(VEHICLE_THING_TYPE, bridgeUID, accountVehicle.vehicleId))
+                                    .withLabel(attributes.vehicleType + " " + attributes.registrationNumber)
+                                    .withBridge(bridgeUID).withProperty(VehicleConfiguration.VIN, attributes.vin)
+                                    .withRepresentationProperty(VehicleConfiguration.VIN).build());
+
+                        } catch (VolvoOnCallException e) {
+                            logger.warn("Error while getting vehicle informations : {}", e.getMessage());
+                        }
+                    });
+                } catch (VolvoOnCallException e) {
+                    logger.warn("Error while discovering vehicle: {}", e.getMessage());
+                }
+            }
+            ;
+        }
+        stopScan();
+    }
+}
index 14fed7a2d9189a23b5d9217edc6651bebc9eaaf2..4d9471e02751ca371211be84e7cac1d4c8832693 100644 (file)
@@ -12,6 +12,9 @@
  */
 package org.openhab.binding.volvooncall.internal.dto;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 
@@ -26,7 +29,7 @@ import com.google.gson.annotations.SerializedName;
 @NonNullByDefault
 public class CustomerAccounts extends VocAnswer {
     @SerializedName("accountVehicleRelations")
-    public @NonNullByDefault({}) String[] accountVehicleRelationsURL;
+    public List<String> accountVehicleRelationsURL = new ArrayList<>();
     public @Nullable String username;
 
     /*
index a7079131ec2c80d2428b571a61b67e1cdc10d2f9..1faf9d56d245a3e9542973ab691da927d68fb9ab 100644 (file)
@@ -15,6 +15,7 @@ package org.openhab.binding.volvooncall.internal.dto;
 import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.UNDEFINED;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.StringType;
 
 /**
  * The {@link HvBattery} is responsible for storing
@@ -26,7 +27,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 public class HvBattery {
     public int hvBatteryLevel = UNDEFINED;
     public int distanceToHVBatteryEmpty = UNDEFINED;
-    public @NonNullByDefault({}) String hvBatteryChargeStatusDerived;
+    public @NonNullByDefault({}) StringType hvBatteryChargeStatusDerived;
     public int timeToHVBatteryFullyCharged = UNDEFINED;
     /*
      * Currently unused in the binding, maybe interesting in the future
index c8dac0e5df0e3cd2b259393ae55741eef1b374c6..6fe15f80529abf2622167ce50cc223342deee708 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.volvooncall.internal.dto;
 
+import java.time.ZonedDateTime;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 
 import com.google.gson.annotations.SerializedName;
@@ -48,7 +50,7 @@ public class PostResponse extends VocAnswer {
     @SerializedName("service")
     public @NonNullByDefault({}) String serviceURL;
     public @NonNullByDefault({}) ServiceType serviceType;
-
+    public @NonNullByDefault({}) ZonedDateTime startTime;
     /*
      * Currently unused in the binding, maybe interesting in the future
      *
@@ -59,7 +61,7 @@ public class PostResponse extends VocAnswer {
      * }
      *
      * private ZonedDateTime statusTimestamp;
-     * private ZonedDateTime startTime;
+     * 
      * private FailureReason failureReason;
      *
      * private Integer customerServiceId;
index 43d908f31be90afa38aaf6afbd47915000998271..f41b7286f04da1bf0a5673a03ca12d9a579ad0d8 100644 (file)
@@ -12,6 +12,7 @@
  */
 package org.openhab.binding.volvooncall.internal.dto;
 
+import java.util.ArrayList;
 import java.util.List;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -27,8 +28,8 @@ import com.google.gson.annotations.SerializedName;
  */
 @NonNullByDefault
 public class Trip {
-    public int id;
-    public @NonNullByDefault({}) List<TripDetail> tripDetails;
+    public long id;
+    public List<TripDetail> tripDetails = new ArrayList<>();
     @SerializedName("trip")
     public @Nullable String tripURL;
 
index 75dd67d9fd6e9f7268ec3556fc17c0baae28cdc1..f73a1ccfcc8ed286fe88f188bca8998f7f5e15c4 100644 (file)
  */
 package org.openhab.binding.volvooncall.internal.dto;
 
+import java.util.ArrayList;
 import java.util.List;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
 
 /**
  * The {@link Trips} is responsible for storing
@@ -25,5 +25,5 @@ import org.eclipse.jdt.annotation.Nullable;
  */
 @NonNullByDefault
 public class Trips extends VocAnswer {
-    public @Nullable List<Trip> trips;
+    public List<Trip> trips = new ArrayList<>();
 }
index 238825077d9a553a7547d93390d4d4723df3a307..34e48add85a2137aa1b2341b52cf866f978a566e 100644 (file)
@@ -13,6 +13,7 @@
 package org.openhab.binding.volvooncall.internal.dto;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.StringType;
 
 /**
  * The {@link TyrePressure} is responsible for storing
@@ -22,10 +23,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
  */
 @NonNullByDefault
 public class TyrePressure {
-    public @NonNullByDefault({}) String frontLeftTyrePressure;
-    public @NonNullByDefault({}) String frontRightTyrePressure;
-    public @NonNullByDefault({}) String rearLeftTyrePressure;
-    public @NonNullByDefault({}) String rearRightTyrePressure;
+    public @NonNullByDefault({}) StringType frontLeftTyrePressure;
+    public @NonNullByDefault({}) StringType frontRightTyrePressure;
+    public @NonNullByDefault({}) StringType rearLeftTyrePressure;
+    public @NonNullByDefault({}) StringType rearRightTyrePressure;
     /*
      * Currently unused in the binding, maybe interesting in the future
      * private ZonedDateTime timestamp;
index 1f5f41c99ee5f3b033074390a30e41d2299672ce..c14427db9f1bfbaa5d5c4d16dd20b73624546d65 100644 (file)
@@ -17,13 +17,13 @@ import static org.openhab.core.library.unit.MetricPrefix.KILO;
 import static org.openhab.core.library.unit.SIUnits.*;
 import static org.openhab.core.library.unit.SmartHomeUnits.*;
 
-import java.io.IOException;
 import java.time.ZonedDateTime;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Stack;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -32,12 +32,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
 import org.openhab.binding.volvooncall.internal.action.VolvoOnCallActions;
+import org.openhab.binding.volvooncall.internal.api.ActionResultController;
+import org.openhab.binding.volvooncall.internal.api.VocHttpApi;
 import org.openhab.binding.volvooncall.internal.config.VehicleConfiguration;
 import org.openhab.binding.volvooncall.internal.dto.Attributes;
 import org.openhab.binding.volvooncall.internal.dto.DoorsStatus;
 import org.openhab.binding.volvooncall.internal.dto.Heater;
 import org.openhab.binding.volvooncall.internal.dto.HvBattery;
 import org.openhab.binding.volvooncall.internal.dto.Position;
+import org.openhab.binding.volvooncall.internal.dto.PostResponse;
 import org.openhab.binding.volvooncall.internal.dto.Status;
 import org.openhab.binding.volvooncall.internal.dto.Trip;
 import org.openhab.binding.volvooncall.internal.dto.TripDetail;
@@ -57,8 +60,9 @@ 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.ThingStatusInfo;
 import org.openhab.core.thing.binding.BaseThingHandler;
-import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
 import org.openhab.core.thing.binding.ThingHandlerService;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.RefreshType;
@@ -67,8 +71,6 @@ import org.openhab.core.types.UnDefType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.gson.JsonSyntaxException;
-
 /**
  * The {@link VehicleHandler} is responsible for handling commands, which are sent
  * to one of the channels.
@@ -80,20 +82,76 @@ public class VehicleHandler extends BaseThingHandler {
     private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
     private final Map<String, String> activeOptions = new HashMap<>();
     private @Nullable ScheduledFuture<?> refreshJob;
+    private final List<ScheduledFuture<?>> pendingActions = new Stack<>();
 
     private Vehicles vehicle = new Vehicles();
     private VehiclePositionWrapper vehiclePosition = new VehiclePositionWrapper(new Position());
     private Status vehicleStatus = new Status();
     private @NonNullByDefault({}) VehicleConfiguration configuration;
-    private Integer lastTripId = 0;
+    private @NonNullByDefault({}) VolvoOnCallBridgeHandler bridgeHandler;
+    private long lastTripId;
 
     public VehicleHandler(Thing thing, VehicleStateDescriptionProvider stateDescriptionProvider) {
         super(thing);
     }
 
-    private Map<String, String> discoverAttributes(VolvoOnCallBridgeHandler bridgeHandler)
-            throws JsonSyntaxException, IOException, VolvoOnCallException {
-        Attributes attributes = bridgeHandler.getURL(vehicle.attributesURL, Attributes.class);
+    @Override
+    public void initialize() {
+        logger.trace("Initializing the Volvo On Call handler for {}", getThing().getUID());
+
+        Bridge bridge = getBridge();
+        initializeBridge(bridge == null ? null : bridge.getHandler(), bridge == null ? null : bridge.getStatus());
+    }
+
+    @Override
+    public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+        logger.debug("bridgeStatusChanged {} for thing {}", bridgeStatusInfo, getThing().getUID());
+
+        Bridge bridge = getBridge();
+        initializeBridge(bridge == null ? null : bridge.getHandler(), bridgeStatusInfo.getStatus());
+    }
+
+    private void initializeBridge(@Nullable ThingHandler thingHandler, @Nullable ThingStatus bridgeStatus) {
+        logger.debug("initializeBridge {} for thing {}", bridgeStatus, getThing().getUID());
+
+        if (thingHandler != null && bridgeStatus != null) {
+            bridgeHandler = (VolvoOnCallBridgeHandler) thingHandler;
+            if (bridgeStatus == ThingStatus.ONLINE) {
+                configuration = getConfigAs(VehicleConfiguration.class);
+                VocHttpApi api = bridgeHandler.getApi();
+                if (api != null) {
+                    try {
+                        vehicle = api.getURL("vehicles/" + configuration.vin, Vehicles.class);
+                        if (thing.getProperties().isEmpty()) {
+                            Map<String, String> properties = discoverAttributes(api);
+                            updateProperties(properties);
+                        }
+
+                        activeOptions.putAll(
+                                thing.getProperties().entrySet().stream().filter(p -> "true".equals(p.getValue()))
+                                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
+
+                        if (thing.getProperties().containsKey(LAST_TRIP_ID)) {
+                            lastTripId = Long.parseLong(thing.getProperties().get(LAST_TRIP_ID));
+                        }
+
+                        updateStatus(ThingStatus.ONLINE);
+                        startAutomaticRefresh(configuration.refresh, api);
+                    } catch (VolvoOnCallException e) {
+                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, e.getMessage());
+                    }
+
+                }
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+            }
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
+        }
+    }
+
+    private Map<String, String> discoverAttributes(VocHttpApi service) throws VolvoOnCallException {
+        Attributes attributes = service.getURL(vehicle.attributesURL, Attributes.class);
 
         Map<String, String> properties = new HashMap<>();
         properties.put(CAR_LOCATOR, attributes.carLocatorSupported.toString());
@@ -112,71 +170,50 @@ public class VehicleHandler extends BaseThingHandler {
         return properties;
     }
 
-    @Override
-    public void initialize() {
-        logger.trace("Initializing the Volvo On Call handler for {}", getThing().getUID());
-
-        VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
-        if (bridgeHandler != null) {
-            configuration = getConfigAs(VehicleConfiguration.class);
-            try {
-                vehicle = bridgeHandler.getURL(SERVICE_URL + "vehicles/" + configuration.vin, Vehicles.class);
-
-                if (thing.getProperties().isEmpty()) {
-                    Map<String, String> properties = discoverAttributes(bridgeHandler);
-                    updateProperties(properties);
-                }
-
-                activeOptions.putAll(thing.getProperties().entrySet().stream().filter(p -> "true".equals(p.getValue()))
-                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
-
-                if (thing.getProperties().containsKey(LAST_TRIP_ID)) {
-                    lastTripId = Integer.parseInt(thing.getProperties().get(LAST_TRIP_ID));
-                }
-
-                updateStatus(ThingStatus.ONLINE);
-                startAutomaticRefresh(configuration.refresh);
-            } catch (JsonSyntaxException | IOException | VolvoOnCallException e) {
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, e.getMessage());
-            }
-        }
-    }
-
     /**
      * Start the job refreshing the vehicle data
      *
      * @param refresh : refresh frequency in minutes
+     * @param service
      */
-    private void startAutomaticRefresh(int refresh) {
+    private void startAutomaticRefresh(int refresh, VocHttpApi service) {
         ScheduledFuture<?> refreshJob = this.refreshJob;
         if (refreshJob == null || refreshJob.isCancelled()) {
-            refreshJob = scheduler.scheduleWithFixedDelay(this::queryApiAndUpdateChannels, 10, refresh,
+            this.refreshJob = scheduler.scheduleWithFixedDelay(() -> queryApiAndUpdateChannels(service), 1, refresh,
                     TimeUnit.MINUTES);
         }
     }
 
-    private void queryApiAndUpdateChannels() {
-        VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
-        if (bridgeHandler != null) {
-            try {
-                vehicleStatus = bridgeHandler.getURL(vehicle.statusURL, Status.class);
-                vehiclePosition = new VehiclePositionWrapper(bridgeHandler.getURL(Position.class, configuration.vin));
-                // Update all channels from the updated data
-                getThing().getChannels().stream().map(Channel::getUID)
-                        .filter(channelUID -> isLinked(channelUID) && !LAST_TRIP_GROUP.equals(channelUID.getGroupId()))
-                        .forEach(channelUID -> {
-                            State state = getValue(channelUID.getGroupId(), channelUID.getIdWithoutGroup(),
-                                    vehicleStatus, vehiclePosition);
-
+    private void queryApiAndUpdateChannels(VocHttpApi service) {
+        try {
+            Status newVehicleStatus = service.getURL(vehicle.statusURL, Status.class);
+            vehiclePosition = new VehiclePositionWrapper(service.getURL(Position.class, configuration.vin));
+            // Update all channels from the updated data
+            getThing().getChannels().stream().map(Channel::getUID)
+                    .filter(channelUID -> isLinked(channelUID) && !LAST_TRIP_GROUP.equals(channelUID.getGroupId()))
+                    .forEach(channelUID -> {
+                        String groupID = channelUID.getGroupId();
+                        if (groupID != null) {
+                            State state = getValue(groupID, channelUID.getIdWithoutGroup(), newVehicleStatus,
+                                    vehiclePosition);
                             updateState(channelUID, state);
-                        });
-                updateTrips(bridgeHandler);
-            } catch (VolvoOnCallException e) {
-                logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
-                freeRefreshJob();
-                startAutomaticRefresh(configuration.refresh);
+                        }
+                    });
+            if (newVehicleStatus.odometer != vehicleStatus.odometer) {
+                triggerChannel(GROUP_OTHER + "#" + CAR_EVENT, EVENT_CAR_MOVED);
+                // We will update trips only if car position has changed to save server queries
+                updateTrips(service);
+            }
+            if (!vehicleStatus.getEngineRunning().equals(newVehicleStatus.getEngineRunning())
+                    && newVehicleStatus.getEngineRunning().get() == OnOffType.ON) {
+                triggerChannel(GROUP_OTHER + "#" + CAR_EVENT, EVENT_CAR_STARTED);
             }
+            vehicleStatus = newVehicleStatus;
+        } catch (VolvoOnCallException e) {
+            logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+            freeRefreshJob();
+            startAutomaticRefresh(configuration.refresh, service);
         }
     }
 
@@ -186,6 +223,7 @@ public class VehicleHandler extends BaseThingHandler {
             refreshJob.cancel(true);
             this.refreshJob = null;
         }
+        pendingActions.stream().filter(f -> !f.isCancelled()).forEach(f -> f.cancel(true));
     }
 
     @Override
@@ -194,34 +232,32 @@ public class VehicleHandler extends BaseThingHandler {
         super.dispose();
     }
 
-    private void updateTrips(VolvoOnCallBridgeHandler bridgeHandler) throws VolvoOnCallException {
+    private void updateTrips(VocHttpApi service) throws VolvoOnCallException {
         // This seems to rewind 100 days by default, did not find any way to filter it
-        Trips carTrips = bridgeHandler.getURL(Trips.class, configuration.vin);
-        List<Trip> tripList = carTrips.trips;
-
-        if (tripList != null) {
-            List<Trip> newTrips = tripList.stream().filter(trip -> trip.id >= lastTripId).collect(Collectors.toList());
-            Collections.reverse(newTrips);
-
-            logger.debug("Trips discovered : {}", newTrips.size());
-
-            if (!newTrips.isEmpty()) {
-                Integer newTripId = newTrips.get(newTrips.size() - 1).id;
-                if (newTripId > lastTripId) {
-                    updateProperty(LAST_TRIP_ID, newTripId.toString());
-                    lastTripId = newTripId;
-                }
-
-                newTrips.stream().map(t -> t.tripDetails.get(0)).forEach(catchUpTrip -> {
-                    logger.debug("Trip found {}", catchUpTrip.getStartTime());
-                    getThing().getChannels().stream().map(Channel::getUID).filter(
-                            channelUID -> isLinked(channelUID) && LAST_TRIP_GROUP.equals(channelUID.getGroupId()))
-                            .forEach(channelUID -> {
-                                State state = getTripValue(channelUID.getIdWithoutGroup(), catchUpTrip);
-                                updateState(channelUID, state);
-                            });
-                });
+        Trips carTrips = service.getURL(Trips.class, configuration.vin);
+        List<Trip> newTrips = carTrips.trips.stream().filter(trip -> trip.id >= lastTripId)
+                .collect(Collectors.toList());
+        Collections.reverse(newTrips);
+
+        logger.debug("Trips discovered : {}", newTrips.size());
+
+        if (!newTrips.isEmpty()) {
+            Long newTripId = newTrips.get(newTrips.size() - 1).id;
+            if (newTripId > lastTripId) {
+                updateProperty(LAST_TRIP_ID, newTripId.toString());
+                triggerChannel(GROUP_OTHER + "#" + CAR_EVENT, EVENT_CAR_STOPPED);
+                lastTripId = newTripId;
             }
+
+            newTrips.stream().map(t -> t.tripDetails.get(0)).forEach(catchUpTrip -> {
+                logger.debug("Trip found {}", catchUpTrip.getStartTime());
+                getThing().getChannels().stream().map(Channel::getUID)
+                        .filter(channelUID -> isLinked(channelUID) && LAST_TRIP_GROUP.equals(channelUID.getGroupId()))
+                        .forEach(channelUID -> {
+                            State state = getTripValue(channelUID.getIdWithoutGroup(), catchUpTrip);
+                            updateState(channelUID, state);
+                        });
+            });
         }
     }
 
@@ -229,21 +265,18 @@ public class VehicleHandler extends BaseThingHandler {
     public void handleCommand(ChannelUID channelUID, Command command) {
         String channelID = channelUID.getIdWithoutGroup();
         if (command instanceof RefreshType) {
-            queryApiAndUpdateChannels();
+            VocHttpApi api = bridgeHandler.getApi();
+            if (api != null) {
+                queryApiAndUpdateChannels(api);
+            }
         } else if (command instanceof OnOffType) {
             OnOffType onOffCommand = (OnOffType) command;
             if (ENGINE_START.equals(channelID) && onOffCommand == OnOffType.ON) {
                 actionStart(5);
-            } else if (REMOTE_HEATER.equals(channelID)) {
-                actionHeater(onOffCommand == OnOffType.ON);
-            } else if (PRECLIMATIZATION.equals(channelID)) {
-                actionPreclimatization(onOffCommand == OnOffType.ON);
+            } else if (REMOTE_HEATER.equals(channelID) || PRECLIMATIZATION.equals(channelID)) {
+                actionHeater(channelID, onOffCommand == OnOffType.ON);
             } else if (CAR_LOCKED.equals(channelID)) {
-                if (onOffCommand == OnOffType.ON) {
-                    actionClose();
-                } else {
-                    actionOpen();
-                }
+                actionOpenClose((onOffCommand == OnOffType.ON) ? LOCK : UNLOCK, onOffCommand);
             }
         }
     }
@@ -271,7 +304,6 @@ public class VehicleHandler extends BaseThingHandler {
             case TRIP_END_POSITION:
                 return tripDetails.getEndPosition();
         }
-
         return UnDefType.NULL;
     }
 
@@ -310,13 +342,13 @@ public class VehicleHandler extends BaseThingHandler {
     private State getTyresValue(String channelId, TyrePressure tyrePressure) {
         switch (channelId) {
             case REAR_RIGHT_TYRE:
-                return new StringType(tyrePressure.rearRightTyrePressure);
+                return tyrePressure.rearRightTyrePressure;
             case REAR_LEFT_TYRE:
-                return new StringType(tyrePressure.rearLeftTyrePressure);
+                return tyrePressure.rearLeftTyrePressure;
             case FRONT_RIGHT_TYRE:
-                return new StringType(tyrePressure.frontRightTyrePressure);
+                return tyrePressure.frontRightTyrePressure;
             case FRONT_LEFT_TYRE:
-                return new StringType(tyrePressure.frontLeftTyrePressure);
+                return tyrePressure.frontLeftTyrePressure;
         }
         return UnDefType.NULL;
     }
@@ -340,8 +372,7 @@ public class VehicleHandler extends BaseThingHandler {
                         ? new QuantityType<>(hvBattery.distanceToHVBatteryEmpty, KILO(METRE))
                         : UnDefType.UNDEF;
             case CHARGE_STATUS:
-                return hvBattery.hvBatteryChargeStatusDerived != null
-                        ? new StringType(hvBattery.hvBatteryChargeStatusDerived)
+                return hvBattery.hvBatteryChargeStatusDerived != null ? hvBattery.hvBatteryChargeStatusDerived
                         : UnDefType.UNDEF;
             case TIME_TO_BATTERY_FULLY_CHARGED:
                 return hvBattery.timeToHVBatteryFullyCharged != UNDEFINED
@@ -351,43 +382,12 @@ public class VehicleHandler extends BaseThingHandler {
                 return hvBattery.timeToHVBatteryFullyCharged != UNDEFINED && hvBattery.timeToHVBatteryFullyCharged > 0
                         ? new DateTimeType(ZonedDateTime.now().plusMinutes(hvBattery.timeToHVBatteryFullyCharged))
                         : UnDefType.UNDEF;
-
         }
         return UnDefType.NULL;
     }
 
-    private State getValue(@Nullable String groupId, String channelId, Status status, VehiclePositionWrapper position) {
+    private State getValue(String groupId, String channelId, Status status, VehiclePositionWrapper position) {
         switch (channelId) {
-            case ODOMETER:
-                return status.odometer != UNDEFINED ? new QuantityType<>((double) status.odometer / 1000, KILO(METRE))
-                        : UnDefType.UNDEF;
-            case TRIPMETER1:
-                return status.tripMeter1 != UNDEFINED
-                        ? new QuantityType<>((double) status.tripMeter1 / 1000, KILO(METRE))
-                        : UnDefType.UNDEF;
-            case TRIPMETER2:
-                return status.tripMeter2 != UNDEFINED
-                        ? new QuantityType<>((double) status.tripMeter2 / 1000, KILO(METRE))
-                        : UnDefType.UNDEF;
-            case DISTANCE_TO_EMPTY:
-                return status.distanceToEmpty != UNDEFINED ? new QuantityType<>(status.distanceToEmpty, KILO(METRE))
-                        : UnDefType.UNDEF;
-            case FUEL_AMOUNT:
-                return status.fuelAmount != UNDEFINED ? new QuantityType<>(status.fuelAmount, LITRE) : UnDefType.UNDEF;
-            case FUEL_LEVEL:
-                return status.fuelAmountLevel != UNDEFINED ? new QuantityType<>(status.fuelAmountLevel, PERCENT)
-                        : UnDefType.UNDEF;
-            case FUEL_CONSUMPTION:
-                return status.averageFuelConsumption != UNDEFINED ? new DecimalType(status.averageFuelConsumption / 10)
-                        : UnDefType.UNDEF;
-            case ACTUAL_LOCATION:
-                return position.getPosition();
-            case CALCULATED_LOCATION:
-                return position.isCalculated();
-            case HEADING:
-                return position.isHeading();
-            case LOCATION_TIMESTAMP:
-                return position.getTimestamp();
             case CAR_LOCKED:
                 // Warning : carLocked is in the Doors group but is part of general status informations.
                 // Did not change it to avoid breaking change for users
@@ -403,162 +403,149 @@ public class VehicleHandler extends BaseThingHandler {
                         : UnDefType.UNDEF;
             case SERVICE_WARNING:
                 return new StringType(status.serviceWarningStatus);
-            case FUEL_ALERT:
-                return status.distanceToEmpty < 100 ? OnOffType.ON : OnOffType.OFF;
             case BULB_FAILURE:
                 return status.aFailedBulb() ? OnOffType.ON : OnOffType.OFF;
             case REMOTE_HEATER:
             case PRECLIMATIZATION:
                 return status.getHeater().map(heater -> getHeaterValue(channelId, heater)).orElse(UnDefType.NULL);
         }
-        if (groupId != null) {
-            switch (groupId) {
-                case GROUP_DOORS:
-                    return status.getDoors().map(doors -> getDoorsValue(channelId, doors)).orElse(UnDefType.NULL);
-                case GROUP_WINDOWS:
-                    return status.getWindows().map(windows -> getWindowsValue(channelId, windows))
-                            .orElse(UnDefType.NULL);
-                case GROUP_TYRES:
-                    return status.getTyrePressure().map(tyres -> getTyresValue(channelId, tyres))
-                            .orElse(UnDefType.NULL);
-                case GROUP_BATTERY:
-                    return status.getHvBattery().map(batteries -> getBatteryValue(channelId, batteries))
-                            .orElse(UnDefType.NULL);
-            }
+        switch (groupId) {
+            case GROUP_TANK:
+                return getTankValue(channelId, status);
+            case GROUP_ODOMETER:
+                return getOdometerValue(channelId, status);
+            case GROUP_POSITION:
+                return getPositionValue(channelId, position);
+            case GROUP_DOORS:
+                return status.getDoors().map(doors -> getDoorsValue(channelId, doors)).orElse(UnDefType.NULL);
+            case GROUP_WINDOWS:
+                return status.getWindows().map(windows -> getWindowsValue(channelId, windows)).orElse(UnDefType.NULL);
+            case GROUP_TYRES:
+                return status.getTyrePressure().map(tyres -> getTyresValue(channelId, tyres)).orElse(UnDefType.NULL);
+            case GROUP_BATTERY:
+                return status.getHvBattery().map(batteries -> getBatteryValue(channelId, batteries))
+                        .orElse(UnDefType.NULL);
         }
         return UnDefType.NULL;
     }
 
-    public void actionHonkBlink(Boolean honk, Boolean blink) {
-        VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
-        if (bridgeHandler != null) {
-            StringBuilder url = new StringBuilder(SERVICE_URL + "vehicles/" + vehicle.vehicleId + "/honk_blink/");
-
-            if (honk && blink && activeOptions.containsKey(HONK_BLINK)) {
-                url.append("both");
-            } else if (honk && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
-                url.append("horn");
-            } else if (blink && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
-                url.append("lights");
-            } else {
-                logger.warn("The vehicle is not capable of this action");
-                return;
-            }
-
-            try {
-                bridgeHandler.postURL(url.toString(), vehiclePosition.getPositionAsJSon());
-            } catch (VolvoOnCallException e) {
-                logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
-            }
+    private State getTankValue(String channelId, Status status) {
+        switch (channelId) {
+            case DISTANCE_TO_EMPTY:
+                return status.distanceToEmpty != UNDEFINED ? new QuantityType<>(status.distanceToEmpty, KILO(METRE))
+                        : UnDefType.UNDEF;
+            case FUEL_AMOUNT:
+                return status.fuelAmount != UNDEFINED ? new QuantityType<>(status.fuelAmount, LITRE) : UnDefType.UNDEF;
+            case FUEL_LEVEL:
+                return status.fuelAmountLevel != UNDEFINED ? new QuantityType<>(status.fuelAmountLevel, PERCENT)
+                        : UnDefType.UNDEF;
+            case FUEL_CONSUMPTION:
+                return status.averageFuelConsumption != UNDEFINED ? new DecimalType(status.averageFuelConsumption / 10)
+                        : UnDefType.UNDEF;
+            case FUEL_ALERT:
+                return status.distanceToEmpty < 100 ? OnOffType.ON : OnOffType.OFF;
         }
+        return UnDefType.UNDEF;
     }
 
-    private void actionOpenClose(String action, OnOffType controlState) {
-        VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
-        if (bridgeHandler != null) {
-            if (activeOptions.containsKey(action)) {
-                if (!vehicleStatus.getCarLocked().isPresent() || vehicleStatus.getCarLocked().get() != controlState) {
-                    try {
-                        StringBuilder address = new StringBuilder(SERVICE_URL);
-                        address.append("vehicles/");
-                        address.append(configuration.vin);
-                        address.append("/");
-                        address.append(action);
-                        bridgeHandler.postURL(address.toString(), "{}");
-                    } catch (VolvoOnCallException e) {
-                        logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
-                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
-                    }
-                } else {
-                    logger.info("The car {} is already {}ed", configuration.vin, action);
-                }
-            } else {
-                logger.warn("The car {} does not support remote {}ing", configuration.vin, action);
-            }
+    private State getOdometerValue(String channelId, Status status) {
+        switch (channelId) {
+            case ODOMETER:
+                return status.odometer != UNDEFINED ? new QuantityType<>((double) status.odometer / 1000, KILO(METRE))
+                        : UnDefType.UNDEF;
+            case TRIPMETER1:
+                return status.tripMeter1 != UNDEFINED
+                        ? new QuantityType<>((double) status.tripMeter1 / 1000, KILO(METRE))
+                        : UnDefType.UNDEF;
+            case TRIPMETER2:
+                return status.tripMeter2 != UNDEFINED
+                        ? new QuantityType<>((double) status.tripMeter2 / 1000, KILO(METRE))
+                        : UnDefType.UNDEF;
         }
+        return UnDefType.UNDEF;
     }
 
-    private void actionHeater(String action, Boolean start) {
-        VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
-        if (bridgeHandler != null) {
-            if (activeOptions.containsKey(action)) {
-                try {
-                    if (action.contains(REMOTE_HEATER)) {
-                        String command = start ? "start" : "stop";
-                        String address = SERVICE_URL + "vehicles/" + configuration.vin + "/heater/" + command;
-                        bridgeHandler.postURL(address, start ? "{}" : null);
-                    } else if (action.contains(PRECLIMATIZATION)) {
-                        String command = start ? "start" : "stop";
-                        String address = SERVICE_URL + "vehicles/" + configuration.vin + "/preclimatization/" + command;
-                        bridgeHandler.postURL(address, start ? "{}" : null);
-                    }
-                } catch (VolvoOnCallException e) {
-                    logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
-                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
-                }
-            } else {
-                logger.warn("The car {} does not support {}", configuration.vin, action);
-            }
+    private State getPositionValue(String channelId, VehiclePositionWrapper position) {
+        switch (channelId) {
+            case ACTUAL_LOCATION:
+                return position.getPosition();
+            case CALCULATED_LOCATION:
+                return position.isCalculated();
+            case HEADING:
+                return position.isHeading();
+            case LOCATION_TIMESTAMP:
+                return position.getTimestamp();
         }
+        return UnDefType.UNDEF;
     }
 
-    public void actionHeater(Boolean start) {
-        actionHeater(REMOTE_HEATER, start);
-    }
-
-    public void actionPreclimatization(Boolean start) {
-        actionHeater(PRECLIMATIZATION, start);
-    }
+    public void actionHonkBlink(Boolean honk, Boolean blink) {
+        StringBuilder url = new StringBuilder("vehicles/" + vehicle.vehicleId + "/honk_blink/");
+
+        if (honk && blink && activeOptions.containsKey(HONK_BLINK)) {
+            url.append("both");
+        } else if (honk && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
+            url.append("horn");
+        } else if (blink && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
+            url.append("lights");
+        } else {
+            logger.warn("The vehicle is not capable of this action");
+            return;
+        }
 
-    public void actionOpen() {
-        actionOpenClose(UNLOCK, OnOffType.OFF);
+        post(url.toString(), vehiclePosition.getPositionAsJSon());
     }
 
-    public void actionClose() {
-        actionOpenClose(LOCK, OnOffType.ON);
+    private void post(String url, @Nullable String param) {
+        VocHttpApi api = bridgeHandler.getApi();
+        if (api != null) {
+            try {
+                PostResponse postResponse = api.postURL(url.toString(), param);
+                if (postResponse != null) {
+                    pendingActions
+                            .add(scheduler.schedule(new ActionResultController(api, postResponse, scheduler, this),
+                                    1000, TimeUnit.MILLISECONDS));
+                }
+            } catch (VolvoOnCallException e) {
+                logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+            }
+        }
+        ;
+        pendingActions.removeIf(ScheduledFuture::isDone);
     }
 
-    public void actionStart(Integer runtime) {
-        VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
-        if (bridgeHandler != null) {
-            if (activeOptions.containsKey(ENGINE_START)) {
-                String url = SERVICE_URL + "vehicles/" + vehicle.vehicleId + "/engine/start";
-                String json = "{\"runtime\":" + runtime.toString() + "}";
-
-                try {
-                    bridgeHandler.postURL(url, json);
-                } catch (VolvoOnCallException e) {
-                    logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
-                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
-                }
+    public void actionOpenClose(String action, OnOffType controlState) {
+        if (activeOptions.containsKey(action)) {
+            if (!vehicleStatus.getCarLocked().isPresent() || vehicleStatus.getCarLocked().get() != controlState) {
+                post(String.format("vehicles/%s/%s", configuration.vin, action), "{}");
             } else {
-                logger.warn("The car {} does not support remote engine starting", vehicle.vehicleId);
+                logger.info("The car {} is already {}ed", configuration.vin, action);
             }
+        } else {
+            logger.warn("The car {} does not support remote {}ing", configuration.vin, action);
         }
     }
 
-    /*
-     * Called by Bridge when it has to notify this of a potential state
-     * update
-     *
-     */
-    void updateIfMatches(String vin) {
-        if (vin.equalsIgnoreCase(configuration.vin)) {
-            queryApiAndUpdateChannels();
+    public void actionHeater(String action, Boolean start) {
+        if (activeOptions.containsKey(action)) {
+            String address = String.format("vehicles/%s/%s/%s", configuration.vin,
+                    action.contains(REMOTE_HEATER) ? "heater" : "preclimatization", start ? "start" : "stop");
+            post(address, start ? "{}" : null);
+        } else {
+            logger.warn("The car {} does not support {}", configuration.vin, action);
         }
     }
 
-    private @Nullable VolvoOnCallBridgeHandler getBridgeHandler() {
-        Bridge bridge = getBridge();
-        if (bridge != null) {
-            BridgeHandler handler = bridge.getHandler();
-            if (handler != null) {
-                return (VolvoOnCallBridgeHandler) handler;
-            }
+    public void actionStart(Integer runtime) {
+        if (activeOptions.containsKey(ENGINE_START)) {
+            String address = String.format("vehicles/%s/engine/start", vehicle.vehicleId);
+            String json = "{\"runtime\":" + runtime.toString() + "}";
+
+            post(address, json);
+        } else {
+            logger.warn("The car {} does not support remote engine starting", vehicle.vehicleId);
         }
-        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
-        return null;
     }
 
     @Override
index e2764334b142c30a86be669cca9072868d75976e..ce3ee8cd694569d607feb42a07594df14ba96f29 100644 (file)
  */
 package org.openhab.binding.volvooncall.internal.handler;
 
-import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import java.time.ZonedDateTime;
-import java.util.List;
-import java.util.Properties;
-import java.util.Stack;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
+import java.util.Collection;
+import java.util.Collections;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
 import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
-import org.openhab.binding.volvooncall.internal.VolvoOnCallException.ErrorType;
-import org.openhab.binding.volvooncall.internal.config.VolvoOnCallBridgeConfiguration;
+import org.openhab.binding.volvooncall.internal.api.VocHttpApi;
+import org.openhab.binding.volvooncall.internal.config.ApiBridgeConfiguration;
+import org.openhab.binding.volvooncall.internal.discovery.VolvoVehicleDiscoveryService;
 import org.openhab.binding.volvooncall.internal.dto.CustomerAccounts;
-import org.openhab.binding.volvooncall.internal.dto.PostResponse;
-import org.openhab.binding.volvooncall.internal.dto.VocAnswer;
-import org.openhab.core.io.net.http.HttpUtil;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.OpenClosedType;
 import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
 import org.openhab.core.types.Command;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonDeserializer;
-import com.google.gson.JsonSyntaxException;
 
 /**
  * The {@link VolvoOnCallBridgeHandler} is responsible for handling commands, which are
@@ -58,144 +43,60 @@ import com.google.gson.JsonSyntaxException;
  */
 @NonNullByDefault
 public class VolvoOnCallBridgeHandler extends BaseBridgeHandler {
-    private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(20);
+
     private final Logger logger = LoggerFactory.getLogger(VolvoOnCallBridgeHandler.class);
-    private final Properties httpHeader = new Properties();
-    private final List<ScheduledFuture<?>> pendingActions = new Stack<>();
     private final Gson gson;
+    private final HttpClient httpClient;
 
-    private @NonNullByDefault({}) CustomerAccounts customerAccount;
+    private @Nullable VocHttpApi api;
 
-    public VolvoOnCallBridgeHandler(Bridge bridge) {
+    public VolvoOnCallBridgeHandler(Bridge bridge, Gson gson, HttpClient httpClient) {
         super(bridge);
-
-        httpHeader.put("cache-control", "no-cache");
-        httpHeader.put("content-type", JSON_CONTENT_TYPE);
-        httpHeader.put("x-device-id", "Device");
-        httpHeader.put("x-originator-type", "App");
-        httpHeader.put("x-os-type", "Android");
-        httpHeader.put("x-os-version", "22");
-        httpHeader.put("Accept", "*/*");
-
-        gson = new GsonBuilder()
-                .registerTypeAdapter(ZonedDateTime.class,
-                        (JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
-                                .parse(json.getAsJsonPrimitive().getAsString().replaceAll("\\+0000", "Z")))
-                .registerTypeAdapter(OpenClosedType.class,
-                        (JsonDeserializer<OpenClosedType>) (json, type,
-                                jsonDeserializationContext) -> json.getAsBoolean() ? OpenClosedType.OPEN
-                                        : OpenClosedType.CLOSED)
-                .registerTypeAdapter(OnOffType.class,
-                        (JsonDeserializer<OnOffType>) (json, type,
-                                jsonDeserializationContext) -> json.getAsBoolean() ? OnOffType.ON : OnOffType.OFF)
-                .create();
+        this.gson = gson;
+        this.httpClient = httpClient;
     }
 
     @Override
     public void initialize() {
         logger.debug("Initializing VolvoOnCall API bridge handler.");
-        VolvoOnCallBridgeConfiguration configuration = getConfigAs(VolvoOnCallBridgeConfiguration.class);
+        ApiBridgeConfiguration configuration = getConfigAs(ApiBridgeConfiguration.class);
 
-        httpHeader.setProperty("Authorization", configuration.getAuthorization());
         try {
-            customerAccount = getURL(SERVICE_URL + "customeraccounts/", CustomerAccounts.class);
-            if (customerAccount.username != null) {
-                updateStatus(ThingStatus.ONLINE);
+            api = new VocHttpApi(configuration, gson, httpClient);
+            CustomerAccounts account = api.getURL("customeraccounts/", CustomerAccounts.class);
+            if (account.username != null) {
+                updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, account.username);
             } else {
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
-                        "Incorrect username or password");
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Incorrect login credentials");
             }
-        } catch (JsonSyntaxException | VolvoOnCallException e) {
+        } catch (VolvoOnCallException e) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
         }
     }
 
     @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
-        logger.debug("VolvoOnCall Bridge is read-only and does not handle commands");
-    }
-
-    public String[] getVehiclesRelationsURL() {
-        if (customerAccount != null) {
-            return customerAccount.accountVehicleRelationsURL;
-        }
-        return new String[0];
-    }
-
-    public <T extends VocAnswer> T getURL(Class<T> objectClass, String vin) throws VolvoOnCallException {
-        String url = SERVICE_URL + "vehicles/" + vin + "/" + objectClass.getSimpleName().toLowerCase();
-        return getURL(url, objectClass);
-    }
-
-    public <T extends VocAnswer> T getURL(String url, Class<T> objectClass) throws VolvoOnCallException {
-        try {
-            String jsonResponse = HttpUtil.executeUrl("GET", url, httpHeader, null, JSON_CONTENT_TYPE, REQUEST_TIMEOUT);
-            logger.debug("Request for : {}", url);
-            logger.debug("Received : {}", jsonResponse);
-            T response = gson.fromJson(jsonResponse, objectClass);
-            String error = response.getErrorLabel();
-            if (error != null) {
-                throw new VolvoOnCallException(error, response.getErrorDescription());
+    public void dispose() {
+        if (api != null) {
+            try {
+                api.dispose();
+                api = null;
+            } catch (Exception e) {
+                logger.warn("Unable to stop VocHttpApi : {}", e.getMessage());
             }
-            return response;
-        } catch (JsonSyntaxException | IOException e) {
-            throw new VolvoOnCallException(e);
         }
     }
 
-    public class ActionResultControler implements Runnable {
-        PostResponse postResponse;
-
-        ActionResultControler(PostResponse postResponse) {
-            this.postResponse = postResponse;
-        }
-
-        @Override
-        public void run() {
-            switch (postResponse.status) {
-                case SUCCESSFULL:
-                case FAILED:
-                    logger.info("Action status : {} for vehicle : {}.", postResponse.status.toString(),
-                            postResponse.vehicleId);
-                    getThing().getThings().stream().filter(VehicleHandler.class::isInstance)
-                            .map(VehicleHandler.class::cast)
-                            .forEach(handler -> handler.updateIfMatches(postResponse.vehicleId));
-                    break;
-                default:
-                    try {
-                        postResponse = getURL(postResponse.serviceURL, PostResponse.class);
-                        scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS);
-                    } catch (VolvoOnCallException e) {
-                        if (e.getType() == ErrorType.SERVICE_UNAVAILABLE) {
-                            scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS);
-                        }
-                    }
-            }
-        }
+    public @Nullable VocHttpApi getApi() {
+        return api;
     }
 
-    void postURL(String URL, @Nullable String body) throws VolvoOnCallException {
-        InputStream inputStream = body != null ? new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)) : null;
-        try {
-            String jsonString = HttpUtil.executeUrl("POST", URL, httpHeader, inputStream, null, REQUEST_TIMEOUT);
-            logger.debug("Post URL: {} Attributes {}", URL, httpHeader);
-            PostResponse postResponse = gson.fromJson(jsonString, PostResponse.class);
-            String error = postResponse.getErrorLabel();
-            if (error == null) {
-                pendingActions
-                        .add(scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS));
-            } else {
-                throw new VolvoOnCallException(error, postResponse.getErrorDescription());
-            }
-            pendingActions.removeIf(ScheduledFuture::isDone);
-        } catch (JsonSyntaxException | IOException e) {
-            throw new VolvoOnCallException(e);
-        }
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(VolvoVehicleDiscoveryService.class);
     }
 
     @Override
-    public void dispose() {
-        super.dispose();
-        pendingActions.stream().filter(f -> !f.isCancelled()).forEach(f -> f.cancel(true));
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        // Do nothing
     }
 }
index cfc5fedfbb8bcd9480069f1bfda854abafb4bd77..c3f5b42e07a505743ec5ec91e2de5fee16118334 100644 (file)
@@ -4,7 +4,7 @@
        xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
 
        <name>VolvoOnCall Binding</name>
-       <description>This binding enables the access to VolvoOnCall features.</description>
+       <description>This binding enables the access to VolvoOnCall services.</description>
        <author>Gaël L'hopital</author>
 
 </binding:binding>
diff --git a/bundles/org.openhab.binding.volvooncall/src/main/resources/OH-INF/i18n/volvooncall_fr.properties b/bundles/org.openhab.binding.volvooncall/src/main/resources/OH-INF/i18n/volvooncall_fr.properties
new file mode 100644 (file)
index 0000000..7546598
--- /dev/null
@@ -0,0 +1,10 @@
+# binding
+binding.volvooncall.name = Extension VolvoOnCall
+binding.volvooncall.description = Cette extension fournit l'accès aux services de Volvo On Call.
+
+# thing types
+thing-type.volvooncall.vocapi.label = API Volvo On Call
+thing-type.volvooncall.vocapi.description = Fournit l'interface avec le service en ligne Volvo On Call. Pour recevoir les données, vous devez vous munir de vos informations de connection (nom d'utilisateur, mot de passe).
+
+thing-type.volvooncall.vehicle.label = Véhicule
+thing-type.volvooncall.vehicle.description = Toutes les informations disponibles sur le véhicule Volvo.
index 9bea49eb049d887bfc626e96ad69b724e2aaf859..c1e71efc149b33a69252471df803e33535d71025 100644 (file)
                                <description>VIN of the vehicle associated with this Thing</description>
                        </parameter>
 
-                       <parameter name="refresh" type="integer" min="5" required="false">
+                       <parameter name="refresh" type="integer" min="5" required="true">
                                <label>Refresh Interval</label>
                                <description>Specifies the refresh interval in minutes.</description>
-                               <default>5</default>
+                               <default>10</default>
+                               <advanced>true</advanced>
                        </parameter>
                </config-description>
        </thing-type>
@@ -51,6 +52,7 @@
                        <channel id="washerFluidLevel" typeId="washerFluidLevel"/>
                        <channel id="serviceWarningStatus" typeId="serviceWarningStatus"/>
                        <channel id="bulbFailure" typeId="bulbFailure"/>
+                       <channel id="carEvent" typeId="carEvent"/>
                </channels>
        </channel-group-type>
 
                <label>Last Trip</label>
                <channels>
                        <channel id="tripConsumption" typeId="fuelQuantity">
-                               <label>Consumption</label>
+                               <label>Trip Consumption</label>
                                <description>Indicates the quantity of fuel consumed by the trip</description>
                        </channel>
                        <channel id="tripDistance" typeId="odometer">
-                               <label>Distance</label>
+                               <label>Trip Distance</label>
                                <description>Distance traveled</description>
                        </channel>
                        <channel id="tripStartTime" typeId="timestamp">
-                               <label>Start Time</label>
+                               <label>Trip Start Time</label>
                                <description>Trip start time</description>
                        </channel>
                        <channel id="tripEndTime" typeId="timestamp">
-                               <label>End Time</label>
+                               <label>Trip End Time</label>
                                <description>Trip end time</description>
                        </channel>
                        <channel id="tripDuration" typeId="tripDuration"/>
                        <channel id="tripStartOdometer" typeId="odometer">
-                               <label>Start Odometer</label>
+                               <label>Trip Start Odometer</label>
                        </channel>
                        <channel id="tripStopOdometer" typeId="odometer">
-                               <label>Stop Odometer</label>
+                               <label>Trip Stop Odometer</label>
                        </channel>
                        <channel id="startPosition" typeId="location">
-                               <label>From</label>
+                               <label>Trip From</label>
                                <description>Starting location of the car</description>
                        </channel>
                        <channel id="endPosition" typeId="location">
-                               <label>To</label>
+                               <label>Trip To</label>
                                <description>Stopping location of the car</description>
                        </channel>
                </channels>
                <label>Doors Opening Status</label>
                <channels>
                        <channel id="frontLeft" typeId="door">
-                               <label>Front Left</label>
+                               <label>Front Left Door</label>
                        </channel>
                        <channel id="frontRight" typeId="door">
-                               <label>Front Right</label>
+                               <label>Front Right Door</label>
                        </channel>
                        <channel id="rearLeft" typeId="door">
-                               <label>Rear Left</label>
+                               <label>Rear Left Door</label>
                        </channel>
                        <channel id="rearRight" typeId="door">
-                               <label>Rear Right</label>
+                               <label>Rear Right Door</label>
                        </channel>
                        <channel id="hood" typeId="door">
                                <label>Hood</label>
                <label>Windows Opening Status</label>
                <channels>
                        <channel id="frontLeftWnd" typeId="window">
-                               <label>Front Left</label>
+                               <label>Front Left Window</label>
                        </channel>
                        <channel id="frontRightWnd" typeId="window">
-                               <label>Front Right</label>
+                               <label>Front Right Window</label>
                        </channel>
                        <channel id="rearLeftWnd" typeId="window">
-                               <label>Rear Left</label>
+                               <label>Rear Left Window</label>
                        </channel>
                        <channel id="rearRightWnd" typeId="window">
-                               <label>Rear Right</label>
+                               <label>Rear Right Window</label>
                        </channel>
                </channels>
        </channel-group-type>
                <label>Tyre pressure status</label>
                <channels>
                        <channel id="frontLeftTyre" typeId="tyrePressure">
-                               <label>Front Left</label>
+                               <label>Front Left Tyre</label>
                        </channel>
                        <channel id="frontRightTyre" typeId="tyrePressure">
-                               <label>Front Right</label>
+                               <label>Front Right Tyre</label>
                        </channel>
                        <channel id="rearLeftTyre" typeId="tyrePressure">
-                               <label>Rear Left</label>
+                               <label>Rear Left Tyre</label>
                        </channel>
                        <channel id="rearRightTyre" typeId="tyrePressure">
-                               <label>Rear Right</label>
+                               <label>Rear Right Tyre</label>
                        </channel>
                </channels>
        </channel-group-type>
                <label>Location Info</label>
                <channels>
                        <channel id="location" typeId="location">
-                               <label>Location</label>
+                               <label>Current Location</label>
                                <description>The position of the vehicle</description>
                        </channel>
                        <channel id="calculatedLocation" typeId="calculatedLocation"/>
                <item-type>Number:Speed</item-type>
                <label>Average speed</label>
                <description>Average speed of the vehicle</description>
-               <state pattern="%d %unit%" readOnly="true"></state>
+               <state pattern="%.2f %unit%" readOnly="true"></state>
        </channel-type>
 
        <channel-type id="fuelQuantity">
        <channel-type id="fuelConsumption" advanced="true">
                <item-type>Number</item-type>
                <label>Average Consumption</label>
-               <description>Indicates the average fuel consumption in L/100km</description>
-               <state pattern="%.1f L/100km" readOnly="true"></state>
+               <description>Indicates the average fuel consumption in l/100km</description>
+               <state pattern="%.1f l/100km" readOnly="true"></state>
        </channel-type>
 
        <channel-type id="location">
                <state readOnly="true"/>
        </channel-type>
 
+       <channel-type id="carEvent">
+               <kind>trigger</kind>
+               <label>Car Event</label>
+               <event>
+                       <options>
+                               <option value="CAR_STOPPED">Car stopped</option>
+                               <option value="CAR_STARTED">Car started</option>
+                               <option value="CAR_MOVED">Car has moved</option>
+                       </options>
+               </event>
+       </channel-type>
+
 </thing:thing-descriptions>