[velux] Add an action to simultaneously set main and vane positions (#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>
This commit is contained in:
Andrew Fiddian-Green
2022-08-15 11:36:25 +01:00
committed by GitHub
parent bb3be91981
commit 41fcdd6c71
5 changed files with 223 additions and 81 deletions

View File

@@ -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;
}

View File

@@ -15,9 +15,11 @@ 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.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 @@ import org.slf4j.LoggerFactory;
*/
@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 class VeluxActions implements ThingActions, IVeluxActions {
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 class VeluxActions implements ThingActions, IVeluxActions {
*/
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 class VeluxActions implements ThingActions, IVeluxActions {
* @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 ((IVeluxActions) actions).moveRelative(nodeId, relativePercent);
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 ((VeluxActions) actions).moveMainAndVane(thingName, mainPercent, vanePercent);
}
}

View File

@@ -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.BridgeCommunicationProto
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<ChannelUID, Thing2VeluxActuator> channel2VeluxActuator = new ConcurrentHashMap<>();
public final Map<ChannelUID, Thing2VeluxActuator> channel2VeluxActuator = new ConcurrentHashMap<>();
/**
* Information retrieved by {@link VeluxBinding#VeluxBinding}.
@@ -819,7 +821,7 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
*/
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(VeluxActions.class);
return Set.of(VeluxActions.class);
}
/**
@@ -827,7 +829,7 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
*
* @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 class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
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<ChannelUID, Thing2VeluxActuator> entry : channel2VeluxActuator.entrySet()) {
if (thingName.equals(entry.getKey().getThingUID().getAsString())) {
return entry.getValue().getProductBridgeIndex();
}
}
return ProductBridgeIndex.UNKNOWN;
}
}

View File

@@ -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