[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
32 changed files with 2402 additions and 470 deletions

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