[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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1625 additions and 592 deletions

View File

@ -34,8 +34,9 @@ The binding supports the following types of Thing.
## Discovery
To simplify the initial provisioning, the binding provides one thing which can be found by autodiscovery.
Unfortunately there is no way to discover Velux bridges themselves within the local network.
But after configuring a Velux Bridge, it is possible to discover all scenes and actuators like windows and rollershutters in that hub.
The binding will automatically discover Velux Bridges within the local network, and place them in the Inbox.
Once a Velux Bridge has been discovered, you will need to enter the `password` Configuration Parameter (see below) before the binding can communicate with it.
And once the Velux Bridge is fully configured, the binding will automatically discover all its respective scenes and actuators (like windows and rollershutters), and place them in the Inbox.
## Thing Configuration
@ -51,7 +52,7 @@ In addition there are some optional Configuration Parameters.
|-------------------------|------------------|:--------:|--------------------------------------------------------------|
| ipAddress | | Yes | Hostname or address for accessing the Velux Bridge. |
| password | velux123 | Yes | Password for authentication against the Velux Bridge.(\*\*) |
| timeoutMsecs | 500 | No | Communication timeout in milliseconds. |
| timeoutMsecs | 2000 | No | Communication timeout in milliseconds. |
| protocol | slip | No | Underlying communication protocol (http/https/slip). |
| tcpPort | 51200 | No | TCP port (80 or 51200) for accessing the Velux Bridge. |
| retries | 5 | No | Number of retries during I/O. |
@ -89,7 +90,7 @@ In addition there are some optional Configuration Parameters.
Notes:
1. To enable a complete invertion of all parameter values (i.e. for Velux windows), use the property `inverted` or add a trailing star to the eight-byte serial number. For an example, see below at item `Velux DG Window Bathroom`.
1. To enable a complete inversion of all parameter values (i.e. for Velux windows), use the property `inverted` or add a trailing star to the eight-byte serial number. For an example, see below at item `Velux DG Window Bathroom`.
2. Somfy devices do not provide a valid serial number to the Velux KLF200 gateway. The bridge reports a registration of the serial number 00:00:00:00:00:00:00:00. Therefore the binding implements a fallback to allow an item specification with a actuator `name` instead of actuator serial number whenever such an invalid serial number occurs. For an example, see below at item `Velux OG Somfy Shutter`.
@ -99,9 +100,10 @@ The Velux Bridge in API version one (firmware version 0.1.1.*) allows activating
So besides the bridge, only one real Thing type exists, namely "scene".
This type of Thing is configured by means of its scene name in the hub.
| Configuration Parameter | Default | Required | Description |
|-------------------------|------------------------|:--------:|-----------------------------------------------------------|
| sceneName | | Yes | Name of the scene in the hub. |
| Configuration Parameter | Default | Required | Description |
|-------------------------|------------------------|:--------:|-----------------------------------------------------------------------|
| sceneName | | Yes | Name of the scene in the hub. |
| velocity | | No | The speed at which the scene will be executed (deafult, silent, fast) |
### Thing Configuration for "vshutter"
@ -128,7 +130,7 @@ The supported Channels and their associated channel types are shown below.
| downtime | Number | Time interval (sec) between last successful and most recent device interaction. |
| doDetection | Switch | Command to activate bridge detection mode. |
### Channels for "window", "rollershutter" Things
### Channels for "window" / "rollershutter" Things
The supported Channels and their associated channel types are shown below.
@ -138,6 +140,15 @@ The supported Channels and their associated channel types are shown below.
| limitMinimum | Rollershutter | Minimum limit position of the window or device. |
| limitMaximum | Rollershutter | Maximum limit position of the window or device. |
The `position` Channel indicates the open/close state of the window (resp. roller shutter) in percent (0% .. 100%) as follows..
- As a general rule the display is the actual physical position.
- If it is moving towards a new target position, the display is the target position.
- After the movement has completed, the display is the final physical position.
- If a window is opened manually, the display is `UNDEF`.
- In case of errors (e.g. window jammed) the display is `UNDEF`.
- If a Somfy actuator is commanded to its 'favorite' position via a Somfy remote control, under some circumstances the display is `UNDEF`. See also Rules below.
### Channels for "actuator" Things
The supported Channels and their associated channel types are shown below.
@ -149,6 +160,8 @@ The supported Channels and their associated channel types are shown below.
| limitMinimum | Rollershutter | Minimum limit position of the window or device. |
| limitMaximum | Rollershutter | Maximum limit position of the window or device. |
See the section above for "window" / "rollershutter" Things for further information concerning the `position` Channel.
### Channels for "scene" Things
The supported Channels and their associated channel types are shown below.
@ -166,6 +179,8 @@ The supported Channel and its associated channel type is shown below.
|--------------|---------------|-----------------------------------------|
| position | Rollershutter | Position of the virtual roller shutter. |
See the section above for "window" / "rollershutter" Things for further information concerning the `position` Channel.
### Channels for "information" Thing
The supported Channel and its associated channel type is shown below.
@ -187,13 +202,13 @@ The bridge Thing provides the following properties.
| Property | Description |
|-------------------|-----------------------------------------------------------------|
| address | IP address of the Bridge |
| check | Result of the check of current item configuration |
| connectionAttempt | Date-Time of last connection attampt |
| connectionSuccess | Date-Time of last successful connection attampt |
| defaultGW | IP address of the Default Gateway of the Bridge |
| DHCP | Flag whether automatic IP configuration is enabled |
| firmware | Software version of the Bridge |
| ipAddress | IP address of the Bridge |
| products | List of all recognized products |
| scenes | List of all defined scenes |
| subnetMask | IP subnetmask of the Bridge |
@ -231,12 +246,14 @@ Frame label="Velux Windows" {
[=> download sample sitemaps file for textual configuration](./doc/conf/sitemaps/velux.sitemap)
### Rules
### Rule for closing windows after a period of time
**Rule for closing windows after a period of time**:
Especially in the colder months, it is advisable to close the window after adequate ventilation. Therefore, automatic closing after one minute is good to save on heating costs.
Especially in the colder months, it is advisable to close the window after adequate ventilation.
Therefore, automatic closing after one minute is good to save on heating costs.
However, to allow the case of intentional prolonged opening, an automatic closure is made only with the window fully open.
Example:
```java
rule "V_WINDOW_changed"
when
@ -245,14 +262,14 @@ then
logInfo("rules.V_WINDOW", "V_WINDOW_changes() called.")
// Get the sensor value
val Number windowState = V_WINDOW.state as DecimalType
logWarn("rules.V_WINDOW", "Window state is "+windowState+".")
logWarn("rules.V_WINDOW", "Window state is " + windowState + ".")
if (windowState < 80) {
if (windowState == 0) {
logWarn("rules.V_WINDOW", "V-WINDOW changed to fully open.")
var int interval = 1
createTimer(now.plusMinutes(interval)) [ |
createTimer(now.plusMinutes(interval)) [ |
logWarn("rules.V_WINDOW:event", "event-V_WINDOW(): setting V-WINDOW to 100.")
sendCommand(V_WINDOW,100)
sendCommand(V_WINDOW, 100)
V_WINDOW.postUpdate(100)
logWarn("rules.V_WINDOW:event", "event-V_WINDOW done.")
]
@ -267,6 +284,69 @@ end
[=> download sample rules file for textual configuration](./doc/conf/rules/velux.rules)
### Rule for rebooting the Bridge
This binding includes a rule action to reboot the Velux Bridge by remote command:
- `boolean isRebooting = rebootBridge()`
_Warning: use this command carefully..._
Example:
```java
rule "Reboot KLF 200"
when
...
then
val veluxActions = getActions("velux", "velux:klf200:myhubname")
if (veluxActions !== null) {
val isRebooting = veluxActions.rebootBridge()
logWarn("Rules", "Velux KLF 200 rebooting: " + isRebooting)
} else {
logWarn("Rules", "Velux KLF 200 actions not found, check thing ID")
}
end
```
### Rule for checking if a Window has been manually opened
In the case that a window has been manually opened, and you then try to move it via the binding, its `position` will become `UNDEF`.
You can exploit this behaviour in a rule to check regularly if a window has been manually opened.
```java
rule "Every 10 minutes, check if window is in manual mode"
when
Time cron "0 0/10 * * * ?" // every 10 minutes
then
if (Velux_Window.state != UNDEF) {
// command the window to its actual position; this will either
// - succeed: the actual position will not change, or
// - fail: the position becomes UNDEF (logged next time this rule executes)
Velux_Window.sendCommand(Velux_Window.state)
} else {
logWarn("Rules", "Velux in Manual mode, trying to close again")
// try to close it
Velux_Window.sendCommand(0)
}
end
```
### Rule for Somfy actuators
If a Somfy actuator is commanded to its 'favorite' position via a Somfy remote control, under some circumstances the display is `UNDEF`.
You can resolve this behaviour in a rule that detects the `UNDEF` position and (re-)commands it to its favorite position.
```java
rule "Somfy Actuator: resolve undefined position"
when
Item Somfy_Actuator changed to UNDEF
then
val favoritePosition = 91
Somfy_Actuator.sendCommand(favoritePosition)
end
```
## Debugging
For those who are interested in more detailed insight of the processing of this binding, a deeper look can be achieved by increased loglevel.

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>