mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
d3b9bd592b
commit
072113f51b
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
@ -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.");
|
||||
|
@ -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);
|
||||
|
@ -104,4 +104,7 @@ public interface BridgeAPI {
|
||||
SetSceneVelocity setSceneVelocity();
|
||||
|
||||
RunScene runScene();
|
||||
|
||||
@Nullable
|
||||
RunReboot runReboot();
|
||||
}
|
||||
|
@ -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 {
|
||||
}
|
@ -29,6 +29,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
*
|
||||
* @author Guenther Schreiner - Initial contribution.
|
||||
*/
|
||||
@Deprecated
|
||||
@NonNullByDefault
|
||||
public abstract class SetSceneVelocity implements BridgeCommunicationProtocol {
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<>();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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:
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -17,5 +17,6 @@
|
||||
<properties>
|
||||
<property name="bundleVersion">N/A</property>
|
||||
</properties>
|
||||
<representation-property>bundleVersion</representation-property>
|
||||
</thing-type>
|
||||
</thing:thing-descriptions>
|
||||
|
@ -32,6 +32,7 @@
|
||||
<property name="check" />
|
||||
-->
|
||||
</properties>
|
||||
<representation-property>ipAddress</representation-property>
|
||||
|
||||
<config-description-ref uri="bridge-type:velux:bridge"/>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user