diff --git a/bundles/org.openhab.binding.velux/README.md b/bundles/org.openhab.binding.velux/README.md index a5d7dd3566634..fc08919fc3521 100644 --- a/bundles/org.openhab.binding.velux/README.md +++ b/bundles/org.openhab.binding.velux/README.md @@ -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. diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxBinding.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxBinding.java index 287234b6074e0..cee51d80a9351 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxBinding.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxBinding.java @@ -79,7 +79,7 @@ public VeluxBinding(@Nullable VeluxBridgeConfiguration uncheckedConfiguration) { 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 VeluxBinding(@Nullable VeluxBridgeConfiguration uncheckedConfiguration) { 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 VeluxBinding(@Nullable VeluxBridgeConfiguration uncheckedConfiguration) { */ 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; } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxBindingConstants.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxBindingConstants.java index c647c8a8d1c77..739be2184638e 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxBindingConstants.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxBindingConstants.java @@ -91,10 +91,15 @@ public class VeluxBindingConstants { // Definitions of different set of Things public static final Set SUPPORTED_THINGS_BINDING = new HashSet<>(Arrays.asList(THING_TYPE_BINDING)); public static final Set SUPPORTED_THINGS_BRIDGE = new HashSet<>(Arrays.asList(THING_TYPE_BRIDGE)); + public static final Set 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 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"; diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxItemType.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxItemType.java index 6745e9c867762..bfb67867f2ce7 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxItemType.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/VeluxItemType.java @@ -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 index 0000000000000..e21a8b63bd4ba --- /dev/null +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/IVeluxActions.java @@ -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 index 0000000000000..ba0d0fc19b39d --- /dev/null +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/VeluxActions.java @@ -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 index 0000000000000..1ac3ba19bcf63 --- /dev/null +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/package-info.java @@ -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; diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridge.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridge.java index 4aef0887da169..e9e900a9bb53d 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridge.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridge.java @@ -20,6 +20,7 @@ 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."); diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridgeSetSceneVelocity.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridgeSetSceneVelocity.java index d9c021c60299f..c93ed1a1c6c8e 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridgeSetSceneVelocity.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridgeSetSceneVelocity.java @@ -31,6 +31,7 @@ * * @author Guenther Schreiner - Initial contribution */ +@Deprecated @NonNullByDefault public class VeluxBridgeSetSceneVelocity { private final Logger logger = LoggerFactory.getLogger(VeluxBridgeSetSceneVelocity.class); diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/BridgeAPI.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/BridgeAPI.java index 2807983fbac44..7f5bcc940ce31 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/BridgeAPI.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/BridgeAPI.java @@ -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 index 0000000000000..8121b4178b208 --- /dev/null +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/RunReboot.java @@ -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; + +/** + * Common bridge communication message scheme supported by the Velux bridge. + *

+ * Message semantic will be defined by the implementations according to the different comm paths. + *

+ * 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 { +} diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/SetSceneVelocity.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/SetSceneVelocity.java index 40da6cec51f4e..3df42814b5ec7 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/SetSceneVelocity.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/SetSceneVelocity.java @@ -29,6 +29,7 @@ * * @author Guenther Schreiner - Initial contribution. */ +@Deprecated @NonNullByDefault public abstract class SetSceneVelocity implements BridgeCommunicationProtocol { diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JCsetSceneVelocity.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JCsetSceneVelocity.java index e96f30f2d561e..d722e70d90cc2 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JCsetSceneVelocity.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JCsetSceneVelocity.java @@ -30,6 +30,7 @@ * * @author Guenther Schreiner - Initial contribution. */ +@Deprecated @NonNullByDefault class JCsetSceneVelocity extends SetSceneVelocity implements JsonBridgeCommunicationProtocol { diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JsonBridgeAPI.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JsonBridgeAPI.java index 99ace70ce080a..945c3a73bb08f 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JsonBridgeAPI.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JsonBridgeAPI.java @@ -31,6 +31,7 @@ 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 @@ public RunScene runScene() { public SetSceneVelocity setSceneVelocity() { return jsonSetSceneVelocity; } + + @Override + public @Nullable RunReboot runReboot() { + return null; + } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JsonVeluxBridge.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JsonVeluxBridge.java index b5092e1a8e06a..d9eb29bd761e3 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JsonVeluxBridge.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/json/JsonVeluxBridge.java @@ -21,9 +21,9 @@ 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); diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetWLANConfig.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetWLANConfig.java index d496cc12f9023..f3a3b705ca04e 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetWLANConfig.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetWLANConfig.java @@ -14,6 +14,7 @@ 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 @@ public boolean isCommunicationSuccessful() { 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 index 0000000000000..c4a8c92d1f556 --- /dev/null +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCrunReboot.java @@ -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: + * Reboot Bridge + *

+ * Common Message semantic: Communication with the bridge and (optionally) storing returned information within the class + * itself. + *

+ * 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}. + *

+ * Methods in addition to the mentioned interface: + *

    + *
  • {@link #runReboot} for rebooting the Velux hub.
  • + *
+ * + * @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; + } +} diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCsetSceneVelocity.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCsetSceneVelocity.java index da21d829e9f8a..68c14b2919e58 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCsetSceneVelocity.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCsetSceneVelocity.java @@ -44,7 +44,8 @@ * * @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); diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SlipBridgeAPI.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SlipBridgeAPI.java index 357ee8cc88cf3..05b15ab50d7e0 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SlipBridgeAPI.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SlipBridgeAPI.java @@ -31,6 +31,7 @@ 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 @@ public RunScene runScene() { public SetSceneVelocity setSceneVelocity() { return slipSetSceneVelocity; } + + @Override + public @Nullable RunReboot runReboot() { + return slipRunReboot; + } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SlipVeluxBridge.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SlipVeluxBridge.java index 16f4c1f35ec1a..6a5347b909c33 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SlipVeluxBridge.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SlipVeluxBridge.java @@ -12,13 +12,13 @@ */ 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.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 @@ /** * SLIP-based 2nd Level I/O interface towards the Velux bridge. *

- * 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. *

* In addition to the generic {@link VeluxBridge} methods, i.e. *

    @@ -53,9 +52,11 @@ *
* * @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 BridgeAPI bridgeAPI() { */ @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 long lastSuccessfulCommunication() { } /** - * Initializes a client/server communication towards Velux 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 success 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(); + } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/Connection.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/Connection.java index d6e32c297cb41..550c678583bdb 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/Connection.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/Connection.java @@ -12,13 +12,16 @@ */ 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 @@ * @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 synchronized byte[] io(VeluxBridgeInstance bridgeInstance, byte[] request 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 synchronized byte[] io(VeluxBridgeInstance bridgeInstance, byte[] request 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 synchronized byte[] io(VeluxBridgeInstance bridgeInstance, byte[] request 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 synchronized byte[] io(VeluxBridgeInstance bridgeInstance, byte[] request 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 boolean isAlive() { */ 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 synchronized void resetConnection() { } logger.trace("resetConnection() on {}: done.", host); } + + @Override + public void close() throws IOException { + resetConnection(); + } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/DataInputStreamWithTimeout.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/DataInputStreamWithTimeout.java index 967277e0f21fd..4116cfd6bb6e8 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/DataInputStreamWithTimeout.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/DataInputStreamWithTimeout.java @@ -12,124 +12,222 @@ */ 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. - *

- * 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. - *

- * 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 slipMessageQueue = new ConcurrentLinkedQueue<>(); + + private InputStream inputStream; + + private @Nullable String pollException = null; + private @Nullable Poller pollRunner = null; + private ExecutorService executor; + + private class Poller implements Callable { + + 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 len bytes of data from the contained - * input stream into an array of bytes. An attempt is made to read - * as many as len bytes, but a smaller number may be read, - * possibly zero. The number of bytes actually read is returned as an - * integer. - * - *

- * This method blocks until input data is available, end of file is - * detected, or an exception is thrown until the given timeout. - * - *

- * If len is zero, then no bytes are read and - * 0 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 -1 is returned; otherwise, at least one - * byte is read and stored into b. - * - *

- * The first byte read is stored into element b[off], the - * next one into b[off+1], and so on. The number of bytes read - * is, at most, equal to len. Let k be the number of - * bytes actually read; these bytes will be stored in elements - * b[off] through b[off+k-1], - * leaving elements b[off+k] through - * b[off+len-1] unaffected. + * Overridden method of {@link Closeable} interface. Stops the polling thread. * - *

- * In every case, elements b[0] through - * b[off] and elements b[off+len] through - * b[b.length-1] 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 b - * @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 - * -1 if there is no more data because the end - * of the stream has been reached. - * @exception NullPointerException If b is null. - * @exception IndexOutOfBoundsException If off is negative, - * len is negative, or len is greater than - * b.length - off - * @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 readTask = new Callable() { - @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 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(); } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/SSLconnection.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/SSLconnection.java index b93bfad8f477d..0e72d69ea5478 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/SSLconnection.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/io/SSLconnection.java @@ -12,9 +12,11 @@ */ 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 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 @@ * @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 @@ public void checkServerTrusted(X509Certificate @Nullable [] arg0, @Nullable Stri */ 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 @@ public void checkServerTrusted(X509Certificate @Nullable [] arg0, @Nullable Stri 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 @@ public void checkServerTrusted(X509Certificate @Nullable [] arg0, @Nullable Stri * @return ready 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. - *

- * 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.. - *

- * 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 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 */ - @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 @@ synchronized void send(byte[] packet) throws IOException { logger.trace("send() finished after having send {} bytes: {}", packet.length, sb.toString()); } } catch (IOException e) { - ready = false; + close(); throw e; } } @@ -198,47 +199,43 @@ synchronized void send(byte[] packet) throws IOException { /** * Method to verify that there is message from the bridge. * - * @return true 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 true 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 packet 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 @@ synchronized byte[] receive() throws IOException { * 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."); } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/utils/KLF200Response.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/utils/KLF200Response.java index 520ce67c72f51..219b58188050f 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/utils/KLF200Response.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/utils/KLF200Response.java @@ -65,7 +65,7 @@ public static void introLogging(Logger logger, short responseCommand, byte[] thi 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 @@ private static boolean check4matchingAnyID(Logger logger, String idName, int req 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 index 0000000000000..1c3511307f9bc --- /dev/null +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/discovery/VeluxBridgeFinder.java @@ -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> { + + 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 call() throws Exception { + final Set 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 discoverBridgeIpAddresses() { + @Nullable + Set 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> 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 discoverIpAddresses(ScheduledExecutorService scheduler) { + try (VeluxBridgeFinder finder = new VeluxBridgeFinder(scheduler)) { + return finder.discoverBridgeIpAddresses(); + } catch (IOException e) { + return new HashSet<>(); + } + } +} diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/discovery/VeluxDiscoveryService.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/discovery/VeluxDiscoveryService.java index 8aa033d38acb4..4eadd57ae1cda 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/discovery/VeluxDiscoveryService.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/discovery/VeluxDiscoveryService.java @@ -20,6 +20,7 @@ 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 @@ * * @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 @@ private void updateLocalization() { * 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 @@ protected void setTranslationProvider(TranslationProvider givenI18nProvider) { * @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 @@ protected synchronized void startScan() { 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 @@ protected synchronized void startScan() { public synchronized void stopScan() { logger.trace("stopScan() called."); super.stopScan(); - removeOlderResults(getTimestampOfLastScan()); logger.trace("stopScan() done."); } @@ -286,4 +286,21 @@ public boolean removeBridge(VeluxBridgeHandler bridge) { 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 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); + } + } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelActuatorPosition.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelActuatorPosition.java index 634326139de9b..c9b83f0633aa1 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelActuatorPosition.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelActuatorPosition.java @@ -19,6 +19,7 @@ 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.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 @@ private ChannelActuatorPosition() { 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()); } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelBridgeLANconfig.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelBridgeLANconfig.java index 0a013a5c108dc..9281e5a81ae38 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelBridgeLANconfig.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelBridgeLANconfig.java @@ -14,11 +14,9 @@ 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 @@ private ChannelBridgeLANconfig() { 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: } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelBridgeWLANconfig.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelBridgeWLANconfig.java index 3857f88a672c7..180c39b475a5e 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelBridgeWLANconfig.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelBridgeWLANconfig.java @@ -14,12 +14,9 @@ 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 @@ private ChannelBridgeWLANconfig() { 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: } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelSceneSilentmode.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelSceneSilentmode.java index 876704a50d78c..2d688bbde0698 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelSceneSilentmode.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelSceneSilentmode.java @@ -42,6 +42,7 @@ * * @author Guenther Schreiner - Initial contribution. */ +@Deprecated @NonNullByDefault final class ChannelSceneSilentmode extends ChannelHandlerTemplate { private static final Logger LOGGER = LoggerFactory.getLogger(ChannelSceneSilentmode.class); diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/VeluxBridgeHandler.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/VeluxBridgeHandler.java index cf178211c424d..0fe7f2efa1c6f 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/VeluxBridgeHandler.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/VeluxBridgeHandler.java @@ -12,9 +12,12 @@ */ 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.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.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.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.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 @@ */ @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 void initialize() { 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 synchronized void dispose() { 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 @@ private void bridgeParamsUpdated() { 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 @@ private synchronized void refreshOpenHAB() { */ 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 @@ private void syncChannelsWithProducts() { 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 void handleCommand(ChannelUID channelUID, Command command) { 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 @@ private synchronized void handleCommandScheduled(ChannelUID channelUID, Command 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 @@ private synchronized void handleCommandScheduled(ChannelUID channelUID, Command 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 @@ private synchronized void handleCommandScheduled(ChannelUID channelUID, Command 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 @@ private synchronized void handleCommandScheduled(ChannelUID channelUID, Command 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 @@ private synchronized void handleCommandScheduled(ChannelUID channelUID, Command 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 @@ private synchronized void handleCommandScheduled(ChannelUID channelUID, Command new java.util.Date(thisBridge.lastSuccessfulCommunication()).toString()); logger.trace("handleCommandScheduled({}) done.", Thread.currentThread()); } + + /** + * Register the exported actions + */ + @Override + public Collection> 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; + } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/VeluxHandler.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/VeluxHandler.java index 6c056035f7fcb..f4c6063199ff5 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/VeluxHandler.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/VeluxHandler.java @@ -115,6 +115,7 @@ public void handleConfigurationUpdate(Map configurationParameter for (Entry 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(); diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/utils/ThingProperty.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/utils/ThingProperty.java index 17136c5d6c272..56ee12079ccf0 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/utils/ThingProperty.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/utils/ThingProperty.java @@ -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 @@ private 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 static void setValue(Thing thing, String propertyName, String propertyVal * @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 static void setValue(ExtendedBaseBridgeHandler bridgeHandler, String prop * @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) { diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxExistingProducts.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxExistingProducts.java index bfcd098368cc3..02b44eef5d72c 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxExistingProducts.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxExistingProducts.java @@ -123,10 +123,10 @@ public boolean update(ProductBridgeIndex bridgeProductIndex, int productState, i 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); diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProduct.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProduct.java index c129af00c92ef..fc3d8f4c7e619 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProduct.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProduct.java @@ -57,6 +57,24 @@ public String toString() { } } + // 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 String toString() { 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 VeluxProduct(VeluxProductName name, VeluxProductType typeId, ProductBridg 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 VeluxProduct clone() { 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 boolean setCurrentPosition(int newCurrentPosition) { * @return target as type int shows the target position of the current operation. */ public int getTarget() { - return target; + return targetPosition; } /** @@ -310,12 +328,12 @@ public int getTarget() { * @return modified 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 int getRemainingTime() { public int getTimeStamp() { return timeStamp; } + + /** + * Returns the display position of the actuator. + *

  • As a general rule it returns currentPosition, except as follows.. + *
  • If the actuator is in a motion state it returns targetPosition + *
  • If the motion state is 'done' but the currentPosition is invalid it returns targetPosition + *
  • If the manual override flag is set it returns the unknown 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; + } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductPosition.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductPosition.java index af2f45c9b9fdb..85160f65e2987 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductPosition.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductPosition.java @@ -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 String toString() { // 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; } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductVelocity.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductVelocity.java index 50f14479b3be2..ceda5825f9ab4 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductVelocity.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductVelocity.java @@ -42,7 +42,7 @@ @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 short getVelocity() { return velocity; } - public static VeluxProductVelocity get(int velocity) { + public static VeluxProductVelocity get(short velocity) { return LOOKUPTYPEID2ENUM.getOrDefault(velocity, VeluxProductVelocity.UNDEFTYPE); } diff --git a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/config/config.xml index 4f6b264151eb7..f0dfed6b1eadd 100644 --- a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/config/config.xml @@ -43,11 +43,11 @@ velux123 - + @text/config.velux.bridge.timeoutMsecs.description false - 500 + 2000 true @@ -57,7 +57,7 @@ 5 true - + @text/config.velux.bridge.refreshMsecs.description false diff --git a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/actuator.xml b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/actuator.xml index 53884c28c3514..eb704bf802719 100644 --- a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/actuator.xml +++ b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/actuator.xml @@ -20,7 +20,7 @@ - serialNumber + serial diff --git a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/binding.xml b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/binding.xml index 51e6720b39eaf..8524d7f717f76 100644 --- a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/binding.xml +++ b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/binding.xml @@ -17,5 +17,6 @@ N/A + bundleVersion diff --git a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/bridge.xml index 3eb97427d962e..c930a9cbf1150 100644 --- a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/bridge.xml @@ -32,6 +32,7 @@ --> + ipAddress diff --git a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/rollershutter.xml b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/rollershutter.xml index c8b8f8910395a..ed19d77c654f3 100644 --- a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/rollershutter.xml +++ b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/rollershutter.xml @@ -20,7 +20,7 @@ - unique + serial diff --git a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/scene.xml b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/scene.xml index 7abd5365b2117..acfa7c11ad175 100644 --- a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/scene.xml +++ b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/scene.xml @@ -17,9 +17,8 @@ Blinds - - unique + sceneName diff --git a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/window.xml b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/window.xml index f6f7ecec71125..d4e8f4e2f53af 100644 --- a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/window.xml +++ b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/window.xml @@ -20,7 +20,7 @@ - serialNumber + serial