[velux] Add support for vane/slat position (#12618)

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
Andrew Fiddian-Green 2022-07-31 14:15:25 +01:00 committed by GitHub
parent ec5794739c
commit b9782484c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 2402 additions and 470 deletions

View File

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

View File

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

View File

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

View File

@ -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 <B>Velux</B> bridge.
* <P>
* 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;
}
}

View File

@ -107,4 +107,7 @@ public interface BridgeAPI {
@Nullable
RunReboot runReboot();
@Nullable
GetProduct getProductStatus();
}

View File

@ -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;
/**
* <B>Common bridge communication message scheme supported by the </B><I>Velux</I><B> bridge.</B>
@ -22,7 +25,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
* In addition to the common methods defined by {@link BridgeCommunicationProtocol}
* each protocol-specific implementation has to provide the following methods:
* <UL>
* <LI>{@link #setNodeAndMainParameter} for defining the intended node and the main parameter value.
* <LI>{@link #setNodeIdAndParameters} for defining the intended node and the main parameter value.
* </UL>
*
* @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);
}

View File

@ -211,4 +211,9 @@ class JsonBridgeAPI implements BridgeAPI {
public @Nullable RunReboot runReboot() {
return null;
}
@Override
public @Nullable GetProduct getProductStatus() {
return null;
}
}

View File

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

View File

@ -19,6 +19,9 @@ 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -49,7 +52,8 @@ import org.slf4j.LoggerFactory;
* @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 @@ class SCgetHouseStatus extends GetHouseStatus implements BridgeCommunicationProt
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 @@ class SCgetHouseStatus extends GetHouseStatus implements BridgeCommunicationProt
* Methods in addition to the interface {@link BridgeCommunicationProtocol}
*/
/**
* @return <b>ntfNodeID</b> returns the Actuator Id as int.
*/
public int getNtfNodeID() {
return ntfNodeID;
public VeluxProduct getProduct() {
logger.trace("getProduct(): returning {}.", product);
return product;
}
/**
* @return <b>ntfState</b> returns the state 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 getNtfState() {
return ntfState;
}
/**
* @return <b>ntfCurrentPosition</b> returns the current position of the Actuator as int.
*/
public int getNtfCurrentPosition() {
return ntfCurrentPosition;
}
/**
* @return <b>ntfTarget</b> returns the target position of the Actuator as int.
*/
public int getNtfTarget() {
return ntfTarget;
public SCgetHouseStatus setCreatorCommand(Command creatorCommand) {
this.creatorCommand = creatorCommand;
return this;
}
}

View File

@ -23,6 +23,7 @@ import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex
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 @@ import org.slf4j.LoggerFactory;
* @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 @@ class SCgetProduct extends GetProduct implements SlipBridgeCommunicationProtocol
}
// 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 @@ class SCgetProduct extends GetProduct implements SlipBridgeCommunicationProtocol
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 @@ class SCgetProduct extends GetProduct implements SlipBridgeCommunicationProtocol
@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;
}
}

View File

@ -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:
* <B>Retrieve Product Status</B>
* <p>
* 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.
* <p>
* 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;
}
}

View File

@ -23,6 +23,7 @@ import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex
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 @@ class SCgetProducts extends GetProducts implements SlipBridgeCommunicationProtoc
}
// 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 @@ class SCgetProducts extends GetProducts implements SlipBridgeCommunicationProtoc
}
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 {

View File

@ -15,11 +15,18 @@ package org.openhab.binding.velux.internal.bridge.slip;
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 @@ import org.slf4j.LoggerFactory;
* @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 @@ class SCrunProductCommand extends RunProductCommand implements SlipBridgeCommuni
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 @@ class SCrunProductCommand extends RunProductCommand implements SlipBridgeCommuni
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 @@ class SCrunProductCommand extends RunProductCommand implements SlipBridgeCommuni
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 @@ class SCrunProductCommand extends RunProductCommand implements SlipBridgeCommuni
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 @@ class SCrunProductCommand extends RunProductCommand implements SlipBridgeCommuni
*/
@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;
}
}

View File

@ -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 @@ class SlipBridgeAPI implements BridgeAPI {
public @Nullable RunReboot runReboot() {
return slipRunReboot;
}
@Override
public @Nullable GetProduct getProductStatus() {
return slipGetProductStatus;
}
}

View File

@ -28,7 +28,6 @@ import org.openhab.binding.velux.internal.bridge.slip.utils.SlipRFC1055;
import org.openhab.binding.velux.internal.development.Threads;
import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
import org.openhab.binding.velux.internal.things.VeluxKLFAPI.Command;
import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -206,15 +205,15 @@ public class SlipVeluxBridge extends VeluxBridge implements Closeable {
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 @@ public class SlipVeluxBridge extends VeluxBridge implements Closeable {
// 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 @@ public class SlipVeluxBridge extends VeluxBridge implements Closeable {
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 @@ public class SlipVeluxBridge extends VeluxBridge implements Closeable {
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 @@ public class SlipVeluxBridge extends VeluxBridge implements Closeable {
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 @@ public class SlipVeluxBridge extends VeluxBridge implements Closeable {
// 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 @@ public class SlipVeluxBridge extends VeluxBridge implements Closeable {
// 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 @@ public class SlipVeluxBridge extends VeluxBridge implements Closeable {
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 @@ public class SlipVeluxBridge extends VeluxBridge implements Closeable {
}
// 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;
}

View File

@ -142,7 +142,7 @@ class DataInputStreamWithTimeout implements Closeable {
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);
}
}

View File

@ -143,7 +143,8 @@ public class KLF200Response {
* @return <b>success</b> 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 class KLF200Response {
* @return <b>success</b> 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);
}
}

View File

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

View File

@ -125,9 +125,8 @@ public class VeluxHandlerFactory extends BaseThingHandlerFactory {
});
}
@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);

View File

@ -14,12 +14,20 @@ package org.openhab.binding.velux.internal.handler;
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 @@ import org.slf4j.LoggerFactory;
* </UL>
*
* @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 @@ final class ChannelActuatorPosition extends ChannelHandlerTemplate {
throw new AssertionError();
}
/*
* List of product states that shall be processed
*/
private static final List<ProductState> STATES_TO_PROCESS = Arrays.asList(ProductState.DONE, ProductState.EXECUTING,
ProductState.MANUAL, ProductState.UNKNOWN);
// Public methods
/**
@ -81,41 +96,83 @@ final class ChannelActuatorPosition extends ChannelHandlerTemplate {
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 @@ final class ChannelActuatorPosition extends ChannelHandlerTemplate {
* 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 @@ final class ChannelActuatorPosition extends ChannelHandlerTemplate {
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);
}
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..
}
if (targetLevel == VeluxProductPosition.UNKNOWN) {
LOGGER.info("handleCommand({},{}): ignoring command.", channelUID.getAsString(), command);
break;
}
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;

View File

@ -107,7 +107,6 @@ final class ChannelVShutterPosition extends ChannelHandlerTemplate {
* 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 @@ final class ChannelVShutterPosition extends ChannelHandlerTemplate {
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);

View File

@ -55,6 +55,7 @@ import org.openhab.binding.velux.internal.things.VeluxExistingScenes;
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 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
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 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
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 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
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 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
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 class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
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;

View File

@ -135,12 +135,11 @@ public class Thing2VeluxActuator {
*
* @return <b>bridgeProductIndex</B> 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 class Thing2VeluxActuator {
*
* @return <b>isKnown</B> as boolean.
*/
@SuppressWarnings("PMD.CompareObjectsWithEquals")
public boolean isKnown() {
return (!(this.getProductBridgeIndex() == ProductBridgeIndex.UNKNOWN));
return (!(ProductBridgeIndex.UNKNOWN.equals(getProductBridgeIndex())));
}
/**
@ -164,12 +162,11 @@ public class Thing2VeluxActuator {
*
* @return <b>isInverted</B> 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;

View File

@ -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<ProductState> PERMITTED_VALUE_STATES = Arrays.asList(ProductState.EXECUTING,
ProductState.DONE);
// Constructor methods
public VeluxExistingProducts() {
@ -110,32 +121,87 @@ public class VeluxExistingProducts {
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);
if (dirty) {
String uniqueIndex = thisProduct.getProductUniqueIndex();
logger.trace("update(): updating by UniqueIndex {}.", uniqueIndex);
existingProductsByUniqueIndex.replace(uniqueIndex, thisProduct);
modifiedProductsByUniqueIndex.put(uniqueIndex, thisProduct);
}
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());
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) {
this.dirty = true;
String uniqueIndex = theProduct.getProductUniqueIndex();
logger.trace("update(): updating by UniqueIndex {}.", uniqueIndex);
existingProductsByUniqueIndex.replace(uniqueIndex, theProduct);
modifiedProductsByUniqueIndex.put(uniqueIndex, theProduct);
}
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) {

View File

@ -156,7 +156,7 @@ public class VeluxKLFAPI {
"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 class VeluxKLFAPI {
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 class VeluxKLFAPI {
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 class VeluxKLFAPI {
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 class VeluxKLFAPI {
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 class VeluxKLFAPI {
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 class VeluxKLFAPI {
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."),
;

View File

@ -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 class VeluxProduct {
}
}
// 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 @@ public class VeluxProduct {
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 class 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 class 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 class VeluxProduct {
* @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 class VeluxProduct {
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 class VeluxProduct {
@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 class VeluxProduct {
@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 class VeluxProduct {
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 <B>modified</B> as type boolean to signal a real modification.
@ -290,8 +428,8 @@ public class VeluxProduct {
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 class VeluxProduct {
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 class VeluxProduct {
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 class VeluxProduct {
* @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 <b>modified</b> 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;
}
}

View File

@ -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 class VeluxProductPosition {
* 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 class VeluxProductPosition {
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 class VeluxProductPosition {
// 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;
}
}

View File

@ -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 @@ public enum VeluxProductType {
// 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);
}
}

View File

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

View File

@ -139,6 +139,14 @@
<category>Blinds</category>
</channel-type>
<channel-type id="vanePosition">
<item-type>Dimmer</item-type>
<label>@text/channel-type.velux.vanePosition.label</label>
<description>@text/channel-type.velux.vanePosition.description</description>
<category>Blinds</category>
<state min="0" max="100"/>
</channel-type>
<channel-type id="state">
<item-type>Switch</item-type>
<label>@text/channel-type.velux.state.label</label>

View File

@ -19,6 +19,7 @@
<channel id="position" typeId="position"/>
<channel id="limitMinimum" typeId="limitMinimum"/>
<channel id="limitMaximum" typeId="limitMaximum"/>
<channel id="vanePosition" typeId="vanePosition"/>
</channels>
<representation-property>serial</representation-property>
<config-description-ref uri="thing-type:velux:rollershutter"/>

View File

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