diff --git a/bundles/org.openhab.binding.velux/README.md b/bundles/org.openhab.binding.velux/README.md index ce0820f823765..0dfc5e258ce1c 100644 --- a/bundles/org.openhab.binding.velux/README.md +++ b/bundles/org.openhab.binding.velux/README.md @@ -14,6 +14,19 @@ For details about the features, see the following websites: - [Velux](https://www.velux.com) - [Velux API](https://www.velux.com/api/klf200) +## Initial Configuration of Devices in the Hub + +This guide assumes that you have already configured your devices in the KLF200 hub. +When the KLF200 hub is started it provides a temporary private Wi-Fi Access Point to facilitate this configuration. +The Velux leaflet B) explains how to access the configuration web page via this temporary private Wi-Fi Access Point and configure your devices. +Note: ending the configuration process prematurely might lead to misconfiguration and require factory resetting your hub and/or devices. + +If you want to add devices to the hub later, you have to access the configuration web page via the temporary private Wi-Fi Access Point once more. +See the chapter "FAQ and Troubleshooting" below if you have any problems setting up the connection to openHAB again afterwards. + +Note: if any device connects to the temporary private Wi-Fi Access Point, it disables the normal LAN connection, thus preventing the binding from connecting. +So make sure this Wi-Fi AP is not permanently running (the default setting is that the AP will turn off after some time). + ## Supported Things The binding supports the following types of Thing. @@ -38,10 +51,6 @@ The binding will automatically discover Velux Bridges within the local network, 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. -Note: When the KLF200 hub is started it provides a temporary private Wi-Fi Access Point for initial configuration. -And if any device connects to this AP, it disables the normal LAN connection, thus preventing the binding from connecting. -So make sure this AP is not permanently on (the default setting is that the AP will turn off after some time). - ## Thing Configuration ### Thing Configuration for "bridge" @@ -172,8 +181,6 @@ The supported Channels and their associated channel types are shown below. The `position`, `limitMinimum`, and `limitMaximum` are the same as described above for "window" Things. The `vanePosition` Channel only applies to Venetian blinds that have tiltable slats. -It can only have a valid position value if the main `position` of the Thing is fully down. -So, if `vanePosition` is commanded to a new value, this will automatically cause the main `position` to move to the fully down position. ### Channels for "actuator" Things @@ -276,6 +283,42 @@ Frame label="Velux Windows" { See [velux.sitemap](doc/conf/sitemaps/velux.sitemap) for more examples. +### Rule for simultaneously moving the main position and the vane position + +This applies to shades or shutters that have both a main position and a vane / tilt position. +On such shades if one sends a vane position command followed shortly by a main position command (or vice versa) the second command will cause the first command to stop. +This problem is most problematic when the two commands are issued simultaneously by a single rule. +In order to solve this problem, there is a rule action to simultaneously set the main position and the vane position. + +_Warning: use this command carefully..._ + +The action is a command method that is called from within a rule. +The method is called with the following syntax `moveMainAndVane(thingName, mainPercent, vanePercent)`. +The meaning of the arguments is described in the table below. +The method returns a `Boolean` whose meaning is also described in the table below. + +| Argument | Type | Example | Description | +|-------------|---------|-------------------------------------|-----------------------------------------------------------------------------------------| +| thingName | String | "velux:rollershutter:hubid:thingid" | The thing name of the shutter. Must be a valid configured thing in the hub. | +| mainPercent | Integer | 75 | The target main position in percent. Integer between 0 and 100. | +| vanePercent | Integer | 25 | The target vane position in percent. Integer between 0 and 100. | +| return | Boolean | `true` | Is `true` if the command was sent sucessfully or `false` if any arguments were invalid. | + +Example: + +```java +rule "Simultaneously Move Main and Vane Positions" +when + ... +then + // note: "velux:klf200:hubid" shall be the thing name of your KLF 200 hub + val veluxActions = getActions("velux", "velux:klf200:hubid") + if (veluxActions !== null) { + val succeeded = veluxActions.moveMainAndVane("velux:rollershutter:hubid:thingid", 75, 25) + } +end +``` + ### Rule for closing windows after a period of time Especially in the colder months, it is advisable to close the window after adequate ventilation. @@ -480,11 +523,15 @@ Notes: - Velux bridges cannot be returned to version one of the firmware after being upgraded to version two. -## Is it possible to run the both communication methods in parallel? +## FAQ and troubleshooting -For environments with the firmware version 0.1.* on the gateway, the interaction with the bridge is limited to the HTTP/JSON based communication, of course. On the other hand, after upgrading the gateway firmware to version 2, it is possible to run the binding either using HTTP/JSON if there is a permanent connectivity towards the WLAN interface of the KLF200 or using SLIP towards the LAN interface of the gateway. For example the Raspberry PI can directly be connected via WLAN to the Velux gateway and providing the other services via the LAN interface (but not vice versa). +### Is it possible to run the both communication methods in parallel? -## Known Limitations +For environments with the firmware version 0.1.* on the gateway, the interaction with the bridge is limited to the HTTP/JSON based communication, of course. +On the other hand, after upgrading the gateway firmware to version 2, it is possible to run the binding either using HTTP/JSON if there is a permanent connectivity towards the WLAN interface of the KLF200 or using SLIP towards the LAN interface of the gateway. +For example the Raspberry PI can directly be connected via WLAN to the Velux gateway and providing the other services via the LAN interface (but not vice versa). + +### Known Limitations The communication based on HTTP/JSON is limited to one connection: If the binding is operational, you won't get access to the Web Frontend in parallel. @@ -493,7 +540,15 @@ The SLIP communication is limited to two connections in parallel, i.e. two diffe Both interfacing methods, HTTP/JSON and SLIP, can be run in parallel. Therefore, on the one hand you can use the Web Frontend for manual control and on the other hand a binding can do all automatic jobs. -## Unknown Velux devices +### Login sequence fails and Connection Refused + +If you get this error first make sure that you entered the right password (the one below SSID on the back of the hub). +If the error persists, it may be due to the temporary Wi-Fi Access Point blocking the LAN (as described above). +To recover from this, first disable the bridge in the UI, disconnect the LAN cable, power cycle your KLF200 and wait a few minutes. +Then reconnect the LAN cable and re-enable the bridge in the UI again. +DO NOT try to connect anything to the temporary Wi-Fi Access Point during this process!! + +### Unknown Velux devices All known Velux devices can be handled by this binding. However, there might be some new ones which will be reported within the logfiles. 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 deleted file mode 100644 index 9f46c3ba3369d..0000000000000 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/action/IVeluxActions.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.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 index 45a20311e8ba6..e31c6eb767a15 100644 --- 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 @@ -15,9 +15,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler; +import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex; 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.library.types.PercentType; import org.openhab.core.thing.binding.ThingActions; import org.openhab.core.thing.binding.ThingActionsScope; import org.openhab.core.thing.binding.ThingHandler; @@ -31,7 +33,7 @@ */ @ThingActionsScope(name = "velux") @NonNullByDefault -public class VeluxActions implements ThingActions, IVeluxActions { +public class VeluxActions implements ThingActions { private final Logger logger = LoggerFactory.getLogger(VeluxActions.class); @@ -49,38 +51,42 @@ public void setThingHandler(@Nullable ThingHandler handler) { 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() + @RuleAction(label = "@text/action.reboot.label", description = "@text/action.reboot.descr") + public @ActionOutput(name = "running", type = "java.lang.Boolean", label = "@text/action.run.label", description = "@text/action.run.descr") Boolean rebootBridge() throws IllegalStateException { logger.trace("rebootBridge(): action called"); - VeluxBridgeHandler bridge = bridgeHandler; - if (bridge == null) { + VeluxBridgeHandler bridgeHandler = this.bridgeHandler; + if (bridgeHandler == null) { throw new IllegalStateException("Bridge instance is null"); } - return bridge.runReboot(); + return bridgeHandler.rebootBridge(); } - @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 { + @RuleAction(label = "@text/action.moveRelative.label", description = "@text/action.moveRelative.descr") + public @ActionOutput(name = "running", type = "java.lang.Boolean", label = "@text/action.run.label", description = "@text/action.run.descr") Boolean moveRelative( + @ActionInput(name = "nodeId", label = "@text/action.node.label", description = "@text/action.node.descr") @Nullable String nodeId, + @ActionInput(name = "relativePercent", label = "@text/action.relative.label", description = "@text/action.relative.descr") @Nullable String relativePercent) + throws NumberFormatException, IllegalStateException, IllegalArgumentException { logger.trace("moveRelative(): action called"); - VeluxBridgeHandler bridge = bridgeHandler; - if (bridge == null) { + VeluxBridgeHandler bridgeHandler = this.bridgeHandler; + if (bridgeHandler == null) { throw new IllegalStateException("Bridge instance is null"); } + if (nodeId == null) { + throw new IllegalArgumentException("Node Id is null"); + } int node = Integer.parseInt(nodeId); if (node < 0 || node > 200) { throw new NumberFormatException("Node Id out of range"); } + if (relativePercent == null) { + throw new IllegalArgumentException("Relative Percent is null"); + } int relPct = Integer.parseInt(relativePercent); if (Math.abs(relPct) > 100) { throw new NumberFormatException("Relative Percent out of range"); } - return bridge.moveRelative(node, relPct); + return bridgeHandler.moveRelative(node, relPct); } /** @@ -93,10 +99,10 @@ public void setThingHandler(@Nullable ThingHandler handler) { */ public static Boolean rebootBridge(@Nullable ThingActions actions) throws IllegalArgumentException, IllegalStateException { - if (!(actions instanceof IVeluxActions)) { + if (!(actions instanceof VeluxActions)) { throw new IllegalArgumentException("Unsupported action"); } - return ((IVeluxActions) actions).rebootBridge(); + return ((VeluxActions) actions).rebootBridge(); } /** @@ -107,14 +113,67 @@ public static Boolean rebootBridge(@Nullable ThingActions actions) * @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 + * @throws NumberFormatException if either of nodeId or relativePercent is not an integer, or out of range */ - public static Boolean moveRelative(@Nullable ThingActions actions, String nodeId, String relativePercent) + public static Boolean moveRelative(@Nullable ThingActions actions, @Nullable String nodeId, + @Nullable String relativePercent) throws IllegalArgumentException, NumberFormatException, IllegalStateException { - if (!(actions instanceof IVeluxActions)) { + if (!(actions instanceof VeluxActions)) { + throw new IllegalArgumentException("Unsupported action"); + } + return ((VeluxActions) actions).moveRelative(nodeId, relativePercent); + } + + @RuleAction(label = "@text/action.moveMainAndVane.label", description = "@text/action.moveMainAndVane.descr") + public @ActionOutput(name = "running", type = "java.lang.Boolean", label = "@text/action.run.label", description = "@text/action.run.descr") Boolean moveMainAndVane( + @ActionInput(name = "thingName", label = "@text/action.thing.label", description = "@text/action.thing.descr") @Nullable String thingName, + @ActionInput(name = "mainPercent", label = "@text/action.main.label", description = "@text/action.main.descr") @Nullable Integer mainPercent, + @ActionInput(name = "vanePercent", label = "@text/action.vane.label", description = "@text/action.vane.descr") @Nullable Integer vanePercent) + throws NumberFormatException, IllegalArgumentException, IllegalStateException { + logger.trace("moveMainAndVane(thingName:'{}', mainPercent:{}, vanePercent:{}) action called", thingName, + mainPercent, vanePercent); + VeluxBridgeHandler bridgeHandler = this.bridgeHandler; + if (bridgeHandler == null) { + throw new IllegalStateException("Bridge instance is null"); + } + if (thingName == null) { + throw new IllegalArgumentException("Thing name is null"); + } + ProductBridgeIndex node = bridgeHandler.getProductBridgeIndex(thingName); + if (ProductBridgeIndex.UNKNOWN.equals(node)) { + throw new IllegalArgumentException("Bridge does not contain a thing with name " + thingName); + } + if (mainPercent == null) { + throw new IllegalArgumentException("Main perent is null"); + } + PercentType mainPercentType = new PercentType(mainPercent); + if (vanePercent == null) { + throw new IllegalArgumentException("Vane perent is null"); + } + PercentType vanePercenType = new PercentType(vanePercent); + return bridgeHandler.moveMainAndVane(node, mainPercentType, vanePercenType); + } + + /** + * Action to simultaneously move the shade main position and vane positions. + * + * + * @param actions ThingActions from the caller + * @param thingName the name of the thing to be moved (e.g. 'velux:rollershutter:hubid:thingid') + * @param mainPercent the desired main position (range 0..100) + * @param vanePercent the desired vane position (range 0..100) + * @return true if the command was sent + * @throws NumberFormatException if any of the arguments are not an integer + * @throws IllegalArgumentException if any of the arguments are invalid + * @throws IllegalStateException if anything else is wrong + */ + public static Boolean moveMainAndVane(@Nullable ThingActions actions, @Nullable String thingName, + @Nullable Integer mainPercent, @Nullable Integer vanePercent) + throws NumberFormatException, IllegalArgumentException, IllegalStateException { + if (!(actions instanceof VeluxActions)) { throw new IllegalArgumentException("Unsupported action"); } - return ((IVeluxActions) actions).moveRelative(nodeId, relativePercent); + return ((VeluxActions) actions).moveMainAndVane(thingName, mainPercent, vanePercent); } } 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 4127eaef59a58..e7b02e5a97ffa 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 @@ -13,8 +13,9 @@ package org.openhab.binding.velux.internal.handler; import java.util.Collection; -import java.util.Collections; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -43,6 +44,7 @@ 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.FunctionalParameters; import org.openhab.binding.velux.internal.bridge.slip.SlipVeluxBridge; import org.openhab.binding.velux.internal.config.VeluxBridgeConfiguration; import org.openhab.binding.velux.internal.development.Threads; @@ -143,14 +145,14 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel * ***** Default visibility Objects ***** */ - VeluxBridge thisBridge = myJsonBridge; + public VeluxBridge thisBridge = myJsonBridge; public BridgeParameters bridgeParameters = new BridgeParameters(); - Localization localization; + public Localization localization; /** * Mapping from ChannelUID to class Thing2VeluxActuator, which return Velux device information, probably cached. */ - Map channel2VeluxActuator = new ConcurrentHashMap<>(); + public final Map channel2VeluxActuator = new ConcurrentHashMap<>(); /** * Information retrieved by {@link VeluxBinding#VeluxBinding}. @@ -819,7 +821,7 @@ private synchronized void handleCommandCommsJob(ChannelUID channelUID, Command c */ @Override public Collection> getServices() { - return Collections.singletonList(VeluxActions.class); + return Set.of(VeluxActions.class); } /** @@ -827,7 +829,7 @@ public Collection> getServices() { * * @return true if the command could be issued */ - public boolean runReboot() { + public boolean rebootBridge() { logger.trace("runReboot() called on {}", getThing().getUID()); RunReboot bcp = thisBridge.bridgeAPI().runReboot(); if (bcp != null) { @@ -907,4 +909,52 @@ public NamedThreadFactory getThreadFactory() { public boolean isDisposing() { return disposing; } + + /** + * Exported method (called by an OpenHAB Rules Action) to simultaneously move the shade main position and the vane + * position. + * + * @param node the node index in the bridge. + * @param mainPosition the desired main position. + * @param vanePosition the desired vane position. + * @return true if the command could be issued. + */ + public Boolean moveMainAndVane(ProductBridgeIndex node, PercentType mainPosition, PercentType vanePosition) { + logger.trace("moveMainAndVane() called on {}", getThing().getUID()); + RunProductCommand bcp = thisBridge.bridgeAPI().runProductCommand(); + if (bcp != null) { + VeluxProduct product = existingProducts().get(node).clone(); + FunctionalParameters functionalParameters = null; + if (product.supportsVanePosition()) { + int vanePos = new VeluxProductPosition(vanePosition).getPositionAsVeluxType(); + product.setVanePosition(vanePos); + functionalParameters = product.getFunctionalParameters(); + } + VeluxProductPosition mainPos = new VeluxProductPosition(mainPosition); + bcp.setNodeIdAndParameters(node.toInt(), mainPos, functionalParameters); + submitCommunicationsJob(() -> { + if (thisBridge.bridgeCommunicate(bcp)) { + logger.trace("moveMainAndVane() command {}sucessfully sent to {}", + bcp.isCommunicationSuccessful() ? "" : "un", getThing().getUID()); + } + }); + return true; + } + return false; + } + + /** + * Get the bridge product index for a given thing name. + * + * @param thingName the thing name + * @return the bridge product index or ProductBridgeIndex.UNKNOWN if not found. + */ + public ProductBridgeIndex getProductBridgeIndex(String thingName) { + for (Entry entry : channel2VeluxActuator.entrySet()) { + if (thingName.equals(entry.getKey().getThingUID().getAsString())) { + return entry.getValue().getProductBridgeIndex(); + } + } + return ProductBridgeIndex.UNKNOWN; + } } diff --git a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/i18n/velux.properties b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/i18n/velux.properties index 6440a35f99895..3163c66d154df 100644 --- a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/i18n/velux.properties +++ b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/i18n/velux.properties @@ -162,3 +162,24 @@ runtime.bridge-offline-login-sequence-failed = Login sequence failed. # channelValue.check-integrity-failed = Integrity check failed: The following scenes are unused: channelValue.check-integrity-ok = Integrity check ok. All scenes are used within Items. +# +# Actions +# +action.reboot.label = Reboot Bridge +action.reboot.descr = Issues a reboot command to the KLF200 bridge +action.moveRelative.label = Move Relative +action.moveRelative.descr = Issues a relative move command to an actuator +action.moveMainAndVane.label = Move main and vane position simultaneously +action.moveMainAndVane.descr = Issues a simultaneous command to move both the main position and the vane position of a shade +action.run.label = Executing +action.run.descr = Indicates the command was issued +action.node.label = Node Id +action.node.descr = Actuator Id in the bridge +action.relative.label = Relative Percent +action.relative.descr = Position delta from current +action.thing.label = Thing Name +action.thing.descr = UID of the actuator thing to be moved +action.main.label = Main Percent +action.main.descr = Position percentage to move to +action.vane.label = Vane Percent +action.vane.descr = Vane position percentage to move to