From 9950fdc13c24715aafda89450441015dbbaabd00 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Sun, 31 Jul 2022 14:15:25 +0100 Subject: [PATCH] [velux] Add support for vane/slat position (#12618) Signed-off-by: Andrew Fiddian-Green Signed-off-by: Andras Uhrin --- bundles/org.openhab.binding.velux/README.md | 23 +- .../velux/internal/VeluxBindingConstants.java | 1 + .../binding/velux/internal/VeluxItemType.java | 1 + .../bridge/VeluxBridgeRunProductCommand.java | 63 -- .../internal/bridge/common/BridgeAPI.java | 3 + .../bridge/common/RunProductCommand.java | 15 +- .../internal/bridge/json/JsonBridgeAPI.java | 5 + .../bridge/slip/FunctionalParameters.java | 207 ++++ .../bridge/slip/SCgetHouseStatus.java | 89 +- .../internal/bridge/slip/SCgetProduct.java | 92 +- .../bridge/slip/SCgetProductStatus.java | 284 ++++++ .../internal/bridge/slip/SCgetProducts.java | 73 +- .../bridge/slip/SCrunProductCommand.java | 126 ++- .../internal/bridge/slip/SlipBridgeAPI.java | 6 + .../internal/bridge/slip/SlipVeluxBridge.java | 70 +- .../slip/io/DataInputStreamWithTimeout.java | 2 +- .../bridge/slip/utils/KLF200Response.java | 7 +- .../discovery/VeluxDiscoveryService.java | 3 +- .../internal/factory/VeluxHandlerFactory.java | 3 +- .../handler/ChannelActuatorPosition.java | 202 ++-- .../handler/ChannelVShutterPosition.java | 3 +- .../internal/handler/VeluxBridgeHandler.java | 43 +- .../handler/utils/Thing2VeluxActuator.java | 13 +- .../things/VeluxExistingProducts.java | 106 +- .../velux/internal/things/VeluxKLFAPI.java | 59 +- .../velux/internal/things/VeluxProduct.java | 374 +++++++- .../internal/things/VeluxProductPosition.java | 63 +- .../internal/things/VeluxProductType.java | 12 +- .../resources/OH-INF/i18n/velux.properties | 2 + .../main/resources/OH-INF/thing/channels.xml | 8 + .../resources/OH-INF/thing/rollershutter.xml | 1 + .../test/TestNotificationsAndDatabase.java | 907 ++++++++++++++++++ 32 files changed, 2399 insertions(+), 467 deletions(-) delete mode 100644 bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridgeRunProductCommand.java create mode 100644 bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/FunctionalParameters.java create mode 100644 bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetProductStatus.java create mode 100644 bundles/org.openhab.binding.velux/src/test/java/org/openhab/binding/velux/test/TestNotificationsAndDatabase.java diff --git a/bundles/org.openhab.binding.velux/README.md b/bundles/org.openhab.binding.velux/README.md index 7aa4290dda466..ce0820f823765 100644 --- a/bundles/org.openhab.binding.velux/README.md +++ b/bundles/org.openhab.binding.velux/README.md @@ -38,6 +38,10 @@ 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" @@ -135,7 +139,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" Things The supported Channels and their associated channel types are shown below. @@ -154,6 +158,23 @@ The `position` Channel indicates the open/close state of the window (resp. rolle - 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 "rollershutter" Things + +The supported Channels and their associated channel types are shown below. + +| Channel | Data Type | Description | +|--------------|---------------|-------------------------------------------------| +| position | Rollershutter | Actual position of the window or device. | +| limitMinimum | Rollershutter | Minimum limit position of the window or device. | +| limitMaximum | Rollershutter | Maximum limit position of the window or device. | +| vanePosition | Dimmer | Vane position of a Venetian blind. | + +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 The supported Channels and their associated channel types are shown below. 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 83937a9be08ba..31472d73a2863 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 @@ -142,6 +142,7 @@ public class VeluxBindingConstants { public static final String CHANNEL_ACTUATOR_SILENTMODE = "silentMode"; public static final String CHANNEL_ACTUATOR_LIMIT_MINIMUM = "limitMinimum"; public static final String CHANNEL_ACTUATOR_LIMIT_MAXIMUM = "limitMaximum"; + public static final String CHANNEL_VANE_POSITION = "vanePosition"; // List of all virtual shutter channel ids public static final String CHANNEL_VSHUTTER_POSITION = "vposition"; 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 dae9308c8d268..722f83396653f 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 @@ -102,6 +102,7 @@ public enum VeluxItemType { ROLLERSHUTTER_POSITION(VeluxBindingConstants.THING_TYPE_VELUX_ROLLERSHUTTER,VeluxBindingConstants.CHANNEL_ACTUATOR_POSITION, TypeFlavor.MANIPULATOR_SHUTTER), ROLLERSHUTTER_LIMIT_MINIMUM(VeluxBindingConstants.THING_TYPE_VELUX_ROLLERSHUTTER,VeluxBindingConstants.CHANNEL_ACTUATOR_LIMIT_MINIMUM,TypeFlavor.MANIPULATOR_SHUTTER), ROLLERSHUTTER_LIMIT_MAXIMUM(VeluxBindingConstants.THING_TYPE_VELUX_ROLLERSHUTTER,VeluxBindingConstants.CHANNEL_ACTUATOR_LIMIT_MAXIMUM,TypeFlavor.MANIPULATOR_SHUTTER), + ROLLERSHUTTER_VANE_POSITION(VeluxBindingConstants.THING_TYPE_VELUX_ROLLERSHUTTER,VeluxBindingConstants.CHANNEL_VANE_POSITION, TypeFlavor.MANIPULATOR_SHUTTER), // WINDOW_POSITION(VeluxBindingConstants.THING_TYPE_VELUX_WINDOW, VeluxBindingConstants.CHANNEL_ACTUATOR_POSITION, TypeFlavor.MANIPULATOR_SHUTTER), WINDOW_LIMIT_MINIMUM(VeluxBindingConstants.THING_TYPE_VELUX_WINDOW, VeluxBindingConstants.CHANNEL_ACTUATOR_LIMIT_MINIMUM,TypeFlavor.MANIPULATOR_SHUTTER), diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridgeRunProductCommand.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridgeRunProductCommand.java deleted file mode 100644 index 7b58c0bb6c69b..0000000000000 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/VeluxBridgeRunProductCommand.java +++ /dev/null @@ -1,63 +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.bridge; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.velux.internal.bridge.common.RunProductCommand; -import org.openhab.binding.velux.internal.things.VeluxProductPosition; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link VeluxBridgeRunProductCommand} represents a complete set of transactions - * for executing a scene defined on the Velux bridge. - *

- * It provides a method {@link VeluxBridgeRunProductCommand#sendCommand} for sending a parameter change command. - * Any parameters are controlled by {@link org.openhab.binding.velux.internal.config.VeluxBridgeConfiguration}. - * - * @see VeluxBridgeProvider - * - * @author Guenther Schreiner - Initial contribution - */ -@NonNullByDefault -public class VeluxBridgeRunProductCommand { - private final Logger logger = LoggerFactory.getLogger(VeluxBridgeRunProductCommand.class); - - // Class access methods - - /** - * Login into bridge, instruct the bridge to pass a command towards an actuator based - * on a well-prepared environment of a {@link VeluxBridgeProvider}. - * - * @param bridge Initialized Velux bridge handler. - * @param nodeId Number of Actuator to be modified. - * @param value Target value for Actuator main parameter. - * @return true if successful, and false otherwise. - */ - public boolean sendCommand(VeluxBridge bridge, int nodeId, VeluxProductPosition value) { - logger.trace("sendCommand(nodeId={},value={}) called.", nodeId, value); - - boolean success = false; - RunProductCommand bcp = bridge.bridgeAPI().runProductCommand(); - if (bcp != null) { - int veluxValue = value.getPositionAsVeluxType(); - - bcp.setNodeAndMainParameter(nodeId, veluxValue); - if (bridge.bridgeCommunicate(bcp) && bcp.isCommunicationSuccessful()) { - success = true; - } - } - logger.debug("sendCommand() finished {}.", (success ? "successfully" : "with failure")); - return success; - } -} 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 7cbfab07d9438..fc97c18044201 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 @@ -107,4 +107,7 @@ public interface BridgeAPI { @Nullable RunReboot runReboot(); + + @Nullable + GetProduct getProductStatus(); } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/RunProductCommand.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/RunProductCommand.java index 9d0d6b04704b3..4bf608ffcd360 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/RunProductCommand.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/common/RunProductCommand.java @@ -13,6 +13,9 @@ package org.openhab.binding.velux.internal.bridge.common; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.velux.internal.bridge.slip.FunctionalParameters; +import org.openhab.binding.velux.internal.things.VeluxProductPosition; /** * Common bridge communication message scheme supported by the Velux bridge. @@ -22,7 +25,7 @@ * In addition to the common methods defined by {@link BridgeCommunicationProtocol} * each protocol-specific implementation has to provide the following methods: *

    - *
  • {@link #setNodeAndMainParameter} for defining the intended node and the main parameter value. + *
  • {@link #setNodeIdAndParameters} for defining the intended node and the main parameter value. *
* * @see BridgeCommunicationProtocol @@ -35,9 +38,11 @@ public abstract class RunProductCommand implements BridgeCommunicationProtocol { /** * Modifies the state of an actuator * - * @param actuatorId Gateway internal actuator identifier (zero to 199). - * @param parameterValue target device state. - * @return reference to the class instance. + * @param nodeId Gateway internal actuator identifier (zero to 199). + * @param mainParameter target device state. + * @param functionalParameters the target Functional Parameters. + * @return true if the method succeeds */ - public abstract RunProductCommand setNodeAndMainParameter(int actuatorId, int parameterValue); + public abstract boolean setNodeIdAndParameters(int nodeId, @Nullable VeluxProductPosition mainParameter, + @Nullable FunctionalParameters functionalParameters); } 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 e85cebe91e5af..b035397c16641 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 @@ -211,4 +211,9 @@ public SetSceneVelocity setSceneVelocity() { public @Nullable RunReboot runReboot() { return null; } + + @Override + public @Nullable GetProduct getProductStatus() { + return null; + } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/FunctionalParameters.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/FunctionalParameters.java new file mode 100644 index 0000000000000..8457c65aa8e65 --- /dev/null +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/FunctionalParameters.java @@ -0,0 +1,207 @@ +/** + * 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.bridge.slip; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.velux.internal.bridge.slip.utils.Packet; +import org.openhab.binding.velux.internal.things.VeluxProductPosition; + +/** + * Implementation of API Functional Parameters. + * Supports an array of of four Functional Parameter values { FP1 .. FP4 } + * + * @author Andrew Fiddian-Green - Initial contribution. + */ + +@NonNullByDefault +public class FunctionalParameters { + private static final int FUNCTIONAL_PARAMETER_COUNT = 4; + + private final int[] values; + + /** + * Private constructor to create a FunctionalParameters instance with all empty values. + */ + private FunctionalParameters() { + values = new int[FUNCTIONAL_PARAMETER_COUNT]; + Arrays.fill(values, VeluxProductPosition.VPP_VELUX_UNKNOWN); + } + + /** + * Public constructor to create a FunctionalParameters instance from one specific value at one specific index. + */ + public FunctionalParameters(int index, int newValue) { + this(); + values[index] = newValue; + } + + @Override + public FunctionalParameters clone() { + FunctionalParameters result = new FunctionalParameters(); + System.arraycopy(values, 0, result.values, 0, FUNCTIONAL_PARAMETER_COUNT); + return result; + } + + @Override + public String toString() { + return String.format("{0x%04X, 0x%04X, 0x%04X, 0x%04X}", values[0], values[1], values[2], values[3]); + } + + /** + * Return the functional parameter value at index. + * + * @return the value at the index. + */ + public int getValue(int index) { + return values[index]; + } + + /** + * Create a Functional Parameters instance from the merger of the data in 'foundationFunctionalParameters' and + * 'substituteFunctionalParameters'. Invalid parameter values are not merged. If either + * 'foundationFunctionalParameters' or 'substituteFunctionalParameters' is null, the merge includes only the data + * from the non null parameter set. And if both sets of parameters are null then the result is also null. + * + * @param foundationFunctionalParameters the Functional Parameters to be used as the foundation for the merge. + * @param substituteFunctionalParameters the Functional Parameters to substituted on top (if they can be). + * @return a new Functional Parameters class instance containing the merged data. + */ + public static @Nullable FunctionalParameters createMergeSubstitute( + @Nullable FunctionalParameters foundationFunctionalParameters, + @Nullable FunctionalParameters substituteFunctionalParameters) { + if (foundationFunctionalParameters == null && substituteFunctionalParameters == null) { + return null; + } + FunctionalParameters result = new FunctionalParameters(); + if (foundationFunctionalParameters != null) { + for (int i = 0; i < FUNCTIONAL_PARAMETER_COUNT; i++) { + if (isNormalPosition(foundationFunctionalParameters.values[i])) { + result.values[i] = foundationFunctionalParameters.values[i]; + } + } + } + if (substituteFunctionalParameters != null) { + for (int i = 0; i < FUNCTIONAL_PARAMETER_COUNT; i++) { + if (isNormalPosition(substituteFunctionalParameters.values[i])) { + result.values[i] = substituteFunctionalParameters.values[i]; + } + } + } + return result; + } + + /** + * Check if a given parameter value is a normal actuator position value (i.e. 0x0000 .. 0xC800). + * + * @param paramValue the value to be checked. + * @return true if it is a normal actuator position value. + */ + public static boolean isNormalPosition(int paramValue) { + return (paramValue >= VeluxProductPosition.VPP_VELUX_MIN) && (paramValue <= VeluxProductPosition.VPP_VELUX_MAX); + } + + /** + * Create a FunctionalParameters instance from the given Packet. Where the parameters are packed into an array of + * two byte integer values. + * + * @param responseData the Packet to read from. + * @param startPosition the read starting position. + * @return this object. + */ + public static @Nullable FunctionalParameters readArray(Packet responseData, int startPosition) { + int pos = startPosition; + boolean isValid = false; + FunctionalParameters result = new FunctionalParameters(); + for (int i = 0; i < FUNCTIONAL_PARAMETER_COUNT; i++) { + int value = responseData.getTwoByteValue(pos); + if (isNormalPosition(value)) { + result.values[i] = value; + isValid = true; + } + pos = pos + 2; + } + return isValid ? result : null; + } + + /** + * Create a FunctionalParameters instance from the given Packet. Where the parameters are packed into an array of + * three byte records each comprising a one byte index followed by a two byte integer value. + * + * @param responseData the Packet to read from. + * @param startPosition the read starting position. + * @return this object. + */ + public static @Nullable FunctionalParameters readArrayIndexed(Packet responseData, int startPosition) { + int pos = startPosition; + boolean isValid = false; + FunctionalParameters result = new FunctionalParameters(); + for (int i = 0; i < FUNCTIONAL_PARAMETER_COUNT; i++) { + int index = responseData.getOneByteValue(pos) - 1; + pos++; + if ((index >= 0) && (index < FUNCTIONAL_PARAMETER_COUNT)) { + int value = responseData.getTwoByteValue(pos); + if (isNormalPosition(value)) { + result.values[index] = value; + isValid = true; + } + } + pos = pos + 2; + } + return isValid ? result : null; + } + + /** + * Write the Functional Parameters to the given packet. Only writes normal valid position values. + * + * @param requestData the Packet to write to. + * @param startPosition the write starting position. + * @return fpIndex a bit map that indicates which of the written Functional Parameters contains a normal valid + * position value. + */ + public int writeArray(Packet requestData, int startPosition) { + int bitMask = 0b10000000; + int pos = startPosition; + int fpIndex = 0; + for (int i = 0; i < FUNCTIONAL_PARAMETER_COUNT; i++) { + if (isNormalPosition(values[i])) { + fpIndex |= bitMask; + requestData.setTwoByteValue(pos, values[i]); + } + pos = pos + 2; + bitMask = bitMask >>> 1; + } + return fpIndex; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof FunctionalParameters)) { + return false; + } + FunctionalParameters other = (FunctionalParameters) obj; + for (int i = 0; i < FUNCTIONAL_PARAMETER_COUNT; i++) { + if (values[i] != other.values[i]) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(values); + }; +} diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetHouseStatus.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetHouseStatus.java index 97212c61fbbc1..7eed6b1faad15 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetHouseStatus.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetHouseStatus.java @@ -19,6 +19,9 @@ import org.openhab.binding.velux.internal.bridge.slip.utils.Packet; 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; +import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex; +import org.openhab.binding.velux.internal.things.VeluxProductName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,7 +52,8 @@ * @author Guenther Schreiner - Initial contribution. */ @NonNullByDefault -class SCgetHouseStatus extends GetHouseStatus implements BridgeCommunicationProtocol, SlipBridgeCommunicationProtocol { +public class SCgetHouseStatus extends GetHouseStatus + implements BridgeCommunicationProtocol, SlipBridgeCommunicationProtocol { private final Logger logger = LoggerFactory.getLogger(SCgetHouseStatus.class); private static final String DESCRIPTION = "Retrieve House Status"; @@ -71,10 +75,8 @@ class SCgetHouseStatus extends GetHouseStatus implements BridgeCommunicationProt private boolean success = false; private boolean finished = false; - private int ntfNodeID; - private int ntfState; - private int ntfCurrentPosition; - private int ntfTarget; + private Command creatorCommand = Command.UNDEFTYPE; + private VeluxProduct product = VeluxProduct.UNKNOWN; /* * =========================================================== @@ -103,32 +105,37 @@ public void setResponse(short responseCommand, byte[] thisResponseData, boolean success = false; finished = true; Packet responseData = new Packet(thisResponseData); - switch (Command.get(responseCommand)) { + Command responseCmd = Command.get(responseCommand); + switch (responseCmd) { case GW_NODE_STATE_POSITION_CHANGED_NTF: if (!KLF200Response.isLengthValid(logger, responseCommand, thisResponseData, 20)) { break; } - ntfNodeID = responseData.getOneByteValue(0); - ntfState = responseData.getOneByteValue(1); - ntfCurrentPosition = responseData.getTwoByteValue(2); - ntfTarget = responseData.getTwoByteValue(4); - @SuppressWarnings("unused") - int ntfFP1CurrentPosition = responseData.getTwoByteValue(6); - @SuppressWarnings("unused") - int ntfFP2CurrentPosition = responseData.getTwoByteValue(8); - @SuppressWarnings("unused") - int ntfFP3CurrentPosition = responseData.getTwoByteValue(10); - @SuppressWarnings("unused") - int ntfFP4CurrentPosition = responseData.getTwoByteValue(12); + int ntfNodeID = responseData.getOneByteValue(0); + int ntfState = responseData.getOneByteValue(1); + int ntfCurrentPosition = responseData.getTwoByteValue(2); + int ntfTarget = responseData.getTwoByteValue(4); + FunctionalParameters ntfFunctionalParameters = FunctionalParameters.readArray(responseData, 6); int ntfRemainingTime = responseData.getTwoByteValue(14); int ntfTimeStamp = responseData.getFourByteValue(16); - // Extracting information items - logger.trace("setResponse(): ntfNodeID={}.", ntfNodeID); - logger.trace("setResponse(): ntfState={}.", ntfState); - logger.trace("setResponse(): ntfCurrentPosition={}.", ntfCurrentPosition); - logger.trace("setResponse(): ntfTarget={}.", ntfTarget); - logger.trace("setResponse(): ntfRemainingTime={}.", ntfRemainingTime); - logger.trace("setResponse(): ntfTimeStamp={}.", ntfTimeStamp); + + if (logger.isTraceEnabled()) { + logger.trace("setResponse(): ntfNodeID={}.", ntfNodeID); + logger.trace("setResponse(): ntfState={}.", ntfState); + logger.trace("setResponse(): ntfCurrentPosition={}.", String.format("0x%04X", ntfCurrentPosition)); + logger.trace("setResponse(): ntfTarget={}.", String.format("0x%04X", ntfTarget)); + logger.trace("setResponse(): ntfFunctionalParameters={} (returns null).", ntfFunctionalParameters); + logger.trace("setResponse(): ntfRemainingTime={}.", ntfRemainingTime); + logger.trace("setResponse(): ntfTimeStamp={}.", ntfTimeStamp); + } + + // this BCP returns wrong functional parameters on some (e.g. Somfy) devices so return null instead + ntfFunctionalParameters = null; + + // create notification product with the returned values + product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(ntfNodeID), ntfState, + ntfCurrentPosition, ntfTarget, ntfFunctionalParameters, creatorCommand); + success = true; break; @@ -153,31 +160,19 @@ public boolean isCommunicationSuccessful() { * Methods in addition to the interface {@link BridgeCommunicationProtocol} */ - /** - * @return ntfNodeID returns the Actuator Id as int. - */ - public int getNtfNodeID() { - return ntfNodeID; - } - - /** - * @return ntfState returns the state of the Actuator as int. - */ - public int getNtfState() { - return ntfState; - } - - /** - * @return ntfCurrentPosition returns the current position of the Actuator as int. - */ - public int getNtfCurrentPosition() { - return ntfCurrentPosition; + public VeluxProduct getProduct() { + logger.trace("getProduct(): returning {}.", product); + return product; } /** - * @return ntfTarget returns the target position of the Actuator as int. + * Change the command id that identifies the API on which 'product' will be created. + * + * @param creatorCommand the API that will be used to create the product instance. + * @return this */ - public int getNtfTarget() { - return ntfTarget; + public SCgetHouseStatus setCreatorCommand(Command creatorCommand) { + this.creatorCommand = creatorCommand; + return this; } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetProduct.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetProduct.java index a375c7696dad8..ef4b7c001c790 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetProduct.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetProduct.java @@ -23,6 +23,7 @@ import org.openhab.binding.velux.internal.things.VeluxProductName; import org.openhab.binding.velux.internal.things.VeluxProductSerialNo; import org.openhab.binding.velux.internal.things.VeluxProductType; +import org.openhab.binding.velux.internal.things.VeluxProductType.ActuatorType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,7 +51,7 @@ * @author Guenther Schreiner - Initial contribution. */ @NonNullByDefault -class SCgetProduct extends GetProduct implements SlipBridgeCommunicationProtocol { +public class SCgetProduct extends GetProduct implements SlipBridgeCommunicationProtocol { private final Logger logger = LoggerFactory.getLogger(SCgetProduct.class); private static final String DESCRIPTION = "Retrieve Product"; @@ -151,62 +152,59 @@ public void setResponse(short responseCommand, byte[] thisResponseData, boolean } // Extracting information items int ntfNodeID = responseData.getOneByteValue(0); - logger.trace("setResponse(): ntfNodeID={}.", ntfNodeID); int ntfOrder = responseData.getTwoByteValue(1); - logger.trace("setResponse(): ntfOrder={}.", ntfOrder); int ntfPlacement = responseData.getOneByteValue(3); - logger.trace("setResponse(): ntfPlacement={}.", ntfPlacement); String ntfName = responseData.getString(4, 64); - logger.trace("setResponse(): ntfName={}.", ntfName); int ntfVelocity = responseData.getOneByteValue(68); - logger.trace("setResponse(): ntfVelocity={}.", ntfVelocity); int ntfNodeTypeSubType = responseData.getTwoByteValue(69); - logger.trace("setResponse(): ntfNodeTypeSubType={} ({}).", ntfNodeTypeSubType, - VeluxProductType.get(ntfNodeTypeSubType)); - logger.trace("setResponse(): derived product description={}.", - VeluxProductType.toString(ntfNodeTypeSubType)); int ntfProductGroup = responseData.getTwoByteValue(71); - logger.trace("setResponse(): ntfProductGroup={}.", ntfProductGroup); int ntfProductType = responseData.getOneByteValue(72); - logger.trace("setResponse(): ntfProductType={}.", ntfProductType); int ntfNodeVariation = responseData.getOneByteValue(73); - logger.trace("setResponse(): ntfNodeVariation={}.", ntfNodeVariation); int ntfPowerMode = responseData.getOneByteValue(74); - logger.trace("setResponse(): ntfPowerMode={}.", ntfPowerMode); int ntfBuildNumber = responseData.getOneByteValue(75); - logger.trace("setResponse(): ntfBuildNumber={}.", ntfBuildNumber); byte[] ntfSerialNumber = responseData.getByteArray(76, 8); - logger.trace("setResponse(): ntfSerialNumber={}.", ntfSerialNumber); int ntfState = responseData.getOneByteValue(84); - logger.trace("setResponse(): ntfState={}.", ntfState); int ntfCurrentPosition = responseData.getTwoByteValue(85); - logger.trace("setResponse(): ntfCurrentPosition={}.", ntfCurrentPosition); int ntfTarget = responseData.getTwoByteValue(87); - logger.trace("setResponse(): ntfTarget={}.", ntfTarget); - int ntfFP1CurrentPosition = responseData.getTwoByteValue(89); - logger.trace("setResponse(): ntfFP1CurrentPosition={}.", ntfFP1CurrentPosition); - int ntfFP2CurrentPosition = responseData.getTwoByteValue(91); - logger.trace("setResponse(): ntfFP2CurrentPosition={}.", ntfFP2CurrentPosition); - int ntfFP3CurrentPosition = responseData.getTwoByteValue(93); - logger.trace("setResponse(): ntfFP3CurrentPosition={}.", ntfFP3CurrentPosition); - int ntfFP4CurrentPosition = responseData.getTwoByteValue(95); - logger.trace("setResponse(): ntfFP4CurrentPosition={}.", ntfFP4CurrentPosition); + FunctionalParameters ntfFunctionalParameters = FunctionalParameters.readArray(responseData, 89); int ntfRemainingTime = responseData.getFourByteValue(97); - logger.trace("setResponse(): ntfRemainingTime={}.", ntfRemainingTime); int ntfTimeStamp = responseData.getFourByteValue(99); - logger.trace("setResponse(): ntfTimeStamp={}.", ntfTimeStamp); int ntfNbrOfAlias = responseData.getOneByteValue(103); - logger.trace("setResponse(): ntfNbrOfAlias={}.", ntfNbrOfAlias); int ntfAliasOne = responseData.getFourByteValue(104); - logger.trace("setResponse(): ntfAliasOne={}.", ntfAliasOne); int ntfAliasTwo = responseData.getFourByteValue(108); - logger.trace("setResponse(): ntfAliasTwo={}.", ntfAliasTwo); int ntfAliasThree = responseData.getFourByteValue(112); - logger.trace("setResponse(): ntfAliasThree={}.", ntfAliasThree); int ntfAliasFour = responseData.getFourByteValue(116); - logger.trace("setResponse(): ntfAliasFour={}.", ntfAliasFour); int ntfAliasFive = responseData.getFourByteValue(120); - logger.trace("setResponse(): ntfAliasFive={}.", ntfAliasFive); + + if (logger.isTraceEnabled()) { + logger.trace("setResponse(): ntfNodeID={}.", ntfNodeID); + logger.trace("setResponse(): ntfOrder={}.", ntfOrder); + logger.trace("setResponse(): ntfPlacement={}.", ntfPlacement); + logger.trace("setResponse(): ntfName={}.", ntfName); + logger.trace("setResponse(): ntfVelocity={}.", ntfVelocity); + logger.trace("setResponse(): ntfNodeTypeSubType={} ({}).", ntfNodeTypeSubType, + VeluxProductType.get(ntfNodeTypeSubType)); + logger.trace("setResponse(): derived product description={}.", + VeluxProductType.toString(ntfNodeTypeSubType)); + logger.trace("setResponse(): ntfProductGroup={}.", ntfProductGroup); + logger.trace("setResponse(): ntfProductType={}.", ntfProductType); + logger.trace("setResponse(): ntfNodeVariation={}.", ntfNodeVariation); + logger.trace("setResponse(): ntfPowerMode={}.", ntfPowerMode); + logger.trace("setResponse(): ntfBuildNumber={}.", ntfBuildNumber); + logger.trace("setResponse(): ntfSerialNumber={}.", VeluxProductSerialNo.toString(ntfSerialNumber)); + logger.trace("setResponse(): ntfState={}.", ntfState); + logger.trace("setResponse(): ntfCurrentPosition={}.", String.format("0x%04X", ntfCurrentPosition)); + logger.trace("setResponse(): ntfTarget={}.", String.format("0x%04X", ntfTarget)); + logger.trace("setResponse(): ntfFunctionalParameters={} (returns null).", ntfFunctionalParameters); + logger.trace("setResponse(): ntfRemainingTime={}.", ntfRemainingTime); + logger.trace("setResponse(): ntfTimeStamp={}.", ntfTimeStamp); + logger.trace("setResponse(): ntfNbrOfAlias={}.", ntfNbrOfAlias); + logger.trace("setResponse(): ntfAliasOne={}.", ntfAliasOne); + logger.trace("setResponse(): ntfAliasTwo={}.", ntfAliasTwo); + logger.trace("setResponse(): ntfAliasThree={}.", ntfAliasThree); + logger.trace("setResponse(): ntfAliasFour={}.", ntfAliasFour); + logger.trace("setResponse(): ntfAliasFive={}.", ntfAliasFive); + } if (!KLF200Response.check4matchingNodeID(logger, reqNodeID, ntfNodeID)) { break; @@ -216,17 +214,24 @@ public void setResponse(short responseCommand, byte[] thisResponseData, boolean ntfName = "#".concat(String.valueOf(ntfNodeID)); logger.debug("setResponse(): device provided invalid name, using '{}' instead.", ntfName); } - String commonSerialNumber = VeluxProductSerialNo.toString(ntfSerialNumber); + + String ntfSerialNumberString = VeluxProductSerialNo.toString(ntfSerialNumber); if (VeluxProductSerialNo.isInvalid(ntfSerialNumber)) { - commonSerialNumber = new String(ntfName); + ntfSerialNumberString = new String(ntfName); logger.debug("setResponse(): device provided invalid serial number, using name '{}' instead.", - commonSerialNumber); + ntfSerialNumberString); } + // this BCP returns wrong functional parameters on some (e.g. Somfy) devices so return null instead + ntfFunctionalParameters = null; + + // create notification product with the returned values product = new VeluxProduct(new VeluxProductName(ntfName), VeluxProductType.get(ntfNodeTypeSubType), - new ProductBridgeIndex(ntfNodeID), ntfOrder, ntfPlacement, ntfVelocity, ntfNodeVariation, - ntfPowerMode, commonSerialNumber, ntfState, ntfCurrentPosition, ntfTarget, ntfRemainingTime, - ntfTimeStamp); + ActuatorType.get(ntfNodeTypeSubType), new ProductBridgeIndex(ntfNodeID), ntfOrder, ntfPlacement, + ntfVelocity, ntfNodeVariation, ntfPowerMode, ntfSerialNumberString, ntfState, + ntfCurrentPosition, ntfTarget, ntfFunctionalParameters, ntfRemainingTime, ntfTimeStamp, + COMMAND); + success = true; break; @@ -255,13 +260,12 @@ public boolean isCommunicationSuccessful() { @Override public void setProductId(int nodeId) { logger.trace("setProductId({}) called.", nodeId); - this.reqNodeID = nodeId; - return; + reqNodeID = nodeId; } @Override public VeluxProduct getProduct() { - logger.trace("getProduct(): returning product {}.", product); + logger.trace("getProduct(): returning {}.", product); return product; } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetProductStatus.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetProductStatus.java new file mode 100644 index 0000000000000..2fd8d4f015dd5 --- /dev/null +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetProductStatus.java @@ -0,0 +1,284 @@ +/** + * 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.bridge.slip; + +import java.util.Random; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.velux.internal.bridge.common.GetProduct; +import org.openhab.binding.velux.internal.bridge.slip.utils.KLF200Response; +import org.openhab.binding.velux.internal.bridge.slip.utils.Packet; +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; +import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex; +import org.openhab.binding.velux.internal.things.VeluxProductName; +import org.openhab.binding.velux.internal.things.VeluxProductPosition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Protocol specific bridge communication supported by the Velux bridge: + * Retrieve Product Status + *

+ * This implements an alternate API set (vs. the API set used by ScgetProduct) for retrieving a product's status. This + * alternate API set was added to the code base because, when using ScgetProduct, some products (e.g. Somfy) would + * produce buggy values in their Functional Parameters when reporting their Vane Position. + *

+ * This API set is the one used (for example) by Home Assistant. + * + * @author Andrew Fiddian-Green - Initial contribution. + */ +@NonNullByDefault +public class SCgetProductStatus extends GetProduct implements SlipBridgeCommunicationProtocol { + private final Logger logger = LoggerFactory.getLogger(SCgetProductStatus.class); + + private static final String DESCRIPTION = "Retrieve Product Status"; + private static final Command COMMAND = Command.GW_STATUS_REQUEST_REQ; + + /* + * RunStatus and StatusReply parameter values (from KLF200 API specification) + */ + private static final int EXECUTION_COMPLETED = 0;// Execution is completed with no errors. + private static final int EXECUTION_FAILED = 1; // Execution has failed. (Get specifics in the following error code) + private static final int EXECUTION_ACTIVE = 2;// Execution is still active + private static final int UNKNOWN_STATUS_REPLY = 0x00; // Used to indicate unknown reply. + private static final int COMMAND_COMPLETED_OK = 0x01; + + /* + * =========================================================== + * Message Content Parameters + */ + + private int reqSessionID = 0; // The session id + private final int reqIndexArrayCount = 1; // One node will be addressed + private int reqNodeId = 1; // This is the node id + private final int reqStatusType = 1; // The current value + private final int reqFPI1 = 0xF0; // Functional Parameter Indicator 1 bit map (set to fetch { FP1 .. FP4 } + private final int reqFPI2 = 0; // Functional Parameter Indicator 2 bit map. + + /* + * =========================================================== + * Message Objects + */ + + private byte[] requestData = new byte[0]; + + /* + * =========================================================== + * Result Objects + */ + + private boolean success = false; + private boolean finished = false; + + private VeluxProduct product = VeluxProduct.UNKNOWN; + + public SCgetProductStatus() { + logger.debug("SCgetProductStatus(Constructor) called."); + Random rand = new Random(); + reqSessionID = rand.nextInt(0x0fff); + logger.debug("SCgetProductStatus(): starting session with the random number {}.", reqSessionID); + } + + /* + * =========================================================== + * Methods required for interface {@link BridgeCommunicationProtocol}. + */ + + @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() { + logger.trace("getRequestDataAsArrayOfBytes() returns data for retrieving node with id {}.", reqNodeId); + reqSessionID = (reqSessionID + 1) & 0xffff; + Packet request = new Packet(new byte[26]); + request.setTwoByteValue(0, reqSessionID); + request.setOneByteValue(2, reqIndexArrayCount); + request.setOneByteValue(3, reqNodeId); + request.setOneByteValue(23, reqStatusType); + request.setOneByteValue(24, reqFPI1); + request.setOneByteValue(25, reqFPI2); + requestData = request.toByteArray(); + return requestData; + } + + @Override + public void setResponse(short responseCommand, byte[] thisResponseData, boolean isSequentialEnforced) { + KLF200Response.introLogging(logger, responseCommand, thisResponseData); + success = false; + finished = false; + Packet responseData = new Packet(thisResponseData); + Command responseCmd = Command.get(responseCommand); + switch (responseCmd) { + case GW_STATUS_REQUEST_CFM: + if (!KLF200Response.isLengthValid(logger, responseCommand, thisResponseData, 3)) { + finished = true; + break; + } + int cfmSessionID = responseData.getTwoByteValue(0); + int cfmStatus = responseData.getOneByteValue(2); + switch (cfmStatus) { + case 0: + logger.info("setResponse(): returned status: Error – Command rejected."); + finished = true; + break; + case 1: + logger.debug("setResponse(): returned status: OK - Command is accepted."); + if (!KLF200Response.check4matchingSessionID(logger, cfmSessionID, reqSessionID)) { + finished = true; + } + break; + default: + logger.warn("setResponse(): returned status={} (not defined).", cfmStatus); + finished = true; + break; + } + break; + + case GW_STATUS_REQUEST_NTF: + if (!KLF200Response.isLengthValid(logger, responseCommand, thisResponseData, 59)) { + finished = true; + break; + } + + // Extracting information items + int ntfSessionID = responseData.getTwoByteValue(0); + int ntfStatusID = responseData.getOneByteValue(2); + int ntfNodeID = responseData.getOneByteValue(3); + int ntfRunStatus = responseData.getOneByteValue(4); + int ntfStatusReply = responseData.getOneByteValue(5); + int ntfStatusType = responseData.getOneByteValue(6); + int ntfStatusCount = responseData.getOneByteValue(7); + int ntfFirstParameterIndex = responseData.getOneByteValue(8); + int ntfFirstParameter = responseData.getTwoByteValue(9); + FunctionalParameters ntfFunctionalParameters = FunctionalParameters.readArrayIndexed(responseData, 11); + + if (logger.isTraceEnabled()) { + logger.trace("setResponse(): ntfSessionID={}.", ntfSessionID); + logger.trace("setResponse(): ntfStatusID={}.", ntfStatusID); + logger.trace("setResponse(): ntfNodeID={}.", ntfNodeID); + logger.trace("setResponse(): ntfRunStatus={}.", ntfRunStatus); + logger.trace("setResponse(): ntfStatusReply={}.", ntfStatusReply); + logger.trace("setResponse(): ntfStatusType={}.", ntfStatusType); + logger.trace("setResponse(): ntfStatusCount={}.", ntfStatusCount); + logger.trace("setResponse(): ntfFirstParameterIndex={}.", ntfFirstParameterIndex); + logger.trace("setResponse(): ntfFirstParameter={}.", String.format("0x%04X", ntfFirstParameter)); + logger.trace("setResponse(): ntfFunctionalParameters={}.", ntfFunctionalParameters); + } + + if (!KLF200Response.check4matchingNodeID(logger, reqNodeId, ntfNodeID)) { + break; + } + + int ntfCurrentPosition; + if ((ntfStatusCount > 0) && (ntfFirstParameterIndex == 0)) { + ntfCurrentPosition = ntfFirstParameter; + } else { + ntfCurrentPosition = VeluxProductPosition.VPP_VELUX_UNKNOWN; + } + + int ntfState; + switch (ntfRunStatus) { + case EXECUTION_ACTIVE: + ntfState = VeluxProduct.ProductState.EXECUTING.value; + break; + case EXECUTION_COMPLETED: + ntfState = VeluxProduct.ProductState.DONE.value; + break; + case EXECUTION_FAILED: + default: + switch (ntfStatusReply) { + case UNKNOWN_STATUS_REPLY: + ntfState = VeluxProduct.ProductState.UNKNOWN.value; + break; + case COMMAND_COMPLETED_OK: + ntfState = VeluxProduct.ProductState.DONE.value; + break; + default: + ntfState = VeluxProduct.ProductState.ERROR.value; + } + break; + } + + // create notification product with the returned values + product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(ntfNodeID), ntfState, + ntfCurrentPosition, VeluxProductPosition.VPP_VELUX_IGNORE, ntfFunctionalParameters, COMMAND); + + success = true; + if (!isSequentialEnforced) { + logger.trace( + "setResponse(): skipping wait for more packets as sequential processing is not enforced."); + finished = true; + } + break; + + case GW_SESSION_FINISHED_NTF: + finished = true; + if (!KLF200Response.isLengthValid(logger, responseCommand, thisResponseData, 2)) { + break; + } + int finishedNtfSessionID = responseData.getTwoByteValue(0); + if (!KLF200Response.check4matchingSessionID(logger, finishedNtfSessionID, reqSessionID)) { + break; + } + logger.debug("setResponse(): finishedNtfSessionID={}.", finishedNtfSessionID); + success = 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; + } + + /* + * =========================================================== + * Methods in addition to the interface {@link BridgeCommunicationProtocol} + * and the abstract class {@link GetProduct} + */ + + @Override + public void setProductId(int nodeId) { + logger.trace("setProductId({}) called.", nodeId); + reqNodeId = nodeId; + } + + @Override + public VeluxProduct getProduct() { + logger.trace("getProduct(): returning {}.", product); + return product; + } +} diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetProducts.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetProducts.java index aab1d2744d842..79209ce2eadce 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetProducts.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCgetProducts.java @@ -23,6 +23,7 @@ import org.openhab.binding.velux.internal.things.VeluxProductName; import org.openhab.binding.velux.internal.things.VeluxProductSerialNo; import org.openhab.binding.velux.internal.things.VeluxProductType; +import org.openhab.binding.velux.internal.things.VeluxProductType.ActuatorType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -150,62 +151,59 @@ public void setResponse(short responseCommand, byte[] thisResponseData, boolean } // Extracting information items int ntfNodeID = responseData.getOneByteValue(0); - logger.trace("setResponse(): ntfNodeID={}.", ntfNodeID); int ntfOrder = responseData.getTwoByteValue(1); - logger.trace("setResponse(): ntfOrder={}.", ntfOrder); int ntfPlacement = responseData.getOneByteValue(3); - logger.trace("setResponse(): ntfPlacement={}.", ntfPlacement); String ntfName = responseData.getString(4, 64); - logger.trace("setResponse(): ntfName={}.", ntfName); int ntfVelocity = responseData.getOneByteValue(68); - logger.trace("setResponse(): ntfVelocity={}.", ntfVelocity); int ntfNodeTypeSubType = responseData.getTwoByteValue(69); - logger.trace("setResponse(): ntfNodeTypeSubType={} ({}).", ntfNodeTypeSubType, - VeluxProductType.get(ntfNodeTypeSubType)); - logger.trace("setResponse(): derived product description={}.", - VeluxProductType.toString(ntfNodeTypeSubType)); int ntfProductGroup = responseData.getOneByteValue(71); - logger.trace("setResponse(): ntfProductGroup={}.", ntfProductGroup); int ntfProductType = responseData.getOneByteValue(72); - logger.trace("setResponse(): ntfProductType={}.", ntfProductType); int ntfNodeVariation = responseData.getOneByteValue(73); - logger.trace("setResponse(): ntfNodeVariation={}.", ntfNodeVariation); int ntfPowerMode = responseData.getOneByteValue(74); - logger.trace("setResponse(): ntfPowerMode={}.", ntfPowerMode); int ntfBuildNumber = responseData.getOneByteValue(75); - logger.trace("setResponse(): ntfBuildNumber={}.", ntfBuildNumber); byte[] ntfSerialNumber = responseData.getByteArray(76, 8); - logger.trace("setResponse(): ntfSerialNumber={}.", ntfSerialNumber); int ntfState = responseData.getOneByteValue(84); - logger.trace("setResponse(): ntfState={}.", ntfState); int ntfCurrentPosition = responseData.getTwoByteValue(85); - logger.trace("setResponse(): ntfCurrentPosition={}.", ntfCurrentPosition); int ntfTarget = responseData.getTwoByteValue(87); - logger.trace("setResponse(): ntfTarget={}.", ntfTarget); - int ntfFP1CurrentPosition = responseData.getTwoByteValue(89); - logger.trace("setResponse(): ntfFP1CurrentPosition={}.", ntfFP1CurrentPosition); - int ntfFP2CurrentPosition = responseData.getTwoByteValue(91); - logger.trace("setResponse(): ntfFP2CurrentPosition={}.", ntfFP2CurrentPosition); - int ntfFP3CurrentPosition = responseData.getTwoByteValue(93); - logger.trace("setResponse(): ntfFP3CurrentPosition={}.", ntfFP3CurrentPosition); - int ntfFP4CurrentPosition = responseData.getTwoByteValue(95); - logger.trace("setResponse(): ntfFP4CurrentPosition={}.", ntfFP4CurrentPosition); + FunctionalParameters ntfFunctionalParameters = FunctionalParameters.readArray(responseData, 89); int ntfRemainingTime = responseData.getTwoByteValue(97); - logger.trace("setResponse(): ntfRemainingTime={}.", ntfRemainingTime); int ntfTimeStamp = responseData.getFourByteValue(99); - logger.trace("setResponse(): ntfTimeStamp={}.", ntfTimeStamp); int ntfNbrOfAlias = responseData.getOneByteValue(103); - logger.trace("setResponse(): ntfNbrOfAlias={}.", ntfNbrOfAlias); int ntfAliasOne = responseData.getFourByteValue(104); - logger.trace("setResponse(): ntfAliasOne={}.", ntfAliasOne); int ntfAliasTwo = responseData.getFourByteValue(108); - logger.trace("setResponse(): ntfAliasTwo={}.", ntfAliasTwo); int ntfAliasThree = responseData.getFourByteValue(112); - logger.trace("setResponse(): ntfAliasThree={}.", ntfAliasThree); int ntfAliasFour = responseData.getFourByteValue(116); - logger.trace("setResponse(): ntfAliasFour={}.", ntfAliasFour); int ntfAliasFive = responseData.getFourByteValue(120); - logger.trace("setResponse(): ntfAliasFive={}.", ntfAliasFive); + + if (logger.isTraceEnabled()) { + logger.trace("setResponse(): ntfNodeID={}.", ntfNodeID); + logger.trace("setResponse(): ntfOrder={}.", ntfOrder); + logger.trace("setResponse(): ntfPlacement={}.", ntfPlacement); + logger.trace("setResponse(): ntfName={}.", ntfName); + logger.trace("setResponse(): ntfVelocity={}.", ntfVelocity); + logger.trace("setResponse(): ntfNodeTypeSubType={} ({}).", ntfNodeTypeSubType, + VeluxProductType.get(ntfNodeTypeSubType)); + logger.trace("setResponse(): derived product description={}.", + VeluxProductType.toString(ntfNodeTypeSubType)); + logger.trace("setResponse(): ntfProductGroup={}.", ntfProductGroup); + logger.trace("setResponse(): ntfProductType={}.", ntfProductType); + logger.trace("setResponse(): ntfNodeVariation={}.", ntfNodeVariation); + logger.trace("setResponse(): ntfPowerMode={}.", ntfPowerMode); + logger.trace("setResponse(): ntfBuildNumber={}.", ntfBuildNumber); + logger.trace("setResponse(): ntfSerialNumber={}.", VeluxProductSerialNo.toString(ntfSerialNumber)); + logger.trace("setResponse(): ntfState={}.", ntfState); + logger.trace("setResponse(): ntfCurrentPosition={}.", String.format("0x%04X", ntfCurrentPosition)); + logger.trace("setResponse(): ntfTarget={}.", String.format("0x%04X", ntfTarget)); + logger.trace("setResponse(): ntfFunctionalParameters={}.", ntfFunctionalParameters); + logger.trace("setResponse(): ntfRemainingTime={}.", ntfRemainingTime); + logger.trace("setResponse(): ntfTimeStamp={}.", ntfTimeStamp); + logger.trace("setResponse(): ntfNbrOfAlias={}.", ntfNbrOfAlias); + logger.trace("setResponse(): ntfAliasOne={}.", ntfAliasOne); + logger.trace("setResponse(): ntfAliasTwo={}.", ntfAliasTwo); + logger.trace("setResponse(): ntfAliasThree={}.", ntfAliasThree); + logger.trace("setResponse(): ntfAliasFour={}.", ntfAliasFour); + logger.trace("setResponse(): ntfAliasFive={}.", ntfAliasFive); + } if ((ntfName.length() == 0) || ntfName.startsWith("_")) { ntfName = "#".concat(String.valueOf(ntfNodeID)); @@ -220,9 +218,10 @@ public void setResponse(short responseCommand, byte[] thisResponseData, boolean } VeluxProduct product = new VeluxProduct(new VeluxProductName(ntfName), - VeluxProductType.get(ntfNodeTypeSubType), new ProductBridgeIndex(ntfNodeID), ntfOrder, - ntfPlacement, ntfVelocity, ntfNodeVariation, ntfPowerMode, commonSerialNumber, ntfState, - ntfCurrentPosition, ntfTarget, ntfRemainingTime, ntfTimeStamp); + VeluxProductType.get(ntfNodeTypeSubType), ActuatorType.get(ntfNodeTypeSubType), + new ProductBridgeIndex(ntfNodeID), ntfOrder, ntfPlacement, ntfVelocity, ntfNodeVariation, + ntfPowerMode, commonSerialNumber, ntfState, ntfCurrentPosition, ntfTarget, + ntfFunctionalParameters, ntfRemainingTime, ntfTimeStamp, COMMAND); if (nextProductArrayItem < totalNumberOfProducts) { productArray[nextProductArrayItem++] = product; } else { diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCrunProductCommand.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCrunProductCommand.java index b043b016b66ae..1b2a0cf912aa6 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCrunProductCommand.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/bridge/slip/SCrunProductCommand.java @@ -15,11 +15,18 @@ import java.util.Random; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.velux.internal.bridge.common.RunProductCommand; import org.openhab.binding.velux.internal.bridge.slip.utils.KLF200Response; import org.openhab.binding.velux.internal.bridge.slip.utils.Packet; 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; +import org.openhab.binding.velux.internal.things.VeluxProduct.DataSource; +import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex; +import org.openhab.binding.velux.internal.things.VeluxProduct.ProductState; +import org.openhab.binding.velux.internal.things.VeluxProductName; +import org.openhab.binding.velux.internal.things.VeluxProductPosition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,7 +53,7 @@ * @author Guenther Schreiner - Initial contribution. */ @NonNullByDefault -class SCrunProductCommand extends RunProductCommand implements SlipBridgeCommunicationProtocol { +public class SCrunProductCommand extends RunProductCommand implements SlipBridgeCommunicationProtocol { private final Logger logger = LoggerFactory.getLogger(SCrunProductCommand.class); private static final String DESCRIPTION = "Send Command to Actuator"; @@ -70,6 +77,7 @@ class SCrunProductCommand extends RunProductCommand implements SlipBridgeCommuni private int reqPL03 = 0; // unused private int reqPL47 = 0; // unused private int reqLockTime = 0; // 30 seconds + private @Nullable FunctionalParameters reqFunctionalParameters = null; /* * =========================================================== @@ -86,6 +94,8 @@ class SCrunProductCommand extends RunProductCommand implements SlipBridgeCommuni private boolean success = false; private boolean finished = false; + private VeluxProduct product = VeluxProduct.UNKNOWN; + /* * =========================================================== * Constructor Method @@ -120,10 +130,15 @@ public CommandNumber getRequestCommand() { public byte[] getRequestDataAsArrayOfBytes() { Packet request = new Packet(new byte[66]); reqSessionID = (reqSessionID + 1) & 0xffff; + request.setTwoByteValue(0, reqSessionID); request.setOneByteValue(2, reqCommandOriginator); request.setOneByteValue(3, reqPriorityLevel); request.setOneByteValue(4, reqParameterActive); + + FunctionalParameters reqFunctionalParameters = this.reqFunctionalParameters; + reqFPI1 = reqFunctionalParameters != null ? reqFunctionalParameters.writeArray(request, 9) : 0; + request.setOneByteValue(5, reqFPI1); request.setOneByteValue(6, reqFPI2); request.setTwoByteValue(7, reqMainParameter); @@ -133,24 +148,39 @@ public byte[] getRequestDataAsArrayOfBytes() { request.setOneByteValue(63, reqPL03); request.setOneByteValue(64, reqPL47); request.setOneByteValue(65, reqLockTime); - logger.trace("getRequestDataAsArrayOfBytes(): ntfSessionID={}.", reqSessionID); - logger.trace("getRequestDataAsArrayOfBytes(): reqCommandOriginator={}.", reqCommandOriginator); - logger.trace("getRequestDataAsArrayOfBytes(): reqPriorityLevel={}.", reqPriorityLevel); - logger.trace("getRequestDataAsArrayOfBytes(): reqParameterActive={}.", reqParameterActive); - logger.trace("getRequestDataAsArrayOfBytes(): reqFPI1={}.", reqFPI1); - logger.trace("getRequestDataAsArrayOfBytes(): reqFPI2={}.", reqFPI2); - logger.trace("getRequestDataAsArrayOfBytes(): reqMainParameter={}.", reqMainParameter); - logger.trace("getRequestDataAsArrayOfBytes(): reqIndexArrayCount={}.", reqIndexArrayCount); - logger.trace("getRequestDataAsArrayOfBytes(): reqIndexArray01={}.", reqIndexArray01); - logger.trace("getRequestDataAsArrayOfBytes(): reqPriorityLevelLock={}.", reqPriorityLevelLock); - logger.trace("getRequestDataAsArrayOfBytes(): reqPL03={}.", reqPL03); - logger.trace("getRequestDataAsArrayOfBytes(): reqPL47={}.", reqPL47); - logger.trace("getRequestDataAsArrayOfBytes(): reqLockTime={}.", reqLockTime); + requestData = request.toByteArray(); - logger.trace("getRequestDataAsArrayOfBytes() data is {}.", new Packet(requestData).toString()); + + if (logger.isTraceEnabled()) { + logger.trace("getRequestDataAsArrayOfBytes(): ntfSessionID={}.", hex(reqSessionID)); + logger.trace("getRequestDataAsArrayOfBytes(): reqCommandOriginator={}.", hex(reqCommandOriginator)); + logger.trace("getRequestDataAsArrayOfBytes(): reqPriorityLevel={}.", hex(reqPriorityLevel)); + logger.trace("getRequestDataAsArrayOfBytes(): reqParameterActive={}.", hex(reqParameterActive)); + logger.trace("getRequestDataAsArrayOfBytes(): reqFPI1={}.", bin(reqFPI1)); + logger.trace("getRequestDataAsArrayOfBytes(): reqFPI2={}.", bin(reqFPI2)); + logger.trace("getRequestDataAsArrayOfBytes(): reqMainParameter={}.", hex(reqMainParameter)); + logger.trace("getRequestDataAsArrayOfBytes(): reqFunctionalParameters={}.", reqFunctionalParameters); + logger.trace("getRequestDataAsArrayOfBytes(): reqIndexArrayCount={}.", hex(reqIndexArrayCount)); + logger.trace("getRequestDataAsArrayOfBytes(): reqIndexArray01={} (reqNodeId={}).", reqIndexArray01, + reqIndexArray01); + logger.trace("getRequestDataAsArrayOfBytes(): reqPriorityLevelLock={}.", hex(reqPriorityLevelLock)); + logger.trace("getRequestDataAsArrayOfBytes(): reqPL03={}.", hex(reqPL03)); + logger.trace("getRequestDataAsArrayOfBytes(): reqPL47={}.", hex(reqPL47)); + logger.trace("getRequestDataAsArrayOfBytes(): reqLockTime={}.", hex(reqLockTime)); + + logger.trace("getRequestDataAsArrayOfBytes() data is {}.", new Packet(requestData).toString()); + } return requestData; } + private String hex(int i) { + return Integer.toHexString(i); + } + + private String bin(int i) { + return Integer.toBinaryString(i); + } + @Override public void setResponse(short responseCommand, byte[] thisResponseData, boolean isSequentialEnforced) { KLF200Response.introLogging(logger, responseCommand, thisResponseData); @@ -201,15 +231,17 @@ public void setResponse(short responseCommand, byte[] thisResponseData, boolean int ntfRunStatus = responseData.getOneByteValue(7); int ntfStatusReply = responseData.getOneByteValue(8); int ntfInformationCode = responseData.getFourByteValue(9); - // Extracting information items - logger.debug("setResponse(): ntfSessionID={} (requested {}).", ntfSessionID, reqSessionID); - logger.debug("setResponse(): ntfStatusiD={}.", ntfStatusiD); - logger.debug("setResponse(): ntfIndex={}.", ntfIndex); - logger.debug("setResponse(): ntfNodeParameter={}.", ntfNodeParameter); - logger.debug("setResponse(): ntfParameterValue={}.", ntfParameterValue); - logger.debug("setResponse(): ntfRunStatus={}.", ntfRunStatus); - logger.debug("setResponse(): ntfStatusReply={}.", ntfStatusReply); - logger.debug("setResponse(): ntfInformationCode={}.", ntfInformationCode); + + if (logger.isTraceEnabled()) { + logger.trace("setResponse(): ntfSessionID={} (requested {}).", ntfSessionID, reqSessionID); + logger.trace("setResponse(): ntfStatusiD={}.", ntfStatusiD); + logger.trace("setResponse(): ntfIndex={}.", ntfIndex); + logger.trace("setResponse(): ntfNodeParameter={}.", ntfNodeParameter); + logger.trace("setResponse(): ntfParameterValue={}.", String.format("0x%04X", ntfParameterValue)); + logger.trace("setResponse(): ntfRunStatus={}.", ntfRunStatus); + logger.trace("setResponse(): ntfStatusReply={}.", ntfStatusReply); + logger.trace("setResponse(): ntfInformationCode={}.", ntfInformationCode); + } if (!KLF200Response.check4matchingSessionID(logger, ntfSessionID, reqSessionID)) { finished = true; @@ -253,11 +285,13 @@ public void setResponse(short responseCommand, byte[] thisResponseData, boolean finished = true; } - // Extracting information items - logger.debug("setResponse(): timeNtfSessionID={}.", timeNtfSessionID); - logger.debug("setResponse(): timeNtfIndex={}.", timeNtfIndex); - logger.debug("setResponse(): timeNtfNodeParameter={}.", timeNtfNodeParameter); - logger.debug("setResponse(): timeNtfSeconds={}.", timeNtfSeconds); + if (logger.isDebugEnabled()) { + logger.debug("setResponse(): timeNtfSessionID={}.", timeNtfSessionID); + logger.debug("setResponse(): timeNtfIndex={}.", timeNtfIndex); + logger.debug("setResponse(): timeNtfNodeParameter={}.", timeNtfNodeParameter); + logger.debug("setResponse(): timeNtfSeconds={}.", timeNtfSeconds); + } + if (!isSequentialEnforced) { logger.trace( "setResponse(): skipping wait for more packets as sequential processing is not enforced."); @@ -303,10 +337,34 @@ public boolean isCommunicationSuccessful() { */ @Override - public SCrunProductCommand setNodeAndMainParameter(int nodeId, int value) { - logger.debug("setNodeAndMainParameter({}) called.", nodeId); - this.reqIndexArray01 = nodeId; - this.reqMainParameter = value; - return this; + public boolean setNodeIdAndParameters(int nodeId, @Nullable VeluxProductPosition mainParameter, + @Nullable FunctionalParameters functionalParameters) { + logger.debug("setNodeIdAndParameters({}) called.", nodeId); + + if ((mainParameter != null) || (functionalParameters != null)) { + reqIndexArray01 = nodeId; + + reqMainParameter = (mainParameter == null) ? VeluxProductPosition.VPP_VELUX_STOP + : mainParameter.getPositionAsVeluxType(); + + int setMainParameter = VeluxProductPosition.isValid(reqMainParameter) ? reqMainParameter + : VeluxProductPosition.VPP_VELUX_IGNORE; + + reqFunctionalParameters = functionalParameters; + + // create notification product that clones the new command positions + product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(reqIndexArray01), + ProductState.EXECUTING.value, setMainParameter, setMainParameter, reqFunctionalParameters, COMMAND) + .overrideDataSource(DataSource.BINDING); + + return true; + } + product = VeluxProduct.UNKNOWN; + return false; + } + + public VeluxProduct getProduct() { + logger.trace("getProduct(): returning {}.", product); + return product; } } 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 a4b705a435c48..5c62c3176923f 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 @@ -104,6 +104,7 @@ class SlipBridgeAPI implements BridgeAPI { private final SetProductLimitation slipSetProductLimitation = new SCsetLimitation(); private final SetSceneVelocity slipSetSceneVelocity = new SCsetSceneVelocity(); private final RunReboot slipRunReboot = new SCrunReboot(); + private final GetProduct slipGetProductStatus = new SCgetProductStatus(); /** * Constructor. @@ -217,4 +218,9 @@ public SetSceneVelocity setSceneVelocity() { public @Nullable RunReboot runReboot() { return slipRunReboot; } + + @Override + public @Nullable GetProduct getProductStatus() { + return slipGetProductStatus; + } } 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 26371b6a1ebd3..c1b0940725793 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 @@ -28,7 +28,6 @@ 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.VeluxProduct.ProductBridgeIndex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -206,15 +205,15 @@ private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProt final boolean isProtocolTraceEnabled = this.bridgeInstance.veluxBridgeConfiguration().isProtocolTraceEnabled; final long expiryTime = System.currentTimeMillis() + COMMUNICATION_TIMEOUT_MSECS; - // logger format string - final String loggerFmt = String.format("bridgeDirectCommunicate() [%s] %s => {} {} {}", - this.bridgeInstance.veluxBridgeConfiguration().ipAddress, txName); + // logger messages + final String logMsg = "bridgeDirectCommunicate() [{}] {} => {} {} {}"; + final String ipAddr = bridgeInstance.veluxBridgeConfiguration().ipAddress; if (isProtocolTraceEnabled) { Threads.findDeadlocked(); } - logger.debug(loggerFmt, "started =>", Thread.currentThread(), ""); + logger.debug(logMsg, ipAddr, txName, "started =>", Thread.currentThread(), ""); boolean looping = false; boolean success = false; @@ -225,41 +224,41 @@ private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProt // handling of the requests switch (txEnum) { case GW_OPENHAB_CLOSE: - logger.trace(loggerFmt, "shut down command", "=> executing", ""); + logger.trace(logMsg, ipAddr, txName, "shut down command", "=> executing", ""); connection.resetConnection(); success = true; break; case GW_OPENHAB_RECEIVEONLY: - logger.trace(loggerFmt, "receive-only mode", "=> checking messages", ""); + logger.trace(logMsg, ipAddr, txName, "receive-only mode", "=> checking messages", ""); if (!connection.isAlive()) { - logger.trace(loggerFmt, "no connection", "=> opening", ""); + logger.trace(logMsg, ipAddr, txName, "no connection", "=> opening", ""); looping = true; } else if (connection.isMessageAvailable()) { - logger.trace(loggerFmt, "message(s) waiting", "=> start reading", ""); + logger.trace(logMsg, ipAddr, txName, "message(s) waiting", "=> start reading", ""); looping = true; } else { - logger.trace(loggerFmt, "no waiting messages", "=> done", ""); + logger.trace(logMsg, ipAddr, txName, "no waiting messages", "=> done", ""); } rcvonly = true; break; default: - logger.trace(loggerFmt, "send mode", "=> preparing command", ""); + logger.trace(logMsg, ipAddr, txName, "send mode", "=> preparing command", ""); SlipEncoding slipEnc = new SlipEncoding(txCmd, txData); if (!slipEnc.isValid()) { - logger.debug(loggerFmt, "slip encoding error", "=> aborting", ""); + logger.debug(logMsg, ipAddr, txName, "slip encoding error", "=> aborting", ""); break; } txPacket = new SlipRFC1055().encode(slipEnc.toMessage()); - logger.trace(loggerFmt, "command ready", "=> start sending", ""); + logger.trace(logMsg, ipAddr, txName, "command ready", "=> start sending", ""); looping = sending = true; } while (looping) { // timeout if (System.currentTimeMillis() > expiryTime) { - logger.warn(loggerFmt, "process loop time out", "=> aborting", "=> PLEASE REPORT !!"); + logger.warn(logMsg, ipAddr, txName, "process loop time out", "=> aborting", "=> PLEASE REPORT !!"); // abort the processing loop break; } @@ -272,9 +271,9 @@ private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProt logger.info("sending command {}", txName); } if (logger.isTraceEnabled()) { - logger.trace(loggerFmt, txName, "=> sending data =>", new Packet(txData)); + logger.trace(logMsg, ipAddr, txName, txName, "=> sending data =>", new Packet(txData)); } else { - logger.debug(loggerFmt, txName, "=> sending data length =>", txData.length); + logger.debug(logMsg, ipAddr, txName, txName, "=> sending data length =>", txData.length); } } rxPacket = connection.io(this.bridgeInstance, sending ? txPacket : emptyPacket); @@ -283,13 +282,13 @@ private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProt 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", ""); + logger.debug(logMsg, ipAddr, txName, "no response", "=> aborting", ""); } // abort the processing loop break; } } catch (IOException e) { - logger.debug(loggerFmt, "i/o error =>", e.getMessage(), "=> aborting"); + logger.debug(logMsg, ipAddr, txName, "i/o error =>", e.getMessage(), "=> aborting"); // abort the processing loop break; } @@ -299,7 +298,7 @@ private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProt try { rfc1055 = new SlipRFC1055().decode(rxPacket); } catch (ParseException e) { - logger.debug(loggerFmt, "parsing error =>", e.getMessage(), "=> aborting"); + logger.debug(logMsg, ipAddr, txName, "parsing error =>", e.getMessage(), "=> aborting"); // abort the processing loop break; } @@ -307,7 +306,7 @@ private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProt // SLIP decode response SlipEncoding slipEnc = new SlipEncoding(rfc1055); if (!slipEnc.isValid()) { - logger.debug(loggerFmt, "slip decode error", "=> aborting", ""); + logger.debug(logMsg, ipAddr, txName, "slip decode error", "=> aborting", ""); // abort the processing loop break; } @@ -320,9 +319,9 @@ private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProt // logging if (logger.isTraceEnabled()) { - logger.trace(loggerFmt, rxName, "=> received data =>", new Packet(rxData)); + logger.trace(logMsg, ipAddr, txName, rxName, "=> received data =>", new Packet(rxData)); } else { - logger.debug(loggerFmt, rxName, "=> received data length =>", rxData.length); + logger.debug(logMsg, ipAddr, txName, rxName, "=> received data length =>", rxData.length); } if (isProtocolTraceEnabled) { logger.info("received message {} => {}", rxName, new Packet(rxData)); @@ -334,53 +333,52 @@ private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProt byte code = rxData[0]; switch (code) { case 7: // busy - logger.trace(loggerFmt, rxName, getErrorText(code), "=> retrying"); + logger.trace(logMsg, ipAddr, txName, rxName, getErrorText(code), "=> retrying"); sending = true; break; case 12: // authentication failed - logger.debug(loggerFmt, rxName, getErrorText(code), "=> aborting"); + logger.debug(logMsg, ipAddr, txName, rxName, getErrorText(code), "=> aborting"); resetAuthentication(); looping = false; break; default: - logger.warn(loggerFmt, rxName, getErrorText(code), "=> aborting"); + logger.warn(logMsg, ipAddr, txName, 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"); + logger.trace(logMsg, ipAddr, txName, rxName, "=> ignorable command", "=> continuing"); break; case GW_NODE_STATE_POSITION_CHANGED_NTF: - logger.trace(loggerFmt, rxName, "=> special command", "=> starting"); - SCgetHouseStatus receiver = new SCgetHouseStatus(); + logger.trace(logMsg, ipAddr, txName, rxName, "=> special command", "=> starting"); + SCgetHouseStatus receiver = new SCgetHouseStatus().setCreatorCommand(txEnum); 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"); + bridgeInstance.existingProducts().update(receiver.getProduct()); + logger.trace(logMsg, ipAddr, txName, rxName, "=> special command", "=> update submitted"); if (rcvonly) { // receive-only: return success to confirm that product(s) were updated success = true; } } - logger.trace(loggerFmt, rxName, "=> special command", "=> continuing"); + logger.trace(logMsg, ipAddr, txName, rxName, "=> special command", "=> continuing"); break; 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"); + logger.trace(logMsg, ipAddr, txName, rxName, "=> parallelism allowed", "=> continuing"); break; } - logger.trace(loggerFmt, rxName, "=> serialism enforced", "=> default processing"); + logger.trace(logMsg, ipAddr, txName, rxName, "=> serialism enforced", "=> default processing"); // fall through => execute default processing default: - logger.trace(loggerFmt, rxName, "=> applying data length =>", rxData.length); + logger.trace(logMsg, ipAddr, txName, rxName, "=> applying data length =>", rxData.length); communication.setResponse(rxCmd, rxData, isSequentialEnforced); looping = !communication.isCommunicationFinished(); success = communication.isCommunicationSuccessful(); @@ -388,7 +386,7 @@ private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProt } // 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")); + logger.debug(logMsg, ipAddr, txName, "finished", "=>", ((success || rcvonly) ? "success" : "failure")); return success; } 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 98c0d5620d881..0edfcb7ebbb14 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 @@ -142,7 +142,7 @@ private void startPolling() { logger.debug("startPolling() called"); slipMessageQueue.clear(); poller = new Poller(inputStream, slipMessageQueue); - executor = Executors.newSingleThreadExecutor(bridge.getThreadFactory()); + ExecutorService executor = this.executor = Executors.newSingleThreadExecutor(bridge.getThreadFactory()); future = executor.submit(poller); } } 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 c897f5d831d11..eac6eb65f6ccc 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 @@ -143,7 +143,8 @@ private static boolean check4matchingAnyID(Logger logger, String idName, int req * @return success of type boolean which signals the success of the communication. */ public static boolean check4matchingNodeID(Logger logger, int reqNodeID, int cfmNodeID) { - logger.trace("check4matchingNodeID() called for requestNodeID {} and responseNodeID {}.", reqNodeID, cfmNodeID); + logger.trace("check4matchingNodeID() called for request NodeID {} and response NodeID {}.", reqNodeID, + cfmNodeID); return check4matchingAnyID(logger, "NodeID", reqNodeID, cfmNodeID); } @@ -157,8 +158,8 @@ public static boolean check4matchingNodeID(Logger logger, int reqNodeID, int cfm * @return success of type boolean which signals the success of the communication. */ public static boolean check4matchingSessionID(Logger logger, int reqSessionID, int cfmSessionID) { - logger.trace("check4matchingSessionID() called for requestNodeID {} and responseNodeID {}.", reqSessionID, - cfmSessionID); + logger.trace("check4matchingSessionID() called for request SessionID {} and response SessionID {}.", + reqSessionID, cfmSessionID); return check4matchingAnyID(logger, "SessionID", reqSessionID, cfmSessionID); } } 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 b6961d073561d..87e6843614beb 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 @@ -68,9 +68,8 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R // Private - @SuppressWarnings("PMD.CompareObjectsWithEquals") private void updateLocalization() { - if (localization == Localization.UNKNOWN && localeProvider != null && i18nProvider != null) { + if (Localization.UNKNOWN.equals(localization) && (localeProvider != null) && (i18nProvider != null)) { logger.trace("updateLocalization(): creating Localization based on locale={},translation={}).", localeProvider, i18nProvider); localization = new Localization(localeProvider, i18nProvider); diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/factory/VeluxHandlerFactory.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/factory/VeluxHandlerFactory.java index 7d29a0662d30d..42a0884116ec6 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/factory/VeluxHandlerFactory.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/factory/VeluxHandlerFactory.java @@ -125,9 +125,8 @@ private void updateBindingState() { }); } - @SuppressWarnings("PMD.CompareObjectsWithEquals") private void updateLocalization() { - if (localization == Localization.UNKNOWN && localeProvider != null && i18nProvider != null) { + if (Localization.UNKNOWN.equals(localization) && (localeProvider != null) && (i18nProvider != null)) { logger.trace("updateLocalization(): creating Localization based on locale={},translation={}).", localeProvider, i18nProvider); localization = new Localization(localeProvider, i18nProvider); 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 e33bbb283bda2..d5cba669abb65 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 @@ -14,12 +14,20 @@ import static org.openhab.binding.velux.internal.VeluxBindingConstants.*; +import java.util.Arrays; +import java.util.List; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.velux.internal.bridge.VeluxBridgeRunProductCommand; import org.openhab.binding.velux.internal.bridge.common.GetProduct; +import org.openhab.binding.velux.internal.bridge.common.RunProductCommand; +import org.openhab.binding.velux.internal.bridge.slip.FunctionalParameters; +import org.openhab.binding.velux.internal.bridge.slip.SCrunProductCommand; import org.openhab.binding.velux.internal.handler.utils.Thing2VeluxActuator; +import org.openhab.binding.velux.internal.things.VeluxExistingProducts; import org.openhab.binding.velux.internal.things.VeluxProduct; +import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex; +import org.openhab.binding.velux.internal.things.VeluxProduct.ProductState; import org.openhab.binding.velux.internal.things.VeluxProductPosition; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; @@ -48,6 +56,7 @@ * * * @author Guenther Schreiner - Initial contribution. + * @author Andrew Fiddian-Green - Refactoring and use alternate API set for Vane Position. */ @NonNullByDefault final class ChannelActuatorPosition extends ChannelHandlerTemplate { @@ -62,6 +71,12 @@ private ChannelActuatorPosition() { throw new AssertionError(); } + /* + * List of product states that shall be processed + */ + private static final List STATES_TO_PROCESS = Arrays.asList(ProductState.DONE, ProductState.EXECUTING, + ProductState.MANUAL, ProductState.UNKNOWN); + // Public methods /** @@ -81,41 +96,83 @@ private ChannelActuatorPosition() { if (thisBridgeHandler.bridgeParameters.actuators.autoRefresh(thisBridgeHandler.thisBridge)) { LOGGER.trace("handleRefresh(): there are some existing products."); } + Thing2VeluxActuator veluxActuator = thisBridgeHandler.channel2VeluxActuator.get(channelUID); if (veluxActuator == null || !veluxActuator.isKnown()) { LOGGER.warn("handleRefresh(): unknown actuator."); break; } - GetProduct bcp = thisBridgeHandler.thisBridge.bridgeAPI().getProduct(); + + GetProduct bcp = null; + switch (channelId) { + case CHANNEL_VANE_POSITION: + bcp = thisBridgeHandler.thisBridge.bridgeAPI().getProductStatus(); + break; + case CHANNEL_ACTUATOR_POSITION: + case CHANNEL_ACTUATOR_STATE: + bcp = thisBridgeHandler.thisBridge.bridgeAPI().getProduct(); + default: + // unknown channel, will exit + } + if (bcp == null) { LOGGER.trace("handleRefresh(): aborting processing as handler is null."); break; } + bcp.setProductId(veluxActuator.getProductBridgeIndex().toInt()); - if (thisBridgeHandler.thisBridge.bridgeCommunicate(bcp) && bcp.isCommunicationSuccessful()) { - try { - VeluxProduct product = bcp.getProduct(); - VeluxProductPosition position = new VeluxProductPosition(product.getDisplayPosition()); - if (position.isValid()) { - if (CHANNEL_ACTUATOR_POSITION.equals(channelId)) { - newState = position.getPositionAsPercentType(veluxActuator.isInverted()); - LOGGER.trace("handleRefresh(): position of actuator is {}%.", newState); - break; - } else if (CHANNEL_ACTUATOR_STATE.equals(channelId)) { - newState = OnOffType.from( - position.getPositionAsPercentType(veluxActuator.isInverted()).intValue() > 50); - LOGGER.trace("handleRefresh(): state of actuator is {}.", newState); - break; + if ((!thisBridgeHandler.thisBridge.bridgeCommunicate(bcp)) || (!bcp.isCommunicationSuccessful())) { + LOGGER.trace("handleRefresh(): bridge communication request failed."); + break; + } + + VeluxProduct newProduct = bcp.getProduct(); + if (STATES_TO_PROCESS.contains(newProduct.getProductState())) { + ProductBridgeIndex productBridgeIndex = newProduct.getBridgeProductIndex(); + VeluxExistingProducts existingProducts = thisBridgeHandler.existingProducts(); + VeluxProduct existingProduct = existingProducts.get(productBridgeIndex); + if (!VeluxProduct.UNKNOWN.equals(existingProduct)) { + switch (channelId) { + case CHANNEL_VANE_POSITION: + case CHANNEL_ACTUATOR_POSITION: + case CHANNEL_ACTUATOR_STATE: { + if (existingProducts.update(newProduct)) { + existingProduct = existingProducts.get(productBridgeIndex); + int posValue = VeluxProductPosition.VPP_VELUX_UNKNOWN; + switch (channelId) { + case CHANNEL_VANE_POSITION: + posValue = existingProduct.getVaneDisplayPosition(); + break; + case CHANNEL_ACTUATOR_POSITION: + case CHANNEL_ACTUATOR_STATE: + posValue = existingProduct.getDisplayPosition(); + } + VeluxProductPosition position = new VeluxProductPosition(posValue); + if (position.isValid()) { + switch (channelId) { + case CHANNEL_VANE_POSITION: + newState = position.getPositionAsPercentType(false); + break; + case CHANNEL_ACTUATOR_POSITION: + newState = position.getPositionAsPercentType(veluxActuator.isInverted()); + break; + case CHANNEL_ACTUATOR_STATE: + newState = OnOffType + .from(position.getPositionAsPercentType(veluxActuator.isInverted()) + .intValue() > 50); + } + } + } } } - LOGGER.trace("handleRefresh(): position of actuator is 'UNDEFINED'."); - newState = UnDefType.UNDEF; - } catch (Exception e) { - LOGGER.warn("handleRefresh(): getProducts() exception: {}.", e.getMessage()); } } + + if (newState == null) { + newState = UnDefType.UNDEF; + } } while (false); // common exit - LOGGER.trace("handleRefresh() returns {}.", newState); + LOGGER.trace("handleRefresh(): new state for channel id '{}' is '{}'.", channelId, newState); return newState; } @@ -129,7 +186,6 @@ private ChannelActuatorPosition() { * information for this channel. * @return newValue ... */ - @SuppressWarnings("PMD.CompareObjectsWithEquals") static @Nullable Command handleCommand(ChannelUID channelUID, String channelId, Command command, VeluxBridgeHandler thisBridgeHandler) { LOGGER.debug("handleCommand({},{},{},{}) called.", channelUID, channelId, command, thisBridgeHandler); @@ -138,48 +194,80 @@ private ChannelActuatorPosition() { if (thisBridgeHandler.bridgeParameters.actuators.autoRefresh(thisBridgeHandler.thisBridge)) { LOGGER.trace("handleCommand(): there are some existing products."); } + Thing2VeluxActuator veluxActuator = thisBridgeHandler.channel2VeluxActuator.get(channelUID); if (veluxActuator == null || !veluxActuator.isKnown()) { LOGGER.warn("handleRefresh(): unknown actuator."); break; } - VeluxProductPosition targetLevel = VeluxProductPosition.UNKNOWN; - if (CHANNEL_ACTUATOR_POSITION.equals(channelId)) { - if (command instanceof UpDownType) { - LOGGER.trace("handleCommand(): found UpDownType.{} command.", command); - targetLevel = UpDownType.UP.equals(command) ^ veluxActuator.isInverted() - ? new VeluxProductPosition(PercentType.ZERO) - : new VeluxProductPosition(PercentType.HUNDRED); - } else if (command instanceof StopMoveType) { - LOGGER.trace("handleCommand(): found StopMoveType.{} command.", command); - targetLevel = StopMoveType.STOP.equals(command) ? new VeluxProductPosition() : targetLevel; - } else if (command instanceof PercentType) { - LOGGER.trace("handleCommand(): found PercentType.{} command", command); - PercentType ptCommand = (PercentType) command; - if (veluxActuator.isInverted()) { - ptCommand = new PercentType(PercentType.HUNDRED.intValue() - ptCommand.intValue()); + + VeluxProductPosition mainParameter = null; + FunctionalParameters functionalParameters = null; + VeluxExistingProducts existingProducts = thisBridgeHandler.existingProducts(); + ProductBridgeIndex productBridgeIndex = veluxActuator.getProductBridgeIndex(); + + switch (channelId) { + case CHANNEL_VANE_POSITION: + if (command instanceof PercentType) { + VeluxProduct existingProductClone = existingProducts.get(productBridgeIndex).clone(); + existingProductClone.setVanePosition( + new VeluxProductPosition((PercentType) command).getPositionAsVeluxType()); + functionalParameters = existingProductClone.getFunctionalParameters(); } - LOGGER.trace("handleCommand(): found command to set level to {}.", ptCommand); - targetLevel = new VeluxProductPosition(ptCommand); - } - } else if (CHANNEL_ACTUATOR_STATE.equals(channelId)) { - if (command instanceof OnOffType) { - LOGGER.trace("handleCommand(): found OnOffType.{} command.", command); - targetLevel = OnOffType.OFF.equals(command) ^ veluxActuator.isInverted() - ? new VeluxProductPosition(PercentType.ZERO) - : new VeluxProductPosition(PercentType.HUNDRED); - } - } - if (targetLevel == VeluxProductPosition.UNKNOWN) { - LOGGER.info("handleCommand({},{}): ignoring command.", channelUID.getAsString(), command); - break; + break; + + case CHANNEL_ACTUATOR_POSITION: + if (command instanceof UpDownType) { + mainParameter = UpDownType.UP.equals(command) ^ veluxActuator.isInverted() + ? new VeluxProductPosition(PercentType.ZERO) + : new VeluxProductPosition(PercentType.HUNDRED); + } else if (command instanceof StopMoveType) { + mainParameter = StopMoveType.STOP.equals(command) ? new VeluxProductPosition() : mainParameter; + } else if (command instanceof PercentType) { + PercentType ptCommand = (PercentType) command; + if (veluxActuator.isInverted()) { + ptCommand = new PercentType(PercentType.HUNDRED.intValue() - ptCommand.intValue()); + } + mainParameter = new VeluxProductPosition(ptCommand); + } + break; + + case CHANNEL_ACTUATOR_STATE: + if (command instanceof OnOffType) { + mainParameter = OnOffType.OFF.equals(command) ^ veluxActuator.isInverted() + ? new VeluxProductPosition(PercentType.ZERO) + : new VeluxProductPosition(PercentType.HUNDRED); + } + break; + + default: + // unknown channel => do nothing.. } - LOGGER.debug("handleCommand(): sending command with target level {}.", targetLevel); - new VeluxBridgeRunProductCommand().sendCommand(thisBridgeHandler.thisBridge, - veluxActuator.getProductBridgeIndex().toInt(), targetLevel); - LOGGER.trace("handleCommand(): The new shutter level will be send through the home monitoring events."); - if (thisBridgeHandler.bridgeParameters.actuators.autoRefresh(thisBridgeHandler.thisBridge)) { - LOGGER.trace("handleCommand(): position of actuators are updated."); + + if ((mainParameter != null) || (functionalParameters != null)) { + LOGGER.debug("handleCommand(): sending command '{}' for channel id '{}'.", command, channelId); + RunProductCommand bcp = thisBridgeHandler.thisBridge.bridgeAPI().runProductCommand(); + boolean success = false; + if (bcp instanceof SCrunProductCommand) { + synchronized (bcp) { + if (bcp.setNodeIdAndParameters(productBridgeIndex.toInt(), mainParameter, functionalParameters) + && thisBridgeHandler.thisBridge.bridgeCommunicate(bcp) + && bcp.isCommunicationSuccessful()) { + success = true; + if (thisBridgeHandler.bridgeParameters.actuators + .autoRefresh(thisBridgeHandler.thisBridge)) { + LOGGER.trace("handleCommand(): actuator position will be updated via polling."); + } + if (existingProducts.update(((SCrunProductCommand) bcp).getProduct())) { + LOGGER.trace("handleCommand(): actuator position immediate update requested."); + } + } + } + } + LOGGER.debug("handleCommand(): sendCommand() finished {}.", + (success ? "successfully" : "with failure")); + } else { + LOGGER.info("handleCommand(): ignoring command '{}' for channel id '{}'.", command, channelId); } } while (false); // common exit return newValue; diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelVShutterPosition.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelVShutterPosition.java index 445a37d320902..d08104c7cb526 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelVShutterPosition.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/ChannelVShutterPosition.java @@ -107,7 +107,6 @@ private ChannelVShutterPosition() { * information for this channel. * @return newValue ... */ - @SuppressWarnings("PMD.CompareObjectsWithEquals") static @Nullable Command handleCommand(ChannelUID channelUID, String channelId, Command command, VeluxBridgeHandler thisBridgeHandler) { LOGGER.debug("handleCommand({},{},{},{}) called.", channelUID, channelId, command, thisBridgeHandler); @@ -148,7 +147,7 @@ private ChannelVShutterPosition() { LOGGER.trace("handleCommand(): scene name is {}.", sceneName); VeluxScene thisScene2 = thisBridgeHandler.bridgeParameters.scenes.getChannel().existingScenes .get(new SceneName(sceneName)); - if (thisScene2 == VeluxScene.UNKNOWN) { + if (VeluxScene.UNKNOWN.equals(thisScene2)) { LOGGER.warn( "handleCommand(): aborting command as scene with name {} is not registered; please check your KLF scene definitions.", sceneName); 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 c8f39b38217b4..4127eaef59a58 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 @@ -55,6 +55,7 @@ import org.openhab.binding.velux.internal.things.VeluxProduct; import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex; import org.openhab.binding.velux.internal.things.VeluxProductPosition; +import org.openhab.binding.velux.internal.things.VeluxProductPosition.PositionType; import org.openhab.binding.velux.internal.utils.Localization; import org.openhab.core.common.AbstractUID; import org.openhab.core.common.NamedThreadFactory; @@ -541,20 +542,27 @@ private void syncChannelsWithProducts() { if (!channelPbi.equals(productPbi)) { continue; } - // Handle value inversion - boolean isInverted = actuator.isInverted(); - logger.trace("syncChannelsWithProducts(): isInverted is {}.", isInverted); - VeluxProductPosition position = new VeluxProductPosition(product.getDisplayPosition()); + boolean isInverted; + VeluxProductPosition position; + if (channelUID.getId().equals(VeluxBindingConstants.CHANNEL_VANE_POSITION)) { + isInverted = false; + position = new VeluxProductPosition(product.getVanePosition()); + } else { + // Handle value inversion + isInverted = actuator.isInverted(); + logger.trace("syncChannelsWithProducts(): isInverted is {}.", isInverted); + 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); - break; + continue; } - logger.trace("syncChannelsWithProducts(): update channel {} to 'UNDEFINED'.", channelUID); + logger.trace("syncChannelsWithProducts(): updating channel {} to 'UNDEFINED'.", channelUID); updateState(channelUID, UnDefType.UNDEF); - break; + continue; } } logger.trace("syncChannelsWithProducts(): resetting dirty flag."); @@ -674,6 +682,7 @@ private synchronized void handleCommandCommsJob(ChannelUID channelUID, Command c case ACTUATOR_STATE: case ROLLERSHUTTER_POSITION: case WINDOW_POSITION: + case ROLLERSHUTTER_VANE_POSITION: newState = ChannelActuatorPosition.handleRefresh(channelUID, channelId, this); break; case ACTUATOR_LIMIT_MINIMUM: @@ -694,9 +703,8 @@ private synchronized void handleCommandCommsJob(ChannelUID channelUID, Command c break; default: - logger.trace( - "handleCommandCommsJob(): cannot handle REFRESH on channel {} as it is of type {}.", - itemName, channelId); + logger.warn("{} Cannot handle REFRESH on channel {} as it is of type {}.", + VeluxBindingConstants.LOGGING_CONTACT, itemName, channelId); } } catch (IllegalArgumentException e) { logger.warn("Cannot handle REFRESH on channel {} as it isn't (yet) known to the bridge.", itemName); @@ -769,6 +777,7 @@ private synchronized void handleCommandCommsJob(ChannelUID channelUID, Command c case ACTUATOR_STATE: case ROLLERSHUTTER_POSITION: case WINDOW_POSITION: + case ROLLERSHUTTER_VANE_POSITION: newValue = ChannelActuatorPosition.handleCommand(channelUID, channelId, command, this); break; case ACTUATOR_LIMIT_MINIMUM: @@ -845,13 +854,17 @@ 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 submitCommunicationsJob(() -> { - if (thisBridge.bridgeCommunicate(bcp)) { - logger.trace("moveRelative() command {}sucessfully sent to {}", - bcp.isCommunicationSuccessful() ? "" : "un", getThing().getUID()); + synchronized (bcp) { + bcp.setNodeIdAndParameters(nodeId, + new VeluxProductPosition(new PercentType(Math.abs(relativePercent))).overridePositionType( + relativePercent > 0 ? PositionType.OFFSET_POSITIVE : PositionType.OFFSET_NEGATIVE), + null); + if (thisBridge.bridgeCommunicate(bcp)) { + logger.trace("moveRelative() command {}sucessfully sent to {}", + bcp.isCommunicationSuccessful() ? "" : "un", getThing().getUID()); + } } }); return true; diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/utils/Thing2VeluxActuator.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/utils/Thing2VeluxActuator.java index 35f2f7c6b45b6..3cb1d8401081f 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/utils/Thing2VeluxActuator.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/handler/utils/Thing2VeluxActuator.java @@ -135,12 +135,11 @@ public Thing2VeluxActuator(VeluxBridgeHandler thisBridgeHandler, ChannelUID this * * @return bridgeProductIndex for accessing the Velux device (or ProductBridgeIndex.UNKNOWN if not found). */ - @SuppressWarnings("PMD.CompareObjectsWithEquals") public ProductBridgeIndex getProductBridgeIndex() { - if (thisProduct == VeluxProduct.UNKNOWN) { + if (VeluxProduct.UNKNOWN.equals(thisProduct)) { mapThing2Velux(); } - if (thisProduct == VeluxProduct.UNKNOWN) { + if (VeluxProduct.UNKNOWN.equals(thisProduct)) { return ProductBridgeIndex.UNKNOWN; } return thisProduct.getBridgeProductIndex(); @@ -152,9 +151,8 @@ public ProductBridgeIndex getProductBridgeIndex() { * * @return isKnown as boolean. */ - @SuppressWarnings("PMD.CompareObjectsWithEquals") public boolean isKnown() { - return (!(this.getProductBridgeIndex() == ProductBridgeIndex.UNKNOWN)); + return (!(ProductBridgeIndex.UNKNOWN.equals(getProductBridgeIndex()))); } /** @@ -164,12 +162,11 @@ public boolean isKnown() { * * @return isInverted for handling of values of the Velux device (or false if not found).. */ - @SuppressWarnings("PMD.CompareObjectsWithEquals") public boolean isInverted() { - if (thisProduct == VeluxProduct.UNKNOWN) { + if (VeluxProduct.UNKNOWN.equals(thisProduct)) { mapThing2Velux(); } - if (thisProduct == VeluxProduct.UNKNOWN) { + if (VeluxProduct.UNKNOWN.equals(thisProduct)) { logger.warn("isInverted(): Thing not found in Velux Bridge."); } return isInverted; 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 a222d9596c8d1..95d533fa6ccd9 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 @@ -12,12 +12,17 @@ */ package org.openhab.binding.velux.internal.things; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.velux.internal.VeluxBindingConstants; +import org.openhab.binding.velux.internal.bridge.slip.FunctionalParameters; +import org.openhab.binding.velux.internal.things.VeluxKLFAPI.Command; import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex; +import org.openhab.binding.velux.internal.things.VeluxProduct.ProductState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,6 +61,12 @@ public class VeluxExistingProducts { */ private boolean dirty; + /* + * Permitted list of product states whose position values shall be accepted. + */ + private static final List PERMITTED_VALUE_STATES = Arrays.asList(ProductState.EXECUTING, + ProductState.DONE); + // Constructor methods public VeluxExistingProducts() { @@ -110,32 +121,87 @@ public boolean register(VeluxProduct newProduct) { return true; } - public boolean update(ProductBridgeIndex bridgeProductIndex, int productState, int productPosition, - int productTarget) { - logger.debug("update(bridgeProductIndex={},productState={},productPosition={},productTarget={}) called.", - bridgeProductIndex.toInt(), productState, productPosition, productTarget); - if (!isRegistered(bridgeProductIndex)) { - logger.warn("update() failed as actuator (with index {}) is not registered.", bridgeProductIndex.toInt()); + /** + * Update the product in the existing products database by applying the data from the new product argument. This + * method may ignore the new product if it was created by certain originating commands, or if the new product has + * certain actuator states. + * + * @param requestingCommand the command that requested the data from the hub and so triggered calling this method. + * @param newProduct the product containing new data. + * + * @return true if the product exists in the database. + */ + public boolean update(VeluxProduct newProduct) { + ProductBridgeIndex productBridgeIndex = newProduct.getBridgeProductIndex(); + if (!isRegistered(productBridgeIndex)) { + logger.warn("update() failed as actuator (with index {}) is not registered.", productBridgeIndex.toInt()); return false; } - VeluxProduct thisProduct = this.get(bridgeProductIndex); - dirty |= thisProduct.setState(productState); - dirty |= thisProduct.setCurrentPosition(productPosition); - dirty |= thisProduct.setTarget(productTarget); + + VeluxProduct theProduct = this.get(productBridgeIndex); + + String oldProduct = ""; + if (logger.isDebugEnabled()) { + oldProduct = theProduct.toString(); + } + + boolean dirty = false; + + // ignore commands with state 'not used' + boolean ignoreNotUsed = (ProductState.NOT_USED == ProductState.of(newProduct.getState())); + + // specially ignore commands from buggy devices (e.g. Somfy) which have bad data + boolean ignoreSpecial = theProduct.isSomfyProduct() + && (Command.GW_OPENHAB_RECEIVEONLY == newProduct.getCreatorCommand()) + && !VeluxProductPosition.isValid(newProduct.getCurrentPosition()) + && !VeluxProductPosition.isValid(newProduct.getTarget()); + + if ((!ignoreNotUsed) && (!ignoreSpecial)) { + int newState = newProduct.getState(); + int theState = theProduct.getState(); + + // always update the actuator state, but only set dirty flag if they are not operationally equivalent + if (theProduct.setState(newState)) { + dirty |= !ProductState.equivalent(theState, newState); + } + + // only update the actual position values if the state is permitted + if (PERMITTED_VALUE_STATES.contains(ProductState.of(newState))) { + int newValue = newProduct.getCurrentPosition(); + if (VeluxProductPosition.isUnknownOrValid(newValue)) { + dirty |= theProduct.setCurrentPosition(newValue); + } + newValue = newProduct.getTarget(); + if (VeluxProductPosition.isUnknownOrValid(newValue)) { + dirty |= theProduct.setTarget(newValue); + } + if (theProduct.supportsVanePosition()) { + FunctionalParameters newFunctionalParameters = newProduct.getFunctionalParameters(); + if (newFunctionalParameters != null) { + dirty |= theProduct.setFunctionalParameters(newFunctionalParameters); + } + } + } + } + + // update modified product database if (dirty) { - String uniqueIndex = thisProduct.getProductUniqueIndex(); + this.dirty = true; + String uniqueIndex = theProduct.getProductUniqueIndex(); logger.trace("update(): updating by UniqueIndex {}.", uniqueIndex); - existingProductsByUniqueIndex.replace(uniqueIndex, thisProduct); - modifiedProductsByUniqueIndex.put(uniqueIndex, thisProduct); + existingProductsByUniqueIndex.replace(uniqueIndex, theProduct); + modifiedProductsByUniqueIndex.put(uniqueIndex, theProduct); } - logger.trace("update() successfully finished (dirty={}).", dirty); - return true; - } - public boolean update(VeluxProduct currentProduct) { - logger.trace("update(currentProduct={}) called.", currentProduct); - return update(currentProduct.getBridgeProductIndex(), currentProduct.getState(), - currentProduct.getCurrentPosition(), currentProduct.getTarget()); + if (logger.isDebugEnabled()) { + if (dirty) { + logger.debug("update() theProduct:{} (previous)", oldProduct); + } + logger.debug("update() newProduct:{} ({})", newProduct, dirty ? "modifier" : "identical"); + logger.debug("update() theProduct:{} ({})", theProduct, dirty ? "modified" : "unchanged"); + } + + return true; } public VeluxProduct get(String productUniqueIndex) { diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxKLFAPI.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxKLFAPI.java index 94a21fe98ad3a..aac38ec3d28c5 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxKLFAPI.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxKLFAPI.java @@ -156,7 +156,7 @@ public enum Command { "Acknowledge to GW_CS_GET_SYSTEM_TABLE_DATA_REQList of nodes in the gateways systemtable."), GW_CS_DISCOVER_NODES_REQ((short) 0x0103, "Start CS DiscoverNodes macro in KLF200."), GW_CS_DISCOVER_NODES_CFM((short) 0x0104, "Acknowledge to GW_CS_DISCOVER_NODES_REQ command."), - GW_CS_DISCOVER_NODES_NTF((short) 0x0105, "Acknowledge to GW_CS_DISCOVER_NODES_REQ command."), + GW_CS_DISCOVER_NODES_NTF((short) 0x0105, "Notification to GW_CS_DISCOVER_NODES_REQ command."), GW_CS_REMOVE_NODES_REQ((short) 0x0106, "Remove one or more nodes in the systemtable."), GW_CS_REMOVE_NODES_CFM((short) 0x0107, "Acknowledge to GW_CS_REMOVE_NODES_REQ."), GW_CS_VIRGIN_STATE_REQ((short) 0x0108, "Clear systemtable and delete system key."), @@ -164,11 +164,11 @@ public enum Command { GW_CS_CONTROLLER_COPY_REQ((short) 0x010A, "Setup KLF200 to get or give a system to or from another io-homecontrol® remote control. By a system means all nodes in the systemtable and the system key."), GW_CS_CONTROLLER_COPY_CFM((short) 0x010B, "Acknowledge to GW_CS_CONTROLLER_COPY_REQ."), - GW_CS_CONTROLLER_COPY_NTF((short) 0x010C, "Acknowledge to GW_CS_CONTROLLER_COPY_REQ."), + GW_CS_CONTROLLER_COPY_NTF((short) 0x010C, "Notification to GW_CS_CONTROLLER_COPY_REQ."), GW_CS_CONTROLLER_COPY_CANCEL_NTF((short) 0x010D, "Cancellation of system copy to other controllers."), GW_CS_RECEIVE_KEY_REQ((short) 0x010E, "Receive system key from another controller."), GW_CS_RECEIVE_KEY_CFM((short) 0x010F, "Acknowledge to GW_CS_RECEIVE_KEY_REQ."), - GW_CS_RECEIVE_KEY_NTF((short) 0x0110, "Acknowledge to GW_CS_RECEIVE_KEY_REQ with status."), + GW_CS_RECEIVE_KEY_NTF((short) 0x0110, "Notification to GW_CS_RECEIVE_KEY_REQ with status."), GW_CS_PGC_JOB_NTF((short) 0x0111, "Information on Product Generic Configuration job initiated by press on PGC button."), GW_CS_SYSTEM_TABLE_UPDATE_NTF((short) 0x0112, @@ -185,27 +185,27 @@ public enum Command { GW_GET_NODE_INFORMATION_REQ((short) 0x0200, "Request extended information of one specific actuator node."), GW_GET_NODE_INFORMATION_CFM((short) 0x0201, "Acknowledge to GW_GET_NODE_INFORMATION_REQ."), - GW_GET_NODE_INFORMATION_NTF((short) 0x0210, "Acknowledge to GW_GET_NODE_INFORMATION_REQ."), + GW_GET_NODE_INFORMATION_NTF((short) 0x0210, "Notification to GW_GET_NODE_INFORMATION_REQ."), GW_GET_ALL_NODES_INFORMATION_REQ((short) 0x0202, "Request extended information of all nodes."), GW_GET_ALL_NODES_INFORMATION_CFM((short) 0x0203, "Acknowledge to GW_GET_ALL_NODES_INFORMATION_REQ"), GW_GET_ALL_NODES_INFORMATION_NTF((short) 0x0204, - "Acknowledge to GW_GET_ALL_NODES_INFORMATION_REQ. Holds node information"), + "Notification to GW_GET_ALL_NODES_INFORMATION_REQ. Holds node information"), GW_GET_ALL_NODES_INFORMATION_FINISHED_NTF((short) 0x0205, - "Acknowledge to GW_GET_ALL_NODES_INFORMATION_REQ. No more nodes."), + "Notificatione to GW_GET_ALL_NODES_INFORMATION_REQ. No more nodes."), GW_SET_NODE_VARIATION_REQ((short) 0x0206, "Set node variation."), GW_SET_NODE_VARIATION_CFM((short) 0x0207, "Acknowledge to GW_SET_NODE_VARIATION_REQ."), GW_SET_NODE_NAME_REQ((short) 0x0208, "Set node name."), GW_SET_NODE_NAME_CFM((short) 0x0209, "Acknowledge to GW_SET_NODE_NAME_REQ."), GW_SET_NODE_VELOCITY_REQ((short) 0x020A, "Set node velocity."), GW_SET_NODE_VELOCITY_CFM((short) 0x020B, "Acknowledge to GW_SET_NODE_VELOCITY_REQ."), - GW_NODE_INFORMATION_CHANGED_NTF((short) 0x020C, "Information has been updated."), - GW_NODE_STATE_POSITION_CHANGED_NTF((short) 0x0211, "Information has been updated."), + GW_NODE_INFORMATION_CHANGED_NTF((short) 0x020C, "Notification that information has been updated."), + GW_NODE_STATE_POSITION_CHANGED_NTF((short) 0x0211, "Notification information has been updated."), GW_SET_NODE_ORDER_AND_PLACEMENT_REQ((short) 0x020D, "Set search order and room placement."), GW_SET_NODE_ORDER_AND_PLACEMENT_CFM((short) 0x020E, "Acknowledge to GW_SET_NODE_ORDER_AND_PLACEMENT_REQ."), GW_GET_GROUP_INFORMATION_REQ((short) 0x0220, "Request information about all defined groups."), GW_GET_GROUP_INFORMATION_CFM((short) 0x0221, "Acknowledge to GW_GET_GROUP_INFORMATION_REQ."), - GW_GET_GROUP_INFORMATION_NTF((short) 0x0230, "Acknowledge to GW_GET_NODE_INFORMATION_REQ."), + GW_GET_GROUP_INFORMATION_NTF((short) 0x0230, "Notification to GW_GET_GROUP_INFORMATION_REQ."), GW_SET_GROUP_INFORMATION_REQ((short) 0x0222, "Change an existing group."), GW_SET_GROUP_INFORMATION_CFM((short) 0x0223, "Acknowledge to GW_SET_GROUP_INFORMATION_REQ."), GW_GROUP_INFORMATION_CHANGED_NTF((short) 0x0224, @@ -216,8 +216,9 @@ public enum Command { GW_NEW_GROUP_CFM((short) 0x0228, ""), GW_GET_ALL_GROUPS_INFORMATION_REQ((short) 0x0229, "Request information about all defined groups."), GW_GET_ALL_GROUPS_INFORMATION_CFM((short) 0x022A, "Acknowledge to GW_GET_ALL_GROUPS_INFORMATION_REQ."), - GW_GET_ALL_GROUPS_INFORMATION_NTF((short) 0x022B, "Acknowledge to GW_GET_ALL_GROUPS_INFORMATION_REQ."), - GW_GET_ALL_GROUPS_INFORMATION_FINISHED_NTF((short) 0x022C, "Acknowledge to GW_GET_ALL_GROUPS_INFORMATION_REQ."), + GW_GET_ALL_GROUPS_INFORMATION_NTF((short) 0x022B, "Notification to GW_GET_ALL_GROUPS_INFORMATION_REQ."), + GW_GET_ALL_GROUPS_INFORMATION_FINISHED_NTF((short) 0x022C, + "Notification to GW_GET_ALL_GROUPS_INFORMATION_REQ."), GW_GROUP_DELETED_NTF((short) 0x022D, "GW_GROUP_DELETED_NTF is broadcasted to all, when a group has been removed."), GW_HOUSE_STATUS_MONITOR_ENABLE_REQ((short) 0x0240, "Enable house status monitor."), @@ -227,55 +228,55 @@ public enum Command { GW_COMMAND_SEND_REQ((short) 0x0300, "Send activating command direct to one or more io-homecontrol® nodes."), GW_COMMAND_SEND_CFM((short) 0x0301, "Acknowledge to GW_COMMAND_SEND_REQ."), - GW_COMMAND_RUN_STATUS_NTF((short) 0x0302, "Gives run status for io-homecontrol® node."), + GW_COMMAND_RUN_STATUS_NTF((short) 0x0302, "Notification gives run status for io-homecontrol® node."), GW_COMMAND_REMAINING_TIME_NTF((short) 0x0303, - "Gives remaining time before io-homecontrol® node enter target position."), + "Notification gives remaining time before io-homecontrol® node enter target position."), GW_SESSION_FINISHED_NTF((short) 0x0304, - "Command send, Status request, Wink, Mode or Stop session is finished."), + "Notification command send, Status request, Wink, Mode or Stop session is finished."), GW_STATUS_REQUEST_REQ((short) 0x0305, "Get status request from one or more io-homecontrol® nodes."), GW_STATUS_REQUEST_CFM((short) 0x0306, "Acknowledge to GW_STATUS_REQUEST_REQ."), GW_STATUS_REQUEST_NTF((short) 0x0307, - "Acknowledge to GW_STATUS_REQUEST_REQ. Status request from one or more io-homecontrol® nodes."), + "Notification to GW_STATUS_REQUEST_REQ. Status request from one or more io-homecontrol® nodes."), GW_WINK_SEND_REQ((short) 0x0308, "Request from one or more io-homecontrol® nodes to Wink."), GW_WINK_SEND_CFM((short) 0x0309, "Acknowledge to GW_WINK_SEND_REQ"), - GW_WINK_SEND_NTF((short) 0x030A, "Status info for performed wink request."), + GW_WINK_SEND_NTF((short) 0x030A, "Notification status info for performed wink request."), GW_SET_LIMITATION_REQ((short) 0x0310, "Set a parameter limitation in an actuator."), GW_SET_LIMITATION_CFM((short) 0x0311, "Acknowledge to GW_SET_LIMITATION_REQ."), GW_GET_LIMITATION_STATUS_REQ((short) 0x0312, "Get parameter limitation in an actuator."), GW_GET_LIMITATION_STATUS_CFM((short) 0x0313, "Acknowledge to GW_GET_LIMITATION_STATUS_REQ."), - GW_LIMITATION_STATUS_NTF((short) 0x0314, "Hold information about limitation."), + GW_LIMITATION_STATUS_NTF((short) 0x0314, "Notification hold information about limitation."), GW_MODE_SEND_REQ((short) 0x0320, "Send Activate Mode to one or more io-homecontrol® nodes."), GW_MODE_SEND_CFM((short) 0x0321, "Acknowledge to GW_MODE_SEND_REQ"), - GW_MODE_SEND_NTF((short) 0x0322, "Notify with Mode activation info."), + GW_MODE_SEND_NTF((short) 0x0322, "Notification with Mode activation info."), GW_INITIALIZE_SCENE_REQ((short) 0x0400, "Prepare gateway to record a scene."), GW_INITIALIZE_SCENE_CFM((short) 0x0401, "Acknowledge to GW_INITIALIZE_SCENE_REQ."), - GW_INITIALIZE_SCENE_NTF((short) 0x0402, "Acknowledge to GW_INITIALIZE_SCENE_REQ."), + GW_INITIALIZE_SCENE_NTF((short) 0x0402, "Notification to GW_INITIALIZE_SCENE_REQ."), GW_INITIALIZE_SCENE_CANCEL_REQ((short) 0x0403, "Cancel record scene process."), GW_INITIALIZE_SCENE_CANCEL_CFM((short) 0x0404, "Acknowledge to GW_INITIALIZE_SCENE_CANCEL_REQ command."), GW_RECORD_SCENE_REQ((short) 0x0405, "Store actuator positions changes since GW_INITIALIZE_SCENE, as a scene."), GW_RECORD_SCENE_CFM((short) 0x0406, "Acknowledge to GW_RECORD_SCENE_REQ."), - GW_RECORD_SCENE_NTF((short) 0x0407, "Acknowledge to GW_RECORD_SCENE_REQ."), + GW_RECORD_SCENE_NTF((short) 0x0407, "Notification to GW_RECORD_SCENE_REQ."), GW_DELETE_SCENE_REQ((short) 0x0408, "Delete a recorded scene."), GW_DELETE_SCENE_CFM((short) 0x0409, "Acknowledge to GW_DELETE_SCENE_REQ."), GW_RENAME_SCENE_REQ((short) 0x040A, "Request a scene to be renamed."), GW_RENAME_SCENE_CFM((short) 0x040B, "Acknowledge to GW_RENAME_SCENE_REQ."), GW_GET_SCENE_LIST_REQ((short) 0x040C, "Request a list of scenes."), GW_GET_SCENE_LIST_CFM((short) 0x040D, "Acknowledge to GW_GET_SCENE_LIST."), - GW_GET_SCENE_LIST_NTF((short) 0x040E, "Acknowledge to GW_GET_SCENE_LIST."), + GW_GET_SCENE_LIST_NTF((short) 0x040E, "Notification to GW_GET_SCENE_LIST."), GW_GET_SCENE_INFOAMATION_REQ((short) 0x040F, "Request extended information for one given scene."), GW_GET_SCENE_INFOAMATION_CFM((short) 0x0410, "Acknowledge to GW_GET_SCENE_INFOAMATION_REQ."), - GW_GET_SCENE_INFOAMATION_NTF((short) 0x0411, "Acknowledge to GW_GET_SCENE_INFOAMATION_REQ."), + GW_GET_SCENE_INFOAMATION_NTF((short) 0x0411, "Notification to GW_GET_SCENE_INFOAMATION_REQ."), GW_ACTIVATE_SCENE_REQ((short) 0x0412, "Request gateway to enter a scene."), GW_ACTIVATE_SCENE_CFM((short) 0x0413, "Acknowledge to GW_ACTIVATE_SCENE_REQ."), GW_STOP_SCENE_REQ((short) 0x0415, "Request all nodes in a given scene to stop at their current position."), GW_STOP_SCENE_CFM((short) 0x0416, "Acknowledge to GW_STOP_SCENE_REQ."), - GW_SCENE_INFORMATION_CHANGED_NTF((short) 0x0419, "A scene has either been changed or removed."), + GW_SCENE_INFORMATION_CHANGED_NTF((short) 0x0419, "Notification a scene has either been changed or removed."), GW_ACTIVATE_PRODUCTGROUP_REQ((short) 0x0447, "Activate a product group in a given direction."), GW_ACTIVATE_PRODUCTGROUP_CFM((short) 0x0448, "Acknowledge to GW_ACTIVATE_PRODUCTGROUP_REQ."), - GW_ACTIVATE_PRODUCTGROUP_NTF((short) 0x0449, "Acknowledge to GW_ACTIVATE_PRODUCTGROUP_REQ."), + GW_ACTIVATE_PRODUCTGROUP_NTF((short) 0x0449, "Notification to GW_ACTIVATE_PRODUCTGROUP_REQ."), GW_GET_CONTACT_INPUT_LINK_LIST_REQ((short) 0x0460, "Get list of assignments to all Contact Input to scene or product group."), @@ -290,11 +291,11 @@ public enum Command { GW_CLEAR_ACTIVATION_LOG_REQ((short) 0x0502, "Request clear all data in activation log."), GW_CLEAR_ACTIVATION_LOG_CFM((short) 0x0503, "Confirm clear all data in activation log."), GW_GET_ACTIVATION_LOG_LINE_REQ((short) 0x0504, "Request line from activation log."), - GW_GET_ACTIVATION_LOG_LINE_CFM((short) 0x0505, "Confirm line from activation log."), - GW_ACTIVATION_LOG_UPDATED_NTF((short) 0x0506, "Confirm line from activation log."), + GW_GET_ACTIVATION_LOG_LINE_CFM((short) 0x0505, "Acknowledge to confirm line from activation log."), + GW_ACTIVATION_LOG_UPDATED_NTF((short) 0x0506, "Notification to confirm line from activation log."), GW_GET_MULTIPLE_ACTIVATION_LOG_LINES_REQ((short) 0x0507, "Request lines from activation log."), - GW_GET_MULTIPLE_ACTIVATION_LOG_LINES_NTF((short) 0x0508, "Error log data from activation log."), - GW_GET_MULTIPLE_ACTIVATION_LOG_LINES_CFM((short) 0x0509, "Confirm lines from activation log."), + GW_GET_MULTIPLE_ACTIVATION_LOG_LINES_NTF((short) 0x0508, "Notification error log data from activation log."), + GW_GET_MULTIPLE_ACTIVATION_LOG_LINES_CFM((short) 0x0509, "Acknowledge to confirm lines from activation log."), GW_SET_UTC_REQ((short) 0x2000, "Request to set UTC time."), GW_SET_UTC_CFM((short) 0x2001, "Acknowledge to GW_SET_UTC_REQ."), @@ -308,7 +309,7 @@ public enum Command { GW_PASSWORD_CHANGE_REQ((short) 0x3002, "Request password change."), GW_PASSWORD_CHANGE_CFM((short) 0x3003, "Acknowledge to GW_PASSWORD_CHANGE_REQ."), GW_PASSWORD_CHANGE_NTF((short) 0x3004, - "Acknowledge to GW_PASSWORD_CHANGE_REQ. Broadcasted to all connected clients."), + "Notification to GW_PASSWORD_CHANGE_REQ. Broadcasted to all connected clients."), ; 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 18f91f918791b..d4dfa3bd4f07b 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 @@ -12,7 +12,13 @@ */ package org.openhab.binding.velux.internal.things; +import java.util.regex.Pattern; + import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.velux.internal.bridge.slip.FunctionalParameters; +import org.openhab.binding.velux.internal.things.VeluxKLFAPI.Command; +import org.openhab.binding.velux.internal.things.VeluxProductType.ActuatorType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,28 +63,90 @@ public String toString() { } } - // State (of movement) of an actuator - public static enum State { + /** + * State (of movement) of an actuator product. + * + * @author AndrewFG - Initial contribution. + */ + public static enum ProductState { NON_EXECUTING(0), ERROR(1), NOT_USED(2), WAITING_FOR_POWER(3), EXECUTING(4), DONE(5), - MANUAL_OVERRIDE(0x80), - UNKNOWN(0xFF); + UNKNOWN(0xFF), + MANUAL(0b10000000); + + private static final int ACTION_MASK = 0b111; + private static final int EQUIVALENT_MASK = ACTION_MASK | MANUAL.value; public final int value; - private State(int value) { + private ProductState(int value) { this.value = value; } + + /** + * Create an ProductState from an integer seed value. + * + * @param value the seed value. + * @return the ProductState. + */ + public static ProductState of(int value) { + if ((value < NON_EXECUTING.value) || (value > UNKNOWN.value)) { + return ERROR; + } + if (value == UNKNOWN.value) { + return UNKNOWN; + } + if ((value & MANUAL.value) != 0) { + return MANUAL; + } + int masked = value & ACTION_MASK; + for (ProductState state : values()) { + if (state.value > DONE.value) { + break; + } + if (masked == state.value) { + return state; + } + } + return ERROR; + } + + /** + * Test if the masked values of two state values are operationally equivalent, including if both values are + * 'unknown' (0xFF). + * + * @param a first value to compare + * @param b second value to compare + * @return true if the masked values are equivalent. + */ + public static boolean equivalent(int a, int b) { + return (a & EQUIVALENT_MASK) == (b & EQUIVALENT_MASK); + } + } + + // pattern to match a Velux serial number '00:00:00:00:00:00:00:00' + private static final Pattern VELUX_SERIAL_NUMBER = Pattern.compile( + "^[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}$"); + + /** + * Indicates the data source where the product's contents came from. + * + * @author AndrewFG - Initial contribution. + */ + public static enum DataSource { + GATEWAY, + BINDING; } // Class internal private VeluxProductName name; private VeluxProductType typeId; + private ActuatorType actuatorType; private ProductBridgeIndex bridgeProductIndex; private boolean v2 = false; @@ -88,11 +156,15 @@ private State(int value) { private int variation = 0; private int powerMode = 0; private String serialNumber = VeluxProductSerialNo.UNKNOWN; - private int state = State.UNKNOWN.value; + private int state = ProductState.UNKNOWN.value; private int currentPosition = 0; private int targetPosition = 0; + private @Nullable FunctionalParameters functionalParameters = null; private int remainingTime = 0; private int timeStamp = 0; + private Command creatorCommand = Command.UNDEFTYPE; + private DataSource dataSource = DataSource.GATEWAY; + private boolean isSomfyProduct; // Constructor @@ -106,6 +178,8 @@ public VeluxProduct() { this.name = VeluxProductName.UNKNOWN; this.typeId = VeluxProductType.UNDEFTYPE; this.bridgeProductIndex = ProductBridgeIndex.UNKNOWN; + this.actuatorType = ActuatorType.UNDEFTYPE; + this.isSomfyProduct = false; } /** @@ -118,10 +192,12 @@ public VeluxProduct() { * value from 0 to 199. */ public VeluxProduct(VeluxProductName name, VeluxProductType typeId, ProductBridgeIndex bridgeProductIndex) { - logger.trace("VeluxProduct(v1,name={}) created.", name.toString()); + logger.trace("VeluxProduct(v1,name={}) created.", name); this.name = name; this.typeId = typeId; this.bridgeProductIndex = bridgeProductIndex; + this.actuatorType = ActuatorType.WINDOW_4_0; + this.isSomfyProduct = false; } /** @@ -142,15 +218,20 @@ public VeluxProduct(VeluxProductName name, VeluxProductType typeId, ProductBridg * @param state This field indicates the operating state of the node. * @param currentPosition This field indicates the current position of the node. * @param target This field indicates the target position of the current operation. + * @param functionalParameters the target Functional Parameters (may be null). * @param remainingTime This field indicates the remaining time for a node activation in seconds. * @param timeStamp UTC time stamp for last known position. + * @param creatorCommand the API command that caused this instance to be created. */ - public VeluxProduct(VeluxProductName name, VeluxProductType typeId, ProductBridgeIndex bridgeProductIndex, - int order, int placement, int velocity, int variation, int powerMode, String serialNumber, int state, - int currentPosition, int target, int remainingTime, int timeStamp) { - logger.trace("VeluxProduct(v2,name={}) created.", name.toString()); + public VeluxProduct(VeluxProductName name, VeluxProductType typeId, ActuatorType actuatorType, + ProductBridgeIndex bridgeProductIndex, int order, int placement, int velocity, int variation, int powerMode, + String serialNumber, int state, int currentPosition, int target, + @Nullable FunctionalParameters functionalParameters, int remainingTime, int timeStamp, + Command creatorCommand) { + logger.trace("VeluxProduct(v2, name={}) created.", name); this.name = name; this.typeId = typeId; + this.actuatorType = actuatorType; this.bridgeProductIndex = bridgeProductIndex; this.v2 = true; this.order = order; @@ -162,8 +243,44 @@ public VeluxProduct(VeluxProductName name, VeluxProductType typeId, ProductBridg this.state = state; this.currentPosition = currentPosition; this.targetPosition = target; + this.functionalParameters = functionalParameters; this.remainingTime = remainingTime; this.timeStamp = timeStamp; + this.creatorCommand = creatorCommand; + + // isSomfyProduct is true if serial number not matching the '00:00:00:00:00:00:00:00' pattern + this.isSomfyProduct = !VELUX_SERIAL_NUMBER.matcher(serialNumber).find(); + } + + /** + * Constructor for a 'notification' product. Such products are used as data transfer objects to carry the limited + * sub + * set of data fields which are returned by 'GW_STATUS_REQUEST_NTF' or 'GW_NODE_STATE_POSITION_CHANGED_NTF' + * notifications, and to transfer those respective field values to another product that had already been created via + * a 'GW_GET_NODE_INFORMATION_NTF' notification, with all the other fields already filled. + * + * @param name the name of the notification command that created the product. + * @param bridgeProductIndex the product bridge index from the notification. + * @param state the actuator state from the notification. + * @param currentPosition the current actuator position from the notification. + * @param target the target position from the notification (may be VeluxProductPosition.VPP_VELUX_IGNORE). + * @param functionalParameters the actuator functional parameters (may be null). + * @param creatorCommand the API command that caused this instance to be created. + */ + public VeluxProduct(VeluxProductName name, ProductBridgeIndex bridgeProductIndex, int state, int currentPosition, + int target, @Nullable FunctionalParameters functionalParameters, Command creatorCommand) { + logger.trace("VeluxProduct(v2, name={}) [notification product] created.", name); + this.v2 = true; + this.typeId = VeluxProductType.UNDEFTYPE; + this.actuatorType = ActuatorType.UNDEFTYPE; + this.name = name; + this.bridgeProductIndex = bridgeProductIndex; + this.state = state; + this.currentPosition = currentPosition; + this.targetPosition = target; + this.functionalParameters = functionalParameters; + this.isSomfyProduct = false; + this.creatorCommand = creatorCommand; } // Utility methods @@ -171,11 +288,13 @@ public VeluxProduct(VeluxProductName name, VeluxProductType typeId, ProductBridg @Override 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.targetPosition, this.remainingTime, this.timeStamp); + FunctionalParameters functionalParameters = this.functionalParameters; + return new VeluxProduct(name, typeId, actuatorType, bridgeProductIndex, order, placement, velocity, + variation, powerMode, serialNumber, state, currentPosition, targetPosition, + functionalParameters == null ? null : functionalParameters.clone(), remainingTime, timeStamp, + creatorCommand); } else { - return new VeluxProduct(this.name, this.typeId, this.bridgeProductIndex); + return new VeluxProduct(name, typeId, bridgeProductIndex); } } @@ -206,16 +325,26 @@ public ProductBridgeIndex getBridgeProductIndex() { @Override public String toString() { if (this.v2) { - return String.format("Product \"%s\" / %s (bridgeIndex=%d,serial=%s,position=%04X)", this.name, this.typeId, - this.bridgeProductIndex.toInt(), this.serialNumber, this.currentPosition); + FunctionalParameters functionalParameters = this.functionalParameters; + return String.format( + "VeluxProduct(v2, creator:%s, dataSource:%s, name:%s, typeId:%s, bridgeIndex:%d, state:%d, serial:%s, position:%04X, target:%04X, functionalParameters:%s)", + creatorCommand.name(), dataSource.name(), name, typeId, bridgeProductIndex.toInt(), state, + serialNumber, currentPosition, targetPosition, + functionalParameters == null ? "null" : functionalParameters.toString()); } else { - return String.format("Product \"%s\" / %s (bridgeIndex %d)", this.name, this.typeId, - this.bridgeProductIndex.toInt()); + return String.format("VeluxProduct(v1, name:%s, typeId:%s, bridgeIndex:%d)", name, typeId, + bridgeProductIndex.toInt()); } } // Class helper methods + /** + * Return the product unique index. + * Either the serial number (for normal Velux devices), or its name (for e.g. Somfy devices). + * + * @return the serial number or its name + */ public String getProductUniqueIndex() { if (!v2 || serialNumber.startsWith(VeluxProductSerialNo.UNKNOWN)) { return name.toString(); @@ -282,6 +411,15 @@ public int getState() { return state; } + /** + * Get the actuator state. + * + * @return state cast to an ActuatorState enum. + */ + public ProductState getProductState() { + return ProductState.of(state); + } + /** * @param newState Update the operating state of the node. * @return modified as type boolean to signal a real modification. @@ -290,8 +428,8 @@ public boolean setState(int newState) { if (this.state == newState) { return false; } else { - logger.trace("setState(name={},index={}) state {} replaced by {}.", name.toString(), - bridgeProductIndex.toInt(), this.state, newState); + logger.trace("setState(name={},index={}) state {} replaced by {}.", name, bridgeProductIndex, state, + newState); this.state = newState; return true; } @@ -312,8 +450,8 @@ public boolean setCurrentPosition(int newCurrentPosition) { if (this.currentPosition == newCurrentPosition) { return false; } else { - logger.trace("setCurrentPosition(name={},index={}) currentPosition {} replaced by {}.", name.toString(), - bridgeProductIndex.toInt(), this.currentPosition, newCurrentPosition); + logger.trace("setCurrentPosition(name={},index={}) currentPosition {} replaced by {}.", name, + bridgeProductIndex, currentPosition, newCurrentPosition); this.currentPosition = newCurrentPosition; return true; } @@ -334,8 +472,8 @@ public boolean setTarget(int newTarget) { if (this.targetPosition == newTarget) { return false; } else { - logger.trace("setCurrentPosition(name={},index={}) target {} replaced by {}.", name.toString(), - bridgeProductIndex.toInt(), this.targetPosition, newTarget); + logger.trace("setTarget(name={},index={}) target {} replaced by {}.", name, bridgeProductIndex, + targetPosition, newTarget); this.targetPosition = newTarget; return true; } @@ -365,24 +503,176 @@ public int getTimeStamp() { * @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; + switch (getProductState()) { + case EXECUTING: + if (VeluxProductPosition.isValid(targetPosition)) { + return targetPosition; + } + break; + case DONE: + if (!VeluxProductPosition.isValid(currentPosition) && VeluxProductPosition.isValid(targetPosition)) { + return targetPosition; + } + break; + case ERROR: + case UNKNOWN: + case MANUAL: + return VeluxProductPosition.VPP_VELUX_UNKNOWN; + default: } - // 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 VeluxProductPosition.isValid(currentPosition) ? currentPosition : VeluxProductPosition.VPP_VELUX_UNKNOWN; + } + + /** + * Get the Functional Parameters. + * + * @return the Functional Parameters. + */ + public @Nullable FunctionalParameters getFunctionalParameters() { + return functionalParameters; + } + + /** + * Set the Functional Parameters. Calls getMergeSubstitute() to merge the existing parameters (if any) and the new + * parameters (if any). + * + * @param newFunctionalParameters the new values of the Functional Parameters, or null if nothing is to be set. + * @return modified if any of the Functional Parameters have been changed. + */ + public boolean setFunctionalParameters(@Nullable FunctionalParameters newFunctionalParameters) { + if ((newFunctionalParameters == null) || newFunctionalParameters.equals(functionalParameters)) { + return false; } - return currentPosition; + functionalParameters = FunctionalParameters.createMergeSubstitute(functionalParameters, + newFunctionalParameters); + return true; + } + + /** + * Determines which of the Functional Parameters contains the vane position. + * As defined in the Velux KLF 200 API Technical Specification Appendix 2 Table 276. + * + * @return the index of the vane position Functional Parameter, or -1 if not supported. + */ + private int getVanePositionIndex() { + switch (actuatorType) { + case BLIND_1_0: + return 0; + case ROLLERSHUTTER_2_1: + case BLIND_17: + case BLIND_18: + return 2; + default: + } + return -1; + } + + /** + * Indicates if the actuator supports a vane position. + * + * @return true if vane position is supported. + */ + public boolean supportsVanePosition() { + return getVanePositionIndex() >= 0; + } + + /** + * Return the vane position. Reads the vane position from the Functional Parameters, or returns 'UNKNOWN' if vane + * position is not supported. + * + * @return the vane position. + */ + public int getVanePosition() { + FunctionalParameters functionalParameters = this.functionalParameters; + int index = getVanePositionIndex(); + if ((index >= 0) && (functionalParameters != null)) { + return functionalParameters.getValue(index); + } + return VeluxProductPosition.VPP_VELUX_UNKNOWN; + } + + /** + * Set the vane position into the appropriate Functional Parameter. If the actuator does not support vane positions + * then a message is logged. + * + * @param vanePosition the new vane position. + */ + public void setVanePosition(int vanePosition) { + int index = getVanePositionIndex(); + if ((index >= 0) && FunctionalParameters.isNormalPosition(vanePosition)) { + functionalParameters = new FunctionalParameters(index, vanePosition); + } else { + functionalParameters = null; + logger.info("setVanePosition(): actuator type {} ({}) does not support vane position {}.", + ActuatorType.get(actuatorType.getNodeType()), actuatorType.getDescription(), vanePosition); + } + } + + /** + * Get the display position of the vanes depending on the product state. + * See 'getDisplayPosition()'. + * + * @return the display position. + */ + public int getVaneDisplayPosition() { + switch (getProductState()) { + case ERROR: + case UNKNOWN: + case MANUAL: + return VeluxProductPosition.VPP_VELUX_UNKNOWN; + default: + } + return getVanePosition(); + } + + /** + * Get the actuator type. + * + * @return the actuator type. + */ + public ActuatorType getActuatorType() { + return this.actuatorType; + } + + /** + * Set the actuator type. + * Only allowed if the current value is undefined. + * + * @param actuatorType the new value for the actuator type. + */ + public void setActuatorType(ActuatorType actuatorType) { + if (this.actuatorType == ActuatorType.UNDEFTYPE) { + this.actuatorType = actuatorType; + this.typeId = actuatorType.getTypeClass(); + } else { + logger.debug("setActuatorType() failed: not allowed to change actuatorType from {} to {}.", + this.actuatorType, actuatorType); + } + } + + /** + * @return true if it is a Somfy product. + */ + public boolean isSomfyProduct() { + return isSomfyProduct; + } + + /** + * Return the API command that caused this instance to be created. + * + * @return the API command. + */ + public Command getCreatorCommand() { + return creatorCommand; + } + + /** + * Override the indicator of the source of the data for this product. + * + * @return the data source. + */ + public VeluxProduct overrideDataSource(DataSource dataSource) { + this.dataSource = dataSource; + return this; } } 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 f6e03b0fdf89e..fce21d3023659 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 @@ -64,13 +64,32 @@ public class VeluxProductPosition { 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 + public static final int VPP_VELUX_RELATIVE_ORIGIN = 0xCCE8; + public static final int VPP_VELUX_RELATIVE_RANGE = 1000; // same for positive and negative offsets + + /** + * Enum that determines whether the position is an absolute value, or a positive / negative offset relative to the + * current position. + * + * @author AndrewFG - Initial contribution. + */ + public static enum PositionType { + ABSOLUTE_VALUE(0f), + OFFSET_POSITIVE(1f), + OFFSET_NEGATIVE(-1f); + + private float value; + + private PositionType(float i) { + value = i; + } + } // Class internal private PercentType position; private boolean isValid = false; + private PositionType positionType = PositionType.ABSOLUTE_VALUE; // Constructor @@ -102,21 +121,28 @@ public VeluxProductPosition(PercentType position, boolean toBeInverted) { * 0xc800, or 0xD200 for stop). */ public VeluxProductPosition(int veluxPosition) { - logger.trace("VeluxProductPosition(constructur with {} as veluxPosition) called.", veluxPosition); - if ((veluxPosition == VPP_VELUX_UNKNOWN) || (veluxPosition == VPP_VELUX_STOP) || (veluxPosition < VPP_VELUX_MIN) - || (veluxPosition > VPP_VELUX_MAX)) { - logger.trace("VeluxProductPosition() gives up."); - this.position = new PercentType(VPP_UNKNOWN); - this.isValid = false; - } else { + logger.trace("VeluxProductPosition(constructor with {} as veluxPosition) called.", veluxPosition); + if (isValid(veluxPosition)) { float result = (ONE * veluxPosition - VPP_VELUX_MIN) / (VPP_VELUX_MAX - VPP_VELUX_MIN); result = Math.round(VPP_OPENHAB_MIN + result * (VPP_OPENHAB_MAX - VPP_OPENHAB_MIN)); - logger.trace("VeluxProductPosition() created with percent-type {}.", (int) result); this.position = new PercentType((int) result); this.isValid = true; + logger.trace("VeluxProductPosition() created with percent-type {}.", (int) result); + } else { + this.position = new PercentType(VPP_UNKNOWN); + this.isValid = false; + logger.trace("VeluxProductPosition() gives up."); } } + public static boolean isValid(int position) { + return (position <= VeluxProductPosition.VPP_VELUX_MAX) && (position >= VeluxProductPosition.VPP_VELUX_MIN); + } + + public static boolean isUnknownOrValid(int position) { + return (position == VeluxProductPosition.VPP_UNKNOWN) || isValid(position); + } + /** * Creation of a Position object to specify a STOP. */ @@ -142,8 +168,15 @@ public PercentType getPositionAsPercentType(boolean toBeInverted) { public int getPositionAsVeluxType() { if (this.isValid) { - float result = (ONE * position.intValue() - VPP_OPENHAB_MIN) / (VPP_OPENHAB_MAX - VPP_OPENHAB_MIN); - result = VPP_VELUX_MIN + result * (VPP_VELUX_MAX - VPP_VELUX_MIN); + float result; + if (positionType == PositionType.ABSOLUTE_VALUE) { + result = (ONE * position.intValue() - VPP_OPENHAB_MIN) / (VPP_OPENHAB_MAX - VPP_OPENHAB_MIN); + result = VPP_VELUX_MIN + result * (VPP_VELUX_MAX - VPP_VELUX_MIN); + } else { + result = VPP_VELUX_RELATIVE_ORIGIN + + ((positionType.value * position.intValue() * VPP_VELUX_RELATIVE_RANGE) + / (VPP_OPENHAB_MAX - VPP_OPENHAB_MIN)); + } return (int) result; } else { return VPP_VELUX_STOP; @@ -161,8 +194,8 @@ public String toString() { // Helper methods - 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; + public VeluxProductPosition overridePositionType(PositionType positionType) { + this.positionType = positionType; + return this; } } diff --git a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductType.java b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductType.java index a2b174fec020a..ef22e251ae085 100644 --- a/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductType.java +++ b/bundles/org.openhab.binding.velux/src/main/java/org/openhab/binding/velux/internal/things/VeluxProductType.java @@ -44,7 +44,7 @@ public enum VeluxProductType { SWITCH, UNDEFTYPE; - private static enum ActuatorType { + public static enum ActuatorType { UNDEFTYPE((short) 0xffff, VeluxBindingConstants.UNKNOWN, VeluxProductType.SWITCH), BLIND_1_0((short) 0x0040, "Interior Venetian Blind", VeluxProductType.SLIDER_SHUTTER), ROLLERSHUTTER_2_0((short) 0x0080, "Roller Shutter", VeluxProductType.SLIDER_SHUTTER), @@ -97,15 +97,19 @@ private ActuatorType(short nodeType, String description, VeluxProductType typeCl // Class access methods - int getNodeType() { + public int getNodeType() { return nodeType; } - String getDescription() { + public String getDescription() { return description; } - static ActuatorType get(int nodeType) { + public VeluxProductType getTypeClass() { + return typeClass; + } + + public static ActuatorType get(int nodeType) { return LOOKUPTYPEID2ENUM.getOrDefault(nodeType, ActuatorType.UNDEFTYPE); } } 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 ea7e920a026b4..6440a35f99895 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 @@ -135,6 +135,8 @@ channel-type.velux.check.description = Result of the check of current item confi # channel-type.velux.position.label = Position channel-type.velux.position.description = Device control (UP, DOWN, STOP, closure 0-100%). +channel-type.velux.vanePosition.label = Vane Position +channel-type.velux.vanePosition.description = Venetian blind vane (tilt) position. channel-type.velux.state.label = State channel-type.velux.state.description = Device control (ON, OFF). channel-type.velux.action.label = Start of a Scene diff --git a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/channels.xml index ed7c0519d5f2c..3a06e242c7387 100644 --- a/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.velux/src/main/resources/OH-INF/thing/channels.xml @@ -139,6 +139,14 @@ Blinds + + Dimmer + + @text/channel-type.velux.vanePosition.description + Blinds + + + Switch 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 ed19d77c654f3..f1cd52956b4d9 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 @@ -19,6 +19,7 @@ + serial diff --git a/bundles/org.openhab.binding.velux/src/test/java/org/openhab/binding/velux/test/TestNotificationsAndDatabase.java b/bundles/org.openhab.binding.velux/src/test/java/org/openhab/binding/velux/test/TestNotificationsAndDatabase.java new file mode 100644 index 0000000000000..56e621387eee1 --- /dev/null +++ b/bundles/org.openhab.binding.velux/src/test/java/org/openhab/binding/velux/test/TestNotificationsAndDatabase.java @@ -0,0 +1,907 @@ +/** + * 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.test; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.openhab.binding.velux.internal.bridge.slip.FunctionalParameters; +import org.openhab.binding.velux.internal.bridge.slip.SCgetHouseStatus; +import org.openhab.binding.velux.internal.bridge.slip.SCgetProduct; +import org.openhab.binding.velux.internal.bridge.slip.SCgetProductStatus; +import org.openhab.binding.velux.internal.bridge.slip.SCrunProductCommand; +import org.openhab.binding.velux.internal.things.VeluxExistingProducts; +import org.openhab.binding.velux.internal.things.VeluxKLFAPI; +import org.openhab.binding.velux.internal.things.VeluxKLFAPI.Command; +import org.openhab.binding.velux.internal.things.VeluxProduct; +import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex; +import org.openhab.binding.velux.internal.things.VeluxProduct.ProductState; +import org.openhab.binding.velux.internal.things.VeluxProductName; +import org.openhab.binding.velux.internal.things.VeluxProductPosition; +import org.openhab.binding.velux.internal.things.VeluxProductPosition.PositionType; +import org.openhab.binding.velux.internal.things.VeluxProductType.ActuatorType; +import org.openhab.core.library.types.PercentType; + +/** + * JUnit test suite to check the proper parsing of actuator notification packets, and to confirm that the existing + * products database is working correctly. + * + * @author Andrew Fiddian-Green - Initial contribution. + */ +@NonNullByDefault +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TestNotificationsAndDatabase { + // validation parameters + private static final byte PRODUCT_INDEX_A = 6; + private static final byte PRODUCT_INDEX_B = 0; + private static final int MAIN_POSITION_A = 0xC800; + private static final int MAIN_POSITION_B = 0x4600; + private static final int VANE_POSITION_A = 0x634f; + private static final int TARGET_POSITION = 0xB800; + private static final int STATE_SOMFY = 0x2D; + + private static final int UNKNOWN_POSITION = VeluxProductPosition.VPP_VELUX_UNKNOWN; + private static final int IGNORE_POSITION = VeluxProductPosition.VPP_VELUX_IGNORE; + private static final int STATE_DONE = VeluxProduct.ProductState.DONE.value; + + private static final ActuatorType ACTUATOR_TYPE_SOMFY = ActuatorType.BLIND_17; + private static final ActuatorType ACTUATOR_TYPE_VELUX = ActuatorType.WINDOW_4_1; + private static final ActuatorType ACTUATOR_TYPE_UNDEF = ActuatorType.UNDEFTYPE; + + // existing products database + private static final VeluxExistingProducts EXISTING_PRODUCTS = new VeluxExistingProducts(); + + private static byte[] toByteArray(String input) { + String[] data = input.split(" "); + byte[] result = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + result[i] = Integer.decode("0x" + data[i]).byteValue(); + } + return result; + } + + private VeluxExistingProducts getExistingProducts() { + return EXISTING_PRODUCTS; + } + + /** + * Confirm the existing products database is initialised. + */ + @Test + @Order(1) + public void testInitialized() { + assertEquals(0, getExistingProducts().getNoMembers()); + } + + /** + * Test the 'supportsVanePosition()' method for two types of products. + */ + @Test + @Order(2) + public void testSupportsVanePosition() { + VeluxProduct product = new VeluxProduct(); + assertFalse(product.supportsVanePosition()); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + assertTrue(product.supportsVanePosition()); + } + + /** + * Test the SCgetProduct command by checking for the correct parsing of a 'GW_GET_NODE_INFORMATION_NTF' notification + * packet. Note: this packet is from a Somfy roller shutter with main and vane position. + */ + @Test + @Order(3) + public void testSCgetProduct() { + // initialise the test parameters + final String packet = "06 00 06 00 48 6F 62 62 79 6B 61 6D 65 72 00 00 00 00 00 00 00 00 00 00 00 00 00 00" + + " 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" + + " 00 00 00 00 00 01 04 40 00 00 00 00 00 00 00 00 00 00 00 00 00 2D C8 00 C8 00 F7 FF F7 FF 00 00 F7 FF 00" + + " 00 4F 00 4A EA 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"; + final Command command = VeluxKLFAPI.Command.GW_GET_NODE_INFORMATION_NTF; + + // initialise the BCP + SCgetProduct bcp = new SCgetProduct(); + bcp.setProductId(PRODUCT_INDEX_A); + + // set the packet response + bcp.setResponse(command.getShort(), toByteArray(packet), false); + + // check BCP status + assertTrue(bcp.isCommunicationSuccessful()); + assertTrue(bcp.isCommunicationFinished()); + + // initialise the product + VeluxProduct product = bcp.getProduct(); + + // check positive assertions + assertEquals(STATE_SOMFY, product.getState()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(MAIN_POSITION_A, product.getTarget()); + assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt()); + assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType()); + assertEquals(UNKNOWN_POSITION, product.getVanePosition()); + assertNull(product.getFunctionalParameters()); + assertTrue(product.supportsVanePosition()); + assertTrue(product.isSomfyProduct()); + assertEquals(ProductState.DONE, product.getProductState()); + + // check negative assertions + assertNotEquals(VANE_POSITION_A, product.getVanePosition()); + + // register in existing products database + VeluxExistingProducts existingProducts = getExistingProducts(); + assertTrue(existingProducts.register(product)); + assertTrue(existingProducts.isRegistered(product)); + assertTrue(existingProducts.isRegistered(product.getBridgeProductIndex())); + assertEquals(1, existingProducts.getNoMembers()); + + // confirm that a dummy product is NOT in the database + assertFalse(existingProducts.isRegistered(new ProductBridgeIndex(99))); + + // check dirty flag + assertTrue(existingProducts.isDirty()); + existingProducts.resetDirtyFlag(); + assertFalse(existingProducts.isDirty()); + + // re-registering the same product should return false + assertFalse(existingProducts.register(product)); + + // updating again with the same data should NOT set the dirty flag + assertTrue(existingProducts.update(product)); + assertFalse(existingProducts.isDirty()); + + // check that the product in the database is indeed the one just created + VeluxProduct existing = existingProducts.get(new ProductBridgeIndex(PRODUCT_INDEX_A)); + assertEquals(product, existing); + assertEquals(1, existingProducts.getNoMembers()); + assertTrue(existingProducts.isRegistered(product.getBridgeProductIndex())); + } + + /** + * Confirm that the product in the existing database has the same values as the product created and added in test 3. + */ + @Test + @Order(4) + public void testExistingUnknownVanePosition() { + VeluxExistingProducts existingProducts = getExistingProducts(); + VeluxProduct product = existingProducts.get(new ProductBridgeIndex(PRODUCT_INDEX_A)); + + // confirm the product details + assertEquals(STATE_SOMFY, product.getState()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(MAIN_POSITION_A, product.getTarget()); + assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt()); + assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType()); + assertEquals(UNKNOWN_POSITION, product.getVanePosition()); + assertNull(product.getFunctionalParameters()); + } + + /** + * Test the SCgetProductStatus command by checking for the correct parsing of a 'GW_STATUS_REQUEST_NTF' notification + * packet. Note: this packet is from a Somfy roller shutter with main and vane position. + */ + @Test + @Order(5) + public void testSCgetProductStatus() { + // initialise the test parameters + final String packet = "00 D8 01 06 00 01 01 02 00 C8 00 03 63 4F 00 00 00 00 00 00 00 00 00 00 00 00 00" + + " 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"; + final Command command = VeluxKLFAPI.Command.GW_STATUS_REQUEST_NTF; + + // initialise the BCP + SCgetProductStatus bcp = new SCgetProductStatus(); + bcp.setProductId(PRODUCT_INDEX_A); + + // set the packet response + bcp.setResponse(command.getShort(), toByteArray(packet), false); + + // check BCP status + assertTrue(bcp.isCommunicationSuccessful()); + assertTrue(bcp.isCommunicationFinished()); + + // initialise the product + VeluxProduct product = bcp.getProduct(); + + // change actuator type + assertEquals(ACTUATOR_TYPE_UNDEF, product.getActuatorType()); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + + // check positive assertions + assertEquals(STATE_DONE, product.getState()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(IGNORE_POSITION, product.getTarget()); + assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt()); + assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + assertNotNull(product.getFunctionalParameters()); + assertEquals(ProductState.DONE, product.getProductState()); + + // test updating the existing product in the database + VeluxExistingProducts existingProducts = getExistingProducts(); + assertTrue(existingProducts.update(product)); + assertTrue(existingProducts.isDirty()); + + // updating again with the same data should NOT set the dirty flag + existingProducts.resetDirtyFlag(); + assertTrue(existingProducts.update(product)); + assertFalse(existingProducts.isDirty()); + } + + /** + * Confirm that the product in the existing database has the same values as the product created and updated to the + * database in test 5. + */ + @Test + @Order(6) + public void testExistingValidVanePosition() { + VeluxExistingProducts existingProducts = getExistingProducts(); + VeluxProduct product = existingProducts.get(new ProductBridgeIndex(PRODUCT_INDEX_A)); + + // confirm the product details + assertEquals(STATE_DONE, product.getState()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(MAIN_POSITION_A, product.getTarget()); + assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt()); + assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + assertNotNull(product.getFunctionalParameters()); + assertEquals(ProductState.DONE, product.getProductState()); + } + + /** + * Test the SCgetHouseStatus command by checking for the correct parsing of a 'GW_NODE_STATE_POSITION_CHANGED_NTF' + * notification packet. Note: this packet is from a Somfy roller shutter with main and vane position. + */ + @Test + @Order(7) + public void testSCgetHouseStatus() { + // initialise the test parameters + final String packet = "06 2D C8 00 B8 00 F7 FF F7 FF 00 00 F7 FF 00 00 4A E5 00 00"; + final short command = VeluxKLFAPI.Command.GW_NODE_STATE_POSITION_CHANGED_NTF.getShort(); + + // initialise the BCP + SCgetHouseStatus bcp = new SCgetHouseStatus(); + + // set the packet response + bcp.setResponse(command, toByteArray(packet), false); + + // check BCP status + assertTrue(bcp.isCommunicationSuccessful()); + assertTrue(bcp.isCommunicationFinished()); + + // initialise the product + VeluxProduct product = bcp.getProduct(); + + // change actuator type + assertEquals(ACTUATOR_TYPE_UNDEF, product.getActuatorType()); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + + // check positive assertions + assertEquals(STATE_SOMFY, product.getState()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt()); + assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType()); + assertEquals(UNKNOWN_POSITION, product.getVanePosition()); + assertNull(product.getFunctionalParameters()); + + // check negative assertions + assertNotEquals(VANE_POSITION_A, product.getVanePosition()); + + VeluxExistingProducts existingProducts = getExistingProducts(); + existingProducts.update(product); + } + + /** + * Confirm that the product in the existing database has the same values as the product created and updated to the + * database in test 7. + */ + @Test + @Order(8) + public void testExistingValidVanePositionWithNewTargetValue() { + VeluxExistingProducts existingProducts = getExistingProducts(); + VeluxProduct product = existingProducts.get(new ProductBridgeIndex(PRODUCT_INDEX_A)); + + // confirm the product details + assertEquals(STATE_SOMFY, product.getState()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt()); + assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + assertNotNull(product.getFunctionalParameters()); + } + + /** + * Test the SCgetProduct by checking for the correct parsing of a 'GW_GET_NODE_INFORMATION_NTF' notification packet. + * Note: this packet is from a Velux roof window without vane position. + */ + @Test + @Order(9) + public void testSCgetProductOnVelux() { + // initialise the test parameters + final String packet = "00 00 00 00 53 68 65 64 20 57 69 6E 64 6F 77 00 00 00 00 00 00 00 00 00 00 00 00 00" + + " 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" + + " 00 00 00 00 00 00 00 00 01 01 01 03 07 00 01 16 56 24 5C 26 14 19 00 FC 05 46 00 46 00 F7 FF F7" + + " FF F7 FF F7 FF 00 00 4F 05 B3 5F 01 D8 03 B2 1C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"; + final short command = VeluxKLFAPI.Command.GW_GET_NODE_INFORMATION_NTF.getShort(); + + // initialise the BCP + SCgetProduct bcp = new SCgetProduct(); + bcp.setProductId(PRODUCT_INDEX_B); + + // set the packet response + bcp.setResponse(command, toByteArray(packet), false); + + // check BCP status + assertTrue(bcp.isCommunicationSuccessful()); + assertTrue(bcp.isCommunicationFinished()); + + // initialise the product + VeluxProduct product = bcp.getProduct(); + + // check positive assertions + assertEquals(STATE_DONE, product.getState()); + assertEquals(MAIN_POSITION_B, product.getCurrentPosition()); + assertEquals(MAIN_POSITION_B, product.getTarget()); + assertEquals(PRODUCT_INDEX_B, product.getBridgeProductIndex().toInt()); + assertEquals(ACTUATOR_TYPE_VELUX, product.getActuatorType()); + assertEquals(UNKNOWN_POSITION, product.getVanePosition()); + assertNull(product.getFunctionalParameters()); + assertFalse(product.isSomfyProduct()); + + // check negative assertions + assertFalse(product.supportsVanePosition()); + assertNotEquals(VANE_POSITION_A, product.getVanePosition()); + + // register in existing products database + VeluxExistingProducts existingProducts = getExistingProducts(); + assertTrue(existingProducts.register(product)); + assertTrue(existingProducts.isRegistered(product)); + assertTrue(existingProducts.isRegistered(product.getBridgeProductIndex())); + assertEquals(2, existingProducts.getNoMembers()); + + // check that the product in the database is indeed the one just created + VeluxProduct existing = existingProducts.get(new ProductBridgeIndex(PRODUCT_INDEX_B)); + assertEquals(product, existing); + assertTrue(existingProducts.isRegistered(product.getBridgeProductIndex())); + } + + /** + * Confirm that the modified products list is functioning. + */ + @Test + @Order(10) + public void testModifiedList() { + VeluxExistingProducts existingProducts = getExistingProducts(); + VeluxProduct[] modified = existingProducts.valuesOfModified(); + + // confirm that the list contains two entries + assertEquals(2, modified.length); + + // confirm the product details for the Somfy product + VeluxProduct product = modified[0]; + assertEquals(STATE_SOMFY, product.getState()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(PRODUCT_INDEX_A, product.getBridgeProductIndex().toInt()); + assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + assertTrue(product.isSomfyProduct()); + assertNotNull(product.getFunctionalParameters()); + + // confirm the product details for the Velux product + product = modified[1]; + assertEquals(STATE_DONE, product.getState()); + assertEquals(MAIN_POSITION_B, product.getCurrentPosition()); + assertEquals(MAIN_POSITION_B, product.getTarget()); + assertEquals(PRODUCT_INDEX_B, product.getBridgeProductIndex().toInt()); + assertEquals(ACTUATOR_TYPE_VELUX, product.getActuatorType()); + assertEquals(UNKNOWN_POSITION, product.getVanePosition()); + assertNull(product.getFunctionalParameters()); + assertFalse(product.isSomfyProduct()); + + // reset the dirty flag + existingProducts.resetDirtyFlag(); + assertFalse(existingProducts.isDirty()); + + // confirm modified list is now empty again + modified = existingProducts.valuesOfModified(); + assertEquals(0, modified.length); + } + + /** + * Test actuator type setting. + */ + @Test + @Order(11) + public void testActuatorTypeSetting() { + VeluxProduct product = new VeluxProduct(); + assertEquals(ACTUATOR_TYPE_UNDEF, product.getActuatorType()); + + // set actuator type + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + assertEquals(ACTUATOR_TYPE_SOMFY, product.getActuatorType()); + + // try to set it again + product.setActuatorType(ACTUATOR_TYPE_VELUX); + assertNotEquals(ACTUATOR_TYPE_VELUX, product.getActuatorType()); + + // try with a clean product + product = new VeluxProduct(); + product.setActuatorType(ACTUATOR_TYPE_VELUX); + assertEquals(ACTUATOR_TYPE_VELUX, product.getActuatorType()); + } + + /** + * Test the SCrunProduct command by creating packet for a given main position and vane position, and checking the + * created packet is as expected. + */ + @Test + @Order(12) + public void testSCrunProductA() { + final String expectedString = "02 1C 08 05 00 20 00 90 00 00 00 00 00 A0 00 00 00 00 00 00 00 00 00 00 00" + + " 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00" + + " 00 00 00 00 00 00 00 00 00"; + final byte[] expectedPacket = toByteArray(expectedString); + final int targetMainPosition = 0x9000; + final int targetVanePosition = 0xA000; + + // initialise the product to be commanded + VeluxProduct product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(PRODUCT_INDEX_A), 0, 0, + 0, null, Command.UNDEFTYPE); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + product.setCurrentPosition(targetMainPosition); + product.setVanePosition(targetVanePosition); + + // create the run product command, and initialise it from the test product's state values + SCrunProductCommand bcp = new SCrunProductCommand(); + bcp.setNodeIdAndParameters(product.getBridgeProductIndex().toInt(), + new VeluxProductPosition(product.getCurrentPosition()), product.getFunctionalParameters()); + + // get the resulting data packet + byte[] actualPacket = bcp.getRequestDataAsArrayOfBytes(); + + // check the packet lengths are the same + assertEquals(expectedPacket.length, actualPacket.length); + + // check the packet contents are identical (note start at i = 2 because session id won't match) + boolean identical = true; + for (int i = 2; i < expectedPacket.length; i++) { + if (actualPacket[i] != expectedPacket[i]) { + identical = false; + } + } + assertTrue(identical); + + // check the resulting updater product state is 'executing' with the new values + product = bcp.getProduct(); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + assertEquals(ProductState.EXECUTING, product.getProductState()); + assertEquals(targetMainPosition, product.getCurrentPosition()); + assertEquals(targetMainPosition, product.getTarget()); + assertEquals(targetMainPosition, product.getDisplayPosition()); + assertEquals(targetVanePosition, product.getVanePosition()); + assertEquals(targetVanePosition, product.getVaneDisplayPosition()); + } + + /** + * Test the SCrunProduct command by creating a packet with some bad values, and checking the product is as expected. + */ + @Test + @Order(12) + public void testSCrunProductB() { + SCrunProductCommand bcp = new SCrunProductCommand(); + + final int errorMainPosition = 0xffff; + VeluxProduct product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(PRODUCT_INDEX_A), 0, 0, + 0, null, Command.UNDEFTYPE); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + product.setCurrentPosition(errorMainPosition); + + bcp.setNodeIdAndParameters(product.getBridgeProductIndex().toInt(), + new VeluxProductPosition(product.getCurrentPosition()), product.getFunctionalParameters()); + product = bcp.getProduct(); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + + assertEquals(ProductState.EXECUTING, product.getProductState()); + assertEquals(IGNORE_POSITION, product.getCurrentPosition()); + assertEquals(IGNORE_POSITION, product.getTarget()); + assertEquals(UNKNOWN_POSITION, product.getDisplayPosition()); + assertEquals(UNKNOWN_POSITION, product.getVanePosition()); + assertEquals(UNKNOWN_POSITION, product.getVaneDisplayPosition()); + } + + /** + * Test the actuator state. + */ + @Test + @Order(14) + public void testActuatorState() { + VeluxProduct product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(PRODUCT_INDEX_A), 0, 0, + 0, null, Command.UNDEFTYPE); + int[] inputStates = { 0, 1, 2, 3, 4, 5, 0x2c, 0x2d, 0xff, 0x80 }; + ProductState[] expected = { ProductState.NON_EXECUTING, ProductState.ERROR, ProductState.NOT_USED, + ProductState.WAITING_FOR_POWER, ProductState.EXECUTING, ProductState.DONE, ProductState.EXECUTING, + ProductState.DONE, ProductState.UNKNOWN, ProductState.MANUAL }; + for (int i = 0; i < inputStates.length; i++) { + product.setState(inputStates[i]); + assertEquals(expected[i], product.getProductState()); + } + } + + /** + * Test actuator positions and display positions. + */ + @Test + @Order(15) + public void testActuatorPositions() { + VeluxProduct product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(PRODUCT_INDEX_A), 0, 0, + 0, null, Command.UNDEFTYPE); + + product.setCurrentPosition(MAIN_POSITION_A); + product.setTarget(TARGET_POSITION); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + product.setVanePosition(VANE_POSITION_A); + + // state uninitialised + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(MAIN_POSITION_A, product.getDisplayPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + + // state = done + product.setState(ProductState.DONE.value); + assertEquals(MAIN_POSITION_A, product.getDisplayPosition()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + + // state = not used + product.setState(ProductState.NOT_USED.value); + assertEquals(MAIN_POSITION_A, product.getDisplayPosition()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + + // state = executing + product.setState(ProductState.EXECUTING.value); + assertEquals(TARGET_POSITION, product.getDisplayPosition()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + + // state = manual + excuting + product.setState(ProductState.MANUAL.value + ProductState.EXECUTING.value); + assertEquals(UNKNOWN_POSITION, product.getDisplayPosition()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(UNKNOWN_POSITION, product.getVaneDisplayPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + + // state = error + product.setState(ProductState.ERROR.value); + assertEquals(UNKNOWN_POSITION, product.getDisplayPosition()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(UNKNOWN_POSITION, product.getVaneDisplayPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + } + + /** + * Test the SCgetHouseStatus command by checking for the correct parsing of a 'GW_NODE_STATE_POSITION_CHANGED_NTF' + * notification packet. Note: this packet is from a Velux roller shutter with main and vane position. + */ + @Test + @Order(16) + public void testSCgetHouseStatusOnVelux() { + // initialise the test parameters + final String packet = "00 2D C8 00 B8 00 F7 FF F7 FF 00 00 F7 FF 00 00 4A E5 00 00"; + final short command = VeluxKLFAPI.Command.GW_NODE_STATE_POSITION_CHANGED_NTF.getShort(); + + // initialise the BCP + SCgetHouseStatus bcp = new SCgetHouseStatus(); + + // set the packet response + bcp.setResponse(command, toByteArray(packet), false); + + // check BCP status + assertTrue(bcp.isCommunicationSuccessful()); + assertTrue(bcp.isCommunicationFinished()); + + // initialise the product + VeluxProduct product = bcp.getProduct(); + + // change actuator type + assertEquals(ACTUATOR_TYPE_UNDEF, product.getActuatorType()); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + + // check negative assertions + assertNotEquals(VANE_POSITION_A, product.getVanePosition()); + + // test updating the existing product in the database + VeluxExistingProducts existingProducts = getExistingProducts(); + + // process this as a receive only command for a Velux product => update IS applied + existingProducts.resetDirtyFlag(); + assertTrue(existingProducts.update(product)); + assertTrue(existingProducts.isDirty()); + + // process this as an information request command for a Velux product => update IS applied + existingProducts.resetDirtyFlag(); + product.setCurrentPosition(MAIN_POSITION_B); + assertTrue(existingProducts.update(product)); + assertTrue(existingProducts.isDirty()); + } + + /** + * Test updating logic with various states applied. + */ + @Test + @Order(17) + public void testUpdatingLogic() { + VeluxExistingProducts existingProducts = getExistingProducts(); + ProductBridgeIndex index = new ProductBridgeIndex(PRODUCT_INDEX_A); + VeluxProduct product = existingProducts.get(index).clone(); + + assertEquals(ProductState.DONE, product.getProductState()); + + // state = done + assertEquals(MAIN_POSITION_A, product.getDisplayPosition()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + + // state = not used + product.setState(ProductState.NOT_USED.value); + product.setCurrentPosition(MAIN_POSITION_A - 1); + product.setTarget(TARGET_POSITION - 1); + product.setVanePosition(VANE_POSITION_A - 1); + existingProducts.update(product); + product = existingProducts.get(index).clone(); + assertEquals(MAIN_POSITION_A, product.getDisplayPosition()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + + // state = manual + excuting + product.setState(ProductState.MANUAL.value + ProductState.EXECUTING.value); + product.setCurrentPosition(MAIN_POSITION_A - 1); + product.setTarget(TARGET_POSITION - 1); + product.setVanePosition(VANE_POSITION_A - 1); + existingProducts.update(product); + product = existingProducts.get(index).clone(); + assertEquals(UNKNOWN_POSITION, product.getDisplayPosition()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(UNKNOWN_POSITION, product.getVaneDisplayPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + + // state = error + product.setState(ProductState.ERROR.value); + product.setCurrentPosition(MAIN_POSITION_A - 1); + product.setTarget(TARGET_POSITION - 1); + product.setVanePosition(VANE_POSITION_A - 1); + existingProducts.update(product); + product = existingProducts.get(index).clone(); + assertEquals(UNKNOWN_POSITION, product.getDisplayPosition()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(UNKNOWN_POSITION, product.getVaneDisplayPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + + // state = executing + product.setState(ProductState.EXECUTING.value); + product.setCurrentPosition(MAIN_POSITION_A - 1); + product.setTarget(TARGET_POSITION - 1); + product.setVanePosition(VANE_POSITION_A - 1); + existingProducts.update(product); + product = existingProducts.get(index).clone(); + assertNotEquals(TARGET_POSITION, product.getDisplayPosition()); + assertNotEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertNotEquals(TARGET_POSITION, product.getTarget()); + assertNotEquals(VANE_POSITION_A, product.getVaneDisplayPosition()); + assertNotEquals(VANE_POSITION_A, product.getVanePosition()); + + // state = done + product.setState(ProductState.EXECUTING.value); + product.setCurrentPosition(MAIN_POSITION_A); + product.setTarget(TARGET_POSITION); + product.setVanePosition(VANE_POSITION_A); + existingProducts.update(product); + product = existingProducts.get(index).clone(); + assertEquals(TARGET_POSITION, product.getDisplayPosition()); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(TARGET_POSITION, product.getTarget()); + assertEquals(VANE_POSITION_A, product.getVaneDisplayPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + } + + /** + * Test updating the existing product in the database with special exceptions. + */ + @Test + @Order(18) + public void testSpecialExceptions() { + VeluxExistingProducts existingProducts = getExistingProducts(); + VeluxProduct existing = existingProducts.get(new ProductBridgeIndex(PRODUCT_INDEX_A)); + + VeluxProduct product; + + // process this as a receive only command for a Somfy product => update IS applied + product = new VeluxProduct(existing.getProductName(), existing.getBridgeProductIndex(), existing.getState(), + existing.getCurrentPosition(), existing.getTarget(), existing.getFunctionalParameters(), + Command.GW_OPENHAB_RECEIVEONLY); + existingProducts.resetDirtyFlag(); + product.setState(ProductState.DONE.value); + product.setCurrentPosition(MAIN_POSITION_B); + assertTrue(existingProducts.update(product)); + assertTrue(existingProducts.isDirty()); + + // process this as an information request command for a Somfy product => update IS applied + product = new VeluxProduct(existing.getProductName(), existing.getBridgeProductIndex(), existing.getState(), + existing.getCurrentPosition(), existing.getTarget(), existing.getFunctionalParameters(), + Command.GW_GET_NODE_INFORMATION_REQ); + existingProducts.resetDirtyFlag(); + product.setCurrentPosition(MAIN_POSITION_A); + assertTrue(existingProducts.update(product)); + assertTrue(existingProducts.isDirty()); + + // process this as a receive only command for a Somfy product with bad data => update NOT applied + product = new VeluxProduct(existing.getProductName(), existing.getBridgeProductIndex(), existing.getState(), + existing.getCurrentPosition(), existing.getTarget(), existing.getFunctionalParameters(), + Command.GW_OPENHAB_RECEIVEONLY); + existingProducts.resetDirtyFlag(); + product.setCurrentPosition(UNKNOWN_POSITION); + product.setTarget(UNKNOWN_POSITION); + assertTrue(existingProducts.update(product)); + assertFalse(existingProducts.isDirty()); + } + + /** + * Test VeluxProductPosition + */ + @Test + @Order(19) + public void testVeluxProductPosition() { + VeluxProductPosition position; + int target; + + // on and inside range limits + assertTrue(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_MIN).isValid()); + assertTrue(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_MAX).isValid()); + assertTrue(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_MAX - 1).isValid()); + assertTrue(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_MIN + 1).isValid()); + + // outside range limits + assertFalse(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_MIN - 1).isValid()); + assertFalse(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_MAX + 1).isValid()); + assertFalse(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_IGNORE).isValid()); + assertFalse(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_DEFAULT).isValid()); + assertFalse(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_STOP).isValid()); + assertFalse(new VeluxProductPosition(VeluxProductPosition.VPP_VELUX_UNKNOWN).isValid()); + + // 80% absolute position + position = new VeluxProductPosition(new PercentType(80)); + assertEquals(0xA000, position.getPositionAsVeluxType()); + assertTrue(position.isValid()); + + // 80% absolute position + position = new VeluxProductPosition(new PercentType(80), false); + assertEquals(0xA000, position.getPositionAsVeluxType()); + assertTrue(position.isValid()); + + // 80% inverted absolute position (i.e. 20%) + position = new VeluxProductPosition(new PercentType(80), true); + assertEquals(0x2800, position.getPositionAsVeluxType()); + assertTrue(position.isValid()); + + // 80% positive relative position + target = VeluxProductPosition.VPP_VELUX_RELATIVE_ORIGIN + + (VeluxProductPosition.VPP_VELUX_RELATIVE_RANGE * 8 / 10); + position = new VeluxProductPosition(new PercentType(80)).overridePositionType(PositionType.OFFSET_POSITIVE); + assertTrue(position.isValid()); + assertEquals(target, position.getPositionAsVeluxType()); + + // 80% negative relative position + target = VeluxProductPosition.VPP_VELUX_RELATIVE_ORIGIN + - (VeluxProductPosition.VPP_VELUX_RELATIVE_RANGE * 8 / 10); + position = new VeluxProductPosition(new PercentType(80)).overridePositionType(PositionType.OFFSET_NEGATIVE); + assertTrue(position.isValid()); + assertEquals(target, position.getPositionAsVeluxType()); + } + + /** + * Test SCrunProductResult results + */ + @Test + @Order(20) + public void testSCrunProductResults() { + SCrunProductCommand bcp = new SCrunProductCommand(); + + // create a dummy product to get some functional parameters from + VeluxProduct product = new VeluxProduct(VeluxProductName.UNKNOWN, new ProductBridgeIndex(PRODUCT_INDEX_A), 0, 0, + 0, null, Command.UNDEFTYPE); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + product.setVanePosition(VANE_POSITION_A); + final FunctionalParameters functionalParameters = product.getFunctionalParameters(); + + boolean ok; + + // test setting both main and vane position + ok = bcp.setNodeIdAndParameters(PRODUCT_INDEX_A, new VeluxProductPosition(MAIN_POSITION_A), + functionalParameters); + assertTrue(ok); + product = bcp.getProduct(); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + + // test setting vane position only + ok = bcp.setNodeIdAndParameters(PRODUCT_INDEX_A, null, functionalParameters); + assertTrue(ok); + product = bcp.getProduct(); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + assertEquals(IGNORE_POSITION, product.getCurrentPosition()); + assertEquals(VANE_POSITION_A, product.getVanePosition()); + + // test setting main position only + ok = bcp.setNodeIdAndParameters(PRODUCT_INDEX_A, new VeluxProductPosition(MAIN_POSITION_A), null); + assertTrue(ok); + product = bcp.getProduct(); + product.setActuatorType(ACTUATOR_TYPE_SOMFY); + assertEquals(MAIN_POSITION_A, product.getCurrentPosition()); + assertEquals(UNKNOWN_POSITION, product.getVanePosition()); + + // test setting neither + ok = bcp.setNodeIdAndParameters(PRODUCT_INDEX_A, null, null); + assertFalse(ok); + } + + /** + * Test SCgetProductStatus exceptional error state processing. + */ + @Test + @Order(21) + public void testErrorStateMapping() { + // initialise the test parameters + final String packet = "0F A3 01 06 01 00 01 02 00 9A 36 03 00 00 00 00 00 00 00 00 00 00 00 00 00" + + " 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" + + " 00 00 00 00 00"; + final Command command = VeluxKLFAPI.Command.GW_STATUS_REQUEST_NTF; + + // initialise the BCP + SCgetProductStatus bcp = new SCgetProductStatus(); + bcp.setProductId(PRODUCT_INDEX_A); + + // set the packet response + bcp.setResponse(command.getShort(), toByteArray(packet), false); + + // check BCP status + assertTrue(bcp.isCommunicationSuccessful()); + assertTrue(bcp.isCommunicationFinished()); + + // check the product state + assertEquals(ProductState.UNKNOWN.value, bcp.getProduct().getState()); + } +}