[velux] hub discovery; representation properties; socket lock up issues (#8777)

* [velux] set explicit timeouts & keepalives on socket
* [velux] implement mdns service
* [velux] fix representation property names
* [velux] fix representation properties
* [velux] finalize mdns
* [velux] spotless
* [velux] use both mDNS and regular DNS to resolve ip addresses
* [velux] complete class rewrite using asynchronous polling thread
* [velux] refactor bridgeDirectCommunicate to simplify looping
* [velux] asynchronous polling means Thread.sleep no longer needed
* [velux] faster synch of actuator changes
* [velux] use single thread executor instead of thread pool
* [velux] faster synch of actuator changes
* [velux] shut down task executor

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
Andrew Fiddian-Green
2020-12-01 17:05:51 +00:00
committed by GitHub
parent d3b9bd592b
commit 072113f51b
44 changed files with 1625 additions and 592 deletions

View File

@@ -79,7 +79,7 @@ public class VeluxBinding extends VeluxBridgeConfiguration {
this.password = uncheckedConfiguration.password;
}
logger.trace("VeluxBinding(): checking {}.", VeluxBridgeConfiguration.BRIDGE_TIMEOUT_MSECS);
if ((uncheckedConfiguration.timeoutMsecs > 0) && (uncheckedConfiguration.timeoutMsecs <= 10000)) {
if ((uncheckedConfiguration.timeoutMsecs >= 500) && (uncheckedConfiguration.timeoutMsecs <= 5000)) {
this.timeoutMsecs = uncheckedConfiguration.timeoutMsecs;
}
logger.trace("VeluxBinding(): checking {}.", VeluxBridgeConfiguration.BRIDGE_RETRIES);
@@ -87,7 +87,7 @@ public class VeluxBinding extends VeluxBridgeConfiguration {
this.retries = uncheckedConfiguration.retries;
}
logger.trace("VeluxBinding(): checking {}.", VeluxBridgeConfiguration.BRIDGE_REFRESH_MSECS);
if ((uncheckedConfiguration.refreshMSecs > 0) && (uncheckedConfiguration.refreshMSecs <= 10000)) {
if ((uncheckedConfiguration.refreshMSecs >= 1000) && (uncheckedConfiguration.refreshMSecs <= 60000)) {
this.refreshMSecs = uncheckedConfiguration.refreshMSecs;
}
this.isBulkRetrievalEnabled = uncheckedConfiguration.isBulkRetrievalEnabled;
@@ -106,15 +106,20 @@ public class VeluxBinding extends VeluxBridgeConfiguration {
*/
public VeluxBridgeConfiguration checked() {
logger.trace("checked() called.");
// @formatter:off
logger.debug("{}Config[{}={},{}={},{}={},{}={},{}={},{}={},{}={},{}={},{}={},{}={}]",
VeluxBindingConstants.BINDING_ID, VeluxBridgeConfiguration.BRIDGE_PROTOCOL, protocol,
VeluxBridgeConfiguration.BRIDGE_IPADDRESS, this.ipAddress, VeluxBridgeConfiguration.BRIDGE_TCPPORT,
tcpPort, VeluxBridgeConfiguration.BRIDGE_PASSWORD, password.replaceAll(".", "*"),
VeluxBridgeConfiguration.BRIDGE_TIMEOUT_MSECS, timeoutMsecs, VeluxBridgeConfiguration.BRIDGE_RETRIES,
retries, VeluxBridgeConfiguration.BRIDGE_REFRESH_MSECS, refreshMSecs,
VeluxBindingConstants.BINDING_ID,
VeluxBridgeConfiguration.BRIDGE_PROTOCOL, protocol,
VeluxBridgeConfiguration.BRIDGE_IPADDRESS, this.ipAddress,
VeluxBridgeConfiguration.BRIDGE_TCPPORT, tcpPort,
VeluxBridgeConfiguration.BRIDGE_PASSWORD, password.replaceAll(".", "*"),
VeluxBridgeConfiguration.BRIDGE_TIMEOUT_MSECS, timeoutMsecs,
VeluxBridgeConfiguration.BRIDGE_RETRIES, retries,
VeluxBridgeConfiguration.BRIDGE_REFRESH_MSECS, refreshMSecs,
VeluxBridgeConfiguration.BRIDGE_IS_BULK_RETRIEVAL_ENABLED, isBulkRetrievalEnabled,
VeluxBridgeConfiguration.BRIDGE_IS_SEQUENTIAL_ENFORCED, isSequentialEnforced,
VeluxBridgeConfiguration.BRIDGE_PROTOCOL_TRACE_ENABLED, isProtocolTraceEnabled);
// @formatter:off
logger.trace("checked() done.");
return this;
}

View File

@@ -91,10 +91,15 @@ public class VeluxBindingConstants {
// Definitions of different set of Things
public static final Set<ThingTypeUID> SUPPORTED_THINGS_BINDING = new HashSet<>(Arrays.asList(THING_TYPE_BINDING));
public static final Set<ThingTypeUID> SUPPORTED_THINGS_BRIDGE = new HashSet<>(Arrays.asList(THING_TYPE_BRIDGE));
public static final Set<ThingTypeUID> SUPPORTED_THINGS_ITEMS = new HashSet<>(
Arrays.asList(THING_TYPE_VELUX_SCENE, THING_TYPE_VELUX_ACTUATOR, THING_TYPE_VELUX_ROLLERSHUTTER,
THING_TYPE_VELUX_WINDOW, THING_TYPE_VELUX_VSHUTTER));
public static final Set<ThingTypeUID> DISCOVERABLE_THINGS = Set.of(THING_TYPE_VELUX_SCENE,
THING_TYPE_VELUX_ACTUATOR, THING_TYPE_VELUX_ROLLERSHUTTER, THING_TYPE_VELUX_WINDOW,
THING_TYPE_VELUX_VSHUTTER, THING_TYPE_BINDING, THING_TYPE_BRIDGE);
// *** List of all Channel ids ***
// List of all binding channel ids
@@ -113,7 +118,7 @@ public class VeluxBindingConstants {
public static final String PROPERTY_BRIDGE_TIMESTAMP_SUCCESS = "connectionSuccess";
public static final String PROPERTY_BRIDGE_TIMESTAMP_ATTEMPT = "connectionAttempt";
public static final String PROPERTY_BRIDGE_FIRMWARE = "firmware";
public static final String PROPERTY_BRIDGE_IPADDRESS = "ipAddress";
public static final String PROPERTY_BRIDGE_ADDRESS = "address";
public static final String PROPERTY_BRIDGE_SUBNETMASK = "subnetMask";
public static final String PROPERTY_BRIDGE_DEFAULTGW = "defaultGW";
public static final String PROPERTY_BRIDGE_DHCP = "DHCP";

View File

@@ -84,7 +84,7 @@ public enum VeluxItemType {
BRIDGE_DO_DETECTION(VeluxBindingConstants.THING_TYPE_BRIDGE, VeluxBindingConstants.CHANNEL_BRIDGE_DO_DETECTION, TypeFlavor.INITIATOR),
BRIDGE_FIRMWARE(VeluxBindingConstants.THING_TYPE_BRIDGE, VeluxBindingConstants.PROPERTY_BRIDGE_FIRMWARE, TypeFlavor.PROPERTY),
BRIDGE_IPADDRESS(VeluxBindingConstants.THING_TYPE_BRIDGE, VeluxBindingConstants.PROPERTY_BRIDGE_IPADDRESS, TypeFlavor.PROPERTY),
BRIDGE_ADDRESS(VeluxBindingConstants.THING_TYPE_BRIDGE, VeluxBindingConstants.PROPERTY_BRIDGE_ADDRESS, TypeFlavor.PROPERTY),
BRIDGE_SUBNETMASK(VeluxBindingConstants.THING_TYPE_BRIDGE, VeluxBindingConstants.PROPERTY_BRIDGE_SUBNETMASK, TypeFlavor.PROPERTY),
BRIDGE_DEFAULTGW(VeluxBindingConstants.THING_TYPE_BRIDGE, VeluxBindingConstants.PROPERTY_BRIDGE_DEFAULTGW, TypeFlavor.PROPERTY),
BRIDGE_DHCP(VeluxBindingConstants.THING_TYPE_BRIDGE, VeluxBindingConstants.PROPERTY_BRIDGE_DHCP, TypeFlavor.PROPERTY),

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.velux.internal.action;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link IVeluxActions} defines rule action interface for rebooting the bridge
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public interface IVeluxActions {
/**
* Action to send a reboot command to a Velux Bridge
*
* @return true if the command was sent
* @throws IllegalStateException if something is wrong
*/
Boolean rebootBridge() throws IllegalStateException;
/**
* Action to send a relative move command to a Velux actuator
*
* @param nodeId the node Id in the bridge
* @param relativePercent the target position relative to its current position (-100% <= relativePercent <= +100%)
* @return true if the command was sent
* @throws NumberFormatException if either of the arguments is not an integer, or out of range
* @throws IllegalStateException if anything else is wrong
*/
Boolean moveRelative(String nodeId, String relativePercent) throws NumberFormatException, IllegalStateException;
}

View File

@@ -0,0 +1,120 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.velux.internal.action;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.ActionOutput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VeluxActions} implementation of the rule action for rebooting the bridge
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@ThingActionsScope(name = "velux")
@NonNullByDefault
public class VeluxActions implements ThingActions, IVeluxActions {
private final Logger logger = LoggerFactory.getLogger(VeluxActions.class);
private @Nullable VeluxBridgeHandler bridgeHandler;
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof VeluxBridgeHandler) {
this.bridgeHandler = (VeluxBridgeHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.bridgeHandler;
}
@Override
@RuleAction(label = "reboot Bridge", description = "issues a reboot command to the KLF200 bridge")
public @ActionOutput(name = "executing", type = "java.lang.Boolean", label = "executing", description = "indicates the command was issued") Boolean rebootBridge()
throws IllegalStateException {
logger.trace("rebootBridge(): action called");
VeluxBridgeHandler bridge = bridgeHandler;
if (bridge == null) {
throw new IllegalStateException("Bridge instance is null");
}
return bridge.runReboot();
}
@Override
@RuleAction(label = "move relative", description = "issues a relative move command to an actuator")
public @ActionOutput(name = "executing", type = "java.lang.Boolean", label = "executing", description = "indicates the command was issued") Boolean moveRelative(
@ActionInput(name = "nodeId", required = true, label = "nodeId", description = "actuator id in the bridge", type = "java.lang.String") String nodeId,
@ActionInput(name = "relativePercent", required = true, label = "relativePercent", description = "position delta from current", type = "java.lang.String") String relativePercent)
throws NumberFormatException, IllegalStateException {
logger.trace("moveRelative(): action called");
VeluxBridgeHandler bridge = bridgeHandler;
if (bridge == null) {
throw new IllegalStateException("Bridge instance is null");
}
int node = Integer.parseInt(nodeId);
if (node < 0 || node > 200) {
throw new NumberFormatException("Node Id out of range");
}
int relPct = Integer.parseInt(relativePercent);
if (Math.abs(relPct) > 100) {
throw new NumberFormatException("Relative Percent out of range");
}
return bridge.moveRelative(node, relPct);
}
/**
* Static method to send a reboot command to a Velux Bridge
*
* @param actions ThingActions from the caller
* @return true if the command was sent
* @throws IllegalArgumentException if actions is invalid
* @throws IllegalStateException if anything else is wrong
*/
public static Boolean rebootBridge(@Nullable ThingActions actions)
throws IllegalArgumentException, IllegalStateException {
if (!(actions instanceof IVeluxActions)) {
throw new IllegalArgumentException("Unsupported action");
}
return ((IVeluxActions) actions).rebootBridge();
}
/**
* Static method to send a relative move command to a Velux actuator
*
* @param actions ThingActions from the caller
* @param nodeId the node Id in the bridge
* @param relativePercent the target position relative to its current position (-100% <= relativePercent <= +100%)
* @return true if the command was sent
* @throws IllegalArgumentException if actions is invalid
* @throws NumberFormatException if either of nodeId or relativePercent is not an integer, or out of range
* @throws IllegalStateException if anything else is wrong
*/
public static Boolean moveRelative(@Nullable ThingActions actions, String nodeId, String relativePercent)
throws IllegalArgumentException, NumberFormatException, IllegalStateException {
if (!(actions instanceof IVeluxActions)) {
throw new IllegalArgumentException("Unsupported action");
}
return ((IVeluxActions) actions).moveRelative(nodeId, relativePercent);
}
}

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2010-2020 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
*/
/**
*
* NOTE: All relevant classes of this binding are below the internal node.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
package org.openhab.binding.velux.internal.action;

View File

@@ -20,6 +20,7 @@ import org.openhab.binding.velux.internal.bridge.common.BridgeAPI;
import org.openhab.binding.velux.internal.bridge.common.BridgeCommunicationProtocol;
import org.openhab.binding.velux.internal.bridge.common.Login;
import org.openhab.binding.velux.internal.bridge.common.Logout;
import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -73,7 +74,7 @@ public abstract class VeluxBridge {
* Handler to access global bridge instance methods
*
*/
protected VeluxBridgeInstance bridgeInstance;
protected VeluxBridgeHandler bridgeInstance;
/*
* ************************
@@ -90,7 +91,7 @@ public abstract class VeluxBridge {
* @param bridgeInstance refers to the binding-wide instance for dealing for common informations
* like existing actuators and predefined scenes.
*/
public VeluxBridge(VeluxBridgeInstance bridgeInstance) {
public VeluxBridge(VeluxBridgeHandler bridgeInstance) {
logger.trace("VeluxBridge(constructor,bridgeInstance={}) called.", bridgeInstance);
this.bridgeInstance = bridgeInstance;
logger.trace("VeluxBridge(constructor) done.");

View File

@@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory;
*
* @author Guenther Schreiner - Initial contribution
*/
@Deprecated
@NonNullByDefault
public class VeluxBridgeSetSceneVelocity {
private final Logger logger = LoggerFactory.getLogger(VeluxBridgeSetSceneVelocity.class);

View File

@@ -104,4 +104,7 @@ public interface BridgeAPI {
SetSceneVelocity setSceneVelocity();
RunScene runScene();
@Nullable
RunReboot runReboot();
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 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.common;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* <B>Common bridge communication message scheme supported by the </B><I>Velux</I><B> bridge.</B>
* <P>
* Message semantic will be defined by the implementations according to the different comm paths.
* <P>
* In addition to the common methods defined by {@link BridgeCommunicationProtocol}
* each protocol-specific implementation has to provide the following methods:
*
* @see BridgeCommunicationProtocol
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public abstract class RunReboot implements BridgeCommunicationProtocol {
}

View File

@@ -29,6 +29,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*
* @author Guenther Schreiner - Initial contribution.
*/
@Deprecated
@NonNullByDefault
public abstract class SetSceneVelocity implements BridgeCommunicationProtocol {

View File

@@ -30,6 +30,7 @@ import org.openhab.binding.velux.internal.bridge.common.SetSceneVelocity;
*
* @author Guenther Schreiner - Initial contribution.
*/
@Deprecated
@NonNullByDefault
class JCsetSceneVelocity extends SetSceneVelocity implements JsonBridgeCommunicationProtocol {

View File

@@ -31,6 +31,7 @@ import org.openhab.binding.velux.internal.bridge.common.RunProductCommand;
import org.openhab.binding.velux.internal.bridge.common.RunProductDiscovery;
import org.openhab.binding.velux.internal.bridge.common.RunProductIdentification;
import org.openhab.binding.velux.internal.bridge.common.RunProductSearch;
import org.openhab.binding.velux.internal.bridge.common.RunReboot;
import org.openhab.binding.velux.internal.bridge.common.RunScene;
import org.openhab.binding.velux.internal.bridge.common.SetHouseStatusMonitor;
import org.openhab.binding.velux.internal.bridge.common.SetProductLimitation;
@@ -205,4 +206,9 @@ class JsonBridgeAPI implements BridgeAPI {
public SetSceneVelocity setSceneVelocity() {
return jsonSetSceneVelocity;
}
@Override
public @Nullable RunReboot runReboot() {
return null;
}
}

View File

@@ -21,9 +21,9 @@ import java.util.TreeSet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.velux.internal.bridge.VeluxBridge;
import org.openhab.binding.velux.internal.bridge.VeluxBridgeInstance;
import org.openhab.binding.velux.internal.bridge.common.BridgeAPI;
import org.openhab.binding.velux.internal.bridge.common.BridgeCommunicationProtocol;
import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
import org.openhab.core.io.net.http.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -84,7 +84,7 @@ public class JsonVeluxBridge extends VeluxBridge {
*
* @param bridgeInstance refers to the binding-wide instance for dealing for common informations.
*/
public JsonVeluxBridge(VeluxBridgeInstance bridgeInstance) {
public JsonVeluxBridge(VeluxBridgeHandler bridgeInstance) {
super(bridgeInstance);
logger.trace("JsonVeluxBridge(constructor) called.");
bridgeAPI = new JsonBridgeAPI(bridgeInstance);

View File

@@ -14,6 +14,7 @@ package org.openhab.binding.velux.internal.bridge.slip;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.velux.internal.VeluxBindingConstants;
import org.openhab.binding.velux.internal.bridge.common.GetWLANConfig;
import org.openhab.binding.velux.internal.bridge.slip.utils.Packet;
import org.openhab.binding.velux.internal.things.VeluxGwWLAN;
@@ -51,8 +52,6 @@ class SCgetWLANConfig extends GetWLANConfig implements SlipBridgeCommunicationPr
private static final String DESCRIPTION = "Retrieve WLAN configuration";
private static final Command COMMAND = Command.GW_GET_NETWORK_SETUP_REQ;
private static final String UNSUPPORTED = "*** unsupported-by-current-gateway-firmware ***";
/*
* Message Objects
*/
@@ -118,6 +117,6 @@ class SCgetWLANConfig extends GetWLANConfig implements SlipBridgeCommunicationPr
public VeluxGwWLAN getWLANConfig() {
logger.trace("getWLANConfig() called.");
// Enhancement idea: Velux should provide an enhanced API.
return new VeluxGwWLAN(UNSUPPORTED, UNSUPPORTED);
return new VeluxGwWLAN(VeluxBindingConstants.UNKNOWN, VeluxBindingConstants.UNKNOWN);
}
}

View File

@@ -0,0 +1,121 @@
/**
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.velux.internal.bridge.common.RunReboot;
import org.openhab.binding.velux.internal.bridge.slip.utils.KLF200Response;
import org.openhab.binding.velux.internal.things.VeluxKLFAPI.Command;
import org.openhab.binding.velux.internal.things.VeluxKLFAPI.CommandNumber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Protocol specific bridge communication supported by the Velux bridge:
* <B>Reboot Bridge</B>
* <P>
* Common Message semantic: Communication with the bridge and (optionally) storing returned information within the class
* itself.
* <P>
* As 3rd level class it defines informations how to send query and receive answer through the
* {@link org.openhab.binding.velux.internal.bridge.VeluxBridgeProvider VeluxBridgeProvider}
* as described by the {@link org.openhab.binding.velux.internal.bridge.slip.SlipBridgeCommunicationProtocol
* SlipBridgeCommunicationProtocol}.
* <P>
* Methods in addition to the mentioned interface:
* <UL>
* <LI>{@link #runReboot} for rebooting the Velux hub.</LI>
* </UL>
*
* @see RunReboot
* @see SlipBridgeCommunicationProtocol
*
* @author Andrew Fiddian-Green - Initial contribution.
*/
@NonNullByDefault
class SCrunReboot extends RunReboot implements SlipBridgeCommunicationProtocol {
private final Logger logger = LoggerFactory.getLogger(SCrunReboot.class);
private static final String DESCRIPTION = "Issue the reboot command";
private static final Command COMMAND = Command.GW_REBOOT_REQ;
/*
* ===========================================================
* Message Objects
*/
private byte[] requestData = new byte[0];
/*
* ===========================================================
* Result Objects
*/
private boolean success = false;
private boolean finished = false;
/*
* ===========================================================
* Methods required for interface {@link SlipBridgeCommunicationProtocol}.
*/
@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() {
return requestData;
}
@Override
public void setResponse(short responseCommand, byte[] thisResponseData, boolean isSequentialEnforced) {
KLF200Response.introLogging(logger, responseCommand, thisResponseData);
success = false;
finished = false;
switch (Command.get(responseCommand)) {
case GW_REBOOT_CFM:
if (!KLF200Response.isLengthValid(logger, responseCommand, thisResponseData, 0)) {
finished = true;
break;
}
success = true;
finished = 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;
}
}

View File

@@ -44,7 +44,8 @@ import org.slf4j.LoggerFactory;
*
* @author Guenther Schreiner - Initial contribution.
*/
// ToDo: THIS MESSAGE EXCHANGE IS AN UNDOCUMENTED FEATURE. Check the updated Velux doc against this implementation.
// TODO: THIS MESSAGE EXCHANGE IS AN UNDOCUMENTED FEATURE. Check the updated Velux doc against this implementation.
@Deprecated
@NonNullByDefault
class SCsetSceneVelocity extends SetSceneVelocity implements SlipBridgeCommunicationProtocol {
private final Logger logger = LoggerFactory.getLogger(SCsetSceneVelocity.class);

View File

@@ -31,6 +31,7 @@ import org.openhab.binding.velux.internal.bridge.common.RunProductCommand;
import org.openhab.binding.velux.internal.bridge.common.RunProductDiscovery;
import org.openhab.binding.velux.internal.bridge.common.RunProductIdentification;
import org.openhab.binding.velux.internal.bridge.common.RunProductSearch;
import org.openhab.binding.velux.internal.bridge.common.RunReboot;
import org.openhab.binding.velux.internal.bridge.common.RunScene;
import org.openhab.binding.velux.internal.bridge.common.SetHouseStatusMonitor;
import org.openhab.binding.velux.internal.bridge.common.SetProductLimitation;
@@ -102,6 +103,7 @@ class SlipBridgeAPI implements BridgeAPI {
private final SetHouseStatusMonitor slipSetHouseMonitor = new SCsetHouseStatusMonitor();
private final SetProductLimitation slipSetProductLimitation = new SCsetLimitation();
private final SetSceneVelocity slipSetSceneVelocity = new SCsetSceneVelocity();
private final RunReboot slipRunReboot = new SCrunReboot();
/**
* Constructor.
@@ -210,4 +212,9 @@ class SlipBridgeAPI implements BridgeAPI {
public SetSceneVelocity setSceneVelocity() {
return slipSetSceneVelocity;
}
@Override
public @Nullable RunReboot runReboot() {
return slipRunReboot;
}
}

View File

@@ -12,13 +12,13 @@
*/
package org.openhab.binding.velux.internal.bridge.slip;
import java.io.Closeable;
import java.io.IOException;
import java.text.ParseException;
import java.util.TreeSet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.velux.internal.VeluxBindingConstants;
import org.openhab.binding.velux.internal.bridge.VeluxBridge;
import org.openhab.binding.velux.internal.bridge.VeluxBridgeInstance;
import org.openhab.binding.velux.internal.bridge.common.BridgeAPI;
import org.openhab.binding.velux.internal.bridge.common.BridgeCommunicationProtocol;
import org.openhab.binding.velux.internal.bridge.slip.io.Connection;
@@ -26,8 +26,8 @@ import org.openhab.binding.velux.internal.bridge.slip.utils.Packet;
import org.openhab.binding.velux.internal.bridge.slip.utils.SlipEncoding;
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.VeluxKLFAPI.CommandNumber;
import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,8 +35,7 @@ import org.slf4j.LoggerFactory;
/**
* SLIP-based 2nd Level I/O interface towards the <B>Velux</B> bridge.
* <P>
* It provides methods for pre- and postcommunication
* as well as a common method for the real communication.
* It provides methods for pre- and post- communication as well as a common method for the real communication.
* <P>
* In addition to the generic {@link VeluxBridge} methods, i.e.
* <UL>
@@ -53,9 +52,11 @@ import org.slf4j.LoggerFactory;
* </UL>
*
* @author Guenther Schreiner - Initial contribution.
* @author Andrew Fiddian-Green - Refactored (simplified) the message processing loop
*/
@NonNullByDefault
public class SlipVeluxBridge extends VeluxBridge {
public class SlipVeluxBridge extends VeluxBridge implements Closeable {
private final Logger logger = LoggerFactory.getLogger(SlipVeluxBridge.class);
/*
@@ -100,7 +101,7 @@ public class SlipVeluxBridge extends VeluxBridge {
*
* @param bridgeInstance refers to the binding-wide instance for dealing for common informations.
*/
public SlipVeluxBridge(VeluxBridgeInstance bridgeInstance) {
public SlipVeluxBridge(VeluxBridgeHandler bridgeInstance) {
super(bridgeInstance);
logger.trace("SlipVeluxBridge(constructor) called.");
bridgeAPI = new SlipBridgeAPI(bridgeInstance);
@@ -153,7 +154,7 @@ public class SlipVeluxBridge extends VeluxBridge {
*/
@Override
protected boolean bridgeDirectCommunicate(BridgeCommunicationProtocol communication, boolean useAuthentication) {
logger.trace("bridgeDirectCommunicate(BCP: {},{}authenticated) called.", communication.name(),
logger.trace("bridgeDirectCommunicate(BCP: {}, {}authenticated) called.", communication.name(),
useAuthentication ? "" : "un");
return bridgeDirectCommunicate((SlipBridgeCommunicationProtocol) communication, useAuthentication);
}
@@ -181,214 +182,242 @@ public class SlipVeluxBridge extends VeluxBridge {
}
/**
* Initializes a client/server communication towards <b>Velux</b> veluxBridge
* based on the Basic I/O interface {@link Connection#io} and parameters
* passed as arguments (see below).
* Initializes a client/server communication towards the Velux Bridge based on the Basic I/O interface
* {@link Connection#io} and parameters passed as arguments (see below).
*
* @param communication Structure of interface type {@link SlipBridgeCommunicationProtocol} describing the
* @param communication a structure of interface type {@link SlipBridgeCommunicationProtocol} describing the
* intended communication, that is request and response interactions as well as appropriate URL
* definition.
* @param useAuthentication boolean flag to decide whether to use authenticated communication.
* @return <b>success</b> of type boolean which signals the success of the communication.
* @param useAuthentication a boolean flag to select whether to use authenticated communication.
* @return a boolean which in general signals the success of the communication, but in the
* special case of receive-only calls, signals if any products were updated during the call
*/
private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProtocol communication,
boolean useAuthentication) {
String host = this.bridgeInstance.veluxBridgeConfiguration().ipAddress;
logger.trace("bridgeDirectCommunicate({},{}authenticated) on {} called.", host, communication.name(),
logger.trace("bridgeDirectCommunicate() '{}', {}authenticated", communication.name(),
useAuthentication ? "" : "un");
assert this.bridgeInstance.veluxBridgeConfiguration().protocol.contentEquals("slip");
// store common parameters as constants for frequent use
final short txCmd = communication.getRequestCommand().toShort();
final byte[] txData = communication.getRequestDataAsArrayOfBytes();
final Command txEnum = Command.get(txCmd);
final String txName = txEnum.toString();
final boolean isSequentialEnforced = this.bridgeInstance.veluxBridgeConfiguration().isSequentialEnforced;
final boolean isProtocolTraceEnabled = this.bridgeInstance.veluxBridgeConfiguration().isProtocolTraceEnabled;
final long expiryTime = System.currentTimeMillis() + COMMUNICATION_TIMEOUT_MSECS;
long communicationStartInMSecs = System.currentTimeMillis();
boolean isSequentialEnforced = this.bridgeInstance.veluxBridgeConfiguration().isSequentialEnforced;
boolean isProtocolTraceEnabled = this.bridgeInstance.veluxBridgeConfiguration().isProtocolTraceEnabled;
// From parameters
short command = communication.getRequestCommand().toShort();
byte[] data = communication.getRequestDataAsArrayOfBytes();
// For further use at different logging statements
String commandString = Command.get(command).toString();
// logger format string
final String loggerFmt = String.format("bridgeDirectCommunicate() [%s] %s => {} {} {}",
this.bridgeInstance.veluxBridgeConfiguration().ipAddress, txName);
if (isProtocolTraceEnabled) {
Threads.findDeadlocked();
}
logger.debug("bridgeDirectCommunicate({},{}authenticated) on {} initiated by {}.", host, commandString,
useAuthentication ? "" : "un", Thread.currentThread());
boolean success = false;
logger.debug(loggerFmt, "started =>", Thread.currentThread(), "");
communication: do {
if (communicationStartInMSecs + COMMUNICATION_TIMEOUT_MSECS < System.currentTimeMillis()) {
logger.warn(
"{} bridgeDirectCommunicate({}) on {}: communication handshake failed (unexpected sequence of requests/responses).",
VeluxBindingConstants.BINDING_VALUES_SEPARATOR, communication.name(), host);
boolean looping = false;
boolean success = false;
boolean sending = false;
boolean rcvonly = false;
byte[] txPacket = emptyPacket;
// handling of the requests
switch (txEnum) {
case GW_OPENHAB_CLOSE:
logger.trace(loggerFmt, "shut down command", "=> executing", "");
connection.resetConnection();
success = true;
break;
case GW_OPENHAB_RECEIVEONLY:
logger.trace(loggerFmt, "receive-only mode", "=> checking messages", "");
if (!connection.isAlive()) {
logger.trace(loggerFmt, "no connection", "=> opening", "");
looping = true;
} else if (connection.isMessageAvailable()) {
logger.trace(loggerFmt, "message(s) waiting", "=> start reading", "");
looping = true;
} else {
logger.trace(loggerFmt, "no waiting messages", "=> done", "");
}
rcvonly = true;
break;
default:
logger.trace(loggerFmt, "send mode", "=> preparing command", "");
SlipEncoding slipEnc = new SlipEncoding(txCmd, txData);
if (!slipEnc.isValid()) {
logger.debug(loggerFmt, "slip encoding error", "=> aborting", "");
break;
}
txPacket = new SlipRFC1055().encode(slipEnc.toMessage());
logger.trace(loggerFmt, "command ready", "=> start sending", "");
looping = sending = true;
}
while (looping) {
// timeout
if (System.currentTimeMillis() > expiryTime) {
logger.warn(loggerFmt, "process loop time out", "=> aborting", "=> PLEASE REPORT !!");
// abort the processing loop
break;
}
// Special handling
if (Command.get(command) == Command.GW_OPENHAB_CLOSE) {
logger.trace("bridgeDirectCommunicate(): special command: shutting down connection.");
connection.resetConnection();
success = true;
continue;
}
// Normal processing
logger.trace("bridgeDirectCommunicate() on {}: working on request {} with {} bytes of data.", host,
commandString, data.length);
byte[] sendBytes = emptyPacket;
if (Command.get(command) == Command.GW_OPENHAB_RECEIVEONLY) {
logger.trace(
"bridgeDirectCommunicate() on {}: special command: determine whether there is any message waiting.",
host);
logger.trace("bridgeDirectCommunicate(): check for a waiting message.");
if (!connection.isMessageAvailable()) {
logger.trace("bridgeDirectCommunicate() on {}: no message waiting, aborting.", host);
break communication;
// send command (optionally), and receive response
byte[] rxPacket;
try {
if (sending) {
if (isProtocolTraceEnabled) {
logger.info("sending command {}", txName);
}
if (logger.isTraceEnabled()) {
logger.trace(loggerFmt, txName, "=> sending data =>", new Packet(txData));
} else {
logger.debug(loggerFmt, txName, "=> sending data length =>", txData.length);
}
}
logger.trace("bridgeDirectCommunicate() on {}: there is a message waiting.", host);
} else {
SlipEncoding t = new SlipEncoding(command, data);
if (!t.isValid()) {
logger.warn("bridgeDirectCommunicate() on {}: SlipEncoding() failed, aborting.", host);
rxPacket = connection.io(this.bridgeInstance, sending ? txPacket : emptyPacket);
// message sent, don't send it again
sending = false;
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", "");
}
// abort the processing loop
break;
}
logger.trace("bridgeDirectCommunicate() on {}: transportEncoding={}.", host, t.toString());
sendBytes = new SlipRFC1055().encode(t.toMessage());
} catch (IOException e) {
logger.debug(loggerFmt, "i/o error =>", e.getMessage(), "=> aborting");
// abort the processing loop
break;
}
do {
if (communicationStartInMSecs + COMMUNICATION_TIMEOUT_MSECS < System.currentTimeMillis()) {
logger.warn("bridgeDirectCommunicate() on {}: receive takes too long. Please report to maintainer.",
host);
break communication;
}
byte[] receivedPacket;
try {
if (sendBytes.length > 0) {
logger.trace("bridgeDirectCommunicate() on {}: sending {} bytes.", host, sendBytes.length);
if (isProtocolTraceEnabled) {
logger.info("Sending command {}.", commandString);
}
} else {
logger.trace("bridgeDirectCommunicate() on {}: initiating receive-only.", host);
// RFC1055 decode response
byte[] rfc1055;
try {
rfc1055 = new SlipRFC1055().decode(rxPacket);
} catch (ParseException e) {
logger.debug(loggerFmt, "parsing error =>", e.getMessage(), "=> aborting");
// abort the processing loop
break;
}
// SLIP decode response
SlipEncoding slipEnc = new SlipEncoding(rfc1055);
if (!slipEnc.isValid()) {
logger.debug(loggerFmt, "slip decode error", "=> aborting", "");
// abort the processing loop
break;
}
// attributes of the received (rx) response
final short rxCmd = slipEnc.getCommand();
final byte[] rxData = slipEnc.getData();
final Command rxEnum = Command.get(rxCmd);
final String rxName = rxEnum.toString();
// logging
if (logger.isTraceEnabled()) {
logger.trace(loggerFmt, rxName, "=> received data =>", new Packet(rxData));
} else {
logger.debug(loggerFmt, rxName, "=> received data length =>", rxData.length);
}
if (isProtocolTraceEnabled) {
logger.info("received message {} => {}", rxName, new Packet(rxData));
}
// handling of the responses
switch (rxEnum) {
case GW_ERROR_NTF:
byte code = rxData[0];
switch (code) {
case 7: // busy
logger.trace(loggerFmt, rxName, getErrorText(code), "=> retrying");
sending = true;
break;
case 12: // authentication failed
logger.debug(loggerFmt, rxName, getErrorText(code), "=> aborting");
resetAuthentication();
looping = false;
break;
default:
logger.warn(loggerFmt, rxName, getErrorText(code), "=> aborting");
looping = false;
}
// (Optionally) Send and receive packet.
receivedPacket = connection.io(this.bridgeInstance, sendBytes);
// Once being sent, it should never be sent again
sendBytes = emptyPacket;
} catch (Exception e) {
logger.warn("bridgeDirectCommunicate() on {}: connection.io returns {}", host, e.getMessage());
break communication;
}
logger.trace("bridgeDirectCommunicate() on {}: received packet {}.", host,
new Packet(receivedPacket).toString());
byte[] response;
try {
response = new SlipRFC1055().decode(receivedPacket);
} catch (ParseException e) {
logger.warn("bridgeDirectCommunicate() on {}: method SlipRFC1055() raised a decoding error: {}.",
host, e.getMessage());
break communication;
}
SlipEncoding tr = new SlipEncoding(response);
if (!tr.isValid()) {
logger.warn("bridgeDirectCommunicate() on {}: method SlipEncoding() raised a decoding error.",
host);
break communication;
}
short responseCommand = tr.getCommand();
byte[] responseData = tr.getData();
logger.debug("bridgeDirectCommunicate() on {}: working on response {} with {} bytes of data.", host,
Command.get(responseCommand).toString(), responseData.length);
if (isProtocolTraceEnabled) {
logger.info("Received answer {}.", Command.get(responseCommand).toString());
}
// Handle some common (unexpected) answers
switch (Command.get(responseCommand)) {
case GW_NODE_INFORMATION_CHANGED_NTF:
logger.trace("bridgeDirectCommunicate() on {}: received GW_NODE_INFORMATION_CHANGED_NTF.",
host);
logger.trace("bridgeDirectCommunicate() on {}: continue with receiving.", host);
continue;
case GW_NODE_STATE_POSITION_CHANGED_NTF:
logger.trace(
"bridgeDirectCommunicate() on {}: received GW_NODE_STATE_POSITION_CHANGED_NTF, special processing of this packet.",
host);
SCgetHouseStatus receiver = new SCgetHouseStatus();
receiver.setResponse(responseCommand, responseData, isSequentialEnforced);
if (receiver.isCommunicationSuccessful()) {
logger.trace("bridgeDirectCommunicate() on {}: existingProducts().update() called.", host);
bridgeInstance.existingProducts().update(new ProductBridgeIndex(receiver.getNtfNodeID()),
receiver.getNtfState(), receiver.getNtfCurrentPosition(), receiver.getNtfTarget());
}
logger.trace("bridgeDirectCommunicate() on {}: continue with receiving.", host);
continue;
case GW_ERROR_NTF:
switch (responseData[0]) {
case 0:
logger.warn(
"bridgeDirectCommunicate() on {}: received GW_ERROR_NTF on {} (Not further defined error), aborting.",
host, commandString);
break communication;
case 1:
logger.warn(
"bridgeDirectCommunicate() on {}: received GW_ERROR_NTF (Unknown Command or command is not accepted at this state) on {}, aborting.",
host, commandString);
break communication;
case 2:
logger.warn(
"bridgeDirectCommunicate() on {}: received GW_ERROR_NTF (ERROR on Frame Structure) on {}, aborting.",
host, commandString);
break communication;
case 7:
logger.trace(
"bridgeDirectCommunicate() on {}: received GW_ERROR_NTF (Busy. Try again later) on {}, retrying.",
host, commandString);
sendBytes = emptyPacket;
continue;
case 8:
logger.warn(
"bridgeDirectCommunicate() on {}: received GW_ERROR_NTF (Bad system table index) on {}, aborting.",
host, commandString);
break communication;
case 12:
logger.warn(
"bridgeDirectCommunicate() on {}: received GW_ERROR_NTF (Not authenticated) on {}, aborting.",
host, commandString);
resetAuthentication();
break communication;
default:
logger.warn(
"bridgeDirectCommunicate() on {}: received GW_ERROR_NTF ({}) on {}, aborting.",
host, responseData[0], commandString);
break communication;
}
case GW_ACTIVATION_LOG_UPDATED_NTF:
logger.info("bridgeDirectCommunicate() on {}: received GW_ACTIVATION_LOG_UPDATED_NTF.", host);
logger.trace("bridgeDirectCommunicate() on {}: continue with receiving.", host);
continue;
break;
case GW_COMMAND_RUN_STATUS_NTF:
case GW_COMMAND_REMAINING_TIME_NTF:
case GW_SESSION_FINISHED_NTF:
if (!isSequentialEnforced) {
logger.trace(
"bridgeDirectCommunicate() on {}: response ignored due to activated parallelism, continue with receiving.",
host);
continue;
}
case GW_NODE_INFORMATION_CHANGED_NTF:
case GW_ACTIVATION_LOG_UPDATED_NTF:
logger.trace(loggerFmt, rxName, "=> ignorable command", "=> continuing");
break;
default:
}
logger.trace("bridgeDirectCommunicate() on {}: passes back command {} and data {}.", host,
new CommandNumber(responseCommand).toString(), new Packet(responseData).toString());
communication.setResponse(responseCommand, responseData, isSequentialEnforced);
} while (!communication.isCommunicationFinished());
success = communication.isCommunicationSuccessful();
} while (false); // communication
logger.debug("bridgeDirectCommunicate({}) on {}: returns {}.", commandString, host,
success ? "success" : "failure");
case GW_NODE_STATE_POSITION_CHANGED_NTF:
logger.trace(loggerFmt, rxName, "=> special command", "=> starting");
SCgetHouseStatus receiver = new SCgetHouseStatus();
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");
if (rcvonly) {
// receive-only: return success to confirm that product(s) were updated
success = true;
}
}
logger.trace(loggerFmt, 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");
break;
}
logger.trace(loggerFmt, rxName, "=> serialism enforced", "=> default processing");
// fall through => execute default processing
default:
logger.trace(loggerFmt, rxName, "=> applying data length =>", rxData.length);
communication.setResponse(rxCmd, rxData, isSequentialEnforced);
looping = !communication.isCommunicationFinished();
success = communication.isCommunicationSuccessful();
}
}
// 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"));
return success;
}
/**
* Return text description of potential GW_ERROR_NTF error codes, for logging purposes
*
* @param errCode is the GW_ERROR_NTF error code
* @return the description message
*/
private static String getErrorText(byte errCode) {
switch (errCode) {
case 0:
return "=> (0) not further defined error";
case 1:
return "=> (1) unknown command or command is not accepted at this state";
case 2:
return "=> (2) error on frame structure";
case 7:
return "=> (7) busy, try again later";
case 8:
return "=> (8) bad system table index";
case 12:
return "=> (12) not authenticated";
}
return String.format("=> (%d) unknown error", errCode);
}
@Override
public void close() throws IOException {
shutdown();
}
}

View File

@@ -12,13 +12,16 @@
*/
package org.openhab.binding.velux.internal.bridge.slip.io;
import java.io.Closeable;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.velux.internal.VeluxBindingConstants;
import org.openhab.binding.velux.internal.bridge.VeluxBridgeInstance;
import org.openhab.binding.velux.internal.bridge.slip.utils.Packet;
import org.openhab.binding.velux.internal.config.VeluxBridgeConfiguration;
import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,7 +42,7 @@ import org.slf4j.LoggerFactory;
* @author Guenther Schreiner - Initial contribution.
*/
@NonNullByDefault
public class Connection {
public class Connection implements Closeable {
private final Logger logger = LoggerFactory.getLogger(Connection.class);
/*
@@ -76,8 +79,10 @@ public class Connection {
* @throws java.net.ConnectException in case of unrecoverable communication failures.
* @throws java.io.IOException in case of continuous communication I/O failures.
*/
public synchronized byte[] io(VeluxBridgeInstance bridgeInstance, byte[] request)
public synchronized byte[] io(VeluxBridgeHandler bridgeInstance, byte[] request)
throws ConnectException, IOException {
VeluxBridgeConfiguration cfg = bridgeInstance.veluxBridgeConfiguration();
host = cfg.ipAddress;
logger.trace("io() on {}: called.", host);
lastCommunicationInMSecs = System.currentTimeMillis();
@@ -89,15 +94,11 @@ public class Connection {
do {
try {
if (!connectivity.isReady()) {
// dispose old connectivity class instances (if any)
resetConnection();
try {
// From configuration
host = bridgeInstance.veluxBridgeConfiguration().ipAddress;
int port = bridgeInstance.veluxBridgeConfiguration().tcpPort;
int timeoutMsecs = bridgeInstance.veluxBridgeConfiguration().timeoutMsecs;
logger.trace("io() on {}: connecting to port {}", host, port);
connectivity = new SSLconnection(host, port);
connectivity.setTimeout(timeoutMsecs);
logger.trace("io() on {}: connecting to port {}", cfg.ipAddress, cfg.tcpPort);
connectivity = new SSLconnection(bridgeInstance);
} catch (ConnectException ce) {
throw new ConnectException(String
.format("raised a non-recoverable error during connection setup: %s", ce.getMessage()));
@@ -107,7 +108,8 @@ public class Connection {
continue;
}
}
if (request.length > 0) {
boolean sending = request.length > 0;
if (sending) {
try {
if (logger.isTraceEnabled()) {
logger.trace("io() on {}: sending packet with {} bytes: {}", host, request.length,
@@ -122,22 +124,15 @@ public class Connection {
logger.info("io() on {}: raised an error during sending: {}.", host, e.getMessage());
break;
}
// Give the bridge some time to breathe
if (bridgeInstance.veluxBridgeConfiguration().timeoutMsecs > 0) {
logger.trace("io() on {}: wait time {} msecs.", host,
bridgeInstance.veluxBridgeConfiguration().timeoutMsecs);
try {
Thread.sleep(bridgeInstance.veluxBridgeConfiguration().timeoutMsecs);
} catch (InterruptedException ie) {
logger.trace("io() on {}: wait interrupted.", host);
}
}
}
byte[] packet = new byte[0];
logger.trace("io() on {}: receiving bytes.", host);
if (connectivity.isReady()) {
packet = connectivity.receive();
// in receive-only mode, a zero length response packet is NOT a timeout
if (sending && (packet.length == 0)) {
throw new SocketTimeoutException("read time out after send");
}
}
if (logger.isTraceEnabled()) {
logger.trace("io() on {}: received packet with {} bytes: {}", host, packet.length,
@@ -168,9 +163,7 @@ public class Connection {
bridgeInstance.veluxBridgeConfiguration().retries);
}
logger.trace("io() on {}: shutting down connection.", host);
if (connectivity.isReady()) {
connectivity.close();
}
resetConnection();
logger.trace("io() on {}: finishes with failure by throwing exception.", host);
throw lastIOE;
}
@@ -192,17 +185,13 @@ public class Connection {
*/
public synchronized boolean isMessageAvailable() {
logger.trace("isMessageAvailable() on {}: called.", host);
try {
if ((connectivity.isReady()) && (connectivity.available())) {
logger.trace("isMessageAvailable() on {}: there is a message waiting.", host);
return true;
}
} catch (IOException e) {
logger.trace("isMessageAvailable() on {}: lost connection due to {}.", host, e.getMessage());
resetConnection();
if (!connectivity.isReady()) {
logger.trace("isMessageAvailable() on {}: lost connection, there may be messages", host);
return false;
}
logger.trace("isMessageAvailable() on {}: no message waiting.", host);
return false;
boolean result = connectivity.available();
logger.trace("isMessageAvailable() on {}: there are {}messages waiting.", host, result ? "" : "no ");
return result;
}
/**
@@ -237,4 +226,9 @@ public class Connection {
}
logger.trace("resetConnection() on {}: done.", host);
}
@Override
public void close() throws IOException {
resetConnection();
}
}

View File

@@ -12,124 +12,222 @@
*/
package org.openhab.binding.velux.internal.bridge.slip.io;
import java.io.DataInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is an extension of {@link java.io.DataInputStream}, which adds timeouts to receive operation.
* <P>
* A data input stream lets an application read primitive Java data
* types from an underlying input stream in a machine-independent
* way. An application uses a data output stream to write data that
* can later be read by a data input stream.
* <p>
* For an in-depth discussion, see:
* https://stackoverflow.com/questions/804951/is-it-possible-to-read-from-a-inputstream-with-a-timeout
* This is an wrapper around {@link java.io.InputStream} to support socket receive operations.
*
* It implements a secondary polling thread to asynchronously read bytes from the socket input stream into a buffer. And
* it parses the bytes into SLIP messages, which are placed on a message queue. Callers can access the SLIP messages in
* this queue independently from the polling thread.
*
* @author Guenther Schreiner - Initial contribution.
* @author Andrew Fiddian-Green - Complete rewrite using asynchronous polling thread.
*/
@NonNullByDefault
class DataInputStreamWithTimeout extends DataInputStream {
class DataInputStreamWithTimeout implements Closeable {
/*
* ***************************
* ***** Private Objects *****
*/
private static final int QUEUE_SIZE = 512;
private static final int BUFFER_SIZE = 512;
private static final int SLEEP_INTERVAL_MSECS = 50;
/**
* Executor for asynchronous read command
*/
ExecutorService executor = Executors.newFixedThreadPool(2);
// special character that marks the first and last byte of a slip message
private static final byte SLIP_MARK = (byte) 0xc0;
/**
* Creates a DataInputStreamWithTimeout that uses the specified
* underlying DataInputStream.
*
* @param in the specified input stream
*/
public DataInputStreamWithTimeout(InputStream in) {
super(in);
}
private final Logger logger = LoggerFactory.getLogger(DataInputStreamWithTimeout.class);
/**
* Reads up to <code>len</code> bytes of data from the contained
* input stream into an array of bytes. An attempt is made to read
* as many as <code>len</code> bytes, but a smaller number may be read,
* possibly zero. The number of bytes actually read is returned as an
* integer.
*
* <p>
* This method blocks until input data is available, end of file is
* detected, or an exception is thrown <B>until</B> the given timeout.
*
* <p>
* If <code>len</code> is zero, then no bytes are read and
* <code>0</code> is returned; otherwise, there is an attempt to read at
* least one byte. If no byte is available because the stream is at end of
* file, the value <code>-1</code> is returned; otherwise, at least one
* byte is read and stored into <code>b</code>.
*
* <p>
* The first byte read is stored into element <code>b[off]</code>, the
* next one into <code>b[off+1]</code>, and so on. The number of bytes read
* is, at most, equal to <code>len</code>. Let <i>k</i> be the number of
* bytes actually read; these bytes will be stored in elements
* <code>b[off]</code> through <code>b[off+</code><i>k</i><code>-1]</code>,
* leaving elements <code>b[off+</code><i>k</i><code>]</code> through
* <code>b[off+len-1]</code> unaffected.
*
* <p>
* In every case, elements <code>b[0]</code> through
* <code>b[off]</code> and elements <code>b[off+len]</code> through
* <code>b[b.length-1]</code> are unaffected.
*
* @param b the buffer into which the data is read.
* @param off the start offset in the destination array <code>b</code>
* @param len the maximum number of bytes read.
* @param timeoutMSecs the maximum duration of this read before throwing a TimeoutException.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end
* of the stream has been reached.
* @exception NullPointerException If <code>b</code> is <code>null</code>.
* @exception IndexOutOfBoundsException If <code>off</code> is negative,
* <code>len</code> is negative, or <code>len</code> is greater than
* <code>b.length - off</code>
* @exception IOException if the first byte cannot be read for any reason
* other than end of file, the stream has been closed and the underlying
* input stream does not support reading after close, or another I/O
* error occurs. Additionally it will occur when the timeout happens.
* @see java.io.DataInputStream#read
*/
public synchronized int read(byte b[], int off, int len, int timeoutMSecs) throws IOException {
// Definition of Method which encapsulates the Read of data
Callable<Integer> readTask = new Callable<Integer>() {
@Override
public Integer call() throws IOException {
return in.read(b, off, len);
private final Queue<byte[]> slipMessageQueue = new ConcurrentLinkedQueue<>();
private InputStream inputStream;
private @Nullable String pollException = null;
private @Nullable Poller pollRunner = null;
private ExecutorService executor;
private class Poller implements Callable<Boolean> {
private boolean interrupted = false;
public void interrupt() {
interrupted = true;
}
/**
* Task that loops to read bytes from {@link InputStream} and build SLIP packets from them. The SLIP packets are
* placed in a {@link ConcurrentLinkedQueue}. It loops continuously until 'interrupt()' or 'Thread.interrupt()'
* are called when terminates early after the next socket read timeout.
*/
@Override
public Boolean call() throws Exception {
byte[] buf = new byte[BUFFER_SIZE];
byte byt;
int i = 0;
// clean start, no exception, empty queue
pollException = null;
slipMessageQueue.clear();
// loop forever or until internally or externally interrupted
while ((!interrupted) && (!Thread.interrupted())) {
try {
buf[i] = byt = (byte) inputStream.read();
if (byt == SLIP_MARK) {
if (i > 0) {
// the minimal slip message is 7 bytes [MM PP LL CC CC KK MM]
if ((i > 5) && (buf[0] == SLIP_MARK)) {
slipMessageQueue.offer(Arrays.copyOfRange(buf, 0, i + 1));
if (slipMessageQueue.size() > QUEUE_SIZE) {
logger.warn("pollRunner() => slip message queue overflow => PLEASE REPORT !!");
slipMessageQueue.poll();
}
}
i = 0;
buf[0] = SLIP_MARK;
continue;
}
}
if (++i >= BUFFER_SIZE) {
i = 0;
}
} catch (SocketTimeoutException e) {
// socket read time outs are OK => keep on polling
continue;
} catch (IOException e) {
// any other exception => stop polling
String msg = e.getMessage();
pollException = msg != null ? msg : "Generic IOException";
logger.debug("pollRunner() stopping '{}'", pollException);
break;
}
}
};
try {
Future<Integer> future = executor.submit(readTask);
return future.get(timeoutMSecs, TimeUnit.MILLISECONDS);
} catch (RejectedExecutionException e) {
throw new IOException("executor failed", e);
} catch (ExecutionException e) {
throw new IOException("execution failed", e);
} catch (InterruptedException e) {
throw new IOException("read interrupted", e);
} catch (TimeoutException e) {
throw new IOException("read timeout", e);
// we only get here if shutdown or an error occurs so free ourself so we can be recreated again
pollRunner = null;
return true;
}
}
/**
* Check if there was an exception on the polling loop task and if so, throw it back on the caller thread.
*
* @throws IOException
*/
private void throwIfPollException() throws IOException {
if (pollException != null) {
logger.debug("passPollException() polling loop exception {}", pollException);
throw new IOException(pollException);
}
}
/**
* Creates a {@link DataInputStreamWithTimeout} as a wrapper around the specified underlying {@link InputStream}
*
* @param stream the specified input stream
* @param bridge the actual Bridge Thing instance
*/
public DataInputStreamWithTimeout(InputStream stream, VeluxBridgeHandler bridge) {
inputStream = stream;
executor = Executors.newSingleThreadExecutor(bridge.getThreadFactory());
}
/**
* Overridden method of {@link Closeable} interface. Stops the polling thread.
*
* @throws IOException
*/
@Override
public void close() throws IOException {
stopPolling();
}
/**
* Reads and removes the next available SLIP message from the queue. If the queue is empty, continue polling
* until either a message is found, or the timeout expires.
*
* @param timeoutMSecs the timeout period in milliseconds.
* @return the next SLIP message if there is one on the queue, or any empty byte[] array if not.
* @throws IOException
*/
public synchronized byte[] readSlipMessage(int timeoutMSecs) throws IOException {
startPolling();
int i = (timeoutMSecs / SLEEP_INTERVAL_MSECS) + 1;
while (i-- >= 0) {
try {
byte[] slip = slipMessageQueue.remove();
logger.trace("readSlipMessage() => return slip message");
return slip;
} catch (NoSuchElementException e) {
// queue empty, wait and continue
}
throwIfPollException();
try {
Thread.sleep(SLEEP_INTERVAL_MSECS);
} catch (InterruptedException e) {
logger.debug("readSlipMessage() => thread interrupt");
throw new IOException("Thread Interrupted");
}
}
logger.debug("readSlipMessage() => no slip message after {}mS => time out", timeoutMSecs);
return new byte[0];
}
/**
* Get the number of incoming messages in the queue
*
* @return the number of incoming messages in the queue
*/
public int available() {
int size = slipMessageQueue.size();
logger.trace("available() => slip message count {}", size);
return size;
}
/**
* Clear the queue
*/
public void flush() {
logger.trace("flush() called");
slipMessageQueue.clear();
}
/**
* Start the polling task
*/
private void startPolling() {
Poller pollRunner = this.pollRunner;
if (pollRunner == null) {
logger.trace("startPolling()");
pollRunner = this.pollRunner = new Poller();
executor.submit(pollRunner);
}
}
/**
* Stop the polling task
*/
private void stopPolling() {
Poller pollRunner = this.pollRunner;
if (pollRunner != null) {
logger.trace("stopPolling()");
pollRunner.interrupt();
this.pollRunner = null;
}
executor.shutdown();
}
}

View File

@@ -12,9 +12,11 @@
*/
package org.openhab.binding.velux.internal.bridge.slip.io;
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
@@ -29,6 +31,8 @@ import javax.net.ssl.X509TrustManager;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.velux.internal.VeluxBindingConstants;
import org.openhab.binding.velux.internal.config.VeluxBridgeConfiguration;
import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,7 +55,7 @@ import org.slf4j.LoggerFactory;
* @author Guenther Schreiner - Initial contribution.
*/
@NonNullByDefault
class SSLconnection {
class SSLconnection implements Closeable {
private final Logger logger = LoggerFactory.getLogger(SSLconnection.class);
// Public definition
@@ -62,13 +66,12 @@ class SSLconnection {
* ***** Private Objects *****
*/
private static final int CONNECTION_BUFFER_SIZE = 4096;
private boolean ready = false;
private @Nullable SSLSocket socket;
private @Nullable DataOutputStream dOut;
private @Nullable DataInputStreamWithTimeout dIn;
private int ioTimeoutMSecs = 60000;
private int readTimeoutMSecs = 2000;
private int connTimeoutMSecs = 6000;
/**
* Fake trust manager to suppress any certificate errors,
@@ -102,21 +105,18 @@ class SSLconnection {
*/
SSLconnection() {
logger.debug("SSLconnection() called.");
ready = false;
logger.trace("SSLconnection() finished.");
}
/**
* Constructor to setup and establish a connection.
*
* @param host as String describing the Service Access Point location i.e. hostname.
* @param port as String describing the Service Access Point location i.e. TCP port.
* @param bridgeInstance the actual Bridge Thing instance
* @throws java.net.ConnectException in case of unrecoverable communication failures.
* @throws java.io.IOException in case of continuous communication I/O failures.
* @throws java.net.UnknownHostException in case of continuous communication I/O failures.
*/
SSLconnection(String host, int port) throws ConnectException, IOException, UnknownHostException {
logger.debug("SSLconnection({},{}) called.", host, port);
SSLconnection(VeluxBridgeHandler bridgeInstance) throws ConnectException, IOException, UnknownHostException {
logger.debug("SSLconnection() called");
logger.info("Starting {} bridge connection.", VeluxBindingConstants.BINDING_ID);
SSLContext ctx = null;
try {
@@ -126,15 +126,27 @@ class SSLconnection {
throw new IOException(String.format("create of an empty trust store failed: %s.", e.getMessage()));
}
logger.trace("SSLconnection(): creating socket...");
// Just for avoidance of Potential null pointer access
SSLSocket socketX = (SSLSocket) ctx.getSocketFactory().createSocket(host, port);
logger.trace("SSLconnection(): starting SSL handshake...");
if (socketX != null) {
socketX.startHandshake();
dOut = new DataOutputStream(socketX.getOutputStream());
dIn = new DataInputStreamWithTimeout(socketX.getInputStream());
ready = true;
socket = socketX;
SSLSocket socket = this.socket = (SSLSocket) ctx.getSocketFactory().createSocket();
if (socket != null) {
VeluxBridgeConfiguration cfg = bridgeInstance.veluxBridgeConfiguration();
readTimeoutMSecs = cfg.timeoutMsecs;
connTimeoutMSecs = Math.max(connTimeoutMSecs, readTimeoutMSecs);
// use longer timeout when establishing the connection
socket.setSoTimeout(connTimeoutMSecs);
socket.setKeepAlive(true);
socket.connect(new InetSocketAddress(cfg.ipAddress, cfg.tcpPort), connTimeoutMSecs);
logger.trace("SSLconnection(): starting SSL handshake...");
socket.startHandshake();
// use shorter timeout for normal communications
socket.setSoTimeout(readTimeoutMSecs);
dOut = new DataOutputStream(socket.getOutputStream());
dIn = new DataInputStreamWithTimeout(socket.getInputStream(), bridgeInstance);
if (logger.isTraceEnabled()) {
logger.trace(
"SSLconnection(): connected... (ip={}, port={}, sslTimeout={}, soTimeout={}, soKeepAlive={})",
cfg.ipAddress, cfg.tcpPort, connTimeoutMSecs, socket.getSoTimeout(),
socket.getKeepAlive() ? "true" : "false");
}
}
logger.trace("SSLconnection() finished.");
}
@@ -150,38 +162,27 @@ class SSLconnection {
* @return <b>ready</b> as boolean for an established connection.
*/
synchronized boolean isReady() {
return ready;
return socket != null && dIn != null && dOut != null;
}
/**
* Method to pass a message towards the bridge.
* This method gets called when we are initiating a new SLIP transaction.
* <p>
* Note that DataOutputStream and DataInputStream are buffered I/O's. The SLIP protocol requires that prior requests
* should have been fully sent over the socket, and their responses should have been fully read from the buffer
* before the next request is initiated. i.e. Both read and write buffers should already be empty. Nevertheless,
* just in case, we do the following..
* <p>
* 1) Flush from the read buffer any orphan response data that may have been left over from prior transactions, and
* 2) Flush the write buffer directly to the socket to ensure that any exceptions are raised immediately, and the
* KLF starts work immediately
* Method to pass a message towards the bridge. This method gets called when we are initiating a new SLIP
* transaction.
*
* @param packet as Array of bytes to be transmitted towards the bridge via the established connection.
* @throws java.io.IOException in case of a communication I/O failure, and sets 'ready' = false
* @param <b>packet</b> as Array of bytes to be transmitted towards the bridge via the established connection.
* @throws java.io.IOException in case of a communication I/O failure
*/
@SuppressWarnings("null")
synchronized void send(byte[] packet) throws IOException {
logger.trace("send() called, writing {} bytes.", packet.length);
DataOutputStream dOutX = dOut;
if (dOutX == null) {
throw new IOException("DataOutputStream not initialised");
}
try {
if (!ready || (dOut == null) || (dIn == null)) {
throw new IOException();
}
// flush the read buffer if (exceptionally) there is orphan response data in it
flushReadBufffer();
// copy packet data to the write buffer
dOut.write(packet, 0, packet.length);
dOutX.write(packet, 0, packet.length);
// force the write buffer data to be written to the socket
dOut.flush();
dOutX.flush();
if (logger.isTraceEnabled()) {
StringBuilder sb = new StringBuilder();
for (byte b : packet) {
@@ -190,7 +191,7 @@ class SSLconnection {
logger.trace("send() finished after having send {} bytes: {}", packet.length, sb.toString());
}
} catch (IOException e) {
ready = false;
close();
throw e;
}
}
@@ -198,47 +199,43 @@ class SSLconnection {
/**
* Method to verify that there is message from the bridge.
*
* @return <b>true</b> if there are any bytes ready to be queried using {@link SSLconnection#receive}.
* @throws java.io.IOException in case of a communication I/O failure.
* @return <b>true</b> if there are any messages ready to be queried using {@link SSLconnection#receive}.
*/
synchronized boolean available() throws IOException {
synchronized boolean available() {
logger.trace("available() called.");
if (!ready || (dIn == null)) {
throw new IOException();
DataInputStreamWithTimeout dInX = dIn;
if (dInX != null) {
int availableMessages = dInX.available();
logger.trace("available(): found {} messages ready to be read (> 0 means true).", availableMessages);
return availableMessages > 0;
}
@SuppressWarnings("null")
int availableBytes = dIn.available();
logger.trace("available(): found {} bytes ready to be read (> 0 means true).", availableBytes);
return availableBytes > 0;
return false;
}
/**
* Method to get a message from the bridge.
*
* @return <b>packet</b> as Array of bytes as received from the bridge via the established connection.
* @throws java.io.IOException in case of a communication I/O failure, and sets 'ready' = false
* @throws java.io.IOException in case of a communication I/O failure.
*/
synchronized byte[] receive() throws IOException {
logger.trace("receive() called.");
DataInputStreamWithTimeout dInX = dIn;
if (dInX == null) {
throw new IOException("DataInputStreamWithTimeout not initialised");
}
try {
if (!ready || (dIn == null)) {
throw new IOException();
}
byte[] message = new byte[CONNECTION_BUFFER_SIZE];
@SuppressWarnings("null")
int messageLength = dIn.read(message, 0, message.length, ioTimeoutMSecs);
byte[] packet = new byte[messageLength];
System.arraycopy(message, 0, packet, 0, messageLength);
byte[] packet = dInX.readSlipMessage(readTimeoutMSecs);
if (logger.isTraceEnabled()) {
StringBuilder sb = new StringBuilder();
for (byte b : packet) {
sb.append(String.format("%02X ", b));
}
logger.trace("receive() finished after having read {} bytes: {}", messageLength, sb.toString());
logger.trace("receive() finished after having read {} bytes: {}", packet.length, sb.toString());
}
return packet;
} catch (IOException e) {
ready = false;
close();
throw e;
}
}
@@ -247,67 +244,39 @@ class SSLconnection {
* Destructor to tear down a connection.
*
* @throws java.io.IOException in case of a communication I/O failure.
* But actually eats all exceptions to ensure sure that all shutdown code is executed
*/
synchronized void close() throws IOException {
@Override
public synchronized void close() throws IOException {
logger.debug("close() called.");
ready = false;
logger.info("Shutting down Velux bridge connection.");
// Just for avoidance of Potential null pointer access
DataInputStreamWithTimeout dInX = dIn;
if (dInX != null) {
dInX.close();
dIn = null;
}
// Just for avoidance of Potential null pointer access
DataOutputStream dOutX = dOut;
if (dOutX != null) {
dOutX.close();
dOut = null;
}
// Just for avoidance of Potential null pointer access
SSLSocket socketX = socket;
if (socketX != null) {
socketX.close();
socket = null;
}
logger.trace("close() finished.");
}
/**
* Parameter modification.
*
* @param timeoutMSecs the maximum duration in milliseconds for read operations.
*/
void setTimeout(int timeoutMSecs) {
logger.debug("setTimeout() set timeout to {} milliseconds.", timeoutMSecs);
ioTimeoutMSecs = timeoutMSecs;
}
/**
* Method to flush the input buffer.
*
* @throws java.io.IOException in case of a communication I/O failure.
*/
private void flushReadBufffer() throws IOException {
logger.trace("flushReadBuffer() called.");
DataInputStreamWithTimeout dInX = dIn;
if (!ready || (dInX == null)) {
throw new IOException();
}
int byteCount = dInX.available();
if (byteCount > 0) {
byte[] byteArray = new byte[byteCount];
dInX.readFully(byteArray);
if (logger.isTraceEnabled()) {
StringBuilder stringBuilder = new StringBuilder();
for (byte currByte : byteArray) {
stringBuilder.append(String.format("%02X ", currByte));
}
logger.trace("flushReadBuffer(): discarded {} unexpected bytes in the input buffer: {}", byteCount,
stringBuilder.toString());
} else {
logger.warn("flushReadBuffer(): discarded {} unexpected bytes in the input buffer", byteCount);
try {
dInX.close();
} catch (IOException e) {
// eat the exception so the following will always be executed
}
}
DataOutputStream dOutX = dOut;
if (dOutX != null) {
try {
dOutX.close();
} catch (IOException e) {
// eat the exception so the following will always be executed
}
}
SSLSocket socketX = socket;
if (socketX != null) {
logger.debug("Shutting down Velux bridge connection.");
try {
socketX.close();
} catch (IOException e) {
// eat the exception so the following will always be executed
}
}
dIn = null;
dOut = null;
socket = null;
logger.trace("close() finished.");
}
}

View File

@@ -65,7 +65,7 @@ public class KLF200Response {
public static void errorLogging(Logger logger, short responseCommand) {
logger.trace("setResponse(): cannot handle response {} ({}).", Command.get(responseCommand).toString(),
new CommandNumber(responseCommand).toString());
logger.warn("Gateway response {} ({}) cannot be handled at this point of interaction.",
logger.debug("Gateway response {} ({}) cannot be handled at this point of interaction.",
Command.get(responseCommand).toString(), new CommandNumber(responseCommand).toString());
}
@@ -125,7 +125,7 @@ public class KLF200Response {
logger.trace("check4matchingAnyID() called for request {} {} and response {} {}.", idName, requestID, idName,
responseID);
if (requestID != responseID) {
logger.warn("Gateway query for {} {} received unexpected response of {} {}.", idName, requestID, idName,
logger.debug("Gateway query for {} {} received unexpected response of {} {}.", idName, requestID, idName,
responseID);
return false;
}

View File

@@ -0,0 +1,330 @@
/**
* Copyright (c) 2010-2020 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.discovery;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class that uses Multicast DNS (mDNS) to discover Velux Bridges and return their ipv4 addresses
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class VeluxBridgeFinder implements Closeable {
private final Logger logger = LoggerFactory.getLogger(VeluxBridgeFinder.class);
// timing constants
private static final int BUFFER_SIZE = 256;
private static final int SLEEP_MSECS = 100;
private static final int SOCKET_TIMEOUT_MSECS = 500;
private static final int SEARCH_DURATION_MSECS = 5000;
private static final int REPEAT_COUNT = 3;
// dns communication constants
private static final int MDNS_PORT = 5353;
private static final String MDNS_ADDR = "224.0.0.251";
// dns flag constants
private static final short FLAGS_QR = (short) 0x8000;
private static final short FLAGS_AA = 0x0400;
// dns message class constants
private static final short CLASS_IN = 0x0001;
private static final short CLASS_MASK = 0x7FFF;
// dns message type constants
private static final short TYPE_PTR = 0x000c;
private static final byte NULL = 0x00;
// Velux bridge identifiers
private static final String KLF_SERVICE_ID = "_http._tcp.local";
private static final String KLF_HOST_PREFIX = "VELUX_KLF_";
private short randomQueryId;
private ScheduledExecutorService executor;
private @Nullable Listener listener = null;
private class Listener implements Callable<Set<String>> {
private boolean interrupted = false;
private boolean started = false;
public void interrupt() {
interrupted = true;
}
public boolean hasStarted() {
return started;
}
/**
* Listens for Velux Bridges and returns their IP addresses. It loops for SEARCH_DURATION_MSECS or until
* 'interrupt()' or 'Thread.interrupted()' are called when it terminates early after the next socket read
* timeout i.e. after SOCKET_TIMEOUT_MSECS
*
* @return a set of strings containing dotted IP addresses e.g. '123.123.123.123'
*/
@Override
public Set<String> call() throws Exception {
final Set<String> ipAddresses = new HashSet<>();
// create a multicast listener socket
try (MulticastSocket rcvSocket = new MulticastSocket(MDNS_PORT)) {
final byte[] rcvBytes = new byte[BUFFER_SIZE];
final long finishTime = System.currentTimeMillis() + SEARCH_DURATION_MSECS;
rcvSocket.setReuseAddress(true);
rcvSocket.joinGroup(InetAddress.getByName(MDNS_ADDR));
rcvSocket.setSoTimeout(SOCKET_TIMEOUT_MSECS);
// tell the caller that we are ready to roll
started = true;
// loop until time out or internally or externally interrupted
while ((System.currentTimeMillis() < finishTime) && (!interrupted) && (!Thread.interrupted())) {
// read next packet
DatagramPacket rcvPacket = new DatagramPacket(rcvBytes, rcvBytes.length);
try {
rcvSocket.receive(rcvPacket);
if (isKlfLanResponse(rcvPacket.getData())) {
ipAddresses.add(rcvPacket.getAddress().getHostAddress());
}
} catch (SocketTimeoutException e) {
// time out is ok, continue listening
continue;
}
}
} catch (IOException e) {
logger.debug("listenerRunnable(): udp socket exception '{}'", e.getMessage());
}
// prevent caller waiting forever in case start up failed
started = true;
return ipAddresses;
}
}
/**
* Build an mDNS query package to query SERVICE_ID looking for host names
*
* @return a byte array containing the query datagram payload, or an empty array if failed
*/
private byte[] buildQuery() {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(BUFFER_SIZE);
DataOutputStream dataStream = new DataOutputStream(byteStream);
try {
dataStream.writeShort(randomQueryId); // id
dataStream.writeShort(0); // flags
dataStream.writeShort(1); // qdCount
dataStream.writeShort(0); // anCount
dataStream.writeShort(0); // nsCount
dataStream.writeShort(0); // arCount
for (String segString : KLF_SERVICE_ID.split("\\.")) {
byte[] segBytes = segString.getBytes(StandardCharsets.UTF_8);
dataStream.writeByte(segBytes.length); // length
dataStream.write(segBytes); // byte string
}
dataStream.writeByte(NULL); // end of name
dataStream.writeShort(TYPE_PTR); // type
dataStream.writeShort(CLASS_IN); // class
return byteStream.toByteArray();
} catch (IOException e) {
// fall through
}
return new byte[0];
}
/**
* Parse an mDNS response package and check if it is from a KLF bridge
*
* @param responsePayload a byte array containing the response datagram payload
* @return true if the response is from a KLF bridge
*/
private boolean isKlfLanResponse(byte[] responsePayload) {
DataInputStream dataStream = new DataInputStream(new ByteArrayInputStream(responsePayload));
try {
// check if the package id matches the query
short id = dataStream.readShort();
if (id == randomQueryId) {
short flags = dataStream.readShort();
boolean isResponse = (flags & FLAGS_QR) == FLAGS_QR;
boolean isAuthoritative = (flags & FLAGS_AA) == FLAGS_AA;
// check if it is an authoritative response
if (isResponse && isAuthoritative) {
short qdCount = dataStream.readShort();
short anCount = dataStream.readShort();
dataStream.readShort(); // nsCount
dataStream.readShort(); // arCount
// check it is an answer (and not a query)
if ((anCount == 0) || (qdCount != 0)) {
return false;
}
// parse the answers
for (short an = 0; an < anCount; an++) {
// parse the name
byte[] str = new byte[BUFFER_SIZE];
int i = 0;
int segLength;
while ((segLength = dataStream.readByte()) > 0) {
i += dataStream.read(str, i, segLength);
str[i] = '.';
i++;
}
String name = new String(str, 0, i, StandardCharsets.UTF_8);
short typ = dataStream.readShort();
short clazz = (short) (CLASS_MASK & dataStream.readShort());
if (!(name.startsWith(KLF_SERVICE_ID)) || (typ != TYPE_PTR) || (clazz != CLASS_IN)) {
return false;
}
// if we got here, the name and response type are valid
dataStream.readInt(); // TTL
dataStream.readShort(); // dataLen
// parse the host name
i = 0;
while ((segLength = dataStream.readByte()) > 0) {
i += dataStream.read(str, i, segLength);
str[i] = '.';
i++;
}
// check if the host name matches
String host = new String(str, 0, i, StandardCharsets.UTF_8);
if (host.startsWith(KLF_HOST_PREFIX)) {
return true;
}
}
}
}
} catch (IOException e) {
// fall through
}
return false;
}
/**
* Private synchronized method that searches for Velux Bridges and returns their IP addresses. Takes
* SEARCH_DURATION_MSECS to complete.
*
* @return a set of strings containing dotted IP addresses e.g. '123.123.123.123'
*/
private synchronized Set<String> discoverBridgeIpAddresses() {
@Nullable
Set<String> result = null;
// create a random query id
Random random = new Random();
randomQueryId = (short) random.nextInt(Short.MAX_VALUE);
// create the listener task and start it
Listener listener = this.listener = new Listener();
// create a datagram socket
try (DatagramSocket socket = new DatagramSocket()) {
// prepare query packet
byte[] dnsBytes = buildQuery();
DatagramPacket dnsPacket = new DatagramPacket(dnsBytes, 0, dnsBytes.length,
InetAddress.getByName(MDNS_ADDR), MDNS_PORT);
// create listener and wait until it has started
Future<Set<String>> future = executor.submit(listener);
while (!listener.hasStarted()) {
Thread.sleep(SLEEP_MSECS);
}
// send the query several times
for (int i = 0; i < REPEAT_COUNT; i++) {
// send the query several times
socket.send(dnsPacket);
Thread.sleep(SLEEP_MSECS);
}
// wait for the listener future to get the result
result = future.get();
} catch (InterruptedException | IOException | ExecutionException e) {
logger.debug("discoverBridgeIpAddresses(): unexpected exception '{}'", e.getMessage());
}
// clean up listener task (just in case) and return
listener.interrupt();
this.listener = null;
return result != null ? result : new HashSet<>();
}
/**
* Constructor
*
* @param executor the caller's task executor
*/
public VeluxBridgeFinder(ScheduledExecutorService executor) {
this.executor = executor;
}
/**
* Interrupt the {@link Listener}
*
* @throws IOException (not)
*/
@Override
public void close() throws IOException {
Listener listener = this.listener;
if (listener != null) {
listener.interrupt();
this.listener = null;
}
}
/**
* Static method to search for Velux Bridges and return their IP addresses. NOTE: it takes SEARCH_DURATION_MSECS to
* complete, so don't call it on the main thread!
*
* @return set of dotted IP address e.g. '123.123.123.123'
*/
public static Set<String> discoverIpAddresses(ScheduledExecutorService scheduler) {
try (VeluxBridgeFinder finder = new VeluxBridgeFinder(scheduler)) {
return finder.discoverBridgeIpAddresses();
} catch (IOException e) {
return new HashSet<>();
}
}
}

View File

@@ -20,6 +20,7 @@ import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.velux.internal.VeluxBindingConstants;
import org.openhab.binding.velux.internal.VeluxBindingProperties;
import org.openhab.binding.velux.internal.config.VeluxBridgeConfiguration;
import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
import org.openhab.binding.velux.internal.things.VeluxProduct;
import org.openhab.binding.velux.internal.things.VeluxProductSerialNo;
@@ -45,11 +46,6 @@ import org.slf4j.LoggerFactory;
*
* @author Guenther Schreiner - Initial contribution.
*/
//
// To-be-discussed: check whether an immediate activation is preferable.
// Might be activated by:
// @Component(service = DiscoveryService.class, configurationPid = "discovery.velux")
//
@NonNullByDefault
@Component(service = DiscoveryService.class, configurationPid = "discovery.velux")
public class VeluxDiscoveryService extends AbstractDiscoveryService implements Runnable {
@@ -57,7 +53,7 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R
// Class internal
private static final int DISCOVER_TIMEOUT_SECONDS = 300;
private static final int DISCOVER_TIMEOUT_SECONDS = 60;
private @NonNullByDefault({}) LocaleProvider localeProvider;
private @NonNullByDefault({}) TranslationProvider i18nProvider;
@@ -80,7 +76,7 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R
* Initializes the {@link VeluxDiscoveryService} without any further information.
*/
public VeluxDiscoveryService() {
super(VeluxBindingConstants.SUPPORTED_THINGS_ITEMS, DISCOVER_TIMEOUT_SECONDS);
super(VeluxBindingConstants.DISCOVERABLE_THINGS, DISCOVER_TIMEOUT_SECONDS);
logger.trace("VeluxDiscoveryService(without Bridge) just initialized.");
}
@@ -107,7 +103,7 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R
* @param localizationHandler Initialized localization handler.
*/
public VeluxDiscoveryService(Localization localizationHandler) {
super(VeluxBindingConstants.SUPPORTED_THINGS_ITEMS, DISCOVER_TIMEOUT_SECONDS);
super(VeluxBindingConstants.DISCOVERABLE_THINGS, DISCOVER_TIMEOUT_SECONDS);
logger.trace("VeluxDiscoveryService(locale={},i18n={}) just initialized.", localeProvider, i18nProvider);
localization = localizationHandler;
}
@@ -143,10 +139,15 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
.withProperty(VeluxBindingProperties.PROPERTY_BINDING_BUNDLEVERSION,
ManifestInformation.getBundleVersion())
.withRepresentationProperty(VeluxBindingProperties.PROPERTY_BINDING_BUNDLEVERSION)
.withLabel(localization.getText("discovery.velux.binding...label")).build();
logger.debug("startScan(): registering new thing {}.", discoveryResult);
thingDiscovered(discoveryResult);
scheduler.execute(() -> {
discoverBridges();
});
if (bridgeHandlers.isEmpty()) {
logger.debug("startScan(): VeluxDiscoveryService cannot proceed due to missing Velux bridge(s).");
} else {
@@ -161,7 +162,6 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R
public synchronized void stopScan() {
logger.trace("stopScan() called.");
super.stopScan();
removeOlderResults(getTimestampOfLastScan());
logger.trace("stopScan() done.");
}
@@ -286,4 +286,21 @@ public class VeluxDiscoveryService extends AbstractDiscoveryService implements R
public boolean isEmpty() {
return bridgeHandlers.isEmpty();
}
/**
* Discover any bridges on the network that are not yet instantiated.
*/
private void discoverBridges() {
// discover the list of IP addresses of bridges on the network
Set<String> foundBridgeIpAddresses = VeluxBridgeFinder.discoverIpAddresses(scheduler);
// publish discovery results
for (String ipAddr : foundBridgeIpAddresses) {
ThingUID thingUID = new ThingUID(THING_TYPE_BRIDGE, ipAddr.replace(".", "_"));
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withThingType(THING_TYPE_BRIDGE)
.withProperty(VeluxBridgeConfiguration.BRIDGE_IPADDRESS, ipAddr)
.withRepresentationProperty(VeluxBridgeConfiguration.BRIDGE_IPADDRESS)
.withLabel(String.format("Velux Bridge (%s)", ipAddr)).build();
thingDiscovered(result);
}
}
}

View File

@@ -19,6 +19,7 @@ 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.handler.utils.Thing2VeluxActuator;
import org.openhab.binding.velux.internal.things.VeluxProduct;
import org.openhab.binding.velux.internal.things.VeluxProductPosition;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
@@ -27,6 +28,7 @@ import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -88,14 +90,16 @@ final class ChannelActuatorPosition extends ChannelHandlerTemplate {
bcp.setProductId(veluxActuator.getProductBridgeIndex().toInt());
if (thisBridgeHandler.thisBridge.bridgeCommunicate(bcp) && bcp.isCommunicationSuccessful()) {
try {
VeluxProductPosition position = new VeluxProductPosition(bcp.getProduct().getCurrentPosition());
VeluxProduct product = bcp.getProduct();
VeluxProductPosition position = new VeluxProductPosition(product.getDisplayPosition());
if (position.isValid()) {
PercentType positionAsPercent = position.getPositionAsPercentType(veluxActuator.isInverted());
LOGGER.trace("handleRefresh(): found actuator at level {}.", positionAsPercent);
newState = positionAsPercent;
} else {
LOGGER.trace("handleRefresh(): level of actuator is unknown.");
PercentType posPercent = position.getPositionAsPercentType(veluxActuator.isInverted());
LOGGER.trace("handleRefresh(): position of actuator is {}%.", posPercent);
newState = posPercent;
break;
}
LOGGER.trace("handleRefresh(): position of actuator is 'UNDEFINED'.");
newState = UnDefType.UNDEF;
} catch (Exception e) {
LOGGER.warn("handleRefresh(): getProducts() exception: {}.", e.getMessage());
}

View File

@@ -14,11 +14,9 @@ package org.openhab.binding.velux.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.velux.internal.VeluxBindingConstants;
import org.openhab.binding.velux.internal.VeluxItemType;
import org.openhab.binding.velux.internal.bridge.VeluxBridgeLANConfig;
import org.openhab.binding.velux.internal.handler.utils.StateUtils;
import org.openhab.binding.velux.internal.handler.utils.ThingProperty;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.State;
import org.slf4j.Logger;
@@ -71,25 +69,17 @@ final class ChannelBridgeLANconfig extends ChannelHandlerTemplate {
VeluxItemType itemType = VeluxItemType.getByThingAndChannel(thisBridgeHandler.thingTypeUIDOf(channelUID),
channelUID.getId());
switch (itemType) {
case BRIDGE_IPADDRESS:
case BRIDGE_ADDRESS:
newState = StateUtils.createState(thisBridgeHandler.bridgeParameters.lanConfig.openHABipAddress);
ThingProperty.setValue(thisBridgeHandler, VeluxBindingConstants.PROPERTY_BRIDGE_IPADDRESS,
thisBridgeHandler.bridgeParameters.lanConfig.openHABipAddress.toString());
break;
case BRIDGE_SUBNETMASK:
newState = StateUtils.createState(thisBridgeHandler.bridgeParameters.lanConfig.openHABsubnetMask);
ThingProperty.setValue(thisBridgeHandler, VeluxBindingConstants.PROPERTY_BRIDGE_SUBNETMASK,
thisBridgeHandler.bridgeParameters.lanConfig.openHABsubnetMask.toString());
break;
case BRIDGE_DEFAULTGW:
newState = StateUtils.createState(thisBridgeHandler.bridgeParameters.lanConfig.openHABdefaultGW);
ThingProperty.setValue(thisBridgeHandler, VeluxBindingConstants.PROPERTY_BRIDGE_DEFAULTGW,
thisBridgeHandler.bridgeParameters.lanConfig.openHABdefaultGW.toString());
break;
case BRIDGE_DHCP:
newState = StateUtils.createState(thisBridgeHandler.bridgeParameters.lanConfig.openHABenabledDHCP);
ThingProperty.setValue(thisBridgeHandler, VeluxBindingConstants.PROPERTY_BRIDGE_DHCP,
thisBridgeHandler.bridgeParameters.lanConfig.openHABenabledDHCP.toString());
default:
}
}

View File

@@ -14,12 +14,9 @@ package org.openhab.binding.velux.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.velux.internal.VeluxBindingConstants;
import org.openhab.binding.velux.internal.VeluxItemType;
import org.openhab.binding.velux.internal.bridge.VeluxBridgeWLANConfig;
import org.openhab.binding.velux.internal.handler.utils.StateUtils;
import org.openhab.binding.velux.internal.handler.utils.ThingProperty;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.State;
import org.slf4j.Logger;
@@ -70,15 +67,13 @@ final class ChannelBridgeWLANconfig extends ChannelHandlerTemplate {
if (thisBridgeHandler.bridgeParameters.wlanConfig.isRetrieved) {
VeluxItemType itemType = VeluxItemType.getByThingAndChannel(thisBridgeHandler.thingTypeUIDOf(channelUID),
channelUID.getId());
String msg = thisBridgeHandler.localization.getText("config.velux.bridge.unAvailable");
switch (itemType) {
case BRIDGE_WLANSSID:
newState = StateUtils.createState(new StringType(msg));
ThingProperty.setValue(thisBridgeHandler, VeluxBindingConstants.PROPERTY_BRIDGE_WLANSSID, msg);
newState = StateUtils.createState(thisBridgeHandler.bridgeParameters.wlanConfig.openHABwlanSSID);
break;
case BRIDGE_WLANPASSWORD:
newState = StateUtils.createState(new StringType(msg));
ThingProperty.setValue(thisBridgeHandler, VeluxBindingConstants.PROPERTY_BRIDGE_WLANPASSWORD, msg);
newState = StateUtils
.createState(thisBridgeHandler.bridgeParameters.wlanConfig.openHABwlanPassword);
break;
default:
}

View File

@@ -42,6 +42,7 @@ import org.slf4j.LoggerFactory;
*
* @author Guenther Schreiner - Initial contribution.
*/
@Deprecated
@NonNullByDefault
final class ChannelSceneSilentmode extends ChannelHandlerTemplate {
private static final Logger LOGGER = LoggerFactory.getLogger(ChannelSceneSilentmode.class);

View File

@@ -12,9 +12,12 @@
*/
package org.openhab.binding.velux.internal.handler;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -23,6 +26,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.velux.internal.VeluxBinding;
import org.openhab.binding.velux.internal.VeluxBindingConstants;
import org.openhab.binding.velux.internal.VeluxItemType;
import org.openhab.binding.velux.internal.action.VeluxActions;
import org.openhab.binding.velux.internal.bridge.VeluxBridge;
import org.openhab.binding.velux.internal.bridge.VeluxBridgeActuators;
import org.openhab.binding.velux.internal.bridge.VeluxBridgeDeviceStatus;
@@ -36,6 +40,8 @@ import org.openhab.binding.velux.internal.bridge.VeluxBridgeSetHouseStatusMonito
import org.openhab.binding.velux.internal.bridge.VeluxBridgeWLANConfig;
import org.openhab.binding.velux.internal.bridge.common.BridgeAPI;
import org.openhab.binding.velux.internal.bridge.common.BridgeCommunicationProtocol;
import org.openhab.binding.velux.internal.bridge.common.RunProductCommand;
import org.openhab.binding.velux.internal.bridge.common.RunReboot;
import org.openhab.binding.velux.internal.bridge.json.JsonVeluxBridge;
import org.openhab.binding.velux.internal.bridge.slip.SlipVeluxBridge;
import org.openhab.binding.velux.internal.config.VeluxBridgeConfiguration;
@@ -50,7 +56,7 @@ import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex
import org.openhab.binding.velux.internal.things.VeluxProductPosition;
import org.openhab.binding.velux.internal.utils.Localization;
import org.openhab.core.common.AbstractUID;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.common.NamedThreadFactory;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
@@ -59,9 +65,11 @@ import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -87,6 +95,7 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements VeluxBridgeInstance, VeluxBridgeProvider {
private final Logger logger = LoggerFactory.getLogger(VeluxBridgeHandler.class);
// Class internal
@@ -102,10 +111,14 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
private int refreshCounter = 0;
/**
* Dedicated thread pool for the long-running bridge communication threads.
* Dedicated task executor for the long-running bridge communication tasks.
*
* Note: there is no point in using multi threaded thread-pool here, since all the submitted (Runnable) tasks are
* anyway forced to go through the same serial pipeline, because they all call the same class level "synchronized"
* method to actually communicate with the KLF bridge via its one single TCP socket connection
*/
private ScheduledExecutorService handleScheduler = ThreadPoolManager
.getScheduledPool(VeluxBindingConstants.BINDING_ID);
private @Nullable ExecutorService taskExecutor = null;
private @Nullable NamedThreadFactory threadFactory = null;
private VeluxBridge myJsonBridge = new JsonVeluxBridge(this);
private VeluxBridge mySlipBridge = new SlipVeluxBridge(this);
@@ -250,10 +263,7 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
logger.warn("initialize(): scheduler is shutdown, aborting the initialization of this bridge.");
return;
}
if (handleScheduler.isShutdown()) {
logger.trace("initialize(): handleScheduler is shutdown, aborting the initialization of this bridge.");
return;
}
getTaskExecutor();
logger.trace("initialize(): preparing background initialization task.");
// Background initialization...
scheduler.execute(() -> {
@@ -291,6 +301,11 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
logger.trace("dispose(): stopping the refresh.");
currentRefreshJob.cancel(true);
}
// shut down the task executor
ExecutorService taskExecutor = this.taskExecutor;
if (taskExecutor != null) {
taskExecutor.shutdownNow();
}
// Background execution of dispose
scheduler.execute(() -> {
logger.trace("dispose.scheduled(): (synchronous) logout initiated.");
@@ -396,32 +411,30 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
private synchronized void refreshOpenHAB() {
logger.debug("refreshOpenHAB() initiated by {} starting cycle {}.", Thread.currentThread(), refreshCounter);
if (handleScheduler.isShutdown()) {
logger.trace("refreshOpenHAB(): handleScheduler is shutdown, recreating a scheduler pool.");
handleScheduler = ThreadPoolManager.getScheduledPool(VeluxBindingConstants.BINDING_ID);
}
logger.trace("refreshOpenHAB(): processing of possible HSM messages.");
// Background execution of bridge related I/O
handleScheduler.execute(() -> {
getTaskExecutor().execute(() -> {
logger.trace("refreshOpenHAB.scheduled() initiated by {} will process HouseStatus.",
Thread.currentThread());
if (new VeluxBridgeGetHouseStatus().evaluateState(thisBridge)) {
logger.trace("refreshOpenHAB.scheduled(): successfully processed of GetHouseStatus()");
logger.trace("refreshOpenHAB.scheduled(): => GetHouseStatus() => updates received => synchronizing");
syncChannelsWithProducts();
} else {
logger.trace("refreshOpenHAB.scheduled(): => GetHouseStatus() => no updates");
}
logger.trace("refreshOpenHAB.scheduled() initiated by {} has finished.", Thread.currentThread());
});
logger.trace(
"refreshOpenHAB(): looping through all (both child things and bridge) linked channels for a need of refresh.");
logger.trace("refreshOpenHAB(): loop through all (child things and bridge) linked channels needing a refresh");
for (ChannelUID channelUID : BridgeChannels.getAllLinkedChannelUIDs(this)) {
if (VeluxItemType.isToBeRefreshedNow(refreshCounter, thingTypeUIDOf(channelUID), channelUID.getId())) {
logger.trace("refreshOpenHAB(): refreshing channel {}.", channelUID);
handleCommand(channelUID, RefreshType.REFRESH);
}
}
logger.trace("refreshOpenHAB(): looping through properties for a need of refresh.");
logger.trace("refreshOpenHAB(): loop through properties needing a refresh");
for (VeluxItemType veluxItem : VeluxItemType.getPropertyEntriesByThing(getThing().getThingTypeUID())) {
if (VeluxItemType.isToBeRefreshedNow(refreshCounter, getThing().getThingTypeUID(),
veluxItem.getIdentifier())) {
@@ -439,11 +452,11 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
*/
private void syncChannelsWithProducts() {
if (!bridgeParameters.actuators.getChannel().existingProducts.isDirty()) {
logger.trace("syncChannelsWithProducts(): no existing products with changed parameters.");
return;
}
logger.trace("syncChannelsWithProducts(): there are some existing products with changed parameters.");
outer: for (VeluxProduct product : bridgeParameters.actuators.getChannel().existingProducts
.valuesOfModified()) {
for (VeluxProduct product : bridgeParameters.actuators.getChannel().existingProducts.valuesOfModified()) {
logger.trace("syncChannelsWithProducts(): actuator {} has changed values.", product.getProductName());
ProductBridgeIndex productPbi = product.getBridgeProductIndex();
logger.trace("syncChannelsWithProducts(): bridge index is {}.", productPbi);
@@ -452,28 +465,29 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
logger.trace("syncChannelsWithProducts(): channel {} not found.", channelUID);
continue;
}
if (!channel2VeluxActuator.get(channelUID).isKnown()) {
Thing2VeluxActuator actuator = channel2VeluxActuator.get(channelUID);
if (!actuator.isKnown()) {
logger.trace("syncChannelsWithProducts(): channel {} not registered on bridge.", channelUID);
continue;
}
ProductBridgeIndex channelPbi = channel2VeluxActuator.get(channelUID).getProductBridgeIndex();
ProductBridgeIndex channelPbi = actuator.getProductBridgeIndex();
if (!channelPbi.equals(productPbi)) {
continue;
}
// Handle value inversion
boolean isInverted = channel2VeluxActuator.get(channelUID).isInverted();
boolean isInverted = actuator.isInverted();
logger.trace("syncChannelsWithProducts(): isInverted is {}.", isInverted);
VeluxProductPosition position = new VeluxProductPosition(product.getCurrentPosition());
VeluxProductPosition 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);
} else {
logger.trace("syncChannelsWithProducts(): update of channel {} to position {} skipped.", channelUID,
position);
break;
}
break outer;
logger.trace("syncChannelsWithProducts(): update channel {} to 'UNDEFINED'.", channelUID);
updateState(channelUID, UnDefType.UNDEF);
break;
}
}
logger.trace("syncChannelsWithProducts(): resetting dirty flag.");
@@ -490,7 +504,7 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
logger.debug("handleCommand({},{}) called.", channelUID.getAsString(), command);
// Background execution of bridge related I/O
handleScheduler.execute(() -> {
getTaskExecutor().execute(() -> {
logger.trace("handleCommand.scheduled({}) Start work with calling handleCommandScheduled().",
Thread.currentThread());
handleCommandScheduled(channelUID, command);
@@ -570,7 +584,9 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
case BRIDGE_FIRMWARE:
newState = ChannelBridgeFirmware.handleRefresh(channelUID, channelId, this);
break;
case BRIDGE_IPADDRESS:
case BRIDGE_ADDRESS:
// delete legacy property name entry (if any) and fall through
ThingProperty.setValue(this, VeluxBridgeConfiguration.BRIDGE_IPADDRESS, null);
case BRIDGE_SUBNETMASK:
case BRIDGE_DEFAULTGW:
case BRIDGE_DHCP:
@@ -599,6 +615,7 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
case ACTUATOR_LIMIT_MINIMUM:
case ROLLERSHUTTER_LIMIT_MINIMUM:
case WINDOW_LIMIT_MINIMUM:
// note: the empty string ("") below is intentional
newState = ChannelActuatorLimitation.handleRefresh(channelUID, "", this);
break;
case ACTUATOR_LIMIT_MAXIMUM:
@@ -624,11 +641,14 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
if (itemType.isChannel()) {
logger.debug("handleCommandScheduled(): updating channel {} to {}.", channelUID, newState);
updateState(channelUID, newState);
}
if (itemType.isProperty()) {
logger.debug("handleCommandScheduled(): updating property {} to {}.", channelUID, newState);
ThingProperty.setValue(this, itemType.getIdentifier(), newState.toString());
} else if (itemType.isProperty()) {
// if property value is 'unknown', null it completely
String val = newState.toString();
if (VeluxBindingConstants.UNKNOWN.equals(val)) {
val = null;
}
logger.debug("handleCommandScheduled(): updating property {} to {}.", channelUID, val);
ThingProperty.setValue(this, itemType.getIdentifier(), val);
}
} else {
logger.info("handleCommandScheduled({},{}): updating of item {} (type {}) failed.",
@@ -662,6 +682,20 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
case SCENE_ACTION:
ChannelSceneAction.handleCommand(channelUID, channelId, command, this);
break;
/*
* NOTA BENE: Setting of a scene silent mode is no longer supported via the KLF API (i.e. the
* GW_SET_NODE_VELOCITY_REQ/CFM command set is no longer supported in the API), so the binding can
* no longer explicitly support a Channel with such a function. Therefore the silent mode Channel
* type was removed from the binding implementation.
*
* By contrast scene actions can still be called with a silent mode argument, so a silent mode
* Configuration Parameter has been introduced as a means for the user to set this argument.
*
* Strictly speaking the following case statement will now never be called, so in theory it,
* AND ALL THE CLASSES BEHIND, could be deleted from the binding CODE BASE. But out of prudence
* it is retained anyway 'just in case'.
*/
case SCENE_SILENTMODE:
ChannelSceneSilentmode.handleCommand(channelUID, channelId, command, this);
break;
@@ -671,7 +705,7 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
case ACTUATOR_STATE:
case ROLLERSHUTTER_POSITION:
case WINDOW_POSITION:
ChannelActuatorPosition.handleCommand(channelUID, channelId, command, this);
newValue = ChannelActuatorPosition.handleCommand(channelUID, channelId, command, this);
break;
case ACTUATOR_LIMIT_MINIMUM:
case ROLLERSHUTTER_LIMIT_MINIMUM:
@@ -706,4 +740,84 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
new java.util.Date(thisBridge.lastSuccessfulCommunication()).toString());
logger.trace("handleCommandScheduled({}) done.", Thread.currentThread());
}
/**
* Register the exported actions
*/
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(VeluxActions.class);
}
/**
* Exported method (called by an OpenHAB Rules Action) to issue a reboot command to the hub.
*
* @return true if the command could be issued
*/
public boolean runReboot() {
logger.trace("runReboot() called on {}", getThing().getUID());
RunReboot bcp = thisBridge.bridgeAPI().runReboot();
if (bcp != null) {
// background execution of reboot process
getTaskExecutor().execute(() -> {
if (thisBridge.bridgeCommunicate(bcp)) {
logger.info("Reboot command {}sucessfully sent to {}", bcp.isCommunicationSuccessful() ? "" : "un",
getThing().getUID());
}
});
return true;
}
return false;
}
/**
* Exported method (called by an OpenHAB Rules Action) to move an actuator relative to its current position
*
* @param nodeId the node to be moved
* @param relativePercent relative position change to the current position (-100% <= relativePercent <= +100%)
* @return true if the command could be issued
*/
public boolean moveRelative(int nodeId, int relativePercent) {
logger.trace("moveRelative() called on {}", getThing().getUID());
RunProductCommand bcp = thisBridge.bridgeAPI().runProductCommand();
if (bcp != null) {
bcp.setNodeAndMainParameter(nodeId, new VeluxProductPosition(new PercentType(Math.abs(relativePercent)))
.getAsRelativePosition((relativePercent >= 0)));
// background execution of moveRelative
getTaskExecutor().execute(() -> {
if (thisBridge.bridgeCommunicate(bcp)) {
logger.trace("moveRelative() command {}sucessfully sent to {}",
bcp.isCommunicationSuccessful() ? "" : "un", getThing().getUID());
}
});
return true;
}
return false;
}
/**
* If necessary initialise the task executor and return it
*
* @return the task executor
*/
private ExecutorService getTaskExecutor() {
ExecutorService taskExecutor = this.taskExecutor;
if (taskExecutor == null || taskExecutor.isShutdown()) {
taskExecutor = this.taskExecutor = Executors.newSingleThreadExecutor(getThreadFactory());
}
return taskExecutor;
}
/**
* If necessary initialise the thread factory and return it
*
* @return the thread factory
*/
public NamedThreadFactory getThreadFactory() {
NamedThreadFactory threadFactory = this.threadFactory;
if (threadFactory == null) {
threadFactory = new NamedThreadFactory(getThing().getUID().getAsString());
}
return threadFactory;
}
}

View File

@@ -115,6 +115,7 @@ public class VeluxHandler extends ExtendedBaseThingHandler {
for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
logger.trace("handleConfigurationUpdate(): found modified config entry {}.",
configurationParameter.getKey());
configuration.put(configurationParameter.getKey(), configurationParameter.getValue());
}
// persist new configuration and reinitialize handler
dispose();

View File

@@ -13,6 +13,7 @@
package org.openhab.binding.velux.internal.handler.utils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
@@ -61,7 +62,7 @@ public class ThingProperty {
* @param propertyName defines the property which is to be modified,
* @param propertyValue defines the new property value.
*/
public static void setValue(Thing thing, String propertyName, String propertyValue) {
public static void setValue(Thing thing, String propertyName, @Nullable String propertyValue) {
thing.setProperty(propertyName, propertyValue);
LOGGER.trace("setValue() {} set to {}.", propertyName, propertyValue);
return;
@@ -75,7 +76,8 @@ public class ThingProperty {
* @param propertyName defines the property which is to be modified.
* @param propertyValue defines the new property value.
*/
public static void setValue(ExtendedBaseBridgeHandler bridgeHandler, String propertyName, String propertyValue) {
public static void setValue(ExtendedBaseBridgeHandler bridgeHandler, String propertyName,
@Nullable String propertyValue) {
setValue(bridgeHandler.getThing(), propertyName, propertyValue);
}
@@ -91,7 +93,7 @@ public class ThingProperty {
* @param propertyValue defines the new property value.
*/
public static void setValue(ExtendedBaseBridgeHandler bridgeHandler, ChannelUID channelUID, String propertyName,
String propertyValue) {
@Nullable String propertyValue) {
ThingUID channelTUID = channelUID.getThingUID();
Thing thingOfChannel = bridgeHandler.getThing().getThing(channelTUID);
if (thingOfChannel == null) {

View File

@@ -121,10 +121,10 @@ public class VeluxExistingProducts {
return false;
}
VeluxProduct thisProduct = this.get(bridgeProductIndex);
if (thisProduct.setState(productState) || thisProduct.setCurrentPosition(productPosition)
|| thisProduct.setTarget(productTarget)) {
dirty = true;
dirty |= thisProduct.setState(productState);
dirty |= thisProduct.setCurrentPosition(productPosition);
dirty |= thisProduct.setTarget(productTarget);
if (dirty) {
String uniqueIndex = thisProduct.isV2() ? thisProduct.getSerialNumber()
: thisProduct.getProductUniqueIndex();
logger.trace("update(): updating by UniqueIndex {}.", uniqueIndex);

View File

@@ -57,6 +57,24 @@ public class VeluxProduct {
}
}
// State (of movement) of an actuator
public static enum State {
NON_EXECUTING(0),
ERROR(1),
NOT_USED(2),
WAITING_FOR_POWER(3),
EXECUTING(4),
DONE(5),
MANUAL_OVERRIDE(0x80),
UNKNOWN(0xFF);
public final int value;
private State(int value) {
this.value = value;
}
}
// Class internal
private VeluxProductName name;
@@ -70,9 +88,9 @@ public class VeluxProduct {
private int variation = 0;
private int powerMode = 0;
private String serialNumber = VeluxProductSerialNo.UNKNOWN;
private int state = 0;
private int state = State.UNKNOWN.value;
private int currentPosition = 0;
private int target = 0;
private int targetPosition = 0;
private int remainingTime = 0;
private int timeStamp = 0;
@@ -143,7 +161,7 @@ public class VeluxProduct {
this.serialNumber = serialNumber;
this.state = state;
this.currentPosition = currentPosition;
this.target = target;
this.targetPosition = target;
this.remainingTime = remainingTime;
this.timeStamp = timeStamp;
}
@@ -155,7 +173,7 @@ public class VeluxProduct {
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.target, this.remainingTime, this.timeStamp);
this.targetPosition, this.remainingTime, this.timeStamp);
} else {
return new VeluxProduct(this.name, this.typeId, this.bridgeProductIndex);
}
@@ -302,7 +320,7 @@ public class VeluxProduct {
* @return <b>target</b> as type int shows the target position of the current operation.
*/
public int getTarget() {
return target;
return targetPosition;
}
/**
@@ -310,12 +328,12 @@ public class VeluxProduct {
* @return <b>modified</b> as boolean to signal a real modification.
*/
public boolean setTarget(int newTarget) {
if (this.target == newTarget) {
if (this.targetPosition == newTarget) {
return false;
} else {
logger.trace("setCurrentPosition(name={},index={}) target {} replaced by {}.", name.toString(),
bridgeProductIndex.toInt(), this.target, newTarget);
this.target = newTarget;
bridgeProductIndex.toInt(), this.targetPosition, newTarget);
this.targetPosition = newTarget;
return true;
}
}
@@ -333,4 +351,35 @@ public class VeluxProduct {
public int getTimeStamp() {
return timeStamp;
}
/**
* Returns the display position of the actuator.
* <li>As a general rule it returns <b>currentPosition</b>, except as follows..
* <li>If the actuator is in a motion state it returns <b>targetPosition</b>
* <li>If the motion state is 'done' but the currentPosition is invalid it returns <b>targetPosition</b>
* <li>If the manual override flag is set it returns the <b>unknown</b> position value
*
* @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;
}
// 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 currentPosition;
}
}

View File

@@ -58,12 +58,14 @@ public class VeluxProductPosition {
private static final int VPP_OPENHAB_MIN = 0;
private static final int VPP_OPENHAB_MAX = 100;
private static final int VPP_VELUX_MIN = 0x0000;
private static final int VPP_VELUX_MAX = 0xc800;
private static final int VPP_VELUX_UNKNOWN = 0xF7FF;
private static final int VPP_VELUX_PERCENTAGE_MIN = 0xc900;
private static final int VPP_VELUX_PERCENTAGE_MAX = 0xd0d0;
public static final int VPP_VELUX_MIN = 0x0000;
public static final int VPP_VELUX_MAX = 0xc800;
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
// Class internal
@@ -159,15 +161,8 @@ public class VeluxProductPosition {
// Helper methods
public static int getRelativePositionAsVeluxType(boolean upwards, PercentType position) {
float result = (VPP_VELUX_PERCENTAGE_MAX + VPP_VELUX_PERCENTAGE_MIN) / 2;
if (upwards) {
result = result + (ONE * position.intValue() - VPP_OPENHAB_MIN) / (VPP_OPENHAB_MAX - VPP_OPENHAB_MIN)
* ((VPP_VELUX_PERCENTAGE_MAX - VPP_VELUX_PERCENTAGE_MIN) / 2);
} else {
result = result - (ONE * position.intValue() - VPP_OPENHAB_MIN) / (VPP_OPENHAB_MAX - VPP_OPENHAB_MIN)
* ((VPP_VELUX_PERCENTAGE_MAX - VPP_VELUX_PERCENTAGE_MIN) / 2);
}
return (int) result;
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;
}
}

View File

@@ -42,7 +42,7 @@ import org.openhab.binding.velux.internal.VeluxBindingConstants;
@NonNullByDefault
public enum VeluxProductVelocity {
DEFAULT((short) 0, "default"),
SILENT((short) 1, "short"),
SILENT((short) 1, "silent"),
FAST((short) 2, "fast"),
VELOCITY_NOT_AVAILABLE((short) 255, ""),
UNDEFTYPE((short) 0xffff, VeluxBindingConstants.UNKNOWN);
@@ -69,7 +69,7 @@ public enum VeluxProductVelocity {
return velocity;
}
public static VeluxProductVelocity get(int velocity) {
public static VeluxProductVelocity get(short velocity) {
return LOOKUPTYPEID2ENUM.getOrDefault(velocity, VeluxProductVelocity.UNDEFTYPE);
}

View File

@@ -43,11 +43,11 @@
<!-- Velux Bridge factory default -->
<default>velux123</default>
</parameter>
<parameter name="timeoutMsecs" type="integer" min="1" step="1" max="60000">
<parameter name="timeoutMsecs" type="integer" min="500" step="1" max="5000">
<label>@text/config.velux.bridge.timeoutMsecs.label</label>
<description>@text/config.velux.bridge.timeoutMsecs.description</description>
<required>false</required>
<default>500</default>
<default>2000</default>
<advanced>true</advanced>
</parameter>
<parameter name="retries" type="integer" min="0" step="1" max="10">
@@ -57,7 +57,7 @@
<default>5</default>
<advanced>true</advanced>
</parameter>
<parameter name="refreshMsecs" type="integer" min="1" step="1" max="60000">
<parameter name="refreshMsecs" type="integer" min="5000" step="1" max="60000">
<label>@text/config.velux.bridge.refreshMsecs.label</label>
<description>@text/config.velux.bridge.refreshMsecs.description</description>
<required>false</required>

View File

@@ -20,7 +20,7 @@
<channel id="limitMinimum" typeId="limitMinimum"/>
<channel id="limitMaximum" typeId="limitMaximum"/>
</channels>
<representation-property>serialNumber</representation-property>
<representation-property>serial</representation-property>
<config-description-ref uri="thing-type:velux:actuator"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -17,5 +17,6 @@
<properties>
<property name="bundleVersion">N/A</property>
</properties>
<representation-property>bundleVersion</representation-property>
</thing-type>
</thing:thing-descriptions>

View File

@@ -32,6 +32,7 @@
<property name="check" />
-->
</properties>
<representation-property>ipAddress</representation-property>
<config-description-ref uri="bridge-type:velux:bridge"/>

View File

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

View File

@@ -17,9 +17,8 @@
<category>Blinds</category>
<channels>
<channel id="action" typeId="action"/>
<channel id="silentMode" typeId="silentMode"/>
</channels>
<representation-property>unique</representation-property>
<representation-property>sceneName</representation-property>
<config-description-ref uri="thing-type:velux:scene"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -20,7 +20,7 @@
<channel id="limitMinimum" typeId="limitMinimum"/>
<channel id="limitMaximum" typeId="limitMaximum"/>
</channels>
<representation-property>serialNumber</representation-property>
<representation-property>serial</representation-property>
<config-description-ref uri="thing-type:velux:window"/>
</thing-type>
</thing:thing-descriptions>