Skip to content

Commit

Permalink
[velux] Add an action to simultaneously set main and vane positions (o…
Browse files Browse the repository at this point in the history
…penhab#13199)

* [velux] add moveMainAndVane action
* [velux] add claridications to read me
* [velux] refactor actions and translate
* [velux] fix thing lookup

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
  • Loading branch information
andrewfg authored and nemerdaud committed Feb 28, 2023
1 parent 5eab3e6 commit dd293f2
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 81 deletions.
75 changes: 65 additions & 10 deletions bundles/org.openhab.binding.velux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand All @@ -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 <B>Velux</B> devices can be handled by this binding.
However, there might be some new ones which will be reported within the logfiles.
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand All @@ -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);
}

/**
Expand All @@ -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();
}

/**
Expand All @@ -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);
}
}
Loading

0 comments on commit dd293f2

Please sign in to comment.