]> git.basschouten.com Git - openhab-addons.git/commitdiff
[velux] hub discovery; representation properties; socket lock up issues (#8777)
authorAndrew Fiddian-Green <software@whitebear.ch>
Tue, 1 Dec 2020 17:05:51 +0000 (17:05 +0000)
committerGitHub <noreply@github.com>
Tue, 1 Dec 2020 17:05:51 +0000 (09:05 -0800)
* [velux] set explicit timeouts & keepalives on socket
* [velux] implement mdns service
* [velux] fix representation property names
* [velux] fix representation properties
* [velux] finalize mdns
* [velux] spotless
* [velux] use both mDNS and regular DNS to resolve ip addresses
* [velux] complete class rewrite using asynchronous polling thread
* [velux] refactor bridgeDirectCommunicate to simplify looping
* [velux] asynchronous polling means Thread.sleep no longer needed
* [velux] faster synch of actuator changes
* [velux] use single thread executor instead of thread pool
* [velux] faster synch of actuator changes
* [velux] shut down task executor

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
44 files changed:
bundles/org.openhab.binding.velux/README.md
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxBinding.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxBindingConstants.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxItemType.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/IVeluxActions.java [new file with mode: 0644]
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/VeluxActions.java [new file with mode: 0644]
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/package-info.java [new file with mode: 0644]
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridge.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridgeSetSceneVelocity.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/BridgeAPI.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/RunReboot.java [new file with mode: 0644]
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/SetSceneVelocity.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JCsetSceneVelocity.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JsonBridgeAPI.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JsonVeluxBridge.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetWLANConfig.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCrunReboot.java [new file with mode: 0644]
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCsetSceneVelocity.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SlipBridgeAPI.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SlipVeluxBridge.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/Connection.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/DataInputStreamWithTimeout.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/SSLconnection.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/utils/KLF200Response.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/discovery/VeluxBridgeFinder.java [new file with mode: 0644]
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/discovery/VeluxDiscoveryService.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelActuatorPosition.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelBridgeLANconfig.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelBridgeWLANconfig.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelSceneSilentmode.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/VeluxBridgeHandler.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/VeluxHandler.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/utils/ThingProperty.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxExistingProducts.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProduct.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductPosition.java
bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductVelocity.java
bundles/org.openhab.binding.velux/src/main/resources/OH-INF/config/config.xml
bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/actuator.xml
bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/binding.xml
bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/bridge.xml
bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/rollershutter.xml
bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/scene.xml
bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/window.xml

index a5d7dd3566634c6ed8f1e56091223ef9a9d7f41a..fc08919fc35210323f5a8a9f41fc95948d2fe6e3 100644 (file)
@@ -34,8 +34,9 @@ The binding supports the following types of Thing.
 ## Discovery
 
 To simplify the initial provisioning, the binding provides one thing which can be found by autodiscovery.
-Unfortunately there is no way to discover Velux bridges themselves within the local network.
-But after configuring a Velux Bridge, it is possible to discover all scenes and actuators like windows and rollershutters in that hub.
+The binding will automatically discover Velux Bridges within the local network, and place them in the Inbox.
+Once a Velux Bridge has been discovered, you will need to enter the `password` Configuration Parameter (see below) before the binding can communicate with it.
+And once the Velux Bridge is fully configured, the binding will automatically discover all its respective scenes and actuators (like windows and rollershutters), and place them in the Inbox.
 
 ## Thing Configuration
 
@@ -51,7 +52,7 @@ In addition there are some optional Configuration Parameters.
 |-------------------------|------------------|:--------:|--------------------------------------------------------------|
 | ipAddress               |                  |   Yes    | Hostname or address for accessing the Velux Bridge.          |
 | password                | velux123         |   Yes    | Password for authentication against the Velux Bridge.(\*\*)  |
-| timeoutMsecs            | 500              |    No    | Communication timeout in milliseconds.                       |
+| timeoutMsecs            | 2000             |    No    | Communication timeout in milliseconds.                       |
 | protocol                | slip             |    No    | Underlying communication protocol (http/https/slip).         |
 | tcpPort                 | 51200            |    No    | TCP port (80 or 51200) for accessing the Velux Bridge.       |
 | retries                 | 5                |    No    | Number of retries during I/O.                                |
@@ -89,7 +90,7 @@ In addition there are some optional Configuration Parameters.
 
 Notes:
 
-1. To enable a complete invertion of all parameter values (i.e. for Velux windows), use the property `inverted` or add a trailing star to the eight-byte serial number. For an example, see below at item `Velux DG Window Bathroom`.
+1. To enable a complete inversion of all parameter values (i.e. for Velux windows), use the property `inverted` or add a trailing star to the eight-byte serial number. For an example, see below at item `Velux DG Window Bathroom`.
 
 2. Somfy devices do not provide a valid serial number to the Velux KLF200 gateway. The bridge reports a registration of the serial number 00:00:00:00:00:00:00:00. Therefore the binding implements a fallback to allow an item specification with a actuator `name` instead of actuator serial number whenever such an invalid serial number occurs. For an example, see below at item `Velux OG Somfy Shutter`.
 
@@ -99,9 +100,10 @@ The Velux Bridge in API version one (firmware version 0.1.1.*) allows activating
 So besides the bridge, only one real Thing type exists, namely "scene".
 This type of Thing is configured by means of its scene name in the hub.
 
-| Configuration Parameter | Default                | Required | Description                                               |
-|-------------------------|------------------------|:--------:|-----------------------------------------------------------|
-| sceneName               |                        |   Yes    | Name of the scene in the hub.                             |
+| Configuration Parameter | Default                | Required | Description                                                           |
+|-------------------------|------------------------|:--------:|-----------------------------------------------------------------------|
+| sceneName               |                        |   Yes    | Name of the scene in the hub.                                         |
+| velocity                |                        |   No     | The speed at which the scene will be executed (deafult, silent, fast) |
 
 ### Thing Configuration for "vshutter"
 
@@ -128,7 +130,7 @@ The supported Channels and their associated channel types are shown below.
 | downtime    | Number    | Time interval (sec) between last successful and most recent device interaction. |
 | doDetection | Switch    | Command to activate bridge detection mode.                                      |
 
-### Channels for "window", "rollershutter" Things
+### Channels for "window" / "rollershutter" Things
 
 The supported Channels and their associated channel types are shown below.
 
@@ -138,6 +140,15 @@ The supported Channels and their associated channel types are shown below.
 | limitMinimum | Rollershutter | Minimum limit position of the window or device. |
 | limitMaximum | Rollershutter | Maximum limit position of the window or device. |
 
+The `position` Channel indicates the open/close state of the window (resp. roller shutter) in percent (0% .. 100%) as follows..
+
+- As a general rule the display is the actual physical position.
+- If it is moving towards a new target position, the display is the target position.
+- After the movement has completed, the display is the final physical position.
+- If a window is opened manually, the display is `UNDEF`.
+- In case of errors (e.g. window jammed) the display is `UNDEF`.
+- If a Somfy actuator is commanded to its 'favorite' position via a Somfy remote control, under some circumstances the display is `UNDEF`. See also Rules below.
+
 ### Channels for "actuator" Things
 
 The supported Channels and their associated channel types are shown below.
@@ -149,6 +160,8 @@ The supported Channels and their associated channel types are shown below.
 | limitMinimum | Rollershutter | Minimum limit position of the window or device. |
 | limitMaximum | Rollershutter | Maximum limit position of the window or device. |
 
+See the section above for "window" / "rollershutter" Things for further information concerning the `position` Channel.
+
 ### Channels for "scene" Things
 
 The supported Channels and their associated channel types are shown below.
@@ -166,6 +179,8 @@ The supported Channel and its associated channel type is shown below.
 |--------------|---------------|-----------------------------------------|
 | position     | Rollershutter | Position of the virtual roller shutter. |
 
+See the section above for "window" / "rollershutter" Things for further information concerning the `position` Channel.
+
 ### Channels for "information" Thing
 
 The supported Channel and its associated channel type is shown below.
@@ -187,13 +202,13 @@ The bridge Thing provides the following properties.
 
 | Property          | Description                                                     |
 |-------------------|-----------------------------------------------------------------|
+| address           | IP address of the Bridge                                        |
 | check             | Result of the check of current item configuration               |
 | connectionAttempt | Date-Time of last connection attampt                            |
 | connectionSuccess | Date-Time of last successful connection attampt                 |
 | defaultGW         | IP address of the Default Gateway of the Bridge                 |
 | DHCP              | Flag whether automatic IP configuration is enabled              |
 | firmware          | Software version of the Bridge                                  |
-| ipAddress         | IP address of the Bridge                                        |
 | products          | List of all recognized products                                 |
 | scenes            | List of all defined scenes                                      |
 | subnetMask        | IP subnetmask of the Bridge                                     |
@@ -231,12 +246,14 @@ Frame label="Velux Windows" {
 
 [=> download sample sitemaps file for textual configuration](./doc/conf/sitemaps/velux.sitemap)
 
-### Rules
+### Rule for closing windows after a period of time
 
-**Rule for closing windows after a period of time**:
-Especially in the colder months, it is advisable to close the window after adequate ventilation. Therefore, automatic closing after one minute is good to save on heating costs.
+Especially in the colder months, it is advisable to close the window after adequate ventilation.
+Therefore, automatic closing after one minute is good to save on heating costs.
 However, to allow the case of intentional prolonged opening, an automatic closure is made only with the window fully open.
 
+Example:
+
 ```java
 rule "V_WINDOW_changed"
 when
@@ -245,14 +262,14 @@ then
        logInfo("rules.V_WINDOW",       "V_WINDOW_changes() called.")
        // Get the sensor value
        val Number windowState = V_WINDOW.state as DecimalType
-       logWarn("rules.V_WINDOW", "Window state is "+windowState+".")
+       logWarn("rules.V_WINDOW", "Window state is " + windowState + ".")
        if (windowState < 80) {
                if (windowState == 0) {
                        logWarn("rules.V_WINDOW", "V-WINDOW changed to fully open.")
                        var int interval = 1
-                               createTimer(now.plusMinutes(interval)) [ |
+                       createTimer(now.plusMinutes(interval)) [ |
                                        logWarn("rules.V_WINDOW:event", "event-V_WINDOW(): setting V-WINDOW to 100.")
-                                       sendCommand(V_WINDOW,100)
+                                       sendCommand(V_WINDOW, 100)
                                        V_WINDOW.postUpdate(100)
                                logWarn("rules.V_WINDOW:event", "event-V_WINDOW done.")
                        ]
@@ -267,6 +284,69 @@ end
 
 [=> download sample rules file for textual configuration](./doc/conf/rules/velux.rules)
 
+### Rule for rebooting the Bridge
+
+This binding includes a rule action to reboot the Velux Bridge by remote command:
+
+- `boolean isRebooting = rebootBridge()`
+
+_Warning: use this command carefully..._
+
+Example:
+
+```java
+rule "Reboot KLF 200"
+when
+       ...
+then
+       val veluxActions = getActions("velux", "velux:klf200:myhubname")
+       if (veluxActions !== null) {
+               val isRebooting = veluxActions.rebootBridge()
+               logWarn("Rules", "Velux KLF 200 rebooting: " + isRebooting)
+       } else {
+               logWarn("Rules", "Velux KLF 200 actions not found, check thing ID")
+       }
+end
+```
+
+### Rule for checking if a Window has been manually opened
+
+In the case that a window has been manually opened, and you then try to move it via the binding, its `position` will become `UNDEF`.
+You can exploit this behaviour in a rule to check regularly if a window has been manually opened.
+
+```java
+rule "Every 10 minutes, check if window is in manual mode"
+when
+       Time cron "0 0/10 * * * ?" // every 10 minutes
+then
+       if (Velux_Window.state != UNDEF) {
+               // command the window to its actual position; this will either
+               // - succeed: the actual position will not change, or
+               // - fail: the position becomes UNDEF (logged next time this rule executes)
+               Velux_Window.sendCommand(Velux_Window.state)
+       } else {
+               logWarn("Rules", "Velux in Manual mode, trying to close again")
+               // try to close it
+               Velux_Window.sendCommand(0)
+       }
+end
+```
+
+### Rule for Somfy actuators
+
+If a Somfy actuator is commanded to its 'favorite' position via a Somfy remote control, under some circumstances the display is `UNDEF`.
+You can resolve this behaviour in a rule that detects the `UNDEF` position and (re-)commands it to its favorite position.
+
+```java
+rule "Somfy Actuator: resolve undefined position"
+when
+    Item Somfy_Actuator changed to UNDEF
+then
+    val favoritePosition = 91
+    Somfy_Actuator.sendCommand(favoritePosition)
+end
+```
+
 ## Debugging
 
 For those who are interested in more detailed insight of the processing of this binding, a deeper look can be achieved by increased loglevel.
index 287234b6074e0c03affb1c05ea26e1b45e18c724..cee51d80a9351339b56247ae86ddd85d7d947b9c 100644 (file)
@@ -79,7 +79,7 @@ public class VeluxBinding extends VeluxBridgeConfiguration {
                 this.password = uncheckedConfiguration.password;
             }
             logger.trace("VeluxBinding(): checking {}.", VeluxBridgeConfiguration.BRIDGE_TIMEOUT_MSECS);
-            if ((uncheckedConfiguration.timeoutMsecs > 0) && (uncheckedConfiguration.timeoutMsecs <= 10000)) {
+            if ((uncheckedConfiguration.timeoutMsecs >= 500) && (uncheckedConfiguration.timeoutMsecs <= 5000)) {
                 this.timeoutMsecs = uncheckedConfiguration.timeoutMsecs;
             }
             logger.trace("VeluxBinding(): checking {}.", VeluxBridgeConfiguration.BRIDGE_RETRIES);
@@ -87,7 +87,7 @@ public class VeluxBinding extends VeluxBridgeConfiguration {
                 this.retries = uncheckedConfiguration.retries;
             }
             logger.trace("VeluxBinding(): checking {}.", VeluxBridgeConfiguration.BRIDGE_REFRESH_MSECS);
-            if ((uncheckedConfiguration.refreshMSecs > 0) && (uncheckedConfiguration.refreshMSecs <= 10000)) {
+            if ((uncheckedConfiguration.refreshMSecs >= 1000) && (uncheckedConfiguration.refreshMSecs <= 60000)) {
                 this.refreshMSecs = uncheckedConfiguration.refreshMSecs;
             }
             this.isBulkRetrievalEnabled = uncheckedConfiguration.isBulkRetrievalEnabled;
@@ -106,15 +106,20 @@ public class VeluxBinding extends VeluxBridgeConfiguration {
      */
     public VeluxBridgeConfiguration checked() {
         logger.trace("checked() called.");
+        // @formatter:off
         logger.debug("{}Config[{}={},{}={},{}={},{}={},{}={},{}={},{}={},{}={},{}={},{}={}]",
-                VeluxBindingConstants.BINDING_ID, VeluxBridgeConfiguration.BRIDGE_PROTOCOL, protocol,
-                VeluxBridgeConfiguration.BRIDGE_IPADDRESS, this.ipAddress, VeluxBridgeConfiguration.BRIDGE_TCPPORT,
-                tcpPort, VeluxBridgeConfiguration.BRIDGE_PASSWORD, password.replaceAll(".", "*"),
-                VeluxBridgeConfiguration.BRIDGE_TIMEOUT_MSECS, timeoutMsecs, VeluxBridgeConfiguration.BRIDGE_RETRIES,
-                retries, VeluxBridgeConfiguration.BRIDGE_REFRESH_MSECS, refreshMSecs,
+                VeluxBindingConstants.BINDING_ID,
+                VeluxBridgeConfiguration.BRIDGE_PROTOCOL, protocol,
+                VeluxBridgeConfiguration.BRIDGE_IPADDRESS, this.ipAddress,
+                VeluxBridgeConfiguration.BRIDGE_TCPPORT, tcpPort,
+                VeluxBridgeConfiguration.BRIDGE_PASSWORD, password.replaceAll(".", "*"),
+                VeluxBridgeConfiguration.BRIDGE_TIMEOUT_MSECS, timeoutMsecs,
+                VeluxBridgeConfiguration.BRIDGE_RETRIES, retries,
+                VeluxBridgeConfiguration.BRIDGE_REFRESH_MSECS, refreshMSecs,
                 VeluxBridgeConfiguration.BRIDGE_IS_BULK_RETRIEVAL_ENABLED, isBulkRetrievalEnabled,
                 VeluxBridgeConfiguration.BRIDGE_IS_SEQUENTIAL_ENFORCED, isSequentialEnforced,
                 VeluxBridgeConfiguration.BRIDGE_PROTOCOL_TRACE_ENABLED, isProtocolTraceEnabled);
+        // @formatter:off
         logger.trace("checked() done.");
         return this;
     }
index c647c8a8d1c77925be792a21598dc73cbeb6b9bb..739be2184638ee4371039e34a0366e832484710e 100644 (file)
@@ -91,10 +91,15 @@ public class VeluxBindingConstants {
     // Definitions of different set of Things
     public static final Set<ThingTypeUID> SUPPORTED_THINGS_BINDING = new HashSet<>(Arrays.asList(THING_TYPE_BINDING));
     public static final Set<ThingTypeUID> SUPPORTED_THINGS_BRIDGE = new HashSet<>(Arrays.asList(THING_TYPE_BRIDGE));
+
     public static final Set<ThingTypeUID> SUPPORTED_THINGS_ITEMS = new HashSet<>(
             Arrays.asList(THING_TYPE_VELUX_SCENE, THING_TYPE_VELUX_ACTUATOR, THING_TYPE_VELUX_ROLLERSHUTTER,
                     THING_TYPE_VELUX_WINDOW, THING_TYPE_VELUX_VSHUTTER));
 
+    public static final Set<ThingTypeUID> DISCOVERABLE_THINGS = Set.of(THING_TYPE_VELUX_SCENE,
+            THING_TYPE_VELUX_ACTUATOR, THING_TYPE_VELUX_ROLLERSHUTTER, THING_TYPE_VELUX_WINDOW,
+            THING_TYPE_VELUX_VSHUTTER, THING_TYPE_BINDING, THING_TYPE_BRIDGE);
+
     // *** List of all Channel ids ***
 
     // List of all binding channel ids
@@ -113,7 +118,7 @@ public class VeluxBindingConstants {
     public static final String PROPERTY_BRIDGE_TIMESTAMP_SUCCESS = "connectionSuccess";
     public static final String PROPERTY_BRIDGE_TIMESTAMP_ATTEMPT = "connectionAttempt";
     public static final String PROPERTY_BRIDGE_FIRMWARE = "firmware";
-    public static final String PROPERTY_BRIDGE_IPADDRESS = "ipAddress";
+    public static final String PROPERTY_BRIDGE_ADDRESS = "address";
     public static final String PROPERTY_BRIDGE_SUBNETMASK = "subnetMask";
     public static final String PROPERTY_BRIDGE_DEFAULTGW = "defaultGW";
     public static final String PROPERTY_BRIDGE_DHCP = "DHCP";
index 6745e9c867762a1feacb81abd9bbe705fa352a8d..bfb67867f2ce7ec6951ed0b4f184f149f37fcc84 100644 (file)
@@ -84,7 +84,7 @@ public enum VeluxItemType {
     BRIDGE_DO_DETECTION(VeluxBindingConstants.THING_TYPE_BRIDGE,                VeluxBindingConstants.CHANNEL_BRIDGE_DO_DETECTION,   TypeFlavor.INITIATOR),
 
     BRIDGE_FIRMWARE(VeluxBindingConstants.THING_TYPE_BRIDGE,                    VeluxBindingConstants.PROPERTY_BRIDGE_FIRMWARE,      TypeFlavor.PROPERTY),
-    BRIDGE_IPADDRESS(VeluxBindingConstants.THING_TYPE_BRIDGE,                   VeluxBindingConstants.PROPERTY_BRIDGE_IPADDRESS,     TypeFlavor.PROPERTY),
+    BRIDGE_ADDRESS(VeluxBindingConstants.THING_TYPE_BRIDGE,                     VeluxBindingConstants.PROPERTY_BRIDGE_ADDRESS,       TypeFlavor.PROPERTY),
     BRIDGE_SUBNETMASK(VeluxBindingConstants.THING_TYPE_BRIDGE,                  VeluxBindingConstants.PROPERTY_BRIDGE_SUBNETMASK,    TypeFlavor.PROPERTY),
     BRIDGE_DEFAULTGW(VeluxBindingConstants.THING_TYPE_BRIDGE,                   VeluxBindingConstants.PROPERTY_BRIDGE_DEFAULTGW,     TypeFlavor.PROPERTY),
     BRIDGE_DHCP(VeluxBindingConstants.THING_TYPE_BRIDGE,                        VeluxBindingConstants.PROPERTY_BRIDGE_DHCP,          TypeFlavor.PROPERTY),
diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/IVeluxActions.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/IVeluxActions.java
new file mode 100644 (file)
index 0000000..e21a8b6
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * 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.velux.internal.action;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link IVeluxActions} defines rule action interface for rebooting the bridge
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ */
+@NonNullByDefault
+public interface IVeluxActions {
+
+    /**
+     * Action to send a reboot command to a Velux Bridge
+     *
+     * @return true if the command was sent
+     * @throws IllegalStateException if something is wrong
+     */
+    Boolean rebootBridge() throws IllegalStateException;
+
+    /**
+     * Action to send a relative move command to a Velux actuator
+     *
+     * @param nodeId the node Id in the bridge
+     * @param relativePercent the target position relative to its current position (-100% <= relativePercent <= +100%)
+     * @return true if the command was sent
+     * @throws NumberFormatException if either of the arguments is not an integer, or out of range
+     * @throws IllegalStateException if anything else is wrong
+     */
+    Boolean moveRelative(String nodeId, String relativePercent) throws NumberFormatException, IllegalStateException;
+}
diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/VeluxActions.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/VeluxActions.java
new file mode 100644 (file)
index 0000000..ba0d0fc
--- /dev/null
@@ -0,0 +1,120 @@
+/**
+ * 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.velux.internal.action;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.ActionOutput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingActionsScope;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link VeluxActions} implementation of the rule action for rebooting the bridge
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ */
+@ThingActionsScope(name = "velux")
+@NonNullByDefault
+public class VeluxActions implements ThingActions, IVeluxActions {
+
+    private final Logger logger = LoggerFactory.getLogger(VeluxActions.class);
+
+    private @Nullable VeluxBridgeHandler bridgeHandler;
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        if (handler instanceof VeluxBridgeHandler) {
+            this.bridgeHandler = (VeluxBridgeHandler) handler;
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return this.bridgeHandler;
+    }
+
+    @Override
+    @RuleAction(label = "reboot Bridge", description = "issues a reboot command to the KLF200 bridge")
+    public @ActionOutput(name = "executing", type = "java.lang.Boolean", label = "executing", description = "indicates the command was issued") Boolean rebootBridge()
+            throws IllegalStateException {
+        logger.trace("rebootBridge(): action called");
+        VeluxBridgeHandler bridge = bridgeHandler;
+        if (bridge == null) {
+            throw new IllegalStateException("Bridge instance is null");
+        }
+        return bridge.runReboot();
+    }
+
+    @Override
+    @RuleAction(label = "move relative", description = "issues a relative move command to an actuator")
+    public @ActionOutput(name = "executing", type = "java.lang.Boolean", label = "executing", description = "indicates the command was issued") Boolean moveRelative(
+            @ActionInput(name = "nodeId", required = true, label = "nodeId", description = "actuator id in the bridge", type = "java.lang.String") String nodeId,
+            @ActionInput(name = "relativePercent", required = true, label = "relativePercent", description = "position delta from current", type = "java.lang.String") String relativePercent)
+            throws NumberFormatException, IllegalStateException {
+        logger.trace("moveRelative(): action called");
+        VeluxBridgeHandler bridge = bridgeHandler;
+        if (bridge == null) {
+            throw new IllegalStateException("Bridge instance is null");
+        }
+        int node = Integer.parseInt(nodeId);
+        if (node < 0 || node > 200) {
+            throw new NumberFormatException("Node Id out of range");
+        }
+        int relPct = Integer.parseInt(relativePercent);
+        if (Math.abs(relPct) > 100) {
+            throw new NumberFormatException("Relative Percent out of range");
+        }
+        return bridge.moveRelative(node, relPct);
+    }
+
+    /**
+     * Static method to send a reboot command to a Velux Bridge
+     *
+     * @param actions ThingActions from the caller
+     * @return true if the command was sent
+     * @throws IllegalArgumentException if actions is invalid
+     * @throws IllegalStateException if anything else is wrong
+     */
+    public static Boolean rebootBridge(@Nullable ThingActions actions)
+            throws IllegalArgumentException, IllegalStateException {
+        if (!(actions instanceof IVeluxActions)) {
+            throw new IllegalArgumentException("Unsupported action");
+        }
+        return ((IVeluxActions) actions).rebootBridge();
+    }
+
+    /**
+     * Static method to send a relative move command to a Velux actuator
+     *
+     * @param actions ThingActions from the caller
+     * @param nodeId the node Id in the bridge
+     * @param relativePercent the target position relative to its current position (-100% <= relativePercent <= +100%)
+     * @return true if the command was sent
+     * @throws IllegalArgumentException if actions is invalid
+     * @throws NumberFormatException if either of nodeId or relativePercent is not an integer, or out of range
+     * @throws IllegalStateException if anything else is wrong
+     */
+    public static Boolean moveRelative(@Nullable ThingActions actions, String nodeId, String relativePercent)
+            throws IllegalArgumentException, NumberFormatException, IllegalStateException {
+        if (!(actions instanceof IVeluxActions)) {
+            throw new IllegalArgumentException("Unsupported action");
+        }
+        return ((IVeluxActions) actions).moveRelative(nodeId, relativePercent);
+    }
+}
diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/package-info.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/package-info.java
new file mode 100644 (file)
index 0000000..1ac3ba1
--- /dev/null
@@ -0,0 +1,19 @@
+/**
+ * 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
+ */
+/**
+ *
+ * NOTE: All relevant classes of this binding are below the internal node.
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ */
+package org.openhab.binding.velux.internal.action;
index 4aef0887da169f0461c67ef13563de64bb4eb635..e9e900a9bb53d115e5ccea6253a5461cfa4fc8ee 100644 (file)
@@ -20,6 +20,7 @@ import org.openhab.binding.velux.internal.bridge.common.BridgeAPI;
 import org.openhab.binding.velux.internal.bridge.common.BridgeCommunicationProtocol;
 import org.openhab.binding.velux.internal.bridge.common.Login;
 import org.openhab.binding.velux.internal.bridge.common.Logout;
+import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -73,7 +74,7 @@ public abstract class VeluxBridge {
      * Handler to access global bridge instance methods
      *
      */
-    protected VeluxBridgeInstance bridgeInstance;
+    protected VeluxBridgeHandler bridgeInstance;
 
     /*
      * ************************
@@ -90,7 +91,7 @@ public abstract class VeluxBridge {
      * @param bridgeInstance refers to the binding-wide instance for dealing for common informations
      *            like existing actuators and predefined scenes.
      */
-    public VeluxBridge(VeluxBridgeInstance bridgeInstance) {
+    public VeluxBridge(VeluxBridgeHandler bridgeInstance) {
         logger.trace("VeluxBridge(constructor,bridgeInstance={}) called.", bridgeInstance);
         this.bridgeInstance = bridgeInstance;
         logger.trace("VeluxBridge(constructor) done.");
index d9c021c60299f00e61409f132636b0c65c2f5bc2..c93ed1a1c6c8ec3e42bfd02479ac07e75e542adf 100644 (file)
@@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory;
  *
  * @author Guenther Schreiner - Initial contribution
  */
+@Deprecated
 @NonNullByDefault
 public class VeluxBridgeSetSceneVelocity {
     private final Logger logger = LoggerFactory.getLogger(VeluxBridgeSetSceneVelocity.class);
index 2807983fbac4445667bcb189005760529b5d392a..7f5bcc940ce317361e7e697282337a2d506b59b2 100644 (file)
@@ -104,4 +104,7 @@ public interface BridgeAPI {
     SetSceneVelocity setSceneVelocity();
 
     RunScene runScene();
+
+    @Nullable
+    RunReboot runReboot();
 }
diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/RunReboot.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/RunReboot.java
new file mode 100644 (file)
index 0000000..8121b41
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * 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.velux.internal.bridge.common;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * <B>Common bridge communication message scheme supported by the </B><I>Velux</I><B> bridge.</B>
+ * <P>
+ * Message semantic will be defined by the implementations according to the different comm paths.
+ * <P>
+ * In addition to the common methods defined by {@link BridgeCommunicationProtocol}
+ * each protocol-specific implementation has to provide the following methods:
+ *
+ * @see BridgeCommunicationProtocol
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ */
+@NonNullByDefault
+public abstract class RunReboot implements BridgeCommunicationProtocol {
+}
index 40da6cec51f4e34c9c91321190dd7e6182adf0d8..3df42814b5ec721b36e19f5ee31dcaf3aa98e723 100644 (file)
@@ -29,6 +29,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
  *
  * @author Guenther Schreiner - Initial contribution.
  */
+@Deprecated
 @NonNullByDefault
 public abstract class SetSceneVelocity implements BridgeCommunicationProtocol {
 
index e96f30f2d561eaf4b30389050f07ccdb407dbfd7..d722e70d90cc22c194d2e543636d94ee27c3d171 100644 (file)
@@ -30,6 +30,7 @@ import org.openhab.binding.velux.internal.bridge.common.SetSceneVelocity;
  *
  * @author Guenther Schreiner - Initial contribution.
  */
+@Deprecated
 @NonNullByDefault
 class JCsetSceneVelocity extends SetSceneVelocity implements JsonBridgeCommunicationProtocol {
 
index 99ace70ce080a6b4432868b810a6977fb97072de..945c3a73bb08f3cf857adb3ecc8994a90956c9d4 100644 (file)
@@ -31,6 +31,7 @@ import org.openhab.binding.velux.internal.bridge.common.RunProductCommand;
 import org.openhab.binding.velux.internal.bridge.common.RunProductDiscovery;
 import org.openhab.binding.velux.internal.bridge.common.RunProductIdentification;
 import org.openhab.binding.velux.internal.bridge.common.RunProductSearch;
+import org.openhab.binding.velux.internal.bridge.common.RunReboot;
 import org.openhab.binding.velux.internal.bridge.common.RunScene;
 import org.openhab.binding.velux.internal.bridge.common.SetHouseStatusMonitor;
 import org.openhab.binding.velux.internal.bridge.common.SetProductLimitation;
@@ -205,4 +206,9 @@ class JsonBridgeAPI implements BridgeAPI {
     public SetSceneVelocity setSceneVelocity() {
         return jsonSetSceneVelocity;
     }
+
+    @Override
+    public @Nullable RunReboot runReboot() {
+        return null;
+    }
 }
index b5092e1a8e06aa0186e7bcfd778eca3fb057c55b..d9eb29bd761e3ea3e0ed9dbe7ccd1c201724aa3d 100644 (file)
@@ -21,9 +21,9 @@ import java.util.TreeSet;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.velux.internal.bridge.VeluxBridge;
-import org.openhab.binding.velux.internal.bridge.VeluxBridgeInstance;
 import org.openhab.binding.velux.internal.bridge.common.BridgeAPI;
 import org.openhab.binding.velux.internal.bridge.common.BridgeCommunicationProtocol;
+import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
 import org.openhab.core.io.net.http.HttpUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -84,7 +84,7 @@ public class JsonVeluxBridge extends VeluxBridge {
      *
      * @param bridgeInstance refers to the binding-wide instance for dealing for common informations.
      */
-    public JsonVeluxBridge(VeluxBridgeInstance bridgeInstance) {
+    public JsonVeluxBridge(VeluxBridgeHandler bridgeInstance) {
         super(bridgeInstance);
         logger.trace("JsonVeluxBridge(constructor) called.");
         bridgeAPI = new JsonBridgeAPI(bridgeInstance);
index d496cc12f902376a4f89ee07b1b9e592e641cc72..f3a3b705ca04e6d4cc5435b080882fc55fd9dd5a 100644 (file)
@@ -14,6 +14,7 @@ package org.openhab.binding.velux.internal.bridge.slip;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.velux.internal.VeluxBindingConstants;
 import org.openhab.binding.velux.internal.bridge.common.GetWLANConfig;
 import org.openhab.binding.velux.internal.bridge.slip.utils.Packet;
 import org.openhab.binding.velux.internal.things.VeluxGwWLAN;
@@ -51,8 +52,6 @@ class SCgetWLANConfig extends GetWLANConfig implements SlipBridgeCommunicationPr
     private static final String DESCRIPTION = "Retrieve WLAN configuration";
     private static final Command COMMAND = Command.GW_GET_NETWORK_SETUP_REQ;
 
-    private static final String UNSUPPORTED = "*** unsupported-by-current-gateway-firmware ***";
-
     /*
      * Message Objects
      */
@@ -118,6 +117,6 @@ class SCgetWLANConfig extends GetWLANConfig implements SlipBridgeCommunicationPr
     public VeluxGwWLAN getWLANConfig() {
         logger.trace("getWLANConfig() called.");
         // Enhancement idea: Velux should provide an enhanced API.
-        return new VeluxGwWLAN(UNSUPPORTED, UNSUPPORTED);
+        return new VeluxGwWLAN(VeluxBindingConstants.UNKNOWN, VeluxBindingConstants.UNKNOWN);
     }
 }
diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCrunReboot.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCrunReboot.java
new file mode 100644 (file)
index 0000000..c4a8c92
--- /dev/null
@@ -0,0 +1,121 @@
+/**
+ * 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.velux.internal.bridge.slip;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.velux.internal.bridge.common.RunReboot;
+import org.openhab.binding.velux.internal.bridge.slip.utils.KLF200Response;
+import org.openhab.binding.velux.internal.things.VeluxKLFAPI.Command;
+import org.openhab.binding.velux.internal.things.VeluxKLFAPI.CommandNumber;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Protocol specific bridge communication supported by the Velux bridge:
+ * <B>Reboot Bridge</B>
+ * <P>
+ * Common Message semantic: Communication with the bridge and (optionally) storing returned information within the class
+ * itself.
+ * <P>
+ * As 3rd level class it defines informations how to send query and receive answer through the
+ * {@link org.openhab.binding.velux.internal.bridge.VeluxBridgeProvider VeluxBridgeProvider}
+ * as described by the {@link org.openhab.binding.velux.internal.bridge.slip.SlipBridgeCommunicationProtocol
+ * SlipBridgeCommunicationProtocol}.
+ * <P>
+ * Methods in addition to the mentioned interface:
+ * <UL>
+ * <LI>{@link #runReboot} for rebooting the Velux hub.</LI>
+ * </UL>
+ *
+ * @see RunReboot
+ * @see SlipBridgeCommunicationProtocol
+ *
+ * @author Andrew Fiddian-Green - Initial contribution.
+ */
+@NonNullByDefault
+class SCrunReboot extends RunReboot implements SlipBridgeCommunicationProtocol {
+    private final Logger logger = LoggerFactory.getLogger(SCrunReboot.class);
+
+    private static final String DESCRIPTION = "Issue the reboot command";
+    private static final Command COMMAND = Command.GW_REBOOT_REQ;
+
+    /*
+     * ===========================================================
+     * Message Objects
+     */
+
+    private byte[] requestData = new byte[0];
+
+    /*
+     * ===========================================================
+     * Result Objects
+     */
+
+    private boolean success = false;
+    private boolean finished = false;
+
+    /*
+     * ===========================================================
+     * Methods required for interface {@link SlipBridgeCommunicationProtocol}.
+     */
+
+    @Override
+    public String name() {
+        return DESCRIPTION;
+    }
+
+    @Override
+    public CommandNumber getRequestCommand() {
+        success = false;
+        finished = false;
+        logger.debug("getRequestCommand() returns {} ({}).", COMMAND.name(), COMMAND.getCommand());
+        return COMMAND.getCommand();
+    }
+
+    @Override
+    public byte[] getRequestDataAsArrayOfBytes() {
+        return requestData;
+    }
+
+    @Override
+    public void setResponse(short responseCommand, byte[] thisResponseData, boolean isSequentialEnforced) {
+        KLF200Response.introLogging(logger, responseCommand, thisResponseData);
+        success = false;
+        finished = false;
+        switch (Command.get(responseCommand)) {
+            case GW_REBOOT_CFM:
+                if (!KLF200Response.isLengthValid(logger, responseCommand, thisResponseData, 0)) {
+                    finished = true;
+                    break;
+                }
+                success = true;
+                finished = true;
+                break;
+
+            default:
+                KLF200Response.errorLogging(logger, responseCommand);
+                finished = true;
+        }
+        KLF200Response.outroLogging(logger, success, finished);
+    }
+
+    @Override
+    public boolean isCommunicationFinished() {
+        return finished;
+    }
+
+    @Override
+    public boolean isCommunicationSuccessful() {
+        return success;
+    }
+}
index da21d829e9f8ab32bc52f2be21d1285b0c623486..68c14b2919e58a776e0ba51c87494707a70f50b1 100644 (file)
@@ -44,7 +44,8 @@ import org.slf4j.LoggerFactory;
  *
  * @author Guenther Schreiner - Initial contribution.
  */
-// ToDo: THIS MESSAGE EXCHANGE IS AN UNDOCUMENTED FEATURE. Check the updated Velux doc against this implementation.
+// TODO: THIS MESSAGE EXCHANGE IS AN UNDOCUMENTED FEATURE. Check the updated Velux doc against this implementation.
+@Deprecated
 @NonNullByDefault
 class SCsetSceneVelocity extends SetSceneVelocity implements SlipBridgeCommunicationProtocol {
     private final Logger logger = LoggerFactory.getLogger(SCsetSceneVelocity.class);
index 357ee8cc88cf35b3b314a03fc53d6d624e3a45a5..05b15ab50d7e071c7efa8c2b33f7e7dcce8c4f32 100644 (file)
@@ -31,6 +31,7 @@ import org.openhab.binding.velux.internal.bridge.common.RunProductCommand;
 import org.openhab.binding.velux.internal.bridge.common.RunProductDiscovery;
 import org.openhab.binding.velux.internal.bridge.common.RunProductIdentification;
 import org.openhab.binding.velux.internal.bridge.common.RunProductSearch;
+import org.openhab.binding.velux.internal.bridge.common.RunReboot;
 import org.openhab.binding.velux.internal.bridge.common.RunScene;
 import org.openhab.binding.velux.internal.bridge.common.SetHouseStatusMonitor;
 import org.openhab.binding.velux.internal.bridge.common.SetProductLimitation;
@@ -102,6 +103,7 @@ class SlipBridgeAPI implements BridgeAPI {
     private final SetHouseStatusMonitor slipSetHouseMonitor = new SCsetHouseStatusMonitor();
     private final SetProductLimitation slipSetProductLimitation = new SCsetLimitation();
     private final SetSceneVelocity slipSetSceneVelocity = new SCsetSceneVelocity();
+    private final RunReboot slipRunReboot = new SCrunReboot();
 
     /**
      * Constructor.
@@ -210,4 +212,9 @@ class SlipBridgeAPI implements BridgeAPI {
     public SetSceneVelocity setSceneVelocity() {
         return slipSetSceneVelocity;
     }
+
+    @Override
+    public @Nullable RunReboot runReboot() {
+        return slipRunReboot;
+    }
 }
index 16f4c1f35ec1a67bcfece3c9a167f835bc3351e6..6a5347b909c33ea75c986f9af717b1349c672cdd 100644 (file)
  */
 package org.openhab.binding.velux.internal.bridge.slip;
 
+import java.io.Closeable;
+import java.io.IOException;
 import java.text.ParseException;
 import java.util.TreeSet;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.velux.internal.VeluxBindingConstants;
 import org.openhab.binding.velux.internal.bridge.VeluxBridge;
-import org.openhab.binding.velux.internal.bridge.VeluxBridgeInstance;
 import org.openhab.binding.velux.internal.bridge.common.BridgeAPI;
 import org.openhab.binding.velux.internal.bridge.common.BridgeCommunicationProtocol;
 import org.openhab.binding.velux.internal.bridge.slip.io.Connection;
@@ -26,8 +26,8 @@ import org.openhab.binding.velux.internal.bridge.slip.utils.Packet;
 import org.openhab.binding.velux.internal.bridge.slip.utils.SlipEncoding;
 import org.openhab.binding.velux.internal.bridge.slip.utils.SlipRFC1055;
 import org.openhab.binding.velux.internal.development.Threads;
+import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
 import org.openhab.binding.velux.internal.things.VeluxKLFAPI.Command;
-import org.openhab.binding.velux.internal.things.VeluxKLFAPI.CommandNumber;
 import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -35,8 +35,7 @@ import org.slf4j.LoggerFactory;
 /**
  * SLIP-based 2nd Level I/O interface towards the <B>Velux</B> bridge.
  * <P>
- * It provides methods for pre- and postcommunication
- * as well as a common method for the real communication.
+ * It provides methods for pre- and post- communication as well as a common method for the real communication.
  * <P>
  * In addition to the generic {@link VeluxBridge} methods, i.e.
  * <UL>
@@ -53,9 +52,11 @@ import org.slf4j.LoggerFactory;
  * </UL>
  *
  * @author Guenther Schreiner - Initial contribution.
+ * @author Andrew Fiddian-Green - Refactored (simplified) the message processing loop
  */
 @NonNullByDefault
-public class SlipVeluxBridge extends VeluxBridge {
+public class SlipVeluxBridge extends VeluxBridge implements Closeable {
+
     private final Logger logger = LoggerFactory.getLogger(SlipVeluxBridge.class);
 
     /*
@@ -100,7 +101,7 @@ public class SlipVeluxBridge extends VeluxBridge {
      *
      * @param bridgeInstance refers to the binding-wide instance for dealing for common informations.
      */
-    public SlipVeluxBridge(VeluxBridgeInstance bridgeInstance) {
+    public SlipVeluxBridge(VeluxBridgeHandler bridgeInstance) {
         super(bridgeInstance);
         logger.trace("SlipVeluxBridge(constructor) called.");
         bridgeAPI = new SlipBridgeAPI(bridgeInstance);
@@ -153,7 +154,7 @@ public class SlipVeluxBridge extends VeluxBridge {
      */
     @Override
     protected boolean bridgeDirectCommunicate(BridgeCommunicationProtocol communication, boolean useAuthentication) {
-        logger.trace("bridgeDirectCommunicate(BCP: {},{}authenticated) called.", communication.name(),
+        logger.trace("bridgeDirectCommunicate(BCP: {}, {}authenticated) called.", communication.name(),
                 useAuthentication ? "" : "un");
         return bridgeDirectCommunicate((SlipBridgeCommunicationProtocol) communication, useAuthentication);
     }
@@ -181,214 +182,242 @@ public class SlipVeluxBridge extends VeluxBridge {
     }
 
     /**
-     * Initializes a client/server communication towards <b>Velux</b> veluxBridge
-     * based on the Basic I/O interface {@link Connection#io} and parameters
-     * passed as arguments (see below).
+     * Initializes a client/server communication towards the Velux Bridge based on the Basic I/O interface
+     * {@link Connection#io} and parameters passed as arguments (see below).
      *
-     * @param communication Structure of interface type {@link SlipBridgeCommunicationProtocol} describing the
+     * @param communication a structure of interface type {@link SlipBridgeCommunicationProtocol} describing the
      *            intended communication, that is request and response interactions as well as appropriate URL
      *            definition.
-     * @param useAuthentication boolean flag to decide whether to use authenticated communication.
-     * @return <b>success</b> of type boolean which signals the success of the communication.
+     * @param useAuthentication a boolean flag to select whether to use authenticated communication.
+     * @return a boolean which in general signals the success of the communication, but in the
+     *         special case of receive-only calls, signals if any products were updated during the call
      */
     private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProtocol communication,
             boolean useAuthentication) {
-        String host = this.bridgeInstance.veluxBridgeConfiguration().ipAddress;
-        logger.trace("bridgeDirectCommunicate({},{}authenticated) on {} called.", host, communication.name(),
+        logger.trace("bridgeDirectCommunicate() '{}', {}authenticated", communication.name(),
                 useAuthentication ? "" : "un");
 
-        assert this.bridgeInstance.veluxBridgeConfiguration().protocol.contentEquals("slip");
-
-        long communicationStartInMSecs = System.currentTimeMillis();
+        // store common parameters as constants for frequent use
+        final short txCmd = communication.getRequestCommand().toShort();
+        final byte[] txData = communication.getRequestDataAsArrayOfBytes();
+        final Command txEnum = Command.get(txCmd);
+        final String txName = txEnum.toString();
+        final boolean isSequentialEnforced = this.bridgeInstance.veluxBridgeConfiguration().isSequentialEnforced;
+        final boolean isProtocolTraceEnabled = this.bridgeInstance.veluxBridgeConfiguration().isProtocolTraceEnabled;
+        final long expiryTime = System.currentTimeMillis() + COMMUNICATION_TIMEOUT_MSECS;
 
-        boolean isSequentialEnforced = this.bridgeInstance.veluxBridgeConfiguration().isSequentialEnforced;
-        boolean isProtocolTraceEnabled = this.bridgeInstance.veluxBridgeConfiguration().isProtocolTraceEnabled;
-
-        // From parameters
-        short command = communication.getRequestCommand().toShort();
-        byte[] data = communication.getRequestDataAsArrayOfBytes();
-        // For further use at different logging statements
-        String commandString = Command.get(command).toString();
+        // logger format string
+        final String loggerFmt = String.format("bridgeDirectCommunicate() [%s] %s => {} {} {}",
+                this.bridgeInstance.veluxBridgeConfiguration().ipAddress, txName);
 
         if (isProtocolTraceEnabled) {
             Threads.findDeadlocked();
         }
 
-        logger.debug("bridgeDirectCommunicate({},{}authenticated) on {} initiated by {}.", host, commandString,
-                useAuthentication ? "" : "un", Thread.currentThread());
-        boolean success = false;
+        logger.debug(loggerFmt, "started =>", Thread.currentThread(), "");
 
-        communication: do {
-            if (communicationStartInMSecs + COMMUNICATION_TIMEOUT_MSECS < System.currentTimeMillis()) {
-                logger.warn(
-                        "{} bridgeDirectCommunicate({}) on {}: communication handshake failed (unexpected sequence of requests/responses).",
-                        VeluxBindingConstants.BINDING_VALUES_SEPARATOR, communication.name(), host);
-                break;
-            }
+        boolean looping = false;
+        boolean success = false;
+        boolean sending = false;
+        boolean rcvonly = false;
+        byte[] txPacket = emptyPacket;
 
-            // Special handling
-            if (Command.get(command) == Command.GW_OPENHAB_CLOSE) {
-                logger.trace("bridgeDirectCommunicate(): special command: shutting down connection.");
+        // handling of the requests
+        switch (txEnum) {
+            case GW_OPENHAB_CLOSE:
+                logger.trace(loggerFmt, "shut down command", "=> executing", "");
                 connection.resetConnection();
                 success = true;
-                continue;
-            }
+                break;
 
-            // Normal processing
-            logger.trace("bridgeDirectCommunicate() on {}: working on request {} with {} bytes of data.", host,
-                    commandString, data.length);
-            byte[] sendBytes = emptyPacket;
-            if (Command.get(command) == Command.GW_OPENHAB_RECEIVEONLY) {
-                logger.trace(
-                        "bridgeDirectCommunicate() on {}: special command: determine whether there is any message waiting.",
-                        host);
-                logger.trace("bridgeDirectCommunicate(): check for a waiting message.");
-                if (!connection.isMessageAvailable()) {
-                    logger.trace("bridgeDirectCommunicate() on {}: no message waiting, aborting.", host);
-                    break communication;
+            case GW_OPENHAB_RECEIVEONLY:
+                logger.trace(loggerFmt, "receive-only mode", "=> checking messages", "");
+                if (!connection.isAlive()) {
+                    logger.trace(loggerFmt, "no connection", "=> opening", "");
+                    looping = true;
+                } else if (connection.isMessageAvailable()) {
+                    logger.trace(loggerFmt, "message(s) waiting", "=> start reading", "");
+                    looping = true;
+                } else {
+                    logger.trace(loggerFmt, "no waiting messages", "=> done", "");
                 }
-                logger.trace("bridgeDirectCommunicate() on {}: there is a message waiting.", host);
-            } else {
-                SlipEncoding t = new SlipEncoding(command, data);
-                if (!t.isValid()) {
-                    logger.warn("bridgeDirectCommunicate() on {}: SlipEncoding() failed, aborting.", host);
+                rcvonly = true;
+                break;
+
+            default:
+                logger.trace(loggerFmt, "send mode", "=> preparing command", "");
+                SlipEncoding slipEnc = new SlipEncoding(txCmd, txData);
+                if (!slipEnc.isValid()) {
+                    logger.debug(loggerFmt, "slip encoding error", "=> aborting", "");
                     break;
                 }
-                logger.trace("bridgeDirectCommunicate() on {}: transportEncoding={}.", host, t.toString());
-                sendBytes = new SlipRFC1055().encode(t.toMessage());
+                txPacket = new SlipRFC1055().encode(slipEnc.toMessage());
+                logger.trace(loggerFmt, "command ready", "=> start sending", "");
+                looping = sending = true;
+        }
+
+        while (looping) {
+            // timeout
+            if (System.currentTimeMillis() > expiryTime) {
+                logger.warn(loggerFmt, "process loop time out", "=> aborting", "=> PLEASE REPORT !!");
+                // abort the processing loop
+                break;
             }
-            do {
-                if (communicationStartInMSecs + COMMUNICATION_TIMEOUT_MSECS < System.currentTimeMillis()) {
-                    logger.warn("bridgeDirectCommunicate() on {}: receive takes too long. Please report to maintainer.",
-                            host);
-                    break communication;
-                }
-                byte[] receivedPacket;
-                try {
-                    if (sendBytes.length > 0) {
-                        logger.trace("bridgeDirectCommunicate() on {}: sending {} bytes.", host, sendBytes.length);
-                        if (isProtocolTraceEnabled) {
-                            logger.info("Sending command {}.", commandString);
-                        }
+
+            // send command (optionally), and receive response
+            byte[] rxPacket;
+            try {
+                if (sending) {
+                    if (isProtocolTraceEnabled) {
+                        logger.info("sending command {}", txName);
+                    }
+                    if (logger.isTraceEnabled()) {
+                        logger.trace(loggerFmt, txName, "=> sending data =>", new Packet(txData));
                     } else {
-                        logger.trace("bridgeDirectCommunicate() on {}: initiating receive-only.", host);
+                        logger.debug(loggerFmt, txName, "=> sending data length =>", txData.length);
                     }
-                    // (Optionally) Send and receive packet.
-                    receivedPacket = connection.io(this.bridgeInstance, sendBytes);
-                    // Once being sent, it should never be sent again
-                    sendBytes = emptyPacket;
-                } catch (Exception e) {
-                    logger.warn("bridgeDirectCommunicate() on {}: connection.io returns {}", host, e.getMessage());
-                    break communication;
-                }
-                logger.trace("bridgeDirectCommunicate() on {}: received packet {}.", host,
-                        new Packet(receivedPacket).toString());
-                byte[] response;
-                try {
-                    response = new SlipRFC1055().decode(receivedPacket);
-                } catch (ParseException e) {
-                    logger.warn("bridgeDirectCommunicate() on {}: method SlipRFC1055() raised a decoding error: {}.",
-                            host, e.getMessage());
-                    break communication;
                 }
-                SlipEncoding tr = new SlipEncoding(response);
-                if (!tr.isValid()) {
-                    logger.warn("bridgeDirectCommunicate() on {}: method SlipEncoding() raised a decoding error.",
-                            host);
-                    break communication;
-                }
-                short responseCommand = tr.getCommand();
-                byte[] responseData = tr.getData();
-                logger.debug("bridgeDirectCommunicate() on {}: working on response {} with {} bytes of data.", host,
-                        Command.get(responseCommand).toString(), responseData.length);
-                if (isProtocolTraceEnabled) {
-                    logger.info("Received answer {}.", Command.get(responseCommand).toString());
+                rxPacket = connection.io(this.bridgeInstance, sending ? txPacket : emptyPacket);
+                // message sent, don't send it again
+                sending = false;
+                if (rxPacket.length == 0) {
+                    // only log in send mode (in receive-only mode, no response is ok)
+                    if (!rcvonly) {
+                        logger.debug(loggerFmt, "no response", "=> aborting", "");
+                    }
+                    // abort the processing loop
+                    break;
                 }
-                // Handle some common (unexpected) answers
-                switch (Command.get(responseCommand)) {
-                    case GW_NODE_INFORMATION_CHANGED_NTF:
-                        logger.trace("bridgeDirectCommunicate() on {}: received GW_NODE_INFORMATION_CHANGED_NTF.",
-                                host);
-                        logger.trace("bridgeDirectCommunicate() on {}: continue with receiving.", host);
-                        continue;
-                    case GW_NODE_STATE_POSITION_CHANGED_NTF:
-                        logger.trace(
-                                "bridgeDirectCommunicate() on {}: received GW_NODE_STATE_POSITION_CHANGED_NTF, special processing of this packet.",
-                                host);
-                        SCgetHouseStatus receiver = new SCgetHouseStatus();
-                        receiver.setResponse(responseCommand, responseData, isSequentialEnforced);
-                        if (receiver.isCommunicationSuccessful()) {
-                            logger.trace("bridgeDirectCommunicate() on {}: existingProducts().update() called.", host);
-                            bridgeInstance.existingProducts().update(new ProductBridgeIndex(receiver.getNtfNodeID()),
-                                    receiver.getNtfState(), receiver.getNtfCurrentPosition(), receiver.getNtfTarget());
-                        }
-                        logger.trace("bridgeDirectCommunicate() on {}: continue with receiving.", host);
-                        continue;
-                    case GW_ERROR_NTF:
-                        switch (responseData[0]) {
-                            case 0:
-                                logger.warn(
-                                        "bridgeDirectCommunicate() on {}: received GW_ERROR_NTF on {} (Not further defined error), aborting.",
-                                        host, commandString);
-                                break communication;
-                            case 1:
-                                logger.warn(
-                                        "bridgeDirectCommunicate() on {}: received GW_ERROR_NTF (Unknown Command or command is not accepted at this state) on {}, aborting.",
-                                        host, commandString);
-                                break communication;
-                            case 2:
-                                logger.warn(
-                                        "bridgeDirectCommunicate() on {}: received GW_ERROR_NTF (ERROR on Frame Structure) on {}, aborting.",
-                                        host, commandString);
-                                break communication;
-                            case 7:
-                                logger.trace(
-                                        "bridgeDirectCommunicate() on {}: received GW_ERROR_NTF (Busy. Try again later) on {}, retrying.",
-                                        host, commandString);
-                                sendBytes = emptyPacket;
-                                continue;
-                            case 8:
-                                logger.warn(
-                                        "bridgeDirectCommunicate() on {}: received GW_ERROR_NTF (Bad system table index) on {}, aborting.",
-                                        host, commandString);
-                                break communication;
-                            case 12:
-                                logger.warn(
-                                        "bridgeDirectCommunicate() on {}: received GW_ERROR_NTF (Not authenticated) on {}, aborting.",
-                                        host, commandString);
-                                resetAuthentication();
-                                break communication;
-                            default:
-                                logger.warn(
-                                        "bridgeDirectCommunicate() on {}: received GW_ERROR_NTF ({}) on {}, aborting.",
-                                        host, responseData[0], commandString);
-                                break communication;
-                        }
-                    case GW_ACTIVATION_LOG_UPDATED_NTF:
-                        logger.info("bridgeDirectCommunicate() on {}: received GW_ACTIVATION_LOG_UPDATED_NTF.", host);
-                        logger.trace("bridgeDirectCommunicate() on {}: continue with receiving.", host);
-                        continue;
-
-                    case GW_COMMAND_RUN_STATUS_NTF:
-                    case GW_COMMAND_REMAINING_TIME_NTF:
-                    case GW_SESSION_FINISHED_NTF:
-                        if (!isSequentialEnforced) {
-                            logger.trace(
-                                    "bridgeDirectCommunicate() on {}: response ignored due to activated parallelism, continue with receiving.",
-                                    host);
-                            continue;
+            } catch (IOException e) {
+                logger.debug(loggerFmt, "i/o error =>", e.getMessage(), "=> aborting");
+                // abort the processing loop
+                break;
+            }
+
+            // RFC1055 decode response
+            byte[] rfc1055;
+            try {
+                rfc1055 = new SlipRFC1055().decode(rxPacket);
+            } catch (ParseException e) {
+                logger.debug(loggerFmt, "parsing error =>", e.getMessage(), "=> aborting");
+                // abort the processing loop
+                break;
+            }
+
+            // SLIP decode response
+            SlipEncoding slipEnc = new SlipEncoding(rfc1055);
+            if (!slipEnc.isValid()) {
+                logger.debug(loggerFmt, "slip decode error", "=> aborting", "");
+                // abort the processing loop
+                break;
+            }
+
+            // attributes of the received (rx) response
+            final short rxCmd = slipEnc.getCommand();
+            final byte[] rxData = slipEnc.getData();
+            final Command rxEnum = Command.get(rxCmd);
+            final String rxName = rxEnum.toString();
+
+            // logging
+            if (logger.isTraceEnabled()) {
+                logger.trace(loggerFmt, rxName, "=> received data =>", new Packet(rxData));
+            } else {
+                logger.debug(loggerFmt, rxName, "=> received data length =>", rxData.length);
+            }
+            if (isProtocolTraceEnabled) {
+                logger.info("received message {} => {}", rxName, new Packet(rxData));
+            }
+
+            // handling of the responses
+            switch (rxEnum) {
+                case GW_ERROR_NTF:
+                    byte code = rxData[0];
+                    switch (code) {
+                        case 7: // busy
+                            logger.trace(loggerFmt, rxName, getErrorText(code), "=> retrying");
+                            sending = true;
+                            break;
+                        case 12: // authentication failed
+                            logger.debug(loggerFmt, rxName, getErrorText(code), "=> aborting");
+                            resetAuthentication();
+                            looping = false;
+                            break;
+                        default:
+                            logger.warn(loggerFmt, rxName, getErrorText(code), "=> aborting");
+                            looping = false;
+                    }
+                    break;
+
+                case GW_NODE_INFORMATION_CHANGED_NTF:
+                case GW_ACTIVATION_LOG_UPDATED_NTF:
+                    logger.trace(loggerFmt, rxName, "=> ignorable command", "=> continuing");
+                    break;
+
+                case GW_NODE_STATE_POSITION_CHANGED_NTF:
+                    logger.trace(loggerFmt, rxName, "=> special command", "=> starting");
+                    SCgetHouseStatus receiver = new SCgetHouseStatus();
+                    receiver.setResponse(rxCmd, rxData, isSequentialEnforced);
+                    if (receiver.isCommunicationSuccessful()) {
+                        bridgeInstance.existingProducts().update(new ProductBridgeIndex(receiver.getNtfNodeID()),
+                                receiver.getNtfState(), receiver.getNtfCurrentPosition(), receiver.getNtfTarget());
+                        logger.trace(loggerFmt, rxName, "=> special command", "=> product updated");
+                        if (rcvonly) {
+                            // receive-only: return success to confirm that product(s) were updated
+                            success = true;
                         }
+                    }
+                    logger.trace(loggerFmt, rxName, "=> special command", "=> continuing");
+                    break;
 
-                    default:
-                }
-                logger.trace("bridgeDirectCommunicate() on {}: passes back command {} and data {}.", host,
-                        new CommandNumber(responseCommand).toString(), new Packet(responseData).toString());
-                communication.setResponse(responseCommand, responseData, isSequentialEnforced);
-            } while (!communication.isCommunicationFinished());
-            success = communication.isCommunicationSuccessful();
-        } while (false); // communication
-        logger.debug("bridgeDirectCommunicate({}) on {}: returns {}.", commandString, host,
-                success ? "success" : "failure");
+                case GW_COMMAND_RUN_STATUS_NTF:
+                case GW_COMMAND_REMAINING_TIME_NTF:
+                case GW_SESSION_FINISHED_NTF:
+                    if (!isSequentialEnforced) {
+                        logger.trace(loggerFmt, rxName, "=> parallelism allowed", "=> continuing");
+                        break;
+                    }
+                    logger.trace(loggerFmt, rxName, "=> serialism enforced", "=> default processing");
+                    // fall through => execute default processing
+
+                default:
+                    logger.trace(loggerFmt, rxName, "=> applying data length =>", rxData.length);
+                    communication.setResponse(rxCmd, rxData, isSequentialEnforced);
+                    looping = !communication.isCommunicationFinished();
+                    success = communication.isCommunicationSuccessful();
+            }
+
+        }
+        // in receive-only mode 'failure` just means that no products were updated, so don't log it as a failure..
+        logger.debug(loggerFmt, "finished", "=>", ((success || rcvonly) ? "success" : "failure"));
         return success;
     }
+
+    /**
+     * Return text description of potential GW_ERROR_NTF error codes, for logging purposes
+     *
+     * @param errCode is the GW_ERROR_NTF error code
+     * @return the description message
+     */
+    private static String getErrorText(byte errCode) {
+        switch (errCode) {
+            case 0:
+                return "=> (0) not further defined error";
+            case 1:
+                return "=> (1) unknown command or command is not accepted at this state";
+            case 2:
+                return "=> (2) error on frame structure";
+            case 7:
+                return "=> (7) busy, try again later";
+            case 8:
+                return "=> (8) bad system table index";
+            case 12:
+                return "=> (12) not authenticated";
+        }
+        return String.format("=> (%d) unknown error", errCode);
+    }
+
+    @Override
+    public void close() throws IOException {
+        shutdown();
+    }
 }
index d6e32c297cb415a3e165f35040c6f8a0f12ea624..550c678583bdb01998b39094203a8e5d3ec7e6cd 100644 (file)
  */
 package org.openhab.binding.velux.internal.bridge.slip.io;
 
+import java.io.Closeable;
 import java.io.IOException;
 import java.net.ConnectException;
+import java.net.SocketTimeoutException;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.velux.internal.VeluxBindingConstants;
-import org.openhab.binding.velux.internal.bridge.VeluxBridgeInstance;
 import org.openhab.binding.velux.internal.bridge.slip.utils.Packet;
+import org.openhab.binding.velux.internal.config.VeluxBridgeConfiguration;
+import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -39,7 +42,7 @@ import org.slf4j.LoggerFactory;
  * @author Guenther Schreiner - Initial contribution.
  */
 @NonNullByDefault
-public class Connection {
+public class Connection implements Closeable {
     private final Logger logger = LoggerFactory.getLogger(Connection.class);
 
     /*
@@ -76,8 +79,10 @@ public class Connection {
      * @throws java.net.ConnectException in case of unrecoverable communication failures.
      * @throws java.io.IOException in case of continuous communication I/O failures.
      */
-    public synchronized byte[] io(VeluxBridgeInstance bridgeInstance, byte[] request)
+    public synchronized byte[] io(VeluxBridgeHandler bridgeInstance, byte[] request)
             throws ConnectException, IOException {
+        VeluxBridgeConfiguration cfg = bridgeInstance.veluxBridgeConfiguration();
+        host = cfg.ipAddress;
         logger.trace("io() on {}: called.", host);
 
         lastCommunicationInMSecs = System.currentTimeMillis();
@@ -89,15 +94,11 @@ public class Connection {
         do {
             try {
                 if (!connectivity.isReady()) {
+                    // dispose old connectivity class instances (if any)
+                    resetConnection();
                     try {
-                        // From configuration
-                        host = bridgeInstance.veluxBridgeConfiguration().ipAddress;
-                        int port = bridgeInstance.veluxBridgeConfiguration().tcpPort;
-                        int timeoutMsecs = bridgeInstance.veluxBridgeConfiguration().timeoutMsecs;
-
-                        logger.trace("io() on {}: connecting to port {}", host, port);
-                        connectivity = new SSLconnection(host, port);
-                        connectivity.setTimeout(timeoutMsecs);
+                        logger.trace("io() on {}: connecting to port {}", cfg.ipAddress, cfg.tcpPort);
+                        connectivity = new SSLconnection(bridgeInstance);
                     } catch (ConnectException ce) {
                         throw new ConnectException(String
                                 .format("raised a non-recoverable error during connection setup: %s", ce.getMessage()));
@@ -107,7 +108,8 @@ public class Connection {
                         continue;
                     }
                 }
-                if (request.length > 0) {
+                boolean sending = request.length > 0;
+                if (sending) {
                     try {
                         if (logger.isTraceEnabled()) {
                             logger.trace("io() on {}: sending packet with {} bytes: {}", host, request.length,
@@ -122,22 +124,15 @@ public class Connection {
                         logger.info("io() on {}: raised an error during sending: {}.", host, e.getMessage());
                         break;
                     }
-
-                    // Give the bridge some time to breathe
-                    if (bridgeInstance.veluxBridgeConfiguration().timeoutMsecs > 0) {
-                        logger.trace("io() on {}: wait time {} msecs.", host,
-                                bridgeInstance.veluxBridgeConfiguration().timeoutMsecs);
-                        try {
-                            Thread.sleep(bridgeInstance.veluxBridgeConfiguration().timeoutMsecs);
-                        } catch (InterruptedException ie) {
-                            logger.trace("io() on {}: wait interrupted.", host);
-                        }
-                    }
                 }
                 byte[] packet = new byte[0];
                 logger.trace("io() on {}: receiving bytes.", host);
                 if (connectivity.isReady()) {
                     packet = connectivity.receive();
+                    // in receive-only mode, a zero length response packet is NOT a timeout
+                    if (sending && (packet.length == 0)) {
+                        throw new SocketTimeoutException("read time out after send");
+                    }
                 }
                 if (logger.isTraceEnabled()) {
                     logger.trace("io() on {}: received packet with {} bytes: {}", host, packet.length,
@@ -168,9 +163,7 @@ public class Connection {
                     bridgeInstance.veluxBridgeConfiguration().retries);
         }
         logger.trace("io() on {}: shutting down connection.", host);
-        if (connectivity.isReady()) {
-            connectivity.close();
-        }
+        resetConnection();
         logger.trace("io() on {}: finishes with failure by throwing exception.", host);
         throw lastIOE;
     }
@@ -192,17 +185,13 @@ public class Connection {
      */
     public synchronized boolean isMessageAvailable() {
         logger.trace("isMessageAvailable() on {}: called.", host);
-        try {
-            if ((connectivity.isReady()) && (connectivity.available())) {
-                logger.trace("isMessageAvailable() on {}: there is a message waiting.", host);
-                return true;
-            }
-        } catch (IOException e) {
-            logger.trace("isMessageAvailable() on {}: lost connection due to {}.", host, e.getMessage());
-            resetConnection();
+        if (!connectivity.isReady()) {
+            logger.trace("isMessageAvailable() on {}: lost connection, there may be messages", host);
+            return false;
         }
-        logger.trace("isMessageAvailable() on {}: no message waiting.", host);
-        return false;
+        boolean result = connectivity.available();
+        logger.trace("isMessageAvailable() on {}: there are {}messages waiting.", host, result ? "" : "no ");
+        return result;
     }
 
     /**
@@ -237,4 +226,9 @@ public class Connection {
         }
         logger.trace("resetConnection() on {}: done.", host);
     }
+
+    @Override
+    public void close() throws IOException {
+        resetConnection();
+    }
 }
index 967277e0f21fd90f2d567a5ae03709549f3b73cd..4116cfd6bb6e8a4ebf2bc0305108a13c550afd3c 100644 (file)
  */
 package org.openhab.binding.velux.internal.bridge.slip.io;
 
-import java.io.DataInputStream;
+import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.SocketTimeoutException;
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+import java.util.Queue;
 import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
- * This is an extension of {@link java.io.DataInputStream}, which adds timeouts to receive operation.
- * <P>
- * A data input stream lets an application read primitive Java data
- * types from an underlying input stream in a machine-independent
- * way. An application uses a data output stream to write data that
- * can later be read by a data input stream.
- * <p>
- * For an in-depth discussion, see:
- * https://stackoverflow.com/questions/804951/is-it-possible-to-read-from-a-inputstream-with-a-timeout
+ * This is an wrapper around {@link java.io.InputStream} to support socket receive operations.
+ *
+ * It implements a secondary polling thread to asynchronously read bytes from the socket input stream into a buffer. And
+ * it parses the bytes into SLIP messages, which are placed on a message queue. Callers can access the SLIP messages in
+ * this queue independently from the polling thread.
  *
  * @author Guenther Schreiner - Initial contribution.
+ * @author Andrew Fiddian-Green - Complete rewrite using asynchronous polling thread.
  */
 @NonNullByDefault
-class DataInputStreamWithTimeout extends DataInputStream {
+class DataInputStreamWithTimeout implements Closeable {
 
-    /*
-     * ***************************
-     * ***** Private Objects *****
-     */
+    private static final int QUEUE_SIZE = 512;
+    private static final int BUFFER_SIZE = 512;
+    private static final int SLEEP_INTERVAL_MSECS = 50;
+
+    // special character that marks the first and last byte of a slip message
+    private static final byte SLIP_MARK = (byte) 0xc0;
+
+    private final Logger logger = LoggerFactory.getLogger(DataInputStreamWithTimeout.class);
+
+    private final Queue<byte[]> slipMessageQueue = new ConcurrentLinkedQueue<>();
+
+    private InputStream inputStream;
+
+    private @Nullable String pollException = null;
+    private @Nullable Poller pollRunner = null;
+    private ExecutorService executor;
+
+    private class Poller implements Callable<Boolean> {
+
+        private boolean interrupted = false;
+
+        public void interrupt() {
+            interrupted = true;
+        }
+
+        /**
+         * Task that loops to read bytes from {@link InputStream} and build SLIP packets from them. The SLIP packets are
+         * placed in a {@link ConcurrentLinkedQueue}. It loops continuously until 'interrupt()' or 'Thread.interrupt()'
+         * are called when terminates early after the next socket read timeout.
+         */
+        @Override
+        public Boolean call() throws Exception {
+            byte[] buf = new byte[BUFFER_SIZE];
+            byte byt;
+            int i = 0;
+
+            // clean start, no exception, empty queue
+            pollException = null;
+            slipMessageQueue.clear();
+
+            // loop forever or until internally or externally interrupted
+            while ((!interrupted) && (!Thread.interrupted())) {
+                try {
+                    buf[i] = byt = (byte) inputStream.read();
+                    if (byt == SLIP_MARK) {
+                        if (i > 0) {
+                            // the minimal slip message is 7 bytes [MM PP LL CC CC KK MM]
+                            if ((i > 5) && (buf[0] == SLIP_MARK)) {
+                                slipMessageQueue.offer(Arrays.copyOfRange(buf, 0, i + 1));
+                                if (slipMessageQueue.size() > QUEUE_SIZE) {
+                                    logger.warn("pollRunner() => slip message queue overflow => PLEASE REPORT !!");
+                                    slipMessageQueue.poll();
+                                }
+                            }
+                            i = 0;
+                            buf[0] = SLIP_MARK;
+                            continue;
+                        }
+                    }
+                    if (++i >= BUFFER_SIZE) {
+                        i = 0;
+                    }
+                } catch (SocketTimeoutException e) {
+                    // socket read time outs are OK => keep on polling
+                    continue;
+                } catch (IOException e) {
+                    // any other exception => stop polling
+                    String msg = e.getMessage();
+                    pollException = msg != null ? msg : "Generic IOException";
+                    logger.debug("pollRunner() stopping '{}'", pollException);
+                    break;
+                }
+            }
+
+            // we only get here if shutdown or an error occurs so free ourself so we can be recreated again
+            pollRunner = null;
+            return true;
+        }
+    }
 
     /**
-     * Executor for asynchronous read command
+     * Check if there was an exception on the polling loop task and if so, throw it back on the caller thread.
+     *
+     * @throws IOException
      */
-    ExecutorService executor = Executors.newFixedThreadPool(2);
+    private void throwIfPollException() throws IOException {
+        if (pollException != null) {
+            logger.debug("passPollException() polling loop exception {}", pollException);
+            throw new IOException(pollException);
+        }
+    }
 
     /**
-     * Creates a DataInputStreamWithTimeout that uses the specified
-     * underlying DataInputStream.
+     * Creates a {@link DataInputStreamWithTimeout} as a wrapper around the specified underlying {@link InputStream}
      *
-     * @param in the specified input stream
+     * @param stream the specified input stream
+     * @param bridge the actual Bridge Thing instance
      */
-    public DataInputStreamWithTimeout(InputStream in) {
-        super(in);
+    public DataInputStreamWithTimeout(InputStream stream, VeluxBridgeHandler bridge) {
+        inputStream = stream;
+        executor = Executors.newSingleThreadExecutor(bridge.getThreadFactory());
     }
 
     /**
-     * Reads up to <code>len</code> bytes of data from the contained
-     * input stream into an array of bytes. An attempt is made to read
-     * as many as <code>len</code> bytes, but a smaller number may be read,
-     * possibly zero. The number of bytes actually read is returned as an
-     * integer.
-     *
-     * <p>
-     * This method blocks until input data is available, end of file is
-     * detected, or an exception is thrown <B>until</B> the given timeout.
-     *
-     * <p>
-     * If <code>len</code> is zero, then no bytes are read and
-     * <code>0</code> is returned; otherwise, there is an attempt to read at
-     * least one byte. If no byte is available because the stream is at end of
-     * file, the value <code>-1</code> is returned; otherwise, at least one
-     * byte is read and stored into <code>b</code>.
-     *
-     * <p>
-     * The first byte read is stored into element <code>b[off]</code>, the
-     * next one into <code>b[off+1]</code>, and so on. The number of bytes read
-     * is, at most, equal to <code>len</code>. Let <i>k</i> be the number of
-     * bytes actually read; these bytes will be stored in elements
-     * <code>b[off]</code> through <code>b[off+</code><i>k</i><code>-1]</code>,
-     * leaving elements <code>b[off+</code><i>k</i><code>]</code> through
-     * <code>b[off+len-1]</code> unaffected.
+     * Overridden method of {@link Closeable} interface. Stops the polling thread.
      *
-     * <p>
-     * In every case, elements <code>b[0]</code> through
-     * <code>b[off]</code> and elements <code>b[off+len]</code> through
-     * <code>b[b.length-1]</code> are unaffected.
+     * @throws IOException
+     */
+    @Override
+    public void close() throws IOException {
+        stopPolling();
+    }
+
+    /**
+     * Reads and removes the next available SLIP message from the queue. If the queue is empty, continue polling
+     * until either a message is found, or the timeout expires.
      *
-     * @param b the buffer into which the data is read.
-     * @param off the start offset in the destination array <code>b</code>
-     * @param len the maximum number of bytes read.
-     * @param timeoutMSecs the maximum duration of this read before throwing a TimeoutException.
-     * @return the total number of bytes read into the buffer, or
-     *         <code>-1</code> if there is no more data because the end
-     *         of the stream has been reached.
-     * @exception NullPointerException If <code>b</code> is <code>null</code>.
-     * @exception IndexOutOfBoundsException If <code>off</code> is negative,
-     *                <code>len</code> is negative, or <code>len</code> is greater than
-     *                <code>b.length - off</code>
-     * @exception IOException if the first byte cannot be read for any reason
-     *                other than end of file, the stream has been closed and the underlying
-     *                input stream does not support reading after close, or another I/O
-     *                error occurs. Additionally it will occur when the timeout happens.
-     * @see java.io.DataInputStream#read
+     * @param timeoutMSecs the timeout period in milliseconds.
+     * @return the next SLIP message if there is one on the queue, or any empty byte[] array if not.
+     * @throws IOException
      */
-    public synchronized int read(byte b[], int off, int len, int timeoutMSecs) throws IOException {
-        // Definition of Method which encapsulates the Read of data
-        Callable<Integer> readTask = new Callable<Integer>() {
-            @Override
-            public Integer call() throws IOException {
-                return in.read(b, off, len);
+    public synchronized byte[] readSlipMessage(int timeoutMSecs) throws IOException {
+        startPolling();
+        int i = (timeoutMSecs / SLEEP_INTERVAL_MSECS) + 1;
+        while (i-- >= 0) {
+            try {
+                byte[] slip = slipMessageQueue.remove();
+                logger.trace("readSlipMessage() => return slip message");
+                return slip;
+            } catch (NoSuchElementException e) {
+                // queue empty, wait and continue
+            }
+            throwIfPollException();
+            try {
+                Thread.sleep(SLEEP_INTERVAL_MSECS);
+            } catch (InterruptedException e) {
+                logger.debug("readSlipMessage() => thread interrupt");
+                throw new IOException("Thread Interrupted");
             }
-        };
-        try {
-            Future<Integer> future = executor.submit(readTask);
-            return future.get(timeoutMSecs, TimeUnit.MILLISECONDS);
-        } catch (RejectedExecutionException e) {
-            throw new IOException("executor failed", e);
-        } catch (ExecutionException e) {
-            throw new IOException("execution failed", e);
-        } catch (InterruptedException e) {
-            throw new IOException("read interrupted", e);
-        } catch (TimeoutException e) {
-            throw new IOException("read timeout", e);
         }
+        logger.debug("readSlipMessage() => no slip message after {}mS => time out", timeoutMSecs);
+        return new byte[0];
+    }
+
+    /**
+     * Get the number of incoming messages in the queue
+     *
+     * @return the number of incoming messages in the queue
+     */
+    public int available() {
+        int size = slipMessageQueue.size();
+        logger.trace("available() => slip message count {}", size);
+        return size;
+    }
+
+    /**
+     * Clear the queue
+     */
+    public void flush() {
+        logger.trace("flush() called");
+        slipMessageQueue.clear();
+    }
+
+    /**
+     * Start the polling task
+     */
+    private void startPolling() {
+        Poller pollRunner = this.pollRunner;
+        if (pollRunner == null) {
+            logger.trace("startPolling()");
+            pollRunner = this.pollRunner = new Poller();
+            executor.submit(pollRunner);
+        }
+    }
+
+    /**
+     * Stop the polling task
+     */
+    private void stopPolling() {
+        Poller pollRunner = this.pollRunner;
+        if (pollRunner != null) {
+            logger.trace("stopPolling()");
+            pollRunner.interrupt();
+            this.pollRunner = null;
+        }
+        executor.shutdown();
     }
 }
index b93bfad8f477d5c861d8447bd30a3c3a5aaeb795..0e72d69ea5478702474f3ac16ff250b4367cdd94 100644 (file)
  */
 package org.openhab.binding.velux.internal.bridge.slip.io;
 
+import java.io.Closeable;
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.net.ConnectException;
+import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
 import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;
@@ -29,6 +31,8 @@ import javax.net.ssl.X509TrustManager;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.velux.internal.VeluxBindingConstants;
+import org.openhab.binding.velux.internal.config.VeluxBridgeConfiguration;
+import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -51,7 +55,7 @@ import org.slf4j.LoggerFactory;
  * @author Guenther Schreiner - Initial contribution.
  */
 @NonNullByDefault
-class SSLconnection {
+class SSLconnection implements Closeable {
     private final Logger logger = LoggerFactory.getLogger(SSLconnection.class);
 
     // Public definition
@@ -62,13 +66,12 @@ class SSLconnection {
      * ***** Private Objects *****
      */
 
-    private static final int CONNECTION_BUFFER_SIZE = 4096;
-
-    private boolean ready = false;
     private @Nullable SSLSocket socket;
     private @Nullable DataOutputStream dOut;
     private @Nullable DataInputStreamWithTimeout dIn;
-    private int ioTimeoutMSecs = 60000;
+
+    private int readTimeoutMSecs = 2000;
+    private int connTimeoutMSecs = 6000;
 
     /**
      * Fake trust manager to suppress any certificate errors,
@@ -102,21 +105,18 @@ class SSLconnection {
      */
     SSLconnection() {
         logger.debug("SSLconnection() called.");
-        ready = false;
-        logger.trace("SSLconnection() finished.");
     }
 
     /**
      * Constructor to setup and establish a connection.
      *
-     * @param host as String describing the Service Access Point location i.e. hostname.
-     * @param port as String describing the Service Access Point location i.e. TCP port.
+     * @param bridgeInstance the actual Bridge Thing instance
      * @throws java.net.ConnectException in case of unrecoverable communication failures.
      * @throws java.io.IOException in case of continuous communication I/O failures.
      * @throws java.net.UnknownHostException in case of continuous communication I/O failures.
      */
-    SSLconnection(String host, int port) throws ConnectException, IOException, UnknownHostException {
-        logger.debug("SSLconnection({},{}) called.", host, port);
+    SSLconnection(VeluxBridgeHandler bridgeInstance) throws ConnectException, IOException, UnknownHostException {
+        logger.debug("SSLconnection() called");
         logger.info("Starting {} bridge connection.", VeluxBindingConstants.BINDING_ID);
         SSLContext ctx = null;
         try {
@@ -126,15 +126,27 @@ class SSLconnection {
             throw new IOException(String.format("create of an empty trust store failed: %s.", e.getMessage()));
         }
         logger.trace("SSLconnection(): creating socket...");
-        // Just for avoidance of Potential null pointer access
-        SSLSocket socketX = (SSLSocket) ctx.getSocketFactory().createSocket(host, port);
-        logger.trace("SSLconnection(): starting SSL handshake...");
-        if (socketX != null) {
-            socketX.startHandshake();
-            dOut = new DataOutputStream(socketX.getOutputStream());
-            dIn = new DataInputStreamWithTimeout(socketX.getInputStream());
-            ready = true;
-            socket = socketX;
+        SSLSocket socket = this.socket = (SSLSocket) ctx.getSocketFactory().createSocket();
+        if (socket != null) {
+            VeluxBridgeConfiguration cfg = bridgeInstance.veluxBridgeConfiguration();
+            readTimeoutMSecs = cfg.timeoutMsecs;
+            connTimeoutMSecs = Math.max(connTimeoutMSecs, readTimeoutMSecs);
+            // use longer timeout when establishing the connection
+            socket.setSoTimeout(connTimeoutMSecs);
+            socket.setKeepAlive(true);
+            socket.connect(new InetSocketAddress(cfg.ipAddress, cfg.tcpPort), connTimeoutMSecs);
+            logger.trace("SSLconnection(): starting SSL handshake...");
+            socket.startHandshake();
+            // use shorter timeout for normal communications
+            socket.setSoTimeout(readTimeoutMSecs);
+            dOut = new DataOutputStream(socket.getOutputStream());
+            dIn = new DataInputStreamWithTimeout(socket.getInputStream(), bridgeInstance);
+            if (logger.isTraceEnabled()) {
+                logger.trace(
+                        "SSLconnection(): connected... (ip={}, port={}, sslTimeout={}, soTimeout={}, soKeepAlive={})",
+                        cfg.ipAddress, cfg.tcpPort, connTimeoutMSecs, socket.getSoTimeout(),
+                        socket.getKeepAlive() ? "true" : "false");
+            }
         }
         logger.trace("SSLconnection() finished.");
     }
@@ -150,38 +162,27 @@ class SSLconnection {
      * @return <b>ready</b> as boolean for an established connection.
      */
     synchronized boolean isReady() {
-        return ready;
+        return socket != null && dIn != null && dOut != null;
     }
 
     /**
-     * Method to pass a message towards the bridge.
-     * This method gets called when we are initiating a new SLIP transaction.
-     * <p>
-     * Note that DataOutputStream and DataInputStream are buffered I/O's. The SLIP protocol requires that prior requests
-     * should have been fully sent over the socket, and their responses should have been fully read from the buffer
-     * before the next request is initiated. i.e. Both read and write buffers should already be empty. Nevertheless,
-     * just in case, we do the following..
-     * <p>
-     * 1) Flush from the read buffer any orphan response data that may have been left over from prior transactions, and
-     * 2) Flush the write buffer directly to the socket to ensure that any exceptions are raised immediately, and the
-     * KLF starts work immediately
+     * Method to pass a message towards the bridge. This method gets called when we are initiating a new SLIP
+     * transaction.
      *
-     * @param packet as Array of bytes to be transmitted towards the bridge via the established connection.
-     * @throws java.io.IOException in case of a communication I/O failure, and sets 'ready' = false
+     * @param <b>packet</b> as Array of bytes to be transmitted towards the bridge via the established connection.
+     * @throws java.io.IOException in case of a communication I/O failure
      */
-    @SuppressWarnings("null")
     synchronized void send(byte[] packet) throws IOException {
         logger.trace("send() called, writing {} bytes.", packet.length);
+        DataOutputStream dOutX = dOut;
+        if (dOutX == null) {
+            throw new IOException("DataOutputStream not initialised");
+        }
         try {
-            if (!ready || (dOut == null) || (dIn == null)) {
-                throw new IOException();
-            }
-            // flush the read buffer if (exceptionally) there is orphan response data in it
-            flushReadBufffer();
             // copy packet data to the write buffer
-            dOut.write(packet, 0, packet.length);
+            dOutX.write(packet, 0, packet.length);
             // force the write buffer data to be written to the socket
-            dOut.flush();
+            dOutX.flush();
             if (logger.isTraceEnabled()) {
                 StringBuilder sb = new StringBuilder();
                 for (byte b : packet) {
@@ -190,7 +191,7 @@ class SSLconnection {
                 logger.trace("send() finished after having send {} bytes: {}", packet.length, sb.toString());
             }
         } catch (IOException e) {
-            ready = false;
+            close();
             throw e;
         }
     }
@@ -198,47 +199,43 @@ class SSLconnection {
     /**
      * Method to verify that there is message from the bridge.
      *
-     * @return <b>true</b> if there are any bytes ready to be queried using {@link SSLconnection#receive}.
-     * @throws java.io.IOException in case of a communication I/O failure.
+     * @return <b>true</b> if there are any messages ready to be queried using {@link SSLconnection#receive}.
      */
-    synchronized boolean available() throws IOException {
+    synchronized boolean available() {
         logger.trace("available() called.");
-        if (!ready || (dIn == null)) {
-            throw new IOException();
+        DataInputStreamWithTimeout dInX = dIn;
+        if (dInX != null) {
+            int availableMessages = dInX.available();
+            logger.trace("available(): found {} messages ready to be read (> 0 means true).", availableMessages);
+            return availableMessages > 0;
         }
-        @SuppressWarnings("null")
-        int availableBytes = dIn.available();
-        logger.trace("available(): found {} bytes ready to be read (> 0 means true).", availableBytes);
-        return availableBytes > 0;
+        return false;
     }
 
     /**
      * Method to get a message from the bridge.
      *
      * @return <b>packet</b> as Array of bytes as received from the bridge via the established connection.
-     * @throws java.io.IOException in case of a communication I/O failure, and sets 'ready' = false
+     * @throws java.io.IOException in case of a communication I/O failure.
      */
     synchronized byte[] receive() throws IOException {
         logger.trace("receive() called.");
+        DataInputStreamWithTimeout dInX = dIn;
+        if (dInX == null) {
+            throw new IOException("DataInputStreamWithTimeout not initialised");
+        }
         try {
-            if (!ready || (dIn == null)) {
-                throw new IOException();
-            }
-            byte[] message = new byte[CONNECTION_BUFFER_SIZE];
-            @SuppressWarnings("null")
-            int messageLength = dIn.read(message, 0, message.length, ioTimeoutMSecs);
-            byte[] packet = new byte[messageLength];
-            System.arraycopy(message, 0, packet, 0, messageLength);
+            byte[] packet = dInX.readSlipMessage(readTimeoutMSecs);
             if (logger.isTraceEnabled()) {
                 StringBuilder sb = new StringBuilder();
                 for (byte b : packet) {
                     sb.append(String.format("%02X ", b));
                 }
-                logger.trace("receive() finished after having read {} bytes: {}", messageLength, sb.toString());
+                logger.trace("receive() finished after having read {} bytes: {}", packet.length, sb.toString());
             }
             return packet;
         } catch (IOException e) {
-            ready = false;
+            close();
             throw e;
         }
     }
@@ -247,67 +244,39 @@ class SSLconnection {
      * Destructor to tear down a connection.
      *
      * @throws java.io.IOException in case of a communication I/O failure.
+     *             But actually eats all exceptions to ensure sure that all shutdown code is executed
      */
-    synchronized void close() throws IOException {
+    @Override
+    public synchronized void close() throws IOException {
         logger.debug("close() called.");
-        ready = false;
-        logger.info("Shutting down Velux bridge connection.");
-        // Just for avoidance of Potential null pointer access
         DataInputStreamWithTimeout dInX = dIn;
         if (dInX != null) {
-            dInX.close();
-            dIn = null;
+            try {
+                dInX.close();
+            } catch (IOException e) {
+                // eat the exception so the following will always be executed
+            }
         }
-        // Just for avoidance of Potential null pointer access
         DataOutputStream dOutX = dOut;
         if (dOutX != null) {
-            dOutX.close();
-            dOut = null;
+            try {
+                dOutX.close();
+            } catch (IOException e) {
+                // eat the exception so the following will always be executed
+            }
         }
-        // Just for avoidance of Potential null pointer access
         SSLSocket socketX = socket;
         if (socketX != null) {
-            socketX.close();
-            socket = null;
-        }
-        logger.trace("close() finished.");
-    }
-
-    /**
-     * Parameter modification.
-     *
-     * @param timeoutMSecs the maximum duration in milliseconds for read operations.
-     */
-    void setTimeout(int timeoutMSecs) {
-        logger.debug("setTimeout() set timeout to {} milliseconds.", timeoutMSecs);
-        ioTimeoutMSecs = timeoutMSecs;
-    }
-
-    /**
-     * Method to flush the input buffer.
-     *
-     * @throws java.io.IOException in case of a communication I/O failure.
-     */
-    private void flushReadBufffer() throws IOException {
-        logger.trace("flushReadBuffer() called.");
-        DataInputStreamWithTimeout dInX = dIn;
-        if (!ready || (dInX == null)) {
-            throw new IOException();
-        }
-        int byteCount = dInX.available();
-        if (byteCount > 0) {
-            byte[] byteArray = new byte[byteCount];
-            dInX.readFully(byteArray);
-            if (logger.isTraceEnabled()) {
-                StringBuilder stringBuilder = new StringBuilder();
-                for (byte currByte : byteArray) {
-                    stringBuilder.append(String.format("%02X ", currByte));
-                }
-                logger.trace("flushReadBuffer(): discarded {} unexpected bytes in the input buffer: {}", byteCount,
-                        stringBuilder.toString());
-            } else {
-                logger.warn("flushReadBuffer(): discarded {} unexpected bytes in the input buffer", byteCount);
+            logger.debug("Shutting down Velux bridge connection.");
+            try {
+                socketX.close();
+            } catch (IOException e) {
+                // eat the exception so the following will always be executed
             }
         }
+        dIn = null;
+        dOut = null;
+        socket = null;
+        logger.trace("close() finished.");
     }
 }
index 520ce67c72f51592963d67d797097620200dc809..219b58188050f8269982d0a11e092b4cddcd072a 100644 (file)
@@ -65,7 +65,7 @@ public class KLF200Response {
     public static void errorLogging(Logger logger, short responseCommand) {
         logger.trace("setResponse(): cannot handle response {} ({}).", Command.get(responseCommand).toString(),
                 new CommandNumber(responseCommand).toString());
-        logger.warn("Gateway response {} ({}) cannot be handled at this point of interaction.",
+        logger.debug("Gateway response {} ({}) cannot be handled at this point of interaction.",
                 Command.get(responseCommand).toString(), new CommandNumber(responseCommand).toString());
     }
 
@@ -125,7 +125,7 @@ public class KLF200Response {
         logger.trace("check4matchingAnyID() called for request {} {} and response {} {}.", idName, requestID, idName,
                 responseID);
         if (requestID != responseID) {
-            logger.warn("Gateway query for {} {} received unexpected response of {} {}.", idName, requestID, idName,
+            logger.debug("Gateway query for {} {} received unexpected response of {} {}.", idName, requestID, idName,
                     responseID);
             return false;
         }
diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/discovery/VeluxBridgeFinder.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/discovery/VeluxBridgeFinder.java
new file mode 100644 (file)
index 0000000..1c35113
--- /dev/null
@@ -0,0 +1,330 @@
+/**
+ * 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.velux.internal.discovery;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.MulticastSocket;
+import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class that uses Multicast DNS (mDNS) to discover Velux Bridges and return their ipv4 addresses
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ */
+@NonNullByDefault
+public class VeluxBridgeFinder implements Closeable {
+
+    private final Logger logger = LoggerFactory.getLogger(VeluxBridgeFinder.class);
+
+    // timing constants
+    private static final int BUFFER_SIZE = 256;
+    private static final int SLEEP_MSECS = 100;
+    private static final int SOCKET_TIMEOUT_MSECS = 500;
+    private static final int SEARCH_DURATION_MSECS = 5000;
+    private static final int REPEAT_COUNT = 3;
+
+    // dns communication constants
+    private static final int MDNS_PORT = 5353;
+    private static final String MDNS_ADDR = "224.0.0.251";
+
+    // dns flag constants
+    private static final short FLAGS_QR = (short) 0x8000;
+    private static final short FLAGS_AA = 0x0400;
+
+    // dns message class constants
+    private static final short CLASS_IN = 0x0001;
+    private static final short CLASS_MASK = 0x7FFF;
+
+    // dns message type constants
+    private static final short TYPE_PTR = 0x000c;
+
+    private static final byte NULL = 0x00;
+
+    // Velux bridge identifiers
+    private static final String KLF_SERVICE_ID = "_http._tcp.local";
+    private static final String KLF_HOST_PREFIX = "VELUX_KLF_";
+
+    private short randomQueryId;
+    private ScheduledExecutorService executor;
+    private @Nullable Listener listener = null;
+
+    private class Listener implements Callable<Set<String>> {
+
+        private boolean interrupted = false;
+        private boolean started = false;
+
+        public void interrupt() {
+            interrupted = true;
+        }
+
+        public boolean hasStarted() {
+            return started;
+        }
+
+        /**
+         * Listens for Velux Bridges and returns their IP addresses. It loops for SEARCH_DURATION_MSECS or until
+         * 'interrupt()' or 'Thread.interrupted()' are called when it terminates early after the next socket read
+         * timeout i.e. after SOCKET_TIMEOUT_MSECS
+         *
+         * @return a set of strings containing dotted IP addresses e.g. '123.123.123.123'
+         */
+        @Override
+        public Set<String> call() throws Exception {
+            final Set<String> ipAddresses = new HashSet<>();
+
+            // create a multicast listener socket
+            try (MulticastSocket rcvSocket = new MulticastSocket(MDNS_PORT)) {
+
+                final byte[] rcvBytes = new byte[BUFFER_SIZE];
+                final long finishTime = System.currentTimeMillis() + SEARCH_DURATION_MSECS;
+
+                rcvSocket.setReuseAddress(true);
+                rcvSocket.joinGroup(InetAddress.getByName(MDNS_ADDR));
+                rcvSocket.setSoTimeout(SOCKET_TIMEOUT_MSECS);
+
+                // tell the caller that we are ready to roll
+                started = true;
+
+                // loop until time out or internally or externally interrupted
+                while ((System.currentTimeMillis() < finishTime) && (!interrupted) && (!Thread.interrupted())) {
+                    // read next packet
+                    DatagramPacket rcvPacket = new DatagramPacket(rcvBytes, rcvBytes.length);
+                    try {
+                        rcvSocket.receive(rcvPacket);
+                        if (isKlfLanResponse(rcvPacket.getData())) {
+                            ipAddresses.add(rcvPacket.getAddress().getHostAddress());
+                        }
+                    } catch (SocketTimeoutException e) {
+                        // time out is ok, continue listening
+                        continue;
+                    }
+                }
+            } catch (IOException e) {
+                logger.debug("listenerRunnable(): udp socket exception '{}'", e.getMessage());
+            }
+            // prevent caller waiting forever in case start up failed
+            started = true;
+            return ipAddresses;
+        }
+    }
+
+    /**
+     * Build an mDNS query package to query SERVICE_ID looking for host names
+     *
+     * @return a byte array containing the query datagram payload, or an empty array if failed
+     */
+    private byte[] buildQuery() {
+        ByteArrayOutputStream byteStream = new ByteArrayOutputStream(BUFFER_SIZE);
+        DataOutputStream dataStream = new DataOutputStream(byteStream);
+        try {
+            dataStream.writeShort(randomQueryId); // id
+            dataStream.writeShort(0); // flags
+            dataStream.writeShort(1); // qdCount
+            dataStream.writeShort(0); // anCount
+            dataStream.writeShort(0); // nsCount
+            dataStream.writeShort(0); // arCount
+            for (String segString : KLF_SERVICE_ID.split("\\.")) {
+                byte[] segBytes = segString.getBytes(StandardCharsets.UTF_8);
+                dataStream.writeByte(segBytes.length); // length
+                dataStream.write(segBytes); // byte string
+            }
+            dataStream.writeByte(NULL); // end of name
+            dataStream.writeShort(TYPE_PTR); // type
+            dataStream.writeShort(CLASS_IN); // class
+            return byteStream.toByteArray();
+        } catch (IOException e) {
+            // fall through
+        }
+        return new byte[0];
+    }
+
+    /**
+     * Parse an mDNS response package and check if it is from a KLF bridge
+     *
+     * @param responsePayload a byte array containing the response datagram payload
+     * @return true if the response is from a KLF bridge
+     */
+    private boolean isKlfLanResponse(byte[] responsePayload) {
+        DataInputStream dataStream = new DataInputStream(new ByteArrayInputStream(responsePayload));
+        try {
+            // check if the package id matches the query
+            short id = dataStream.readShort();
+            if (id == randomQueryId) {
+                short flags = dataStream.readShort();
+                boolean isResponse = (flags & FLAGS_QR) == FLAGS_QR;
+                boolean isAuthoritative = (flags & FLAGS_AA) == FLAGS_AA;
+
+                // check if it is an authoritative response
+                if (isResponse && isAuthoritative) {
+                    short qdCount = dataStream.readShort();
+                    short anCount = dataStream.readShort();
+
+                    dataStream.readShort(); // nsCount
+                    dataStream.readShort(); // arCount
+
+                    // check it is an answer (and not a query)
+                    if ((anCount == 0) || (qdCount != 0)) {
+                        return false;
+                    }
+
+                    // parse the answers
+                    for (short an = 0; an < anCount; an++) {
+                        // parse the name
+                        byte[] str = new byte[BUFFER_SIZE];
+                        int i = 0;
+                        int segLength;
+                        while ((segLength = dataStream.readByte()) > 0) {
+                            i += dataStream.read(str, i, segLength);
+                            str[i] = '.';
+                            i++;
+                        }
+                        String name = new String(str, 0, i, StandardCharsets.UTF_8);
+                        short typ = dataStream.readShort();
+                        short clazz = (short) (CLASS_MASK & dataStream.readShort());
+                        if (!(name.startsWith(KLF_SERVICE_ID)) || (typ != TYPE_PTR) || (clazz != CLASS_IN)) {
+                            return false;
+                        }
+
+                        // if we got here, the name and response type are valid
+                        dataStream.readInt(); // TTL
+                        dataStream.readShort(); // dataLen
+
+                        // parse the host name
+                        i = 0;
+                        while ((segLength = dataStream.readByte()) > 0) {
+                            i += dataStream.read(str, i, segLength);
+                            str[i] = '.';
+                            i++;
+                        }
+
+                        // check if the host name matches
+                        String host = new String(str, 0, i, StandardCharsets.UTF_8);
+                        if (host.startsWith(KLF_HOST_PREFIX)) {
+                            return true;
+                        }
+                    }
+                }
+            }
+        } catch (IOException e) {
+            // fall through
+        }
+        return false;
+    }
+
+    /**
+     * Private synchronized method that searches for Velux Bridges and returns their IP addresses. Takes
+     * SEARCH_DURATION_MSECS to complete.
+     *
+     * @return a set of strings containing dotted IP addresses e.g. '123.123.123.123'
+     */
+    private synchronized Set<String> discoverBridgeIpAddresses() {
+        @Nullable
+        Set<String> result = null;
+
+        // create a random query id
+        Random random = new Random();
+        randomQueryId = (short) random.nextInt(Short.MAX_VALUE);
+
+        // create the listener task and start it
+        Listener listener = this.listener = new Listener();
+
+        // create a datagram socket
+        try (DatagramSocket socket = new DatagramSocket()) {
+            // prepare query packet
+            byte[] dnsBytes = buildQuery();
+            DatagramPacket dnsPacket = new DatagramPacket(dnsBytes, 0, dnsBytes.length,
+                    InetAddress.getByName(MDNS_ADDR), MDNS_PORT);
+
+            // create listener and wait until it has started
+            Future<Set<String>> future = executor.submit(listener);
+            while (!listener.hasStarted()) {
+                Thread.sleep(SLEEP_MSECS);
+            }
+
+            // send the query several times
+            for (int i = 0; i < REPEAT_COUNT; i++) {
+                // send the query several times
+                socket.send(dnsPacket);
+                Thread.sleep(SLEEP_MSECS);
+            }
+
+            // wait for the listener future to get the result
+            result = future.get();
+        } catch (InterruptedException | IOException | ExecutionException e) {
+            logger.debug("discoverBridgeIpAddresses(): unexpected exception '{}'", e.getMessage());
+        }
+
+        // clean up listener task (just in case) and return
+        listener.interrupt();
+        this.listener = null;
+        return result != null ? result : new HashSet<>();
+    }
+
+    /**
+     * Constructor
+     *
+     * @param executor the caller's task executor
+     */
+    public VeluxBridgeFinder(ScheduledExecutorService executor) {
+        this.executor = executor;
+    }
+
+    /**
+     * Interrupt the {@link Listener}
+     *
+     * @throws IOException (not)
+     */
+    @Override
+    public void close() throws IOException {
+        Listener listener = this.listener;
+        if (listener != null) {
+            listener.interrupt();
+            this.listener = null;
+        }
+    }
+
+    /**
+     * Static method to search for Velux Bridges and return their IP addresses. NOTE: it takes SEARCH_DURATION_MSECS to
+     * complete, so don't call it on the main thread!
+     *
+     * @return set of dotted IP address e.g. '123.123.123.123'
+     */
+    public static Set<String> discoverIpAddresses(ScheduledExecutorService scheduler) {
+        try (VeluxBridgeFinder finder = new VeluxBridgeFinder(scheduler)) {
+            return finder.discoverBridgeIpAddresses();
+        } catch (IOException e) {
+            return new HashSet<>();
+        }
+    }
+}
index 8aa033d38acb49c3e35a2934514a7f7f89e7b834..4eadd57ae1cdaa0207233dfded8c9ba3a0416e27 100644 (file)
@@ -20,6 +20,7 @@ import java.util.Set;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.velux.internal.VeluxBindingConstants;
 import org.openhab.binding.velux.internal.VeluxBindingProperties;
+import org.openhab.binding.velux.internal.config.VeluxBridgeConfiguration;
 import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
 import org.openhab.binding.velux.internal.things.VeluxProduct;
 import org.openhab.binding.velux.internal.things.VeluxProductSerialNo;
@@ -45,11 +46,6 @@ import org.slf4j.LoggerFactory;
  *
  * @author Guenther Schreiner - Initial contribution.
  */
-//
-// To-be-discussed: check whether an immediate activation is preferable.
-// Might be activated by:
-// @Component(service = DiscoveryService.class, configurationPid = "discovery.velux")
-//
 @NonNullByDefault
 @Component(service = DiscoveryService.class, configurationPid = "discovery.velux")
 public class VeluxDiscoveryService extends AbstractDiscoveryService implements Runnable {
@@ -57,7 +53,7 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R
 
     // Class internal
 
-    private static final int DISCOVER_TIMEOUT_SECONDS = 300;
+    private static final int DISCOVER_TIMEOUT_SECONDS = 60;
 
     private @NonNullByDefault({}) LocaleProvider localeProvider;
     private @NonNullByDefault({}) TranslationProvider i18nProvider;
@@ -80,7 +76,7 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R
      * Initializes the {@link VeluxDiscoveryService} without any further information.
      */
     public VeluxDiscoveryService() {
-        super(VeluxBindingConstants.SUPPORTED_THINGS_ITEMS, DISCOVER_TIMEOUT_SECONDS);
+        super(VeluxBindingConstants.DISCOVERABLE_THINGS, DISCOVER_TIMEOUT_SECONDS);
         logger.trace("VeluxDiscoveryService(without Bridge) just initialized.");
     }
 
@@ -107,7 +103,7 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R
      * @param localizationHandler Initialized localization handler.
      */
     public VeluxDiscoveryService(Localization localizationHandler) {
-        super(VeluxBindingConstants.SUPPORTED_THINGS_ITEMS, DISCOVER_TIMEOUT_SECONDS);
+        super(VeluxBindingConstants.DISCOVERABLE_THINGS, DISCOVER_TIMEOUT_SECONDS);
         logger.trace("VeluxDiscoveryService(locale={},i18n={}) just initialized.", localeProvider, i18nProvider);
         localization = localizationHandler;
     }
@@ -143,10 +139,15 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R
         DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
                 .withProperty(VeluxBindingProperties.PROPERTY_BINDING_BUNDLEVERSION,
                         ManifestInformation.getBundleVersion())
+                .withRepresentationProperty(VeluxBindingProperties.PROPERTY_BINDING_BUNDLEVERSION)
                 .withLabel(localization.getText("discovery.velux.binding...label")).build();
         logger.debug("startScan(): registering new thing {}.", discoveryResult);
         thingDiscovered(discoveryResult);
 
+        scheduler.execute(() -> {
+            discoverBridges();
+        });
+
         if (bridgeHandlers.isEmpty()) {
             logger.debug("startScan(): VeluxDiscoveryService cannot proceed due to missing Velux bridge(s).");
         } else {
@@ -161,7 +162,6 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R
     public synchronized void stopScan() {
         logger.trace("stopScan() called.");
         super.stopScan();
-        removeOlderResults(getTimestampOfLastScan());
         logger.trace("stopScan() done.");
     }
 
@@ -286,4 +286,21 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R
     public boolean isEmpty() {
         return bridgeHandlers.isEmpty();
     }
+
+    /**
+     * Discover any bridges on the network that are not yet instantiated.
+     */
+    private void discoverBridges() {
+        // discover the list of IP addresses of bridges on the network
+        Set<String> foundBridgeIpAddresses = VeluxBridgeFinder.discoverIpAddresses(scheduler);
+        // publish discovery results
+        for (String ipAddr : foundBridgeIpAddresses) {
+            ThingUID thingUID = new ThingUID(THING_TYPE_BRIDGE, ipAddr.replace(".", "_"));
+            DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withThingType(THING_TYPE_BRIDGE)
+                    .withProperty(VeluxBridgeConfiguration.BRIDGE_IPADDRESS, ipAddr)
+                    .withRepresentationProperty(VeluxBridgeConfiguration.BRIDGE_IPADDRESS)
+                    .withLabel(String.format("Velux Bridge (%s)", ipAddr)).build();
+            thingDiscovered(result);
+        }
+    }
 }
index 634326139de9b664820a25430860476b393a353a..c9b83f0633aa1cb0e3fee7fc737bf5e96e73d48d 100644 (file)
@@ -19,6 +19,7 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.velux.internal.bridge.VeluxBridgeRunProductCommand;
 import org.openhab.binding.velux.internal.bridge.common.GetProduct;
 import org.openhab.binding.velux.internal.handler.utils.Thing2VeluxActuator;
+import org.openhab.binding.velux.internal.things.VeluxProduct;
 import org.openhab.binding.velux.internal.things.VeluxProductPosition;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.PercentType;
@@ -27,6 +28,7 @@ import org.openhab.core.library.types.UpDownType;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -88,14 +90,16 @@ final class ChannelActuatorPosition extends ChannelHandlerTemplate {
             bcp.setProductId(veluxActuator.getProductBridgeIndex().toInt());
             if (thisBridgeHandler.thisBridge.bridgeCommunicate(bcp) && bcp.isCommunicationSuccessful()) {
                 try {
-                    VeluxProductPosition position = new VeluxProductPosition(bcp.getProduct().getCurrentPosition());
+                    VeluxProduct product = bcp.getProduct();
+                    VeluxProductPosition position = new VeluxProductPosition(product.getDisplayPosition());
                     if (position.isValid()) {
-                        PercentType positionAsPercent = position.getPositionAsPercentType(veluxActuator.isInverted());
-                        LOGGER.trace("handleRefresh(): found actuator at level {}.", positionAsPercent);
-                        newState = positionAsPercent;
-                    } else {
-                        LOGGER.trace("handleRefresh(): level of actuator is unknown.");
+                        PercentType posPercent = position.getPositionAsPercentType(veluxActuator.isInverted());
+                        LOGGER.trace("handleRefresh(): position of actuator is {}%.", posPercent);
+                        newState = posPercent;
+                        break;
                     }
+                    LOGGER.trace("handleRefresh(): position of actuator is 'UNDEFINED'.");
+                    newState = UnDefType.UNDEF;
                 } catch (Exception e) {
                     LOGGER.warn("handleRefresh(): getProducts() exception: {}.", e.getMessage());
                 }
index 0a013a5c108dca81def572444e1e528c0ca693e3..9281e5a81ae3806cd237da967aa67c7efc18f248 100644 (file)
@@ -14,11 +14,9 @@ package org.openhab.binding.velux.internal.handler;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.velux.internal.VeluxBindingConstants;
 import org.openhab.binding.velux.internal.VeluxItemType;
 import org.openhab.binding.velux.internal.bridge.VeluxBridgeLANConfig;
 import org.openhab.binding.velux.internal.handler.utils.StateUtils;
-import org.openhab.binding.velux.internal.handler.utils.ThingProperty;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.types.State;
 import org.slf4j.Logger;
@@ -71,25 +69,17 @@ final class ChannelBridgeLANconfig extends ChannelHandlerTemplate {
             VeluxItemType itemType = VeluxItemType.getByThingAndChannel(thisBridgeHandler.thingTypeUIDOf(channelUID),
                     channelUID.getId());
             switch (itemType) {
-                case BRIDGE_IPADDRESS:
+                case BRIDGE_ADDRESS:
                     newState = StateUtils.createState(thisBridgeHandler.bridgeParameters.lanConfig.openHABipAddress);
-                    ThingProperty.setValue(thisBridgeHandler, VeluxBindingConstants.PROPERTY_BRIDGE_IPADDRESS,
-                            thisBridgeHandler.bridgeParameters.lanConfig.openHABipAddress.toString());
                     break;
                 case BRIDGE_SUBNETMASK:
                     newState = StateUtils.createState(thisBridgeHandler.bridgeParameters.lanConfig.openHABsubnetMask);
-                    ThingProperty.setValue(thisBridgeHandler, VeluxBindingConstants.PROPERTY_BRIDGE_SUBNETMASK,
-                            thisBridgeHandler.bridgeParameters.lanConfig.openHABsubnetMask.toString());
                     break;
                 case BRIDGE_DEFAULTGW:
                     newState = StateUtils.createState(thisBridgeHandler.bridgeParameters.lanConfig.openHABdefaultGW);
-                    ThingProperty.setValue(thisBridgeHandler, VeluxBindingConstants.PROPERTY_BRIDGE_DEFAULTGW,
-                            thisBridgeHandler.bridgeParameters.lanConfig.openHABdefaultGW.toString());
                     break;
                 case BRIDGE_DHCP:
                     newState = StateUtils.createState(thisBridgeHandler.bridgeParameters.lanConfig.openHABenabledDHCP);
-                    ThingProperty.setValue(thisBridgeHandler, VeluxBindingConstants.PROPERTY_BRIDGE_DHCP,
-                            thisBridgeHandler.bridgeParameters.lanConfig.openHABenabledDHCP.toString());
                 default:
             }
         }
index 3857f88a672c76ea98cee01a522b6c8062160adf..180c39b475a5e3de217d76bfd3aca326337b339a 100644 (file)
@@ -14,12 +14,9 @@ package org.openhab.binding.velux.internal.handler;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.velux.internal.VeluxBindingConstants;
 import org.openhab.binding.velux.internal.VeluxItemType;
 import org.openhab.binding.velux.internal.bridge.VeluxBridgeWLANConfig;
 import org.openhab.binding.velux.internal.handler.utils.StateUtils;
-import org.openhab.binding.velux.internal.handler.utils.ThingProperty;
-import org.openhab.core.library.types.StringType;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.types.State;
 import org.slf4j.Logger;
@@ -70,15 +67,13 @@ final class ChannelBridgeWLANconfig extends ChannelHandlerTemplate {
         if (thisBridgeHandler.bridgeParameters.wlanConfig.isRetrieved) {
             VeluxItemType itemType = VeluxItemType.getByThingAndChannel(thisBridgeHandler.thingTypeUIDOf(channelUID),
                     channelUID.getId());
-            String msg = thisBridgeHandler.localization.getText("config.velux.bridge.unAvailable");
             switch (itemType) {
                 case BRIDGE_WLANSSID:
-                    newState = StateUtils.createState(new StringType(msg));
-                    ThingProperty.setValue(thisBridgeHandler, VeluxBindingConstants.PROPERTY_BRIDGE_WLANSSID, msg);
+                    newState = StateUtils.createState(thisBridgeHandler.bridgeParameters.wlanConfig.openHABwlanSSID);
                     break;
                 case BRIDGE_WLANPASSWORD:
-                    newState = StateUtils.createState(new StringType(msg));
-                    ThingProperty.setValue(thisBridgeHandler, VeluxBindingConstants.PROPERTY_BRIDGE_WLANPASSWORD, msg);
+                    newState = StateUtils
+                            .createState(thisBridgeHandler.bridgeParameters.wlanConfig.openHABwlanPassword);
                     break;
                 default:
             }
index 876704a50d78ccd5398906e0d46130fae6c94c63..2d688bbde06984b13b74ca6cb6932aea0771b12b 100644 (file)
@@ -42,6 +42,7 @@ import org.slf4j.LoggerFactory;
  *
  * @author Guenther Schreiner - Initial contribution.
  */
+@Deprecated
 @NonNullByDefault
 final class ChannelSceneSilentmode extends ChannelHandlerTemplate {
     private static final Logger LOGGER = LoggerFactory.getLogger(ChannelSceneSilentmode.class);
index cf178211c424da4e1530385e763c67670ae12a89..0fe7f2efa1c6fd066d952ec4fac5b0fd5ec3063e 100644 (file)
  */
 package org.openhab.binding.velux.internal.handler;
 
+import java.util.Collection;
+import java.util.Collections;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
@@ -23,6 +26,7 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.velux.internal.VeluxBinding;
 import org.openhab.binding.velux.internal.VeluxBindingConstants;
 import org.openhab.binding.velux.internal.VeluxItemType;
+import org.openhab.binding.velux.internal.action.VeluxActions;
 import org.openhab.binding.velux.internal.bridge.VeluxBridge;
 import org.openhab.binding.velux.internal.bridge.VeluxBridgeActuators;
 import org.openhab.binding.velux.internal.bridge.VeluxBridgeDeviceStatus;
@@ -36,6 +40,8 @@ import org.openhab.binding.velux.internal.bridge.VeluxBridgeSetHouseStatusMonito
 import org.openhab.binding.velux.internal.bridge.VeluxBridgeWLANConfig;
 import org.openhab.binding.velux.internal.bridge.common.BridgeAPI;
 import org.openhab.binding.velux.internal.bridge.common.BridgeCommunicationProtocol;
+import org.openhab.binding.velux.internal.bridge.common.RunProductCommand;
+import org.openhab.binding.velux.internal.bridge.common.RunReboot;
 import org.openhab.binding.velux.internal.bridge.json.JsonVeluxBridge;
 import org.openhab.binding.velux.internal.bridge.slip.SlipVeluxBridge;
 import org.openhab.binding.velux.internal.config.VeluxBridgeConfiguration;
@@ -50,7 +56,7 @@ import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex
 import org.openhab.binding.velux.internal.things.VeluxProductPosition;
 import org.openhab.binding.velux.internal.utils.Localization;
 import org.openhab.core.common.AbstractUID;
-import org.openhab.core.common.ThreadPoolManager;
+import org.openhab.core.common.NamedThreadFactory;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.PercentType;
@@ -59,9 +65,11 @@ import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.ThingHandlerService;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.RefreshType;
 import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -87,6 +95,7 @@ import org.slf4j.LoggerFactory;
  */
 @NonNullByDefault
 public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements VeluxBridgeInstance, VeluxBridgeProvider {
+
     private final Logger logger = LoggerFactory.getLogger(VeluxBridgeHandler.class);
 
     // Class internal
@@ -102,10 +111,14 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
     private int refreshCounter = 0;
 
     /**
-     * Dedicated thread pool for the long-running bridge communication threads.
+     * Dedicated task executor for the long-running bridge communication tasks.
+     *
+     * Note: there is no point in using multi threaded thread-pool here, since all the submitted (Runnable) tasks are
+     * anyway forced to go through the same serial pipeline, because they all call the same class level "synchronized"
+     * method to actually communicate with the KLF bridge via its one single TCP socket connection
      */
-    private ScheduledExecutorService handleScheduler = ThreadPoolManager
-            .getScheduledPool(VeluxBindingConstants.BINDING_ID);
+    private @Nullable ExecutorService taskExecutor = null;
+    private @Nullable NamedThreadFactory threadFactory = null;
 
     private VeluxBridge myJsonBridge = new JsonVeluxBridge(this);
     private VeluxBridge mySlipBridge = new SlipVeluxBridge(this);
@@ -250,10 +263,7 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
             logger.warn("initialize(): scheduler is shutdown, aborting the initialization of this bridge.");
             return;
         }
-        if (handleScheduler.isShutdown()) {
-            logger.trace("initialize(): handleScheduler is shutdown, aborting the initialization of this bridge.");
-            return;
-        }
+        getTaskExecutor();
         logger.trace("initialize(): preparing background initialization task.");
         // Background initialization...
         scheduler.execute(() -> {
@@ -291,6 +301,11 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
             logger.trace("dispose(): stopping the refresh.");
             currentRefreshJob.cancel(true);
         }
+        // shut down the task executor
+        ExecutorService taskExecutor = this.taskExecutor;
+        if (taskExecutor != null) {
+            taskExecutor.shutdownNow();
+        }
         // Background execution of dispose
         scheduler.execute(() -> {
             logger.trace("dispose.scheduled(): (synchronous) logout initiated.");
@@ -396,32 +411,30 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
 
     private synchronized void refreshOpenHAB() {
         logger.debug("refreshOpenHAB() initiated by {} starting cycle {}.", Thread.currentThread(), refreshCounter);
-
-        if (handleScheduler.isShutdown()) {
-            logger.trace("refreshOpenHAB(): handleScheduler is shutdown, recreating a scheduler pool.");
-            handleScheduler = ThreadPoolManager.getScheduledPool(VeluxBindingConstants.BINDING_ID);
-        }
-
         logger.trace("refreshOpenHAB(): processing of possible HSM messages.");
+
         // Background execution of bridge related I/O
-        handleScheduler.execute(() -> {
+        getTaskExecutor().execute(() -> {
             logger.trace("refreshOpenHAB.scheduled() initiated by {} will process HouseStatus.",
                     Thread.currentThread());
             if (new VeluxBridgeGetHouseStatus().evaluateState(thisBridge)) {
-                logger.trace("refreshOpenHAB.scheduled(): successfully processed of GetHouseStatus()");
+                logger.trace("refreshOpenHAB.scheduled(): => GetHouseStatus() => updates received => synchronizing");
+                syncChannelsWithProducts();
+            } else {
+                logger.trace("refreshOpenHAB.scheduled(): => GetHouseStatus() => no updates");
             }
             logger.trace("refreshOpenHAB.scheduled() initiated by {} has finished.", Thread.currentThread());
         });
 
-        logger.trace(
-                "refreshOpenHAB(): looping through all (both child things and bridge) linked channels for a need of refresh.");
+        logger.trace("refreshOpenHAB(): loop through all (child things and bridge) linked channels needing a refresh");
         for (ChannelUID channelUID : BridgeChannels.getAllLinkedChannelUIDs(this)) {
             if (VeluxItemType.isToBeRefreshedNow(refreshCounter, thingTypeUIDOf(channelUID), channelUID.getId())) {
                 logger.trace("refreshOpenHAB(): refreshing channel {}.", channelUID);
                 handleCommand(channelUID, RefreshType.REFRESH);
             }
         }
-        logger.trace("refreshOpenHAB(): looping through properties for a need of refresh.");
+
+        logger.trace("refreshOpenHAB(): loop through properties needing a refresh");
         for (VeluxItemType veluxItem : VeluxItemType.getPropertyEntriesByThing(getThing().getThingTypeUID())) {
             if (VeluxItemType.isToBeRefreshedNow(refreshCounter, getThing().getThingTypeUID(),
                     veluxItem.getIdentifier())) {
@@ -439,11 +452,11 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
      */
     private void syncChannelsWithProducts() {
         if (!bridgeParameters.actuators.getChannel().existingProducts.isDirty()) {
+            logger.trace("syncChannelsWithProducts(): no existing products with changed parameters.");
             return;
         }
         logger.trace("syncChannelsWithProducts(): there are some existing products with changed parameters.");
-        outer: for (VeluxProduct product : bridgeParameters.actuators.getChannel().existingProducts
-                .valuesOfModified()) {
+        for (VeluxProduct product : bridgeParameters.actuators.getChannel().existingProducts.valuesOfModified()) {
             logger.trace("syncChannelsWithProducts(): actuator {} has changed values.", product.getProductName());
             ProductBridgeIndex productPbi = product.getBridgeProductIndex();
             logger.trace("syncChannelsWithProducts(): bridge index is {}.", productPbi);
@@ -452,28 +465,29 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
                     logger.trace("syncChannelsWithProducts(): channel {} not found.", channelUID);
                     continue;
                 }
-                if (!channel2VeluxActuator.get(channelUID).isKnown()) {
+                Thing2VeluxActuator actuator = channel2VeluxActuator.get(channelUID);
+                if (!actuator.isKnown()) {
                     logger.trace("syncChannelsWithProducts(): channel {} not registered on bridge.", channelUID);
                     continue;
                 }
-                ProductBridgeIndex channelPbi = channel2VeluxActuator.get(channelUID).getProductBridgeIndex();
+                ProductBridgeIndex channelPbi = actuator.getProductBridgeIndex();
                 if (!channelPbi.equals(productPbi)) {
                     continue;
                 }
                 // Handle value inversion
-                boolean isInverted = channel2VeluxActuator.get(channelUID).isInverted();
+                boolean isInverted = actuator.isInverted();
                 logger.trace("syncChannelsWithProducts(): isInverted is {}.", isInverted);
-                VeluxProductPosition position = new VeluxProductPosition(product.getCurrentPosition());
+                VeluxProductPosition position = new VeluxProductPosition(product.getDisplayPosition());
                 if (position.isValid()) {
                     PercentType positionAsPercent = position.getPositionAsPercentType(isInverted);
                     logger.debug("syncChannelsWithProducts(): updating channel {} to position {}%.", channelUID,
                             positionAsPercent);
                     updateState(channelUID, positionAsPercent);
-                } else {
-                    logger.trace("syncChannelsWithProducts(): update of channel {} to position {} skipped.", channelUID,
-                            position);
+                    break;
                 }
-                break outer;
+                logger.trace("syncChannelsWithProducts(): update channel {} to 'UNDEFINED'.", channelUID);
+                updateState(channelUID, UnDefType.UNDEF);
+                break;
             }
         }
         logger.trace("syncChannelsWithProducts(): resetting dirty flag.");
@@ -490,7 +504,7 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
         logger.debug("handleCommand({},{}) called.", channelUID.getAsString(), command);
 
         // Background execution of bridge related I/O
-        handleScheduler.execute(() -> {
+        getTaskExecutor().execute(() -> {
             logger.trace("handleCommand.scheduled({}) Start work with calling handleCommandScheduled().",
                     Thread.currentThread());
             handleCommandScheduled(channelUID, command);
@@ -570,7 +584,9 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
                         case BRIDGE_FIRMWARE:
                             newState = ChannelBridgeFirmware.handleRefresh(channelUID, channelId, this);
                             break;
-                        case BRIDGE_IPADDRESS:
+                        case BRIDGE_ADDRESS:
+                            // delete legacy property name entry (if any) and fall through
+                            ThingProperty.setValue(this, VeluxBridgeConfiguration.BRIDGE_IPADDRESS, null);
                         case BRIDGE_SUBNETMASK:
                         case BRIDGE_DEFAULTGW:
                         case BRIDGE_DHCP:
@@ -599,6 +615,7 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
                         case ACTUATOR_LIMIT_MINIMUM:
                         case ROLLERSHUTTER_LIMIT_MINIMUM:
                         case WINDOW_LIMIT_MINIMUM:
+                            // note: the empty string ("") below is intentional
                             newState = ChannelActuatorLimitation.handleRefresh(channelUID, "", this);
                             break;
                         case ACTUATOR_LIMIT_MAXIMUM:
@@ -624,11 +641,14 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
                     if (itemType.isChannel()) {
                         logger.debug("handleCommandScheduled(): updating channel {} to {}.", channelUID, newState);
                         updateState(channelUID, newState);
-                    }
-                    if (itemType.isProperty()) {
-                        logger.debug("handleCommandScheduled(): updating property {} to {}.", channelUID, newState);
-                        ThingProperty.setValue(this, itemType.getIdentifier(), newState.toString());
-
+                    } else if (itemType.isProperty()) {
+                        // if property value is 'unknown', null it completely
+                        String val = newState.toString();
+                        if (VeluxBindingConstants.UNKNOWN.equals(val)) {
+                            val = null;
+                        }
+                        logger.debug("handleCommandScheduled(): updating property {} to {}.", channelUID, val);
+                        ThingProperty.setValue(this, itemType.getIdentifier(), val);
                     }
                 } else {
                     logger.info("handleCommandScheduled({},{}): updating of item {} (type {}) failed.",
@@ -662,6 +682,20 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
                     case SCENE_ACTION:
                         ChannelSceneAction.handleCommand(channelUID, channelId, command, this);
                         break;
+
+                    /*
+                     * NOTA BENE: Setting of a scene silent mode is no longer supported via the KLF API (i.e. the
+                     * GW_SET_NODE_VELOCITY_REQ/CFM command set is no longer supported in the API), so the binding can
+                     * no longer explicitly support a Channel with such a function. Therefore the silent mode Channel
+                     * type was removed from the binding implementation.
+                     *
+                     * By contrast scene actions can still be called with a silent mode argument, so a silent mode
+                     * Configuration Parameter has been introduced as a means for the user to set this argument.
+                     *
+                     * Strictly speaking the following case statement will now never be called, so in theory it,
+                     * AND ALL THE CLASSES BEHIND, could be deleted from the binding CODE BASE. But out of prudence
+                     * it is retained anyway 'just in case'.
+                     */
                     case SCENE_SILENTMODE:
                         ChannelSceneSilentmode.handleCommand(channelUID, channelId, command, this);
                         break;
@@ -671,7 +705,7 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
                     case ACTUATOR_STATE:
                     case ROLLERSHUTTER_POSITION:
                     case WINDOW_POSITION:
-                        ChannelActuatorPosition.handleCommand(channelUID, channelId, command, this);
+                        newValue = ChannelActuatorPosition.handleCommand(channelUID, channelId, command, this);
                         break;
                     case ACTUATOR_LIMIT_MINIMUM:
                     case ROLLERSHUTTER_LIMIT_MINIMUM:
@@ -706,4 +740,84 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
                 new java.util.Date(thisBridge.lastSuccessfulCommunication()).toString());
         logger.trace("handleCommandScheduled({}) done.", Thread.currentThread());
     }
+
+    /**
+     * Register the exported actions
+     */
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singletonList(VeluxActions.class);
+    }
+
+    /**
+     * Exported method (called by an OpenHAB Rules Action) to issue a reboot command to the hub.
+     *
+     * @return true if the command could be issued
+     */
+    public boolean runReboot() {
+        logger.trace("runReboot() called on {}", getThing().getUID());
+        RunReboot bcp = thisBridge.bridgeAPI().runReboot();
+        if (bcp != null) {
+            // background execution of reboot process
+            getTaskExecutor().execute(() -> {
+                if (thisBridge.bridgeCommunicate(bcp)) {
+                    logger.info("Reboot command {}sucessfully sent to {}", bcp.isCommunicationSuccessful() ? "" : "un",
+                            getThing().getUID());
+                }
+            });
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Exported method (called by an OpenHAB Rules Action) to move an actuator relative to its current position
+     *
+     * @param nodeId the node to be moved
+     * @param relativePercent relative position change to the current position (-100% <= relativePercent <= +100%)
+     * @return true if the command could be issued
+     */
+    public boolean moveRelative(int nodeId, int relativePercent) {
+        logger.trace("moveRelative() called on {}", getThing().getUID());
+        RunProductCommand bcp = thisBridge.bridgeAPI().runProductCommand();
+        if (bcp != null) {
+            bcp.setNodeAndMainParameter(nodeId, new VeluxProductPosition(new PercentType(Math.abs(relativePercent)))
+                    .getAsRelativePosition((relativePercent >= 0)));
+            // background execution of moveRelative
+            getTaskExecutor().execute(() -> {
+                if (thisBridge.bridgeCommunicate(bcp)) {
+                    logger.trace("moveRelative() command {}sucessfully sent to {}",
+                            bcp.isCommunicationSuccessful() ? "" : "un", getThing().getUID());
+                }
+            });
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * If necessary initialise the task executor and return it
+     *
+     * @return the task executor
+     */
+    private ExecutorService getTaskExecutor() {
+        ExecutorService taskExecutor = this.taskExecutor;
+        if (taskExecutor == null || taskExecutor.isShutdown()) {
+            taskExecutor = this.taskExecutor = Executors.newSingleThreadExecutor(getThreadFactory());
+        }
+        return taskExecutor;
+    }
+
+    /**
+     * If necessary initialise the thread factory and return it
+     *
+     * @return the thread factory
+     */
+    public NamedThreadFactory getThreadFactory() {
+        NamedThreadFactory threadFactory = this.threadFactory;
+        if (threadFactory == null) {
+            threadFactory = new NamedThreadFactory(getThing().getUID().getAsString());
+        }
+        return threadFactory;
+    }
 }
index 6c056035f7fcb4b43a0f895ca90dc6a1fed2ecfa..f4c6063199ff5975eadac9b8719838947a6b4086 100644 (file)
@@ -115,6 +115,7 @@ public class VeluxHandler extends ExtendedBaseThingHandler {
             for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
                 logger.trace("handleConfigurationUpdate(): found modified config entry {}.",
                         configurationParameter.getKey());
+                configuration.put(configurationParameter.getKey(), configurationParameter.getValue());
             }
             // persist new configuration and reinitialize handler
             dispose();
index 17136c5d6c272c9370d4f252e25c44af439e3f49..56ee12079ccf0d1edc78ed7374c95aa1d4e20c37 100644 (file)
@@ -13,6 +13,7 @@
 package org.openhab.binding.velux.internal.handler.utils;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingUID;
@@ -61,7 +62,7 @@ public class ThingProperty {
      * @param propertyName defines the property which is to be modified,
      * @param propertyValue defines the new property value.
      */
-    public static void setValue(Thing thing, String propertyName, String propertyValue) {
+    public static void setValue(Thing thing, String propertyName, @Nullable String propertyValue) {
         thing.setProperty(propertyName, propertyValue);
         LOGGER.trace("setValue() {} set to {}.", propertyName, propertyValue);
         return;
@@ -75,7 +76,8 @@ public class ThingProperty {
      * @param propertyName defines the property which is to be modified.
      * @param propertyValue defines the new property value.
      */
-    public static void setValue(ExtendedBaseBridgeHandler bridgeHandler, String propertyName, String propertyValue) {
+    public static void setValue(ExtendedBaseBridgeHandler bridgeHandler, String propertyName,
+            @Nullable String propertyValue) {
         setValue(bridgeHandler.getThing(), propertyName, propertyValue);
     }
 
@@ -91,7 +93,7 @@ public class ThingProperty {
      * @param propertyValue defines the new property value.
      */
     public static void setValue(ExtendedBaseBridgeHandler bridgeHandler, ChannelUID channelUID, String propertyName,
-            String propertyValue) {
+            @Nullable String propertyValue) {
         ThingUID channelTUID = channelUID.getThingUID();
         Thing thingOfChannel = bridgeHandler.getThing().getThing(channelTUID);
         if (thingOfChannel == null) {
index 9f277ed1eeb23794a316eb8b2dad17540746c35b..51c18f312541a373bff65a31cfe51bf6d4745ef7 100644 (file)
@@ -121,10 +121,10 @@ public class VeluxExistingProducts {
             return false;
         }
         VeluxProduct thisProduct = this.get(bridgeProductIndex);
-        if (thisProduct.setState(productState) || thisProduct.setCurrentPosition(productPosition)
-                || thisProduct.setTarget(productTarget)) {
-            dirty = true;
-
+        dirty |= thisProduct.setState(productState);
+        dirty |= thisProduct.setCurrentPosition(productPosition);
+        dirty |= thisProduct.setTarget(productTarget);
+        if (dirty) {
             String uniqueIndex = thisProduct.isV2() ? thisProduct.getSerialNumber()
                     : thisProduct.getProductUniqueIndex();
             logger.trace("update(): updating by UniqueIndex {}.", uniqueIndex);
index c129af00c92ef8194a01fd70f075d8c0a36f21bc..fc3d8f4c7e619d10b5f1bce21c420ca11d562770 100644 (file)
@@ -57,6 +57,24 @@ public class VeluxProduct {
         }
     }
 
+    // State (of movement) of an actuator
+    public static enum State {
+        NON_EXECUTING(0),
+        ERROR(1),
+        NOT_USED(2),
+        WAITING_FOR_POWER(3),
+        EXECUTING(4),
+        DONE(5),
+        MANUAL_OVERRIDE(0x80),
+        UNKNOWN(0xFF);
+
+        public final int value;
+
+        private State(int value) {
+            this.value = value;
+        }
+    }
+
     // Class internal
 
     private VeluxProductName name;
@@ -70,9 +88,9 @@ public class VeluxProduct {
     private int variation = 0;
     private int powerMode = 0;
     private String serialNumber = VeluxProductSerialNo.UNKNOWN;
-    private int state = 0;
+    private int state = State.UNKNOWN.value;
     private int currentPosition = 0;
-    private int target = 0;
+    private int targetPosition = 0;
     private int remainingTime = 0;
     private int timeStamp = 0;
 
@@ -143,7 +161,7 @@ public class VeluxProduct {
         this.serialNumber = serialNumber;
         this.state = state;
         this.currentPosition = currentPosition;
-        this.target = target;
+        this.targetPosition = target;
         this.remainingTime = remainingTime;
         this.timeStamp = timeStamp;
     }
@@ -155,7 +173,7 @@ public class VeluxProduct {
         if (this.v2) {
             return new VeluxProduct(this.name, this.typeId, this.bridgeProductIndex, this.order, this.placement,
                     this.velocity, this.variation, this.powerMode, this.serialNumber, this.state, this.currentPosition,
-                    this.target, this.remainingTime, this.timeStamp);
+                    this.targetPosition, this.remainingTime, this.timeStamp);
         } else {
             return new VeluxProduct(this.name, this.typeId, this.bridgeProductIndex);
         }
@@ -302,7 +320,7 @@ public class VeluxProduct {
      * @return <b>target</b> as type int shows the target position of the current operation.
      */
     public int getTarget() {
-        return target;
+        return targetPosition;
     }
 
     /**
@@ -310,12 +328,12 @@ public class VeluxProduct {
      * @return <b>modified</b> as boolean to signal a real modification.
      */
     public boolean setTarget(int newTarget) {
-        if (this.target == newTarget) {
+        if (this.targetPosition == newTarget) {
             return false;
         } else {
             logger.trace("setCurrentPosition(name={},index={}) target {} replaced by {}.", name.toString(),
-                    bridgeProductIndex.toInt(), this.target, newTarget);
-            this.target = newTarget;
+                    bridgeProductIndex.toInt(), this.targetPosition, newTarget);
+            this.targetPosition = newTarget;
             return true;
         }
     }
@@ -333,4 +351,35 @@ public class VeluxProduct {
     public int getTimeStamp() {
         return timeStamp;
     }
+
+    /**
+     * Returns the display position of the actuator.
+     * <li>As a general rule it returns <b>currentPosition</b>, except as follows..
+     * <li>If the actuator is in a motion state it returns <b>targetPosition</b>
+     * <li>If the motion state is 'done' but the currentPosition is invalid it returns <b>targetPosition</b>
+     * <li>If the manual override flag is set it returns the <b>unknown</b> position value
+     *
+     * @return The display position of the actuator
+     */
+    public int getDisplayPosition() {
+        // manual override flag set: position is 'unknown'
+        if ((state & State.MANUAL_OVERRIDE.value) != 0) {
+            return VeluxProductPosition.VPP_VELUX_UNKNOWN;
+        }
+        // only check other conditions if targetPosition is valid and differs from currentPosition
+        if ((targetPosition != currentPosition) && (targetPosition <= VeluxProductPosition.VPP_VELUX_MAX)
+                && (targetPosition >= VeluxProductPosition.VPP_VELUX_MIN)) {
+            int state = this.state & 0xf;
+            // actuator is in motion: for quicker UI update, return targetPosition
+            if ((state > State.ERROR.value) && (state < State.DONE.value)) {
+                return targetPosition;
+            }
+            // motion complete but currentPosition is not valid: return targetPosition
+            if ((state == State.DONE.value) && ((currentPosition > VeluxProductPosition.VPP_VELUX_MAX)
+                    || (currentPosition < VeluxProductPosition.VPP_VELUX_MIN))) {
+                return targetPosition;
+            }
+        }
+        return currentPosition;
+    }
 }
index af2f45c9b9fdb893b300069336cd1c50c6234499..85160f65e2987c5e8321f4e82a50633c4e38bb97 100644 (file)
@@ -58,12 +58,14 @@ public class VeluxProductPosition {
 
     private static final int VPP_OPENHAB_MIN = 0;
     private static final int VPP_OPENHAB_MAX = 100;
-    private static final int VPP_VELUX_MIN = 0x0000;
-    private static final int VPP_VELUX_MAX = 0xc800;
-    private static final int VPP_VELUX_UNKNOWN = 0xF7FF;
 
-    private static final int VPP_VELUX_PERCENTAGE_MIN = 0xc900;
-    private static final int VPP_VELUX_PERCENTAGE_MAX = 0xd0d0;
+    public static final int VPP_VELUX_MIN = 0x0000;
+    public static final int VPP_VELUX_MAX = 0xc800;
+    public static final int VPP_VELUX_UNKNOWN = 0xF7FF;
+
+    // relative mode commands
+    private static final int VPP_VELUX_RELATIVE_ORIGIN = 0xCCE8;
+    private static final int VPP_VELUX_RELATIVE_RANGE = 1000; // same for positive and negative offsets
 
     // Class internal
 
@@ -159,15 +161,8 @@ public class VeluxProductPosition {
 
     // Helper methods
 
-    public static int getRelativePositionAsVeluxType(boolean upwards, PercentType position) {
-        float result = (VPP_VELUX_PERCENTAGE_MAX + VPP_VELUX_PERCENTAGE_MIN) / 2;
-        if (upwards) {
-            result = result + (ONE * position.intValue() - VPP_OPENHAB_MIN) / (VPP_OPENHAB_MAX - VPP_OPENHAB_MIN)
-                    * ((VPP_VELUX_PERCENTAGE_MAX - VPP_VELUX_PERCENTAGE_MIN) / 2);
-        } else {
-            result = result - (ONE * position.intValue() - VPP_OPENHAB_MIN) / (VPP_OPENHAB_MAX - VPP_OPENHAB_MIN)
-                    * ((VPP_VELUX_PERCENTAGE_MAX - VPP_VELUX_PERCENTAGE_MIN) / 2);
-        }
-        return (int) result;
+    public int getAsRelativePosition(boolean positive) {
+        int offset = position.intValue() * VPP_VELUX_RELATIVE_RANGE / (VPP_OPENHAB_MAX - VPP_OPENHAB_MIN);
+        return positive ? VPP_VELUX_RELATIVE_ORIGIN + offset : VPP_VELUX_RELATIVE_ORIGIN - offset;
     }
 }
index 50f14479b3be22587d722f2cc7759139fcfd8a02..ceda5825f9ab4552b3e1d61d17c6804b9b260bd8 100644 (file)
@@ -42,7 +42,7 @@ import org.openhab.binding.velux.internal.VeluxBindingConstants;
 @NonNullByDefault
 public enum VeluxProductVelocity {
     DEFAULT((short) 0, "default"),
-    SILENT((short) 1, "short"),
+    SILENT((short) 1, "silent"),
     FAST((short) 2, "fast"),
     VELOCITY_NOT_AVAILABLE((short) 255, ""),
     UNDEFTYPE((short) 0xffff, VeluxBindingConstants.UNKNOWN);
@@ -69,7 +69,7 @@ public enum VeluxProductVelocity {
         return velocity;
     }
 
-    public static VeluxProductVelocity get(int velocity) {
+    public static VeluxProductVelocity get(short velocity) {
         return LOOKUPTYPEID2ENUM.getOrDefault(velocity, VeluxProductVelocity.UNDEFTYPE);
     }
 
index 4f6b264151eb7614394ff527b2316b504ff75d01..f0dfed6b1eadd79acff1ce9405e7dbf8879b460b 100644 (file)
                        <!-- Velux Bridge factory default -->
                        <default>velux123</default>
                </parameter>
-               <parameter name="timeoutMsecs" type="integer" min="1" step="1" max="60000">
+               <parameter name="timeoutMsecs" type="integer" min="500" step="1" max="5000">
                        <label>@text/config.velux.bridge.timeoutMsecs.label</label>
                        <description>@text/config.velux.bridge.timeoutMsecs.description</description>
                        <required>false</required>
-                       <default>500</default>
+                       <default>2000</default>
                        <advanced>true</advanced>
                </parameter>
                <parameter name="retries" type="integer" min="0" step="1" max="10">
@@ -57,7 +57,7 @@
                        <default>5</default>
                        <advanced>true</advanced>
                </parameter>
-               <parameter name="refreshMsecs" type="integer" min="1" step="1" max="60000">
+               <parameter name="refreshMsecs" type="integer" min="5000" step="1" max="60000">
                        <label>@text/config.velux.bridge.refreshMsecs.label</label>
                        <description>@text/config.velux.bridge.refreshMsecs.description</description>
                        <required>false</required>
index 53884c28c35142b4afe95bb79d1a617e57cee601..eb704bf802719655f0d7a4c698dc98e5e96cfb1e 100644 (file)
@@ -20,7 +20,7 @@
                        <channel id="limitMinimum" typeId="limitMinimum"/>
                        <channel id="limitMaximum" typeId="limitMaximum"/>
                </channels>
-               <representation-property>serialNumber</representation-property>
+               <representation-property>serial</representation-property>
                <config-description-ref uri="thing-type:velux:actuator"/>
        </thing-type>
 </thing:thing-descriptions>
index 51e6720b39eaf6ee42a7034a351d9cfcecb068d8..8524d7f717f761c7b4edfdebb836657af086966f 100644 (file)
@@ -17,5 +17,6 @@
                <properties>
                        <property name="bundleVersion">N/A</property>
                </properties>
+               <representation-property>bundleVersion</representation-property>
        </thing-type>
 </thing:thing-descriptions>
index 3eb97427d962ee2b5cbf52ad2946aae57975cfb1..c930a9cbf1150086e83c16a6d644f4e988183dcf 100644 (file)
@@ -32,6 +32,7 @@
                                <property name="check" />
                        -->
                </properties>
+               <representation-property>ipAddress</representation-property>
 
                <config-description-ref uri="bridge-type:velux:bridge"/>
 
index c8b8f8910395aaab46e35f88b94b152b62b057de..ed19d77c654f3bf813dc77190bae9ccbab53095d 100644 (file)
@@ -20,7 +20,7 @@
                        <channel id="limitMinimum" typeId="limitMinimum"/>
                        <channel id="limitMaximum" typeId="limitMaximum"/>
                </channels>
-               <representation-property>unique</representation-property>
+               <representation-property>serial</representation-property>
                <config-description-ref uri="thing-type:velux:rollershutter"/>
        </thing-type>
 </thing:thing-descriptions>
index 7abd5365b2117479c1861d38411cb86772751857..acfa7c11ad175694f19e6274eb1e3c4eca2fc6d4 100644 (file)
@@ -17,9 +17,8 @@
                <category>Blinds</category>
                <channels>
                        <channel id="action" typeId="action"/>
-                       <channel id="silentMode" typeId="silentMode"/>
                </channels>
-               <representation-property>unique</representation-property>
+               <representation-property>sceneName</representation-property>
                <config-description-ref uri="thing-type:velux:scene"/>
        </thing-type>
 </thing:thing-descriptions>
index f6f7ecec71125e748d69c861aa04d043072c6e6b..d4e8f4e2f53afb7dc74cfb55184ddd86d051787d 100644 (file)
@@ -20,7 +20,7 @@
                        <channel id="limitMinimum" typeId="limitMinimum"/>
                        <channel id="limitMaximum" typeId="limitMaximum"/>
                </channels>
-               <representation-property>serialNumber</representation-property>
+               <representation-property>serial</representation-property>
                <config-description-ref uri="thing-type:velux:window"/>
        </thing-type>
 </thing:thing-descriptions>