[knx] Add support for KNX IP Secure (#12709)
* [knx] Add support for KNX IP Secure * add support for KNX IP Secure, new options SECURETUNNEL and SECUREROUTER, refers to #8872 * add config options for credentials for secure connections * update user documentation * add test cases Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
This commit is contained in:
parent
21aa9c6aff
commit
8a0b7a042c
@ -29,7 +29,7 @@ The IP Gateway is the most commonly used way to connect to the KNX bus. At its b
|
|||||||
|
|
||||||
| Name | Required | Description | Default value |
|
| Name | Required | Description | Default value |
|
||||||
|---------------------|--------------|--------------------------------------------------------------------------------------------------------------|------------------------------------------------------|
|
|---------------------|--------------|--------------------------------------------------------------------------------------------------------------|------------------------------------------------------|
|
||||||
| type | Yes | The IP connection type for connecting to the KNX bus (`TUNNEL` or `ROUTER`) | - |
|
| type | Yes | The IP connection type for connecting to the KNX bus (`TUNNEL`, `ROUTER`, `SECURETUNNEL` or `SECUREROUTER`) | - |
|
||||||
| ipAddress | for `TUNNEL` | Network address of the KNX/IP gateway. If type `ROUTER` is set, the IPv4 Multicast Address can be set. | for `TUNNEL`: \<nothing\>, for `ROUTER`: 224.0.23.12 |
|
| ipAddress | for `TUNNEL` | Network address of the KNX/IP gateway. If type `ROUTER` is set, the IPv4 Multicast Address can be set. | for `TUNNEL`: \<nothing\>, for `ROUTER`: 224.0.23.12 |
|
||||||
| portNumber | for `TUNNEL` | Port number of the KNX/IP gateway | 3671 |
|
| portNumber | for `TUNNEL` | Port number of the KNX/IP gateway | 3671 |
|
||||||
| localIp | No | Network address of the local host to be used to set up the connection to the KNX/IP gateway | the system-wide configured primary interface address |
|
| localIp | No | Network address of the local host to be used to set up the connection to the KNX/IP gateway | the system-wide configured primary interface address |
|
||||||
@ -39,6 +39,10 @@ The IP Gateway is the most commonly used way to connect to the KNX bus. At its b
|
|||||||
| responseTimeout | No | Timeout in seconds to wait for a response from the KNX bus | 10 |
|
| responseTimeout | No | Timeout in seconds to wait for a response from the KNX bus | 10 |
|
||||||
| readRetriesLimit | No | Limits the read retries while initialization from the KNX bus | 3 |
|
| readRetriesLimit | No | Limits the read retries while initialization from the KNX bus | 3 |
|
||||||
| autoReconnectPeriod | No | Seconds between connect retries when KNX link has been lost (0 means never). | 0 |
|
| autoReconnectPeriod | No | Seconds between connect retries when KNX link has been lost (0 means never). | 0 |
|
||||||
|
| routerBackboneKey | No | KNX secure: Backbone key for secure router mode | - |
|
||||||
|
| tunnelUserId | No | KNX secure: Tunnel user id for secure tunnel mode (if specified, it must be a number >0) | - |
|
||||||
|
| tunnelUserPassword | No | KNX secure: Tunnel user key for secure tunnel mode | - |
|
||||||
|
| tunnelDeviceAuthentication | No | KNX secure: Tunnel device authentication for secure tunnel mode | - |
|
||||||
|
|
||||||
|
|
||||||
### Serial Gateway
|
### Serial Gateway
|
||||||
@ -208,6 +212,35 @@ Each configuration parameter has a `mainGA` where commands are written to and op
|
|||||||
The `dpt` element is optional. If omitted, the corresponding default value will be used (see the channel descriptions above).
|
The `dpt` element is optional. If omitted, the corresponding default value will be used (see the channel descriptions above).
|
||||||
|
|
||||||
|
|
||||||
|
## KNX Secure
|
||||||
|
|
||||||
|
> NOTE: Support for KNX Secure is partly implemented for openHAB and should be considered as experimental.
|
||||||
|
|
||||||
|
### KNX IP Secure
|
||||||
|
|
||||||
|
KNX IP Secure protects the traffic between openHAB and your KNX installation.
|
||||||
|
It **requires a KNX Secure Router or a Secure IP Interface** and a KNX installation **with security features enabled in ETS tool**.
|
||||||
|
|
||||||
|
For *Secure routing* mode, the so called `backbone key` needs to be configured in openHAB.
|
||||||
|
It is created by the ETS tool and cannot be changed via the ETS user interface.
|
||||||
|
|
||||||
|
- The backbone key can be extracted from Security report (ETS, Reports, Security, look for a 32-digit key) and specified in parameter `routerBackboneKey`.
|
||||||
|
|
||||||
|
For *Secure tunneling* with a Secure IP Interface (or a router in tunneling mode), more parameters are required.
|
||||||
|
A unique device authentication key, and a specific tunnel identifier and password need to be available.
|
||||||
|
|
||||||
|
- All information can be looked up in ETS and provided separately: `tunnelDeviceAuthentication`, `tunnelUserPassword`.
|
||||||
|
`tunnelUserId` is a number which is not directly visible in ETS, but can be looked up in keyring export or deduced (typically 2 for the first tunnel of a device, 3 for the second one, ...).
|
||||||
|
`tunnelUserPasswort` is set in ETS in the properties of the tunnel (below the IP interface you will see the different tunnels listed) denoted as "Password". `tunnelDeviceAuthentication` is set in the properties of the IP interface itself, check for a tab "IP" and a description "Authentication Code".
|
||||||
|
|
||||||
|
### KNX Data Secure
|
||||||
|
|
||||||
|
KNX Data Secure protects the content of messages on the KNX bus. In a KNX installation, both classic and secure group addresses can coexist.
|
||||||
|
Data Secure does _not_ necessarily require a KNX Secure Router or a Secure IP Interface, but a KNX installation with newer KNX devices which support Data Secure and with **security features enabled in ETS tool**.
|
||||||
|
|
||||||
|
> NOTE: **openHAB currently ignores messages with secure group addresses.**
|
||||||
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
The following two templates are sufficient for almost all purposes.
|
The following two templates are sufficient for almost all purposes.
|
||||||
|
|||||||
@ -54,6 +54,10 @@ public class KNXBindingConstants {
|
|||||||
public static final String PORT_NUMBER = "portNumber";
|
public static final String PORT_NUMBER = "portNumber";
|
||||||
public static final String SERIAL_PORT = "serialPort";
|
public static final String SERIAL_PORT = "serialPort";
|
||||||
public static final String USE_CEMI = "useCemi";
|
public static final String USE_CEMI = "useCemi";
|
||||||
|
public static final String ROUTER_BACKBONE_GROUP_KEY = "routerBackboneGroupKey";
|
||||||
|
public static final String TUNNEL_USER_ID = "tunnelUserId";
|
||||||
|
public static final String TUNNEL_USER_PASSWORD = "tunnelUserPassword";
|
||||||
|
public static final String TUNNEL_DEVICE_AUTHENTICATION = "tunnelDeviceAuthentication";
|
||||||
|
|
||||||
// The default multicast ip address (see <a
|
// The default multicast ip address (see <a
|
||||||
// href="http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xml">iana</a> EIBnet/IP
|
// href="http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xml">iana</a> EIBnet/IP
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
package org.openhab.binding.knx.internal.client;
|
package org.openhab.binding.knx.internal.client;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
@ -40,6 +41,7 @@ import tuwien.auto.calimero.FrameEvent;
|
|||||||
import tuwien.auto.calimero.GroupAddress;
|
import tuwien.auto.calimero.GroupAddress;
|
||||||
import tuwien.auto.calimero.IndividualAddress;
|
import tuwien.auto.calimero.IndividualAddress;
|
||||||
import tuwien.auto.calimero.KNXException;
|
import tuwien.auto.calimero.KNXException;
|
||||||
|
import tuwien.auto.calimero.KNXIllegalArgumentException;
|
||||||
import tuwien.auto.calimero.datapoint.CommandDP;
|
import tuwien.auto.calimero.datapoint.CommandDP;
|
||||||
import tuwien.auto.calimero.datapoint.Datapoint;
|
import tuwien.auto.calimero.datapoint.Datapoint;
|
||||||
import tuwien.auto.calimero.device.ProcessCommunicationResponder;
|
import tuwien.auto.calimero.device.ProcessCommunicationResponder;
|
||||||
@ -55,6 +57,7 @@ import tuwien.auto.calimero.process.ProcessCommunicator;
|
|||||||
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
|
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
|
||||||
import tuwien.auto.calimero.process.ProcessEvent;
|
import tuwien.auto.calimero.process.ProcessEvent;
|
||||||
import tuwien.auto.calimero.process.ProcessListener;
|
import tuwien.auto.calimero.process.ProcessListener;
|
||||||
|
import tuwien.auto.calimero.secure.KnxSecureException;
|
||||||
import tuwien.auto.calimero.secure.SecureApplicationLayer;
|
import tuwien.auto.calimero.secure.SecureApplicationLayer;
|
||||||
import tuwien.auto.calimero.secure.Security;
|
import tuwien.auto.calimero.secure.Security;
|
||||||
|
|
||||||
@ -66,6 +69,14 @@ import tuwien.auto.calimero.secure.Security;
|
|||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClient {
|
public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClient {
|
||||||
|
public enum ClientState {
|
||||||
|
INIT,
|
||||||
|
RUNNING,
|
||||||
|
INTERRUPTED,
|
||||||
|
DISPOSE
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientState state = ClientState.INIT;
|
||||||
|
|
||||||
private static final int MAX_SEND_ATTEMPTS = 2;
|
private static final int MAX_SEND_ATTEMPTS = 2;
|
||||||
|
|
||||||
@ -146,7 +157,11 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
|
|||||||
|
|
||||||
private boolean scheduleReconnectJob() {
|
private boolean scheduleReconnectJob() {
|
||||||
if (autoReconnectPeriod > 0) {
|
if (autoReconnectPeriod > 0) {
|
||||||
connectJob = knxScheduler.schedule(this::connect, autoReconnectPeriod, TimeUnit.SECONDS);
|
// schedule connect job, for the first connection ignore autoReconnectPeriod and use 1 sec
|
||||||
|
final long reconnectDelayS = (state == ClientState.INIT) ? 1 : autoReconnectPeriod;
|
||||||
|
final String prefix = (state == ClientState.INIT) ? "re" : "";
|
||||||
|
logger.debug("Bridge {} scheduling {}connect in {}s", thingUID, prefix, reconnectDelayS);
|
||||||
|
connectJob = knxScheduler.schedule(this::connect, reconnectDelayS, TimeUnit.SECONDS);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
@ -154,7 +169,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void cancelReconnectJob() {
|
private void cancelReconnectJob() {
|
||||||
ScheduledFuture<?> currentReconnectJob = connectJob;
|
final ScheduledFuture<?> currentReconnectJob = connectJob;
|
||||||
if (currentReconnectJob != null) {
|
if (currentReconnectJob != null) {
|
||||||
currentReconnectJob.cancel(true);
|
currentReconnectJob.cancel(true);
|
||||||
connectJob = null;
|
connectJob = null;
|
||||||
@ -171,55 +186,111 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
|
|||||||
}
|
}
|
||||||
|
|
||||||
private synchronized boolean connect() {
|
private synchronized boolean connect() {
|
||||||
|
if (state == ClientState.INIT) {
|
||||||
|
state = ClientState.RUNNING;
|
||||||
|
} else if (state == ClientState.DISPOSE) {
|
||||||
|
logger.trace("connect() ignored, closing down");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (isConnected()) {
|
if (isConnected()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
// We have a valid "connection" object, this is ensured by IPClient.java.
|
||||||
|
// "releaseConnection" is actually removing all registered users of this connection and stopping
|
||||||
|
// all threads.
|
||||||
|
// Note that this will also kill this function in the following call to sleep in case of a
|
||||||
|
// connection loss -> restart is via triggered via scheduledReconnect in handler for InterruptedException.
|
||||||
releaseConnection();
|
releaseConnection();
|
||||||
|
Thread.sleep(1000);
|
||||||
|
logger.debug("Bridge {} is connecting to KNX bus", thingUID);
|
||||||
|
|
||||||
logger.debug("Bridge {} is connecting to the KNX bus", thingUID);
|
// now establish (possibly encrypted) connection, according to settings (tunnel, routing, secure...)
|
||||||
|
|
||||||
KNXNetworkLink link = establishConnection();
|
KNXNetworkLink link = establishConnection();
|
||||||
this.link = link;
|
this.link = link;
|
||||||
|
|
||||||
|
// ManagementProcedures provided by Calimero: allow managing other KNX devices, e.g. check if an address is
|
||||||
|
// reachable.
|
||||||
|
// Note for KNX Secure: ManagmentProcedueresImpl currently does not provide a ctor with external SAL,
|
||||||
|
// it internally creates an instance of ManagementClientImpl, which uses
|
||||||
|
// Security.defaultInstallation().deviceToolKeys()
|
||||||
|
// Protected ctor using given ManagementClientImpl is avalable (custom class to be inherited)
|
||||||
managementProcedures = new ManagementProceduresImpl(link);
|
managementProcedures = new ManagementProceduresImpl(link);
|
||||||
|
|
||||||
|
// ManagementClient provided by Calimero: allow reading device info, etc.
|
||||||
|
// Note for KNX Secure: ManagementClientImpl does not provide a ctor with external SAL in Calimero 2.5,
|
||||||
|
// is uses global Security.defaultInstalltion().deviceToolKeys()
|
||||||
|
// Current main branch includes a protected ctor (custom class to be inherited)
|
||||||
|
// TODO Calimero>2.5: check if there is a new way to provide security info, there is a new protected ctor
|
||||||
|
// TODO check if we can avoid creating another ManagementClient and re-use this from ManagemntProcedures
|
||||||
ManagementClient managementClient = new ManagementClientImpl(link);
|
ManagementClient managementClient = new ManagementClientImpl(link);
|
||||||
managementClient.responseTimeout(Duration.ofSeconds(responseTimeout));
|
managementClient.responseTimeout(Duration.ofSeconds(responseTimeout));
|
||||||
this.managementClient = managementClient;
|
this.managementClient = managementClient;
|
||||||
|
|
||||||
|
// OH helper for reading device info, based on managementClient above
|
||||||
deviceInfoClient = new DeviceInfoClientImpl(managementClient);
|
deviceInfoClient = new DeviceInfoClientImpl(managementClient);
|
||||||
|
|
||||||
|
// ProcessCommunicator provides main KNX communication (Calimero).
|
||||||
|
// Note for KNX Secure: SAL to be provided
|
||||||
ProcessCommunicator processCommunicator = new ProcessCommunicatorImpl(link);
|
ProcessCommunicator processCommunicator = new ProcessCommunicatorImpl(link);
|
||||||
processCommunicator.responseTimeout(Duration.ofSeconds(responseTimeout));
|
processCommunicator.responseTimeout(Duration.ofSeconds(responseTimeout));
|
||||||
processCommunicator.addProcessListener(processListener);
|
processCommunicator.addProcessListener(processListener);
|
||||||
this.processCommunicator = processCommunicator;
|
this.processCommunicator = processCommunicator;
|
||||||
|
|
||||||
|
// ProcessCommunicationResponder provides responses to requests from KNX bus (Calimero).
|
||||||
|
// Note for KNX Secure: SAL to be provided
|
||||||
ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link,
|
ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link,
|
||||||
new SecureApplicationLayer(link, Security.defaultInstallation()));
|
new SecureApplicationLayer(link, Security.defaultInstallation()));
|
||||||
this.responseCommunicator = responseCommunicator;
|
this.responseCommunicator = responseCommunicator;
|
||||||
|
|
||||||
|
// register this class, callbacks will be triggered
|
||||||
link.addLinkListener(this);
|
link.addLinkListener(this);
|
||||||
|
|
||||||
|
// create a job carrying out read requests
|
||||||
busJob = knxScheduler.scheduleWithFixedDelay(() -> readNextQueuedDatapoint(), 0, readingPause,
|
busJob = knxScheduler.scheduleWithFixedDelay(() -> readNextQueuedDatapoint(), 0, readingPause,
|
||||||
TimeUnit.MILLISECONDS);
|
TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
statusUpdateCallback.updateStatus(ThingStatus.ONLINE);
|
statusUpdateCallback.updateStatus(ThingStatus.ONLINE);
|
||||||
connectJob = null;
|
connectJob = null;
|
||||||
|
|
||||||
|
logger.info("Bridge {} connected to KNX bus", thingUID);
|
||||||
|
|
||||||
|
state = ClientState.RUNNING;
|
||||||
return true;
|
return true;
|
||||||
} catch (KNXException | InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
logger.debug("Error connecting to the bus: {}", e.getMessage(), e);
|
final var lastState = state;
|
||||||
|
state = ClientState.INTERRUPTED;
|
||||||
|
|
||||||
|
logger.trace("Bridge {}, connection interrupted", thingUID);
|
||||||
|
|
||||||
|
disconnect(e);
|
||||||
|
if (lastState != ClientState.DISPOSE) {
|
||||||
|
scheduleReconnectJob();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (KNXException | KnxSecureException e) {
|
||||||
|
logger.debug("Bridge {} cannot connect: {}", thingUID, e.getMessage());
|
||||||
disconnect(e);
|
disconnect(e);
|
||||||
scheduleReconnectJob();
|
scheduleReconnectJob();
|
||||||
return false;
|
return false;
|
||||||
|
} catch (KNXIllegalArgumentException e) {
|
||||||
|
logger.debug("Bridge {} cannot connect: {}", thingUID, e.getMessage());
|
||||||
|
disconnect(e, Optional.of(ThingStatusDetail.CONFIGURATION_ERROR));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void disconnect(@Nullable Exception e) {
|
private void disconnect(@Nullable Exception e) {
|
||||||
|
disconnect(e, Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void disconnect(@Nullable Exception e, Optional<ThingStatusDetail> detail) {
|
||||||
releaseConnection();
|
releaseConnection();
|
||||||
if (e != null) {
|
if (e != null) {
|
||||||
String message = e.getLocalizedMessage();
|
final String message = e.getLocalizedMessage();
|
||||||
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, detail.orElse(ThingStatusDetail.COMMUNICATION_ERROR),
|
||||||
message != null ? message : "");
|
message != null ? message : "");
|
||||||
} else {
|
} else {
|
||||||
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE);
|
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE);
|
||||||
@ -227,22 +298,27 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("null")
|
@SuppressWarnings("null")
|
||||||
private void releaseConnection() {
|
protected void releaseConnection() {
|
||||||
logger.debug("Bridge {} is disconnecting from the KNX bus", thingUID);
|
logger.debug("Bridge {} is disconnecting from KNX bus", thingUID);
|
||||||
readDatapoints.clear();
|
var tmplink = link;
|
||||||
|
if (tmplink != null) {
|
||||||
|
link.removeLinkListener(this);
|
||||||
|
}
|
||||||
busJob = nullify(busJob, j -> j.cancel(true));
|
busJob = nullify(busJob, j -> j.cancel(true));
|
||||||
deviceInfoClient = null;
|
readDatapoints.clear();
|
||||||
managementProcedures = nullify(managementProcedures, mp -> mp.detach());
|
|
||||||
managementClient = nullify(managementClient, mc -> mc.detach());
|
|
||||||
link = nullify(link, l -> l.close());
|
|
||||||
processCommunicator = nullify(processCommunicator, pc -> {
|
|
||||||
pc.removeProcessListener(processListener);
|
|
||||||
pc.detach();
|
|
||||||
});
|
|
||||||
responseCommunicator = nullify(responseCommunicator, rc -> {
|
responseCommunicator = nullify(responseCommunicator, rc -> {
|
||||||
rc.removeProcessListener(processListener);
|
rc.removeProcessListener(processListener);
|
||||||
rc.detach();
|
rc.detach();
|
||||||
});
|
});
|
||||||
|
processCommunicator = nullify(processCommunicator, pc -> {
|
||||||
|
pc.removeProcessListener(processListener);
|
||||||
|
pc.detach();
|
||||||
|
});
|
||||||
|
deviceInfoClient = null;
|
||||||
|
managementClient = nullify(managementClient, mc -> mc.detach());
|
||||||
|
managementProcedures = nullify(managementProcedures, mp -> mp.detach());
|
||||||
|
link = nullify(link, l -> l.close());
|
||||||
|
logger.trace("Bridge {} disconnected from KNX bus", thingUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> @Nullable T nullify(T target, @Nullable Consumer<T> lastWill) {
|
private <T> @Nullable T nullify(T target, @Nullable Consumer<T> lastWill) {
|
||||||
@ -276,6 +352,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
|
|||||||
return typeHelper.toDPTValue(type, dpt);
|
return typeHelper.toDPTValue(type, dpt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// datapoint is null at end of the list, warning is misleading
|
||||||
@SuppressWarnings("null")
|
@SuppressWarnings("null")
|
||||||
private void readNextQueuedDatapoint() {
|
private void readNextQueuedDatapoint() {
|
||||||
if (!connectIfNotAutomatic()) {
|
if (!connectIfNotAutomatic()) {
|
||||||
@ -316,6 +393,8 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
|
state = ClientState.DISPOSE;
|
||||||
|
|
||||||
cancelReconnectJob();
|
cancelReconnectJob();
|
||||||
disconnect(null);
|
disconnect(null);
|
||||||
}
|
}
|
||||||
@ -420,7 +499,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
|
|||||||
ProcessCommunicator processCommunicator = this.processCommunicator;
|
ProcessCommunicator processCommunicator = this.processCommunicator;
|
||||||
KNXNetworkLink link = this.link;
|
KNXNetworkLink link = this.link;
|
||||||
if (processCommunicator == null || link == null) {
|
if (processCommunicator == null || link == null) {
|
||||||
logger.debug("Cannot write to the KNX bus (processCommuicator: {}, link: {})",
|
logger.debug("Cannot write to KNX bus (processCommuicator: {}, link: {})",
|
||||||
processCommunicator == null ? "Not OK" : "OK",
|
processCommunicator == null ? "Not OK" : "OK",
|
||||||
link == null ? "Not OK" : (link.isOpen() ? "Open" : "Closed"));
|
link == null ? "Not OK" : (link.isOpen() ? "Open" : "Closed"));
|
||||||
return;
|
return;
|
||||||
@ -439,7 +518,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
|
|||||||
ProcessCommunicationResponder responseCommunicator = this.responseCommunicator;
|
ProcessCommunicationResponder responseCommunicator = this.responseCommunicator;
|
||||||
KNXNetworkLink link = this.link;
|
KNXNetworkLink link = this.link;
|
||||||
if (responseCommunicator == null || link == null) {
|
if (responseCommunicator == null || link == null) {
|
||||||
logger.debug("Cannot write to the KNX bus (responseCommunicator: {}, link: {})",
|
logger.debug("Cannot write to KNX bus (responseCommunicator: {}, link: {})",
|
||||||
responseCommunicator == null ? "Not OK" : "OK",
|
responseCommunicator == null ? "Not OK" : "OK",
|
||||||
link == null ? "Not OK" : (link.isOpen() ? "Open" : "Closed"));
|
link == null ? "Not OK" : (link.isOpen() ? "Open" : "Closed"));
|
||||||
return;
|
return;
|
||||||
@ -475,10 +554,10 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
|
|||||||
break;
|
break;
|
||||||
} catch (KNXException e) {
|
} catch (KNXException e) {
|
||||||
if (i < MAX_SEND_ATTEMPTS - 1) {
|
if (i < MAX_SEND_ATTEMPTS - 1) {
|
||||||
logger.debug("Value '{}' could not be sent to the KNX bus using datapoint '{}': {}. Will retry.",
|
logger.debug("Value '{}' could not be sent to KNX bus using datapoint '{}': {}. Will retry.", type,
|
||||||
type, datapoint, e.getLocalizedMessage());
|
datapoint, e.getLocalizedMessage());
|
||||||
} else {
|
} else {
|
||||||
logger.warn("Value '{}' could not be sent to the KNX bus using datapoint '{}': {}. Giving up now.",
|
logger.warn("Value '{}' could not be sent to KNX bus using datapoint '{}': {}. Giving up now.",
|
||||||
type, datapoint, e.getLocalizedMessage());
|
type, datapoint, e.getLocalizedMessage());
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import tuwien.auto.calimero.link.medium.KNXMediumSettings;
|
|||||||
public class CustomKNXNetworkLinkIP extends KNXNetworkLinkIP {
|
public class CustomKNXNetworkLinkIP extends KNXNetworkLinkIP {
|
||||||
|
|
||||||
public static final int TUNNELING = KNXNetworkLinkIP.TUNNELING;
|
public static final int TUNNELING = KNXNetworkLinkIP.TUNNELING;
|
||||||
|
public static final int TUNNELINGV2 = KNXNetworkLinkIP.TunnelingV2;
|
||||||
public static final int ROUTING = KNXNetworkLinkIP.ROUTING;
|
public static final int ROUTING = KNXNetworkLinkIP.ROUTING;
|
||||||
|
|
||||||
CustomKNXNetworkLinkIP(final int serviceMode, KNXnetIPConnection conn, KNXMediumSettings settings)
|
CustomKNXNetworkLinkIP(final int serviceMode, KNXnetIPConnection conn, KNXMediumSettings settings)
|
||||||
|
|||||||
@ -61,6 +61,11 @@ public class DeviceInfoClientImpl implements DeviceInfoClient {
|
|||||||
return result;
|
return result;
|
||||||
} catch (KNXException e) {
|
} catch (KNXException e) {
|
||||||
logger.debug("Could not {} of {}: {}", task, address, e.getMessage());
|
logger.debug("Could not {} of {}: {}", task, address, e.getMessage());
|
||||||
|
try {
|
||||||
|
// avoid trashing the log on connection loss
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
logger.trace("Interrupted to {}", task);
|
logger.trace("Interrupted to {}", task);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import java.net.InetSocketAddress;
|
|||||||
import java.net.NetworkInterface;
|
import java.net.NetworkInterface;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
@ -32,6 +33,9 @@ import tuwien.auto.calimero.knxnetip.KNXnetIPConnection;
|
|||||||
import tuwien.auto.calimero.knxnetip.KNXnetIPRouting;
|
import tuwien.auto.calimero.knxnetip.KNXnetIPRouting;
|
||||||
import tuwien.auto.calimero.knxnetip.KNXnetIPTunnel;
|
import tuwien.auto.calimero.knxnetip.KNXnetIPTunnel;
|
||||||
import tuwien.auto.calimero.knxnetip.KNXnetIPTunnel.TunnelingLayer;
|
import tuwien.auto.calimero.knxnetip.KNXnetIPTunnel.TunnelingLayer;
|
||||||
|
import tuwien.auto.calimero.knxnetip.SecureConnection;
|
||||||
|
import tuwien.auto.calimero.knxnetip.TcpConnection;
|
||||||
|
import tuwien.auto.calimero.knxnetip.TcpConnection.SecureSession;
|
||||||
import tuwien.auto.calimero.link.KNXNetworkLink;
|
import tuwien.auto.calimero.link.KNXNetworkLink;
|
||||||
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
|
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
|
||||||
import tuwien.auto.calimero.link.medium.KNXMediumSettings;
|
import tuwien.auto.calimero.link.medium.KNXMediumSettings;
|
||||||
@ -46,23 +50,43 @@ import tuwien.auto.calimero.link.medium.TPSettings;
|
|||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class IPClient extends AbstractKNXClient {
|
public class IPClient extends AbstractKNXClient {
|
||||||
|
|
||||||
|
public enum IpConnectionType {
|
||||||
|
TUNNEL,
|
||||||
|
ROUTER,
|
||||||
|
SECURE_TUNNEL,
|
||||||
|
SECURE_ROUTER
|
||||||
|
};
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(IPClient.class);
|
private final Logger logger = LoggerFactory.getLogger(IPClient.class);
|
||||||
|
|
||||||
private static final String MODE_ROUTER = "ROUTER";
|
private static final String MODE_ROUTER = "ROUTER";
|
||||||
private static final String MODE_TUNNEL = "TUNNEL";
|
private static final String MODE_TUNNEL = "TUNNEL";
|
||||||
|
private static final String MODE_SECURE_ROUTER = "SECURE ROUTER";
|
||||||
|
private static final String MODE_SECURE_TUNNEL = "SECURE TUNNEL";
|
||||||
|
private static final long PAUSE_ON_TCP_SESSION_CLOSE_MS = 1000;
|
||||||
|
|
||||||
private final int ipConnectionType;
|
private final IpConnectionType ipConnectionType;
|
||||||
private final String ip;
|
private final String ip;
|
||||||
private final String localSource;
|
private final String localSource;
|
||||||
private final int port;
|
private final int port;
|
||||||
@Nullable
|
@Nullable
|
||||||
private final InetSocketAddress localEndPoint;
|
private final InetSocketAddress localEndPoint;
|
||||||
private final boolean useNAT;
|
private final boolean useNAT;
|
||||||
|
private final byte[] secureRoutingBackboneGroupKey;
|
||||||
|
private final long secureRoutingLatencyToleranceMs;
|
||||||
|
private final byte[] secureTunnelDevKey;
|
||||||
|
private final int secureTunnelUser;
|
||||||
|
private final byte[] secureTunnelUserKey;
|
||||||
|
private final ThingUID thingUID;
|
||||||
|
|
||||||
public IPClient(int ipConnectionType, String ip, String localSource, int port,
|
@Nullable
|
||||||
@Nullable InetSocketAddress localEndPoint, boolean useNAT, int autoReconnectPeriod, ThingUID thingUID,
|
SecureSession tcpSession;
|
||||||
int responseTimeout, int readingPause, int readRetriesLimit, ScheduledExecutorService knxScheduler,
|
|
||||||
StatusUpdateCallback statusUpdateCallback) {
|
public IPClient(IpConnectionType ipConnectionType, String ip, String localSource, int port,
|
||||||
|
@Nullable InetSocketAddress localEndPoint, boolean useNAT, int autoReconnectPeriod,
|
||||||
|
byte[] secureRoutingBackboneGroupKey, long secureRoutingLatencyToleranceMs, byte[] secureTunnelDevKey,
|
||||||
|
int secureTunnelUser, byte[] secureTunnelUserKey, ThingUID thingUID, int responseTimeout, int readingPause,
|
||||||
|
int readRetriesLimit, ScheduledExecutorService knxScheduler, StatusUpdateCallback statusUpdateCallback) {
|
||||||
super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler,
|
super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler,
|
||||||
statusUpdateCallback);
|
statusUpdateCallback);
|
||||||
this.ipConnectionType = ipConnectionType;
|
this.ipConnectionType = ipConnectionType;
|
||||||
@ -71,6 +95,13 @@ public class IPClient extends AbstractKNXClient {
|
|||||||
this.port = port;
|
this.port = port;
|
||||||
this.localEndPoint = localEndPoint;
|
this.localEndPoint = localEndPoint;
|
||||||
this.useNAT = useNAT;
|
this.useNAT = useNAT;
|
||||||
|
this.secureRoutingBackboneGroupKey = secureRoutingBackboneGroupKey;
|
||||||
|
this.secureRoutingLatencyToleranceMs = secureRoutingLatencyToleranceMs;
|
||||||
|
this.secureTunnelDevKey = secureTunnelDevKey;
|
||||||
|
this.secureTunnelUser = secureTunnelUser;
|
||||||
|
this.secureTunnelUserKey = secureTunnelUserKey;
|
||||||
|
this.thingUID = thingUID;
|
||||||
|
tcpSession = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -82,23 +113,44 @@ public class IPClient extends AbstractKNXClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String connectionTypeToString() {
|
private String connectionTypeToString() {
|
||||||
return ipConnectionType == CustomKNXNetworkLinkIP.ROUTING ? MODE_ROUTER : MODE_TUNNEL;
|
if (ipConnectionType == IpConnectionType.ROUTER) {
|
||||||
|
return MODE_ROUTER;
|
||||||
|
}
|
||||||
|
if (ipConnectionType == IpConnectionType.TUNNEL) {
|
||||||
|
return MODE_TUNNEL;
|
||||||
|
}
|
||||||
|
if (ipConnectionType == IpConnectionType.SECURE_ROUTER) {
|
||||||
|
return MODE_SECURE_ROUTER;
|
||||||
|
}
|
||||||
|
if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) {
|
||||||
|
return MODE_SECURE_TUNNEL;
|
||||||
|
}
|
||||||
|
return "unknown connection type";
|
||||||
|
}
|
||||||
|
|
||||||
|
private KNXNetworkLinkIP createKNXNetworkLinkIP(IpConnectionType ipConnectionType,
|
||||||
|
@Nullable InetSocketAddress localEP, @Nullable InetSocketAddress remoteEP, boolean useNAT,
|
||||||
|
KNXMediumSettings settings) throws KNXException, InterruptedException {
|
||||||
|
// Calimero service mode, ROUTING for both classic and secure routing
|
||||||
|
int serviceMode = CustomKNXNetworkLinkIP.ROUTING;
|
||||||
|
if (ipConnectionType == IpConnectionType.TUNNEL) {
|
||||||
|
serviceMode = CustomKNXNetworkLinkIP.TUNNELING;
|
||||||
|
} else if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) {
|
||||||
|
serviceMode = CustomKNXNetworkLinkIP.TUNNELINGV2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private KNXNetworkLinkIP createKNXNetworkLinkIP(int serviceMode, @Nullable InetSocketAddress localEP,
|
|
||||||
@Nullable InetSocketAddress remoteEP, boolean useNAT, KNXMediumSettings settings)
|
|
||||||
throws KNXException, InterruptedException {
|
|
||||||
// creating the connection here as a workaround for
|
// creating the connection here as a workaround for
|
||||||
// https://github.com/calimero-project/calimero-core/issues/57
|
// https://github.com/calimero-project/calimero-core/issues/57
|
||||||
KNXnetIPConnection conn = getConnection(serviceMode, localEP, remoteEP, useNAT);
|
KNXnetIPConnection conn = getConnection(ipConnectionType, localEP, remoteEP, useNAT);
|
||||||
return new CustomKNXNetworkLinkIP(serviceMode, conn, settings);
|
return new CustomKNXNetworkLinkIP(serviceMode, conn, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
private KNXnetIPConnection getConnection(int serviceMode, @Nullable InetSocketAddress localEP,
|
private KNXnetIPConnection getConnection(IpConnectionType ipConnectionType, @Nullable InetSocketAddress localEP,
|
||||||
@Nullable InetSocketAddress remoteEP, boolean useNAT) throws KNXException, InterruptedException {
|
@Nullable InetSocketAddress remoteEP, boolean useNAT) throws KNXException, InterruptedException {
|
||||||
KNXnetIPConnection conn;
|
KNXnetIPConnection conn;
|
||||||
switch (serviceMode) {
|
switch (ipConnectionType) {
|
||||||
case CustomKNXNetworkLinkIP.TUNNELING:
|
case TUNNEL:
|
||||||
|
case SECURE_TUNNEL:
|
||||||
InetSocketAddress local = localEP;
|
InetSocketAddress local = localEP;
|
||||||
if (local == null) {
|
if (local == null) {
|
||||||
try {
|
try {
|
||||||
@ -107,9 +159,23 @@ public class IPClient extends AbstractKNXClient {
|
|||||||
throw new KNXException("no local host available");
|
throw new KNXException("no local host available");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) {
|
||||||
|
logger.trace("creating new TCP connection");
|
||||||
|
if (tcpSession != null) {
|
||||||
|
logger.debug("tcpSession might still be open");
|
||||||
|
}
|
||||||
|
// using .clone for the keys is essential - otherwise Calimero clears the array and a reconnect will
|
||||||
|
// fail
|
||||||
|
tcpSession = TcpConnection.newTcpConnection(localEP, remoteEP).newSecureSession(secureTunnelUser,
|
||||||
|
secureTunnelUserKey.clone(), secureTunnelDevKey.clone());
|
||||||
|
conn = SecureConnection.newTunneling(TunnelingLayer.LinkLayer, tcpSession,
|
||||||
|
new IndividualAddress(localSource));
|
||||||
|
} else {
|
||||||
conn = new KNXnetIPTunnel(TunnelingLayer.LinkLayer, local, remoteEP, useNAT);
|
conn = new KNXnetIPTunnel(TunnelingLayer.LinkLayer, local, remoteEP, useNAT);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case CustomKNXNetworkLinkIP.ROUTING:
|
case ROUTER:
|
||||||
|
case SECURE_ROUTER:
|
||||||
NetworkInterface netIf = null;
|
NetworkInterface netIf = null;
|
||||||
if (localEP != null && !localEP.isUnresolved()) {
|
if (localEP != null && !localEP.isUnresolved()) {
|
||||||
try {
|
try {
|
||||||
@ -119,11 +185,39 @@ public class IPClient extends AbstractKNXClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
final InetAddress mcast = remoteEP != null ? remoteEP.getAddress() : null;
|
final InetAddress mcast = remoteEP != null ? remoteEP.getAddress() : null;
|
||||||
|
if (ipConnectionType == IpConnectionType.SECURE_ROUTER) {
|
||||||
|
conn = SecureConnection.newRouting(netIf, mcast, secureRoutingBackboneGroupKey,
|
||||||
|
Duration.ofMillis(secureRoutingLatencyToleranceMs));
|
||||||
|
} else {
|
||||||
conn = new KNXnetIPRouting(netIf, mcast);
|
conn = new KNXnetIPRouting(netIf, mcast);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new KNXIllegalArgumentException("unknown service mode");
|
throw new KNXIllegalArgumentException("unknown service mode");
|
||||||
}
|
}
|
||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void closeTcpConnection() {
|
||||||
|
final SecureSession toBeClosed = tcpSession;
|
||||||
|
if (toBeClosed != null) {
|
||||||
|
tcpSession = null;
|
||||||
|
logger.debug("Bridge {} closing TCP connection", thingUID);
|
||||||
|
try {
|
||||||
|
toBeClosed.close();
|
||||||
|
try {
|
||||||
|
Thread.sleep(PAUSE_ON_TCP_SESSION_CLOSE_MS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.debug("closing TCP connection failed: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void releaseConnection() {
|
||||||
|
closeTcpConnection();
|
||||||
|
super.releaseConnection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,10 @@ public class IPBridgeConfiguration extends BridgeConfiguration {
|
|||||||
private BigDecimal portNumber = BigDecimal.valueOf(0);
|
private BigDecimal portNumber = BigDecimal.valueOf(0);
|
||||||
private String localIp = "";
|
private String localIp = "";
|
||||||
private String localSourceAddr = "";
|
private String localSourceAddr = "";
|
||||||
|
private String routerBackboneKey = "";
|
||||||
|
private String tunnelUserId = "";
|
||||||
|
private String tunnelUserPassword = "";
|
||||||
|
private String tunnelDeviceAuthentication = "";
|
||||||
|
|
||||||
public Boolean getUseNAT() {
|
public Boolean getUseNAT() {
|
||||||
return useNAT;
|
return useNAT;
|
||||||
@ -55,4 +59,20 @@ public class IPBridgeConfiguration extends BridgeConfiguration {
|
|||||||
public String getLocalSourceAddr() {
|
public String getLocalSourceAddr() {
|
||||||
return localSourceAddr;
|
return localSourceAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRouterBackboneKey() {
|
||||||
|
return routerBackboneKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTunnelUserId() {
|
||||||
|
return tunnelUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTunnelUserPassword() {
|
||||||
|
return tunnelUserPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTunnelDeviceAuthentication() {
|
||||||
|
return tunnelDeviceAuthentication;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -162,7 +162,8 @@ public abstract class AbstractKNXThingHandler extends BaseThingHandler implement
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (KNXException e) {
|
} catch (KNXException e) {
|
||||||
logger.debug("An error occurred while testing the reachability of a thing '{}'", getThing().getUID(), e);
|
logger.debug("An error occurred while testing the reachability of a thing '{}': {}", getThing().getUID(),
|
||||||
|
e.getMessage());
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,7 +192,8 @@ public abstract class AbstractKNXThingHandler extends BaseThingHandler implement
|
|||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.ONLINE);
|
||||||
}
|
}
|
||||||
} catch (KNXFormatException e) {
|
} catch (KNXFormatException e) {
|
||||||
logger.debug("An exception occurred while setting the individual address '{}'", config.getAddress(), e);
|
logger.debug("An exception occurred while setting the individual address '{}': {}", config.getAddress(),
|
||||||
|
e.getMessage());
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getLocalizedMessage());
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getLocalizedMessage());
|
||||||
}
|
}
|
||||||
getClient().registerGroupAddressListener(this);
|
getClient().registerGroupAddressListener(this);
|
||||||
|
|||||||
@ -14,11 +14,11 @@ package org.openhab.binding.knx.internal.handler;
|
|||||||
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.knx.internal.KNXBindingConstants;
|
import org.openhab.binding.knx.internal.KNXBindingConstants;
|
||||||
import org.openhab.binding.knx.internal.client.CustomKNXNetworkLinkIP;
|
|
||||||
import org.openhab.binding.knx.internal.client.IPClient;
|
import org.openhab.binding.knx.internal.client.IPClient;
|
||||||
import org.openhab.binding.knx.internal.client.KNXClient;
|
import org.openhab.binding.knx.internal.client.KNXClient;
|
||||||
import org.openhab.binding.knx.internal.client.NoOpClient;
|
import org.openhab.binding.knx.internal.client.NoOpClient;
|
||||||
@ -30,6 +30,8 @@ import org.openhab.core.thing.ThingStatusDetail;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import tuwien.auto.calimero.secure.KnxSecureException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link IPBridgeThingHandler} is responsible for handling commands, which are
|
* The {@link IPBridgeThingHandler} is responsible for handling commands, which are
|
||||||
* sent to one of the channels. It implements a KNX/IP Gateway, that either acts a a
|
* sent to one of the channels. It implements a KNX/IP Gateway, that either acts a a
|
||||||
@ -43,10 +45,13 @@ import org.slf4j.LoggerFactory;
|
|||||||
public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
|
public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
|
||||||
private static final String MODE_ROUTER = "ROUTER";
|
private static final String MODE_ROUTER = "ROUTER";
|
||||||
private static final String MODE_TUNNEL = "TUNNEL";
|
private static final String MODE_TUNNEL = "TUNNEL";
|
||||||
|
private static final String MODE_SECURE_ROUTER = "SECUREROUTER";
|
||||||
|
private static final String MODE_SECURE_TUNNEL = "SECURETUNNEL";
|
||||||
|
private @Nullable Future<?> initJob = null;
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(IPBridgeThingHandler.class);
|
private final Logger logger = LoggerFactory.getLogger(IPBridgeThingHandler.class);
|
||||||
|
|
||||||
private @Nullable IPClient client;
|
private @Nullable IPClient client = null;
|
||||||
private final NetworkAddressService networkAddressService;
|
private final NetworkAddressService networkAddressService;
|
||||||
|
|
||||||
public IPBridgeThingHandler(Bridge bridge, NetworkAddressService networkAddressService) {
|
public IPBridgeThingHandler(Bridge bridge, NetworkAddressService networkAddressService) {
|
||||||
@ -56,7 +61,40 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
|
// initialisation would take too long and show a warning during binding startup
|
||||||
|
// KNX secure is adding serious delay
|
||||||
|
updateStatus(ThingStatus.UNKNOWN);
|
||||||
|
initJob = scheduler.submit(() -> {
|
||||||
|
initializeLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeLater() {
|
||||||
IPBridgeConfiguration config = getConfigAs(IPBridgeConfiguration.class);
|
IPBridgeConfiguration config = getConfigAs(IPBridgeConfiguration.class);
|
||||||
|
boolean securityAvailable = false;
|
||||||
|
try {
|
||||||
|
securityAvailable = initializeSecurity(config.getRouterBackboneKey(),
|
||||||
|
config.getTunnelDeviceAuthentication(), config.getTunnelUserId(), config.getTunnelUserPassword());
|
||||||
|
if (securityAvailable) {
|
||||||
|
logger.debug("KNX secure: router backboneGroupKey is {} set",
|
||||||
|
((secureRouting.backboneGroupKey.length == 16) ? "properly" : "not"));
|
||||||
|
boolean tunnelOk = ((secureTunnel.user > 0) && (secureTunnel.devKey.length == 16)
|
||||||
|
&& (secureTunnel.userKey.length == 16));
|
||||||
|
logger.debug("KNX secure: tunnel keys are {} set", (tunnelOk ? "properly" : "not"));
|
||||||
|
} else {
|
||||||
|
logger.debug("KNX security not configured");
|
||||||
|
}
|
||||||
|
} catch (KnxSecureException e) {
|
||||||
|
logger.debug("{}, {}", thing.getUID(), e.toString());
|
||||||
|
|
||||||
|
String message = e.getLocalizedMessage();
|
||||||
|
if (message == null) {
|
||||||
|
message = e.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "KNX security: " + message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int autoReconnectPeriod = config.getAutoReconnectPeriod();
|
int autoReconnectPeriod = config.getAutoReconnectPeriod();
|
||||||
if (autoReconnectPeriod != 0 && autoReconnectPeriod < 30) {
|
if (autoReconnectPeriod != 0 && autoReconnectPeriod < 30) {
|
||||||
logger.info("autoReconnectPeriod for {} set to {}s, allowed range is 0 (never) or >30", thing.getUID(),
|
logger.info("autoReconnectPeriod for {} set to {}s, allowed range is 0 (never) or >30", thing.getUID(),
|
||||||
@ -70,19 +108,63 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
|
|||||||
String ip = config.getIpAddress();
|
String ip = config.getIpAddress();
|
||||||
InetSocketAddress localEndPoint = null;
|
InetSocketAddress localEndPoint = null;
|
||||||
boolean useNAT = false;
|
boolean useNAT = false;
|
||||||
int ipConnectionType;
|
|
||||||
|
IPClient.IpConnectionType ipConnectionType;
|
||||||
if (MODE_TUNNEL.equalsIgnoreCase(connectionTypeString)) {
|
if (MODE_TUNNEL.equalsIgnoreCase(connectionTypeString)) {
|
||||||
useNAT = config.getUseNAT();
|
useNAT = config.getUseNAT();
|
||||||
ipConnectionType = CustomKNXNetworkLinkIP.TUNNELING;
|
ipConnectionType = IPClient.IpConnectionType.TUNNEL;
|
||||||
|
} else if (MODE_SECURE_TUNNEL.equalsIgnoreCase(connectionTypeString)) {
|
||||||
|
useNAT = config.getUseNAT();
|
||||||
|
ipConnectionType = IPClient.IpConnectionType.SECURE_TUNNEL;
|
||||||
|
|
||||||
|
if (!securityAvailable) {
|
||||||
|
logger.warn("Bridge {} missing security configuration for secure tunnel", thing.getUID());
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||||
|
"Security configuration missing for secure tunnel");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean tunnelOk = ((secureTunnel.user > 0) && (secureTunnel.devKey.length == 16)
|
||||||
|
&& (secureTunnel.userKey.length == 16));
|
||||||
|
if (!tunnelOk) {
|
||||||
|
logger.warn("Bridge {} incomplete security configuration for secure tunnel", thing.getUID());
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||||
|
"Security configuration for secure tunnel is incomplete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("KNX secure tunneling needs a few seconds to establish connection");
|
||||||
|
// user id, key, devAuth are already stored
|
||||||
} else if (MODE_ROUTER.equalsIgnoreCase(connectionTypeString)) {
|
} else if (MODE_ROUTER.equalsIgnoreCase(connectionTypeString)) {
|
||||||
useNAT = false;
|
useNAT = false;
|
||||||
if (ip.isEmpty()) {
|
if (ip.isEmpty()) {
|
||||||
ip = KNXBindingConstants.DEFAULT_MULTICAST_IP;
|
ip = KNXBindingConstants.DEFAULT_MULTICAST_IP;
|
||||||
}
|
}
|
||||||
ipConnectionType = CustomKNXNetworkLinkIP.ROUTING;
|
ipConnectionType = IPClient.IpConnectionType.ROUTER;
|
||||||
} else {
|
} else if (MODE_SECURE_ROUTER.equalsIgnoreCase(connectionTypeString)) {
|
||||||
|
useNAT = false;
|
||||||
|
if (ip.isEmpty()) {
|
||||||
|
ip = KNXBindingConstants.DEFAULT_MULTICAST_IP;
|
||||||
|
}
|
||||||
|
ipConnectionType = IPClient.IpConnectionType.SECURE_ROUTER;
|
||||||
|
|
||||||
|
if (!securityAvailable) {
|
||||||
|
logger.warn("Bridge {} missing security configuration for secure routing", thing.getUID());
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||||
MessageFormat.format("Unknown IP connection type {0}. Known types are either 'TUNNEL' or 'ROUTER'",
|
"Security configuration missing for secure routing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (secureRouting.backboneGroupKey.length != 16) {
|
||||||
|
// failed to read shared backbone group key from config
|
||||||
|
logger.warn("Bridge {} missing security configuration for secure routing", thing.getUID());
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||||
|
"backboneGroupKey required for secure routing; please configure");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug("KNX secure routing needs a few seconds to establish connection");
|
||||||
|
} else {
|
||||||
|
logger.debug("Bridge {} unknown connection type", thing.getUID());
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, MessageFormat.format(
|
||||||
|
"Unknown IP connection type {0}. Known types are either 'TUNNEL', 'ROUTER', 'SECURETUNNEL', or 'SECUREROUTER'",
|
||||||
connectionTypeString));
|
connectionTypeString));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -95,23 +177,39 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
|
|||||||
|
|
||||||
updateStatus(ThingStatus.UNKNOWN);
|
updateStatus(ThingStatus.UNKNOWN);
|
||||||
client = new IPClient(ipConnectionType, ip, localSource, port, localEndPoint, useNAT, autoReconnectPeriod,
|
client = new IPClient(ipConnectionType, ip, localSource, port, localEndPoint, useNAT, autoReconnectPeriod,
|
||||||
thing.getUID(), config.getResponseTimeout().intValue(), config.getReadingPause().intValue(),
|
secureRouting.backboneGroupKey, secureRouting.latencyToleranceMs, secureTunnel.devKey,
|
||||||
config.getReadRetriesLimit().intValue(), getScheduler(), this);
|
secureTunnel.user, secureTunnel.userKey, thing.getUID(), config.getResponseTimeout().intValue(),
|
||||||
|
config.getReadingPause().intValue(), config.getReadRetriesLimit().intValue(), getScheduler(), this);
|
||||||
|
|
||||||
final var tmpClient = client;
|
final var tmpClient = client;
|
||||||
if (tmpClient != null) {
|
if (tmpClient != null) {
|
||||||
tmpClient.initialize();
|
tmpClient.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.trace("Bridge {} completed KNX scheduled initialization", thing.getUID());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
super.dispose();
|
final var tmpInitJob = initJob;
|
||||||
|
if (tmpInitJob != null) {
|
||||||
|
while (!tmpInitJob.isDone()) {
|
||||||
|
logger.trace("Bridge {}, shutdown during init, trying to cancel", thing.getUID());
|
||||||
|
tmpInitJob.cancel(true);
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.trace("Bridge {}, cancellation interrupted", thing.getUID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initJob = null;
|
||||||
|
}
|
||||||
final var tmpClient = client;
|
final var tmpClient = client;
|
||||||
if (tmpClient != null) {
|
if (tmpClient != null) {
|
||||||
tmpClient.dispose();
|
tmpClient.dispose();
|
||||||
client = null;
|
client = null;
|
||||||
}
|
}
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -29,27 +29,135 @@ import org.openhab.core.thing.binding.BaseBridgeHandler;
|
|||||||
import org.openhab.core.types.Command;
|
import org.openhab.core.types.Command;
|
||||||
|
|
||||||
import tuwien.auto.calimero.IndividualAddress;
|
import tuwien.auto.calimero.IndividualAddress;
|
||||||
|
import tuwien.auto.calimero.knxnetip.SecureConnection;
|
||||||
import tuwien.auto.calimero.mgmt.Destination;
|
import tuwien.auto.calimero.mgmt.Destination;
|
||||||
|
import tuwien.auto.calimero.secure.KnxSecureException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link KNXBridgeBaseThingHandler} is responsible for handling commands, which are
|
* The {@link KNXBridgeBaseThingHandler} is responsible for handling commands, which are
|
||||||
* sent to one of the channels.
|
* sent to one of the channels.
|
||||||
*
|
*
|
||||||
* @author Simon Kaufmann - Initial contribution and API
|
* @author Simon Kaufmann - Initial contribution and API
|
||||||
|
* @author Holger Friedrich - KNX Secure configuration
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implements StatusUpdateCallback {
|
public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implements StatusUpdateCallback {
|
||||||
|
|
||||||
|
public static class SecureTunnelConfig {
|
||||||
|
public SecureTunnelConfig() {
|
||||||
|
devKey = new byte[0];
|
||||||
|
userKey = new byte[0];
|
||||||
|
user = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] devKey;
|
||||||
|
public byte[] userKey;
|
||||||
|
public int user = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SecureRoutingConfig {
|
||||||
|
public SecureRoutingConfig() {
|
||||||
|
backboneGroupKey = new byte[0];
|
||||||
|
latencyToleranceMs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] backboneGroupKey;
|
||||||
|
public long latencyToleranceMs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
protected ConcurrentHashMap<IndividualAddress, Destination> destinations = new ConcurrentHashMap<>();
|
protected ConcurrentHashMap<IndividualAddress, Destination> destinations = new ConcurrentHashMap<>();
|
||||||
private final ScheduledExecutorService knxScheduler = ThreadPoolManager.getScheduledPool("knx");
|
private final ScheduledExecutorService knxScheduler = ThreadPoolManager.getScheduledPool("knx");
|
||||||
private final ScheduledExecutorService backgroundScheduler = Executors.newSingleThreadScheduledExecutor();
|
private final ScheduledExecutorService backgroundScheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
protected SecureRoutingConfig secureRouting;
|
||||||
|
protected SecureTunnelConfig secureTunnel;
|
||||||
|
|
||||||
public KNXBridgeBaseThingHandler(Bridge bridge) {
|
public KNXBridgeBaseThingHandler(Bridge bridge) {
|
||||||
super(bridge);
|
super(bridge);
|
||||||
|
secureRouting = new SecureRoutingConfig();
|
||||||
|
secureTunnel = new SecureTunnelConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract KNXClient getClient();
|
protected abstract KNXClient getClient();
|
||||||
|
|
||||||
|
/***
|
||||||
|
* Initialize KNX secure if configured (full interface)
|
||||||
|
*
|
||||||
|
* @param cRouterBackboneGroupKey shared key for secure router mode.
|
||||||
|
* @param cTunnelDevAuth device password for IP interface in tunnel mode.
|
||||||
|
* @param cTunnelUser user id for tunnel mode. Must be an integer >0.
|
||||||
|
* @param cTunnelPassword user password for tunnel mode.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
protected boolean initializeSecurity(String cRouterBackboneGroupKey, String cTunnelDevAuth, String cTunnelUser,
|
||||||
|
String cTunnelPassword) throws KnxSecureException {
|
||||||
|
secureRouting = new SecureRoutingConfig();
|
||||||
|
secureTunnel = new SecureTunnelConfig();
|
||||||
|
|
||||||
|
boolean securityInitialized = false;
|
||||||
|
|
||||||
|
// step 1: secure routing, backbone group key manually specified in OH config
|
||||||
|
if (!cRouterBackboneGroupKey.isBlank()) {
|
||||||
|
// provided in config
|
||||||
|
String key = cRouterBackboneGroupKey.trim().replaceFirst("^0x", "").trim().replace(" ", "");
|
||||||
|
if (!key.isEmpty()) {
|
||||||
|
// helper may throw KnxSecureException
|
||||||
|
secureRouting.backboneGroupKey = secHelperParseBackboneKey(key);
|
||||||
|
securityInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// step 2: check if valid tunnel parameters are specified in config
|
||||||
|
if (!cTunnelDevAuth.isBlank()) {
|
||||||
|
secureTunnel.devKey = SecureConnection.hashDeviceAuthenticationPassword(cTunnelDevAuth.toCharArray());
|
||||||
|
securityInitialized = true;
|
||||||
|
}
|
||||||
|
if (!cTunnelPassword.isBlank()) {
|
||||||
|
secureTunnel.userKey = SecureConnection.hashUserPassword(cTunnelPassword.toCharArray());
|
||||||
|
securityInitialized = true;
|
||||||
|
}
|
||||||
|
if (!cTunnelUser.isBlank()) {
|
||||||
|
String user = cTunnelUser.trim();
|
||||||
|
try {
|
||||||
|
secureTunnel.user = Integer.decode(user);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new KnxSecureException("tunnelUser must be a number >0");
|
||||||
|
}
|
||||||
|
if (secureTunnel.user <= 0) {
|
||||||
|
throw new KnxSecureException("tunnelUser must be a number >0");
|
||||||
|
}
|
||||||
|
securityInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// step 5: router: load latencyTolerance
|
||||||
|
// default to 2000ms
|
||||||
|
// this parameter is currently not exposed in config, it may later be set by using the keyring
|
||||||
|
secureRouting.latencyToleranceMs = 2000;
|
||||||
|
|
||||||
|
return securityInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* converts hex string (32 characters) to byte[16]
|
||||||
|
*
|
||||||
|
* @param hexstring 32 characters hex
|
||||||
|
* @return key in byte array format
|
||||||
|
*/
|
||||||
|
public static byte[] secHelperParseBackboneKey(String hexstring) throws KnxSecureException {
|
||||||
|
if (hexstring.length() != 32) {
|
||||||
|
throw new KnxSecureException("backbone key must be 32 characters (16 byte hex notation)");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] parsed = new byte[16];
|
||||||
|
try {
|
||||||
|
for (byte i = 0; i < 16; i++) {
|
||||||
|
parsed[i] = (byte) Integer.parseInt(hexstring.substring(2 * i, 2 * i + 2), 16);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new KnxSecureException("backbone key configured, cannot parse hex string, illegal character", e);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||||
// Nothing to do here
|
// Nothing to do here
|
||||||
|
|||||||
@ -50,8 +50,8 @@ public class SerialBridgeThingHandler extends KNXBridgeBaseThingHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
super.dispose();
|
|
||||||
client.dispose();
|
client.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -24,6 +24,8 @@ thing-type.config.knx.device.readInterval.label = Read Interval
|
|||||||
thing-type.config.knx.device.readInterval.description = Interval (in seconds) between attempts to read the status group addresses on the bus
|
thing-type.config.knx.device.readInterval.description = Interval (in seconds) between attempts to read the status group addresses on the bus
|
||||||
thing-type.config.knx.ip.autoReconnectPeriod.label = Auto Reconnect Period
|
thing-type.config.knx.ip.autoReconnectPeriod.label = Auto Reconnect Period
|
||||||
thing-type.config.knx.ip.autoReconnectPeriod.description = Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s
|
thing-type.config.knx.ip.autoReconnectPeriod.description = Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s
|
||||||
|
thing-type.config.knx.ip.group.knxsecure.label = KNX secure
|
||||||
|
thing-type.config.knx.ip.group.knxsecure.description = Settings for KNX secure. Optional. Requires KNX secure features to be active in KNX installation.
|
||||||
thing-type.config.knx.ip.ipAddress.label = Network Address
|
thing-type.config.knx.ip.ipAddress.label = Network Address
|
||||||
thing-type.config.knx.ip.ipAddress.description = Network address of the KNX/IP gateway
|
thing-type.config.knx.ip.ipAddress.description = Network address of the KNX/IP gateway
|
||||||
thing-type.config.knx.ip.localIp.label = Local Network Address
|
thing-type.config.knx.ip.localIp.label = Local Network Address
|
||||||
@ -38,10 +40,20 @@ thing-type.config.knx.ip.readingPause.label = Reading Pause
|
|||||||
thing-type.config.knx.ip.readingPause.description = Time in milliseconds of how long should be paused between two read requests to the bus during initialization
|
thing-type.config.knx.ip.readingPause.description = Time in milliseconds of how long should be paused between two read requests to the bus during initialization
|
||||||
thing-type.config.knx.ip.responseTimeout.label = Response Timeout
|
thing-type.config.knx.ip.responseTimeout.label = Response Timeout
|
||||||
thing-type.config.knx.ip.responseTimeout.description = Seconds to wait for a response from the KNX bus
|
thing-type.config.knx.ip.responseTimeout.description = Seconds to wait for a response from the KNX bus
|
||||||
|
thing-type.config.knx.ip.routerBackboneKey.label = Router backbone key
|
||||||
|
thing-type.config.knx.ip.routerBackboneKey.description = Backbone key for secure router mode. 16 bytes in hex notation. Can also be found in ETS security report.
|
||||||
|
thing-type.config.knx.ip.tunnelDeviceAuthentication.label = Tunnel device authentication
|
||||||
|
thing-type.config.knx.ip.tunnelDeviceAuthentication.description = Tunnel device authentication for secure tunnel mode.
|
||||||
|
thing-type.config.knx.ip.tunnelUserId.label = Tunnel user id
|
||||||
|
thing-type.config.knx.ip.tunnelUserId.description = Tunnel user id for secure tunnel mode.
|
||||||
|
thing-type.config.knx.ip.tunnelUserPassword.label = Tunnel user password
|
||||||
|
thing-type.config.knx.ip.tunnelUserPassword.description = Tunnel user key for secure tunnel mode.
|
||||||
thing-type.config.knx.ip.type.label = IP Connection Type
|
thing-type.config.knx.ip.type.label = IP Connection Type
|
||||||
thing-type.config.knx.ip.type.description = The ip connection type for connecting to the KNX bus. Could be either TUNNEL or ROUTER
|
thing-type.config.knx.ip.type.description = The IP connection type for connecting to the KNX bus. Could be either TUNNEL, ROUTER, SECURETUNNEL, or SECUREROUTER
|
||||||
thing-type.config.knx.ip.type.option.TUNNEL = Tunnel
|
thing-type.config.knx.ip.type.option.TUNNEL = Tunnel
|
||||||
thing-type.config.knx.ip.type.option.ROUTER = Router
|
thing-type.config.knx.ip.type.option.ROUTER = Router
|
||||||
|
thing-type.config.knx.ip.type.option.SECURETUNNEL = Secure tunnel (experimental, use advanced options to configure)
|
||||||
|
thing-type.config.knx.ip.type.option.SECUREROUTER = Secure router (experimental, use advanced options to configure)
|
||||||
thing-type.config.knx.ip.useNAT.label = Use NAT
|
thing-type.config.knx.ip.useNAT.label = Use NAT
|
||||||
thing-type.config.knx.ip.useNAT.description = Set to "true" when having network address translation between this server and the gateway
|
thing-type.config.knx.ip.useNAT.description = Set to "true" when having network address translation between this server and the gateway
|
||||||
thing-type.config.knx.serial.autoReconnectPeriod.label = Auto Reconnect Period
|
thing-type.config.knx.serial.autoReconnectPeriod.label = Auto Reconnect Period
|
||||||
@ -134,3 +146,8 @@ channel-type.config.knx.rollershutter.upDown.label = Address
|
|||||||
channel-type.config.knx.rollershutter.upDown.description = The group address(es) in Group Address Notation to move the shutter in the DOWN or UP direction
|
channel-type.config.knx.rollershutter.upDown.description = The group address(es) in Group Address Notation to move the shutter in the DOWN or UP direction
|
||||||
channel-type.config.knx.single.ga.label = Address
|
channel-type.config.knx.single.ga.label = Address
|
||||||
channel-type.config.knx.single.ga.description = The group address(es) in Group Address Notation
|
channel-type.config.knx.single.ga.description = The group address(es) in Group Address Notation
|
||||||
|
|
||||||
|
# thing types config
|
||||||
|
|
||||||
|
thing-type.config.knx.serial.group.knxsecure.label = KNX secure
|
||||||
|
thing-type.config.knx.serial.group.knxsecure.description = Settings for KNX secure. Requires KNX secure features to be active in KNX installation.
|
||||||
|
|||||||
@ -9,12 +9,19 @@
|
|||||||
<description>This is a KNX IP interface or router</description>
|
<description>This is a KNX IP interface or router</description>
|
||||||
|
|
||||||
<config-description>
|
<config-description>
|
||||||
|
<parameter-group name="knxsecure">
|
||||||
|
<label>KNX secure</label>
|
||||||
|
<description>Settings for KNX secure. Optional. Requires KNX secure features to be active in KNX installation.</description>
|
||||||
|
</parameter-group>
|
||||||
<parameter name="type" type="text" required="true">
|
<parameter name="type" type="text" required="true">
|
||||||
<label>IP Connection Type</label>
|
<label>IP Connection Type</label>
|
||||||
<description>The ip connection type for connecting to the KNX bus. Could be either TUNNEL or ROUTER</description>
|
<description>The IP connection type for connecting to the KNX bus. Could be either TUNNEL, ROUTER, SECURETUNNEL, or
|
||||||
|
SECUREROUTER</description>
|
||||||
<options>
|
<options>
|
||||||
<option value="TUNNEL">Tunnel</option>
|
<option value="TUNNEL">Tunnel</option>
|
||||||
<option value="ROUTER">Router</option>
|
<option value="ROUTER">Router</option>
|
||||||
|
<option value="SECURETUNNEL">Secure tunnel (experimental, use advanced options to configure)</option>
|
||||||
|
<option value="SECUREROUTER">Secure router (experimental, use advanced options to configure)</option>
|
||||||
</options>
|
</options>
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="ipAddress" type="text">
|
<parameter name="ipAddress" type="text">
|
||||||
@ -64,6 +71,31 @@
|
|||||||
<description>Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s</description>
|
<description>Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s</description>
|
||||||
<default>60</default>
|
<default>60</default>
|
||||||
</parameter>
|
</parameter>
|
||||||
|
<parameter name="routerBackboneKey" type="text" groupName="knxsecure">
|
||||||
|
<context>password</context>
|
||||||
|
<label>Router backbone key</label>
|
||||||
|
<description>Backbone key for secure router mode. 16 bytes
|
||||||
|
in hex notation. Can also be found
|
||||||
|
in ETS security report.</description>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="tunnelUserId" type="text" groupName="knxsecure">
|
||||||
|
<label>Tunnel user id</label>
|
||||||
|
<description>Tunnel user id for secure tunnel mode.</description>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="tunnelUserPassword" type="text" groupName="knxsecure">
|
||||||
|
<context>password</context>
|
||||||
|
<label>Tunnel user password</label>
|
||||||
|
<description>Tunnel user key for secure tunnel mode.</description>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="tunnelDeviceAuthentication" type="text" groupName="knxsecure">
|
||||||
|
<context>password</context>
|
||||||
|
<label>Tunnel device authentication</label>
|
||||||
|
<description>Tunnel device authentication for secure tunnel mode.</description>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
</config-description>
|
</config-description>
|
||||||
</bridge-type>
|
</bridge-type>
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.knx.internal.handler;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openhab.core.net.NetworkAddressService;
|
||||||
|
import org.openhab.core.thing.Bridge;
|
||||||
|
|
||||||
|
import tuwien.auto.calimero.secure.KnxSecureException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Holger Friedrich - initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class KNXBridgeBaseThingHandlerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSecurityHelpers() {
|
||||||
|
// now check router settings:
|
||||||
|
String bbKeyHex = "D947B12DDECAD528B1D5A88FD347F284";
|
||||||
|
byte[] bbKeyParsedLower = KNXBridgeBaseThingHandler.secHelperParseBackboneKey(bbKeyHex.toLowerCase());
|
||||||
|
byte[] bbKeyParsedUpper = KNXBridgeBaseThingHandler.secHelperParseBackboneKey(bbKeyHex);
|
||||||
|
assertEquals(16, bbKeyParsedUpper.length);
|
||||||
|
assertArrayEquals(bbKeyParsedUpper, bbKeyParsedLower);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("null")
|
||||||
|
public void testInitializeSecurity() {
|
||||||
|
Bridge bridge = mock(Bridge.class);
|
||||||
|
NetworkAddressService nas = mock(NetworkAddressService.class);
|
||||||
|
IPBridgeThingHandler handler = new IPBridgeThingHandler(bridge, nas);
|
||||||
|
|
||||||
|
// no config given
|
||||||
|
assertFalse(handler.initializeSecurity("", "", "", ""));
|
||||||
|
|
||||||
|
// router password configured, length must be 16 bytes in hex notation
|
||||||
|
assertTrue(handler.initializeSecurity("D947B12DDECAD528B1D5A88FD347F284", "", "", ""));
|
||||||
|
assertTrue(handler.initializeSecurity("0xD947B12DDECAD528B1D5A88FD347F284", "", "", ""));
|
||||||
|
assertThrows(KnxSecureException.class, () -> {
|
||||||
|
handler.initializeSecurity("wrongLength", "", "", "");
|
||||||
|
});
|
||||||
|
|
||||||
|
// tunnel configuration
|
||||||
|
assertTrue(handler.initializeSecurity("", "da", "1", "pw"));
|
||||||
|
// cTunnelUser is restricted to a number >0
|
||||||
|
assertThrows(KnxSecureException.class, () -> {
|
||||||
|
handler.initializeSecurity("", "da", "0", "pw");
|
||||||
|
});
|
||||||
|
assertThrows(KnxSecureException.class, () -> {
|
||||||
|
handler.initializeSecurity("", "da", "eins", "pw");
|
||||||
|
});
|
||||||
|
// at least one setting for tunnel is given, count as try to configure secure tunnel
|
||||||
|
// plausibility is checked during initialize()
|
||||||
|
assertTrue(handler.initializeSecurity("", "da", "", ""));
|
||||||
|
assertTrue(handler.initializeSecurity("", "", "1", ""));
|
||||||
|
assertTrue(handler.initializeSecurity("", "", "", "pw"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user