mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[serial] Add TCP and binary protocol support (#16205)
* Added code to convert binary data into a hexa-decimal 'charset' Signed-off-by: Roland Tapken <dev@cybso.de>
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
# Serial Binding
|
||||
|
||||
The Serial binding allows openHAB to communicate over serial ports attached to the openHAB server.
|
||||
The Serial binding allows openHAB to communicate over serial ports attached to the openHAB server and TCP sockets.
|
||||
|
||||
The binding allows data to be sent and received from a serial port.
|
||||
The binding allows data to be sent and received from a serial port or TCP socket.
|
||||
The binding does not support any particular serial protocols and simply reads what is available and sends what is provided.
|
||||
|
||||
The binding can be used to communicate with simple serial devices for which a dedicated openHAB binding does not exist.
|
||||
|
||||
## Overview
|
||||
|
||||
The Serial binding represents a serial port as a bridge thing and data matching defined patterns as things connected to the bridge.
|
||||
The Serial binding represents a serial port as a bridge thing, a TCP socket as an alternative bridge thing, and data matching defined patterns as things connected to the bridge.
|
||||
|
||||
### Serial Bridge
|
||||
|
||||
@@ -18,12 +18,25 @@ A Serial Bridge thing (`serialBridge`) represents a single serial port.
|
||||
The bridge supports a String channel which is set to the currently received data from the serial port.
|
||||
Sending a command to this channel sends the command as a string to the serial port.
|
||||
|
||||
To communicate with protocols requiring binary data it is possible to select a special 'HEX' charset.
|
||||
This results into all binary data being converted into space-separacted hexadecimal strings that can be parsed using regular expressions.
|
||||
In this mode, the input data is also expected to be encoded as hexadecimal characters.
|
||||
|
||||
The bridge also supports a String channel which encodes the received data as the string representation of a RawType to handle data that is
|
||||
not supported by the REST interface.
|
||||
A command sent to this channel will only be sent to the serial port if it is encoded as the string representation of a RawType.
|
||||
|
||||
A trigger channel is also provided which triggers when data is received.
|
||||
|
||||
### TCP Bridge
|
||||
|
||||
The TCP bridge is an alternative implementation of the Serial Bridge that uses TCP sockets instead of serial ports.
|
||||
|
||||
### TCP Server bridge
|
||||
|
||||
The TCP Server Bridge is similar to the _TCP Bridge_ but waits for an incoming connection on a given port.
|
||||
Currently, only one connection per bridge is allowed.
|
||||
|
||||
### Serial Device
|
||||
|
||||
A Serial Device thing (`serialDevice`) can be used to represent data matching a defined pattern as a device.
|
||||
@@ -44,14 +57,38 @@ When using a Serial Device the expectation is that the received data for each de
|
||||
|
||||
The configuration for the `serialBridge` consists of the following parameters:
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------- | --------------------------------------------------------------------------------------- |
|
||||
| serialPort | The serial port to use (e.g. Linux: /dev/ttyUSB0, Windows: COM1) (mandatory) |
|
||||
| baudRate | Set the baud rate. Valid values: 4800, 9600, 19200, 38400, 57600, 115200 (default 9600) |
|
||||
| dataBits | Set the data bits. Valid values: 5, 6, 7, 8 (default 8) |
|
||||
| parity | Set the parity. Valid values: N(one), O(dd), E(even), M(ark), S(pace) (default N) |
|
||||
| stopBits | Set the stop bits. Valid values: 1, 1.5, 2 (default 1) |
|
||||
| charset | The charset to use for converting between bytes and string (e.g. UTF-8,ISO-8859-1) |
|
||||
| Parameter | Description |
|
||||
|------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| serialPort | The serial port to use (e.g. Linux: /dev/ttyUSB0, Windows: COM1) (mandatory) |
|
||||
| baudRate | Set the baud rate. Valid values: 4800, 9600, 19200, 38400, 57600, 115200 (default 9600) |
|
||||
| dataBits | Set the data bits. Valid values: 5, 6, 7, 8 (default 8) |
|
||||
| parity | Set the parity. Valid values: N(one), O(dd), E(even), M(ark), S(pace) (default N) |
|
||||
| stopBits | Set the stop bits. Valid values: 1, 1.5, 2 (default 1) |
|
||||
| charset | The charset to use for converting between bytes and string (e.g. UTF-8,ISO-8859-1). Enter 'HEX' to convert binary data into hexadecimal strings separated by space. |
|
||||
| eolPattern | In charset=HEX mode, a regular expression is required to match the binaries equivalent of an 'End of line' character. For example, '\bFF' would match a byte value of 255 as end of the current response. |
|
||||
|
||||
The configuration for the `tcpBridge` consists of the following parameters:
|
||||
|
||||
| Parameter | Description |
|
||||
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| address | The IP or hostname to connect to. |
|
||||
| port | The number of the TCP port to connect to |
|
||||
| timeout | Socket timeout in seconds (0 = no timeout) |
|
||||
| keepAlive | Enable socket keep-alive |
|
||||
| reconnectInterval | Interval in seconds for automatic reconnect after connection failure (defaults to 10 seconds) |
|
||||
| charset | The charset to use for converting between bytes and string (e.g. UTF-8,ISO-8859-1). Enter 'HEX' to convert binary data into hexadecimal strings separated by space. |
|
||||
| eolPattern | In charset=HEX mode, a regular expression is required to match the binaries equivalent of an 'End of line' character. For example, '\bFF' would match a byte value of 255 as end of the current response. |
|
||||
|
||||
The configuration for the `tcpServerBridge` consists of the following parameters:
|
||||
|
||||
| Parameter | Description |
|
||||
|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| port | The number of the TCP port to listen to |
|
||||
| bindAddress | The IP to bind to. |
|
||||
| timeout | Socket timeout in seconds (0 = no timeout) |
|
||||
| keepAlive | Enable socket keep-alive |
|
||||
| charset | The charset to use for converting between bytes and string (e.g. UTF-8,ISO-8859-1). Enter 'HEX' to convert binary data into hexadecimal strings separated by space. |
|
||||
| eolPattern | In charset=HEX mode, a regular expression is required to match the binaries equivalent of an 'End of line' character. For example, '\bFF' would match a byte value of 255 as end of the current response. |
|
||||
|
||||
The configuration for the `serialDevice` consists of the following parameters:
|
||||
|
||||
@@ -79,20 +116,22 @@ The channels supported by the `serialDevice` are:
|
||||
| `switch` | Switch | Channel for receiving commands from a Switch. The channel can be configured to apply a transform on the received data to convert to the channel state. The channel can be configured to apply a simple mapping for the ON and OFF commands. |
|
||||
| `rollershutter` | Rollershutter | Channel for receiving commands from a Rollershutter. The channel can be configured to apply a transform on the received data to convert to the channel state. The channel can be configured to apply a simple mapping for the UP, DOWN and STOP commands. |
|
||||
|
||||
The configuration for the `serialBridge` channels consists of the following parameters:
|
||||
The configuration for the `serialDevice` channels consists of the following parameters:
|
||||
|
||||
| Parameter | Description | Supported Channels |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `stateTransformation` | One or more transformation (concatenated with `∩`) used to convert device data to channel state, e.g. `REGEX(.*?STATE=(.*?);.*)` | string, number, dimmer, switch, rollershutter |
|
||||
| `commandTransformation` | One or more transformation (concatenated with `∩`) used to convert command to device data, e.g. `JS(device.js)` | string, number, dimmer, switch, rollershutter |
|
||||
| `commandFormat` | Format string applied to the command before transform, e.g. `ID=671;COMMAND=%s` | string, number, dimmer, rollershutter |
|
||||
| `onValue` | Send this value when receiving an ON command | switch, dimmer |
|
||||
| `offValue` | Send this value when receiving an OFF command | switch, dimmer |
|
||||
| `increaseValue` | Send this value when receiving an INCREASE command | dimmer |
|
||||
| `decreaseValue` | Send this value when receiving a DECREASE command | dimmer |
|
||||
| `upValue` | Send this value when receiving an UP command | rollershutter |
|
||||
| `downValue` | Send this value when receiving a DOWN command | rollershutter |
|
||||
| `stopValue` | Send this value when receiving a STOP command | rollershutter |
|
||||
| Parameter | Description | Supported Channels |
|
||||
| ----------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------| --------------------------------------------- |
|
||||
| `stateTransformation` | One or more transformation (concatenated with `∩`) used to convert device data to channel state, e.g. `REGEX(.*?STATE=(.*?);.*)` | string, number, dimmer, switch, rollershutter |
|
||||
| `commandTransformation` | One or more transformation (concatenated with `∩`) used to convert command to device data, e.g. `JS(device.js)` | string, number, dimmer, switch, rollershutter |
|
||||
| `commandFormat` | Format string applied to the command before transform, e.g. `ID=671;COMMAND=%s` | string, number, dimmer, rollershutter |
|
||||
| `onValue` | Send this value when receiving an ON command | switch, dimmer |
|
||||
| `offValue` | Send this value when receiving an OFF command | switch, dimmer |
|
||||
| `increaseValue` | Send this value when receiving an INCREASE command | dimmer |
|
||||
| `decreaseValue` | Send this value when receiving a DECREASE command | dimmer |
|
||||
| `upValue` | Send this value when receiving an UP command | rollershutter |
|
||||
| `downValue` | Send this value when receiving a DOWN command | rollershutter |
|
||||
| `stopValue` | Send this value when receiving a STOP command | rollershutter |
|
||||
| `refreshCommand` | Command that should be issued to receive the current channel state | string, number, dimmer, switch, rollershutter |
|
||||
| `refreshInterval` | A value greater than the default of 0 will trigger a periodical "Refresh Command" for services that do not automatically push changed values to the client | string, number, dimmer, switch, rollershutter |
|
||||
|
||||
Transformations can be chained in the UI by listing each transformation on a separate line, or by separating them with the mathematical intersection character "∩".
|
||||
Transformations are defined using this syntax: `TYPE(FUNCTION)`, e.g.: `JSONPATH($.path)`.
|
||||
|
||||
+3
-1
@@ -27,7 +27,9 @@ public class SerialBindingConstants {
|
||||
private static final String BINDING_ID = "serial";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "serialBridge");
|
||||
public static final ThingTypeUID THING_TYPE_SERIAL_BRIDGE = new ThingTypeUID(BINDING_ID, "serialBridge");
|
||||
public static final ThingTypeUID THING_TYPE_TCP_BRIDGE = new ThingTypeUID(BINDING_ID, "tcpBridge");
|
||||
public static final ThingTypeUID THING_TYPE_TCP_SERVER_BRIDGE = new ThingTypeUID(BINDING_ID, "tcpServerBridge");
|
||||
public static final ThingTypeUID THING_TYPE_DEVICE = new ThingTypeUID(BINDING_ID, "serialDevice");
|
||||
|
||||
// List of all Channel ids
|
||||
|
||||
+10
-4
@@ -12,8 +12,7 @@
|
||||
*/
|
||||
package org.openhab.binding.serial.internal;
|
||||
|
||||
import static org.openhab.binding.serial.internal.SerialBindingConstants.THING_TYPE_BRIDGE;
|
||||
import static org.openhab.binding.serial.internal.SerialBindingConstants.THING_TYPE_DEVICE;
|
||||
import static org.openhab.binding.serial.internal.SerialBindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@@ -21,6 +20,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.serial.internal.handler.SerialBridgeHandler;
|
||||
import org.openhab.binding.serial.internal.handler.SerialDeviceHandler;
|
||||
import org.openhab.binding.serial.internal.handler.TcpBridgeHandler;
|
||||
import org.openhab.binding.serial.internal.handler.TcpServerBridgeHandler;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
@@ -42,7 +43,8 @@ import org.osgi.service.component.annotations.Reference;
|
||||
@Component(configurationPid = "binding.serial", service = ThingHandlerFactory.class)
|
||||
public class SerialHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, THING_TYPE_DEVICE);
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SERIAL_BRIDGE,
|
||||
THING_TYPE_TCP_BRIDGE, THING_TYPE_TCP_SERVER_BRIDGE, THING_TYPE_DEVICE);
|
||||
|
||||
private final SerialPortManager serialPortManager;
|
||||
|
||||
@@ -60,8 +62,12 @@ public class SerialHandlerFactory extends BaseThingHandlerFactory {
|
||||
protected @Nullable ThingHandler createHandler(final Thing thing) {
|
||||
final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
|
||||
if (THING_TYPE_SERIAL_BRIDGE.equals(thingTypeUID)) {
|
||||
return new SerialBridgeHandler((Bridge) thing, serialPortManager);
|
||||
} else if (THING_TYPE_TCP_BRIDGE.equals(thingTypeUID)) {
|
||||
return new TcpBridgeHandler((Bridge) thing);
|
||||
} else if (THING_TYPE_TCP_SERVER_BRIDGE.equals(thingTypeUID)) {
|
||||
return new TcpServerBridgeHandler((Bridge) thing);
|
||||
} else if (THING_TYPE_DEVICE.equals(thingTypeUID)) {
|
||||
return new SerialDeviceHandler(thing);
|
||||
}
|
||||
|
||||
+16
@@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||
* Class describing the channel user configuration
|
||||
*
|
||||
* @author Mike Major - Initial contribution
|
||||
* @author Roland Tapken - Added refreshValue and refreshInterval
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChannelConfig {
|
||||
@@ -73,4 +74,19 @@ public class ChannelConfig {
|
||||
* Decrease value
|
||||
*/
|
||||
public @Nullable String decreaseValue;
|
||||
|
||||
/**
|
||||
* Command for refesh command
|
||||
*/
|
||||
public @Nullable String refreshValue;
|
||||
|
||||
/**
|
||||
* Automatic refresh interval.
|
||||
*
|
||||
* This value is only required if the peer has to send the “Refresh Value”
|
||||
* command regularly in order to return the current properties. It is not
|
||||
* required if the peer automatically forwards changed values to its
|
||||
* clients.
|
||||
*/
|
||||
public int refreshInterval = 0;
|
||||
}
|
||||
|
||||
+14
-1
@@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory;
|
||||
* the ability to transform the device data into the channel state.
|
||||
*
|
||||
* @author Mike Major - Initial contribution
|
||||
* @author Roland Tapken - Added refresh value and refresh interval
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class DeviceChannel {
|
||||
@@ -74,7 +75,7 @@ public abstract class DeviceChannel {
|
||||
* @param data the command to transform
|
||||
* @return the transformed data if the transform produced a result.
|
||||
*/
|
||||
protected Optional<String> transformCommand(final String data) {
|
||||
public Optional<String> transformCommand(final String data) {
|
||||
return commandTransform.apply(data);
|
||||
}
|
||||
|
||||
@@ -102,4 +103,16 @@ public abstract class DeviceChannel {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public String getRefreshValue() {
|
||||
String command = config.refreshValue;
|
||||
if (command == null) {
|
||||
return "";
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
public int getRefreshInterval() {
|
||||
return config.refreshInterval;
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -32,7 +32,6 @@ public class DeviceChannelFactory {
|
||||
/**
|
||||
* Create a {@link DeviceChannel} for the channel type
|
||||
*
|
||||
* @param valueTransformationProvider the transformation provider
|
||||
* @param channelConfig the channel configuration
|
||||
* @param channelTypeID the channel type id
|
||||
* @return the DeviceChannel or null if the channel type is not supported.
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.serial.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Class describing the serial bridge user configuration
|
||||
*
|
||||
* @author Roland Tapken - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class CommonBridgeConfiguration {
|
||||
|
||||
/**
|
||||
* Charset
|
||||
*/
|
||||
public @Nullable String charset;
|
||||
|
||||
/**
|
||||
* EOL regex for charset = HEX
|
||||
*/
|
||||
public @Nullable String eolPattern;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CommonBridgeConfiguration [charset=" + charset + ", eolPattern=" + eolPattern + "]";
|
||||
}
|
||||
}
|
||||
+347
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.serial.internal.handler;
|
||||
|
||||
import static org.openhab.binding.serial.internal.SerialBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.IllegalCharsetNameException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.charset.UnsupportedCharsetException;
|
||||
import java.util.Base64;
|
||||
import java.util.HexFormat;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.RawType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.*;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link CommonBridgeHandler} is responsible for handling commands, which
|
||||
* are sent to one of the channels.
|
||||
*
|
||||
* @author Mike Major - Initial contribution
|
||||
* @author Roland Tapken - Added code for charset=HEX and channel refresh
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class CommonBridgeHandler extends BaseBridgeHandler {
|
||||
|
||||
protected final Logger logger = LoggerFactory.getLogger(CommonBridgeHandler.class);
|
||||
|
||||
protected CommonBridgeConfiguration config = new CommonBridgeConfiguration();
|
||||
|
||||
protected @Nullable InputStream inputStream;
|
||||
protected @Nullable OutputStream outputStream;
|
||||
|
||||
private Charset charset = StandardCharsets.UTF_8;
|
||||
|
||||
private boolean binaryHexData = false;
|
||||
|
||||
private @Nullable Pattern eolPattern;
|
||||
|
||||
private @Nullable String lastValue;
|
||||
|
||||
protected final AtomicBoolean readerActive = new AtomicBoolean(false);
|
||||
|
||||
@Nullable
|
||||
private ScheduledFuture<?> reader;
|
||||
|
||||
public CommonBridgeHandler(final Bridge bridge) {
|
||||
super(bridge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final ChannelUID channelUID, final Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
final String lastValue = this.lastValue;
|
||||
|
||||
if (lastValue != null) {
|
||||
refresh(channelUID.getId(), lastValue);
|
||||
}
|
||||
} else {
|
||||
switch (channelUID.getId()) {
|
||||
case STRING_CHANNEL -> writeString(command.toFullString(), false);
|
||||
case BINARY_CHANNEL -> writeString(command.toFullString(), true);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkAndProcessConfiguration(CommonBridgeConfiguration config) {
|
||||
this.config = config;
|
||||
|
||||
try {
|
||||
String strCharset = config.charset;
|
||||
if (strCharset != null) {
|
||||
if ("hex".equalsIgnoreCase(strCharset)) {
|
||||
binaryHexData = true;
|
||||
logger.debug("{} converting to hex", getLogPrefix());
|
||||
} else {
|
||||
binaryHexData = false;
|
||||
Charset charset = Charset.forName(strCharset);
|
||||
logger.debug("{} charset '{}' set", getLogPrefix(), charset);
|
||||
this.charset = charset;
|
||||
}
|
||||
}
|
||||
} catch (final IllegalCharsetNameException | UnsupportedCharsetException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Invalid charset");
|
||||
return false;
|
||||
}
|
||||
|
||||
String eolPatternStr = config.eolPattern;
|
||||
this.eolPattern = null;
|
||||
if (eolPatternStr != null && !eolPatternStr.isBlank()) {
|
||||
try {
|
||||
Pattern eolPattern = Pattern.compile(eolPatternStr, Pattern.CASE_INSENSITIVE);
|
||||
this.eolPattern = eolPattern;
|
||||
logger.debug("{} eolPattern '{}' set", getLogPrefix(), eolPattern);
|
||||
} catch (IllegalArgumentException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
|
||||
"Invalid EOL sequence");
|
||||
return false;
|
||||
}
|
||||
} else if (binaryHexData) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
|
||||
"EOL pattern required for charset = HEX");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void disposeReader() {
|
||||
final InputStream inputStream = this.inputStream;
|
||||
this.inputStream = null;
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (final IOException e) {
|
||||
logger.debug("Error while closing the input stream: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
final OutputStream outputStream = this.outputStream;
|
||||
this.outputStream = null;
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (final IOException e) {
|
||||
logger.debug("Error while closing the output stream: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
readerActive.set(false);
|
||||
final ScheduledFuture<?> reader = this.reader;
|
||||
this.reader = null;
|
||||
if (reader != null) {
|
||||
reader.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
disposeReader();
|
||||
lastValue = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a string to the serial port.
|
||||
*
|
||||
* @param string the string to send
|
||||
*/
|
||||
public void writeString(final String string) {
|
||||
writeString(string, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the channel with the last received data
|
||||
*
|
||||
* @param channelUID the channel to refresh
|
||||
* @param data the data to use
|
||||
*/
|
||||
protected void refresh(final String channelUID, final String data) {
|
||||
if (!isLinked(channelUID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channelUID) {
|
||||
case STRING_CHANNEL -> updateState(channelUID, new StringType(data));
|
||||
case BINARY_CHANNEL -> {
|
||||
String sb = "data:" + RawType.DEFAULT_MIME_TYPE + ";base64,"
|
||||
+ Base64.getEncoder().encodeToString(data.getBytes(charset));
|
||||
updateState(channelUID, new StringType(sb));
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void receiveAndProcessNow() {
|
||||
if (readerActive.compareAndSet(false, true)) {
|
||||
reader = scheduler.schedule(() -> receiveAndProcess(new StringBuilder(), true), 0, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from the serial port and process the data
|
||||
*
|
||||
* @param sb the string builder to receive the data
|
||||
* @param firstAttempt indicates if this is the first read attempt without waiting
|
||||
*/
|
||||
protected void receiveAndProcess(final StringBuilder sb, final boolean firstAttempt) {
|
||||
final InputStream inputStream = this.inputStream;
|
||||
|
||||
if (inputStream == null) {
|
||||
readerActive.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
synchronized (inputStream) {
|
||||
if (firstAttempt || inputStream.available() > 0) {
|
||||
final byte[] readBuffer = new byte[20];
|
||||
|
||||
String line = "";
|
||||
boolean binaryHexData = this.binaryHexData;
|
||||
Pattern eolPattern = this.eolPattern;
|
||||
Charset charset = this.charset;
|
||||
|
||||
// read data from serial device
|
||||
while (inputStream.available() > 0) {
|
||||
final int bytes = inputStream.read(readBuffer);
|
||||
if (binaryHexData) {
|
||||
for (int i = 0; i < bytes; i++) {
|
||||
if (eolPattern == null) {
|
||||
// Should not happen, but the code actually allows this to happen,
|
||||
// so just make the compiler happy and suppress any warnings.
|
||||
throw new IOException(
|
||||
"Failed to parse input stream as HEX pattern: Parameter 'eolPattern' is null.");
|
||||
}
|
||||
|
||||
line += String.format("%02X", readBuffer[i]);
|
||||
if (eolPattern.matcher(line).find()) {
|
||||
sb.append(line).append(System.lineSeparator());
|
||||
line = "";
|
||||
} else {
|
||||
line += " ";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sb.append(new String(readBuffer, 0, bytes, charset));
|
||||
}
|
||||
}
|
||||
line = line.trim();
|
||||
if (!line.isEmpty()) {
|
||||
sb.append(line);
|
||||
}
|
||||
|
||||
// Add wait states around reading the stream, so that interrupted transmissions
|
||||
// are merged
|
||||
if (readerActive.get()) {
|
||||
reader = scheduler.schedule(() -> receiveAndProcess(sb, false), 100, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
} else {
|
||||
final String result = sb.toString();
|
||||
|
||||
processInput(result);
|
||||
result.lines().forEach(l -> getThing().getThings().forEach(t -> {
|
||||
final SerialDeviceHandler device = (SerialDeviceHandler) t.getHandler();
|
||||
if (device != null) {
|
||||
device.handleData(l);
|
||||
}
|
||||
}));
|
||||
|
||||
lastValue = result;
|
||||
|
||||
if (readerActive.compareAndSet(true, false)) {
|
||||
// Check we haven't received more data while processing
|
||||
if (inputStream.available() > 0 && readerActive.compareAndSet(false, true)) {
|
||||
reader = scheduler.schedule(() -> receiveAndProcess(new StringBuilder(), true), 0,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
logger.debug("Error reading from serial port: {}", e.getMessage(), e);
|
||||
readerActive.set(false);
|
||||
handleIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void processInput(String result) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a string to the serial port.
|
||||
*
|
||||
* @param string the string to send
|
||||
* @param isRawType the string should be handled as a RawType
|
||||
*/
|
||||
private void writeString(final String string, final boolean isRawType) {
|
||||
final OutputStream outputStream = this.outputStream;
|
||||
|
||||
if (outputStream == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Writing '{}' to {}", string, getLogPrefix());
|
||||
try {
|
||||
synchronized (outputStream) {
|
||||
// write string to serial port
|
||||
if (isRawType) {
|
||||
final RawType rt = RawType.valueOf(string);
|
||||
outputStream.write(rt.getBytes());
|
||||
} else if (binaryHexData) {
|
||||
outputStream.write(parseHexString(string));
|
||||
} else {
|
||||
outputStream.write(string.getBytes(charset));
|
||||
}
|
||||
|
||||
outputStream.flush();
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
logger.warn("Error writing '{}' to {}: {}", string, getLogPrefix(), e.getMessage());
|
||||
handleIOException(e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Error writing '{}' to {}: {}", string, getLogPrefix(), e.getMessage());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] parseHexString(String input) {
|
||||
input = input.replaceAll("\\s", "");
|
||||
return HexFormat.of().parseHex(input);
|
||||
}
|
||||
|
||||
protected void handleIOException(IOException e) {
|
||||
// Only used in TcpBridgeHandler
|
||||
}
|
||||
|
||||
protected abstract String getLogPrefix();
|
||||
}
|
||||
+3
-7
@@ -21,7 +21,7 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||
* @author Mike Major - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SerialBridgeConfiguration {
|
||||
public class SerialBridgeConfiguration extends CommonBridgeConfiguration {
|
||||
/**
|
||||
* Serial port name
|
||||
*/
|
||||
@@ -47,14 +47,10 @@ public class SerialBridgeConfiguration {
|
||||
*/
|
||||
public String stopBits = "1";
|
||||
|
||||
/**
|
||||
* Charset
|
||||
*/
|
||||
public @Nullable String charset;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SerialBridgeConfiguration [serialPort=" + serialPort + ", Baudrate=" + baudRate + ", Databits="
|
||||
+ dataBits + ", Parity=" + parity + ", Stopbits=" + stopBits + ", charset=" + charset + "]";
|
||||
+ dataBits + ", Parity=" + parity + ", Stopbits=" + stopBits + ", charset=" + charset + ", eolPattern="
|
||||
+ eolPattern + "]";
|
||||
}
|
||||
}
|
||||
|
||||
+23
-219
@@ -12,21 +12,10 @@
|
||||
*/
|
||||
package org.openhab.binding.serial.internal.handler;
|
||||
|
||||
import static org.openhab.binding.serial.internal.SerialBindingConstants.BINARY_CHANNEL;
|
||||
import static org.openhab.binding.serial.internal.SerialBindingConstants.STRING_CHANNEL;
|
||||
import static org.openhab.binding.serial.internal.SerialBindingConstants.TRIGGER_CHANNEL;
|
||||
import static org.openhab.binding.serial.internal.SerialBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.IllegalCharsetNameException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.TooManyListenersException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@@ -39,84 +28,36 @@ import org.openhab.core.io.transport.serial.SerialPortEventListener;
|
||||
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
|
||||
import org.openhab.core.library.types.RawType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.CommonTriggerEvents;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link SerialBridgeHandler} is responsible for handling commands, which
|
||||
* are sent to one of the channels.
|
||||
*
|
||||
* @author Mike Major - Initial contribution
|
||||
* @author Roland Tapken - Refactored common code into CommonBridgeHandler
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SerialBridgeHandler extends BaseBridgeHandler implements SerialPortEventListener {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SerialBridgeHandler.class);
|
||||
public class SerialBridgeHandler extends CommonBridgeHandler implements SerialPortEventListener {
|
||||
|
||||
private SerialBridgeConfiguration config = new SerialBridgeConfiguration();
|
||||
|
||||
private final SerialPortManager serialPortManager;
|
||||
|
||||
private @Nullable SerialPort serialPort;
|
||||
|
||||
private @Nullable InputStream inputStream;
|
||||
private @Nullable OutputStream outputStream;
|
||||
|
||||
private Charset charset = StandardCharsets.UTF_8;
|
||||
|
||||
private @Nullable String lastValue;
|
||||
|
||||
private final AtomicBoolean readerActive = new AtomicBoolean(false);
|
||||
private @Nullable ScheduledFuture<?> reader;
|
||||
|
||||
public SerialBridgeHandler(final Bridge bridge, final SerialPortManager serialPortManager) {
|
||||
super(bridge);
|
||||
this.serialPortManager = serialPortManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final ChannelUID channelUID, final Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
final String lastValue = this.lastValue;
|
||||
|
||||
if (lastValue != null) {
|
||||
refresh(channelUID.getId(), lastValue);
|
||||
}
|
||||
} else {
|
||||
switch (channelUID.getId()) {
|
||||
case STRING_CHANNEL:
|
||||
writeString(command.toFullString(), false);
|
||||
break;
|
||||
case BINARY_CHANNEL:
|
||||
writeString(command.toFullString(), true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(SerialBridgeConfiguration.class);
|
||||
|
||||
try {
|
||||
if (config.charset != null) {
|
||||
charset = Charset.forName(config.charset);
|
||||
}
|
||||
logger.debug("Serial port '{}' charset '{}' set", config.serialPort, charset);
|
||||
} catch (final IllegalCharsetNameException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Invalid charset");
|
||||
if (!checkAndProcessConfiguration(config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -163,6 +104,19 @@ public class SerialBridgeHandler extends BaseBridgeHandler implements SerialPort
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processInput(String result) {
|
||||
if (isLinked(TRIGGER_CHANNEL)) {
|
||||
triggerChannel(TRIGGER_CHANNEL, CommonTriggerEvents.PRESSED);
|
||||
}
|
||||
if (isLinked(STRING_CHANNEL)) {
|
||||
refresh(STRING_CHANNEL, result);
|
||||
}
|
||||
if (isLinked(BINARY_CHANNEL)) {
|
||||
refresh(BINARY_CHANNEL, result);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
final SerialPort serialPort = this.serialPort;
|
||||
@@ -172,172 +126,22 @@ public class SerialBridgeHandler extends BaseBridgeHandler implements SerialPort
|
||||
this.serialPort = null;
|
||||
}
|
||||
|
||||
final InputStream inputStream = this.inputStream;
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (final IOException e) {
|
||||
logger.debug("Error while closing the input stream: {}", e.getMessage());
|
||||
}
|
||||
this.inputStream = null;
|
||||
}
|
||||
|
||||
final OutputStream outputStream = this.outputStream;
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (final IOException e) {
|
||||
logger.debug("Error while closing the output stream: {}", e.getMessage());
|
||||
}
|
||||
this.outputStream = null;
|
||||
}
|
||||
|
||||
readerActive.set(false);
|
||||
final ScheduledFuture<?> reader = this.reader;
|
||||
if (reader != null) {
|
||||
reader.cancel(false);
|
||||
this.reader = null;
|
||||
}
|
||||
|
||||
lastValue = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialEvent(final SerialPortEvent event) {
|
||||
switch (event.getEventType()) {
|
||||
case SerialPortEvent.DATA_AVAILABLE:
|
||||
if (readerActive.compareAndSet(false, true)) {
|
||||
reader = scheduler.schedule(() -> receiveAndProcess(new StringBuilder(), true), 0,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
receiveAndProcessNow();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a string to the serial port.
|
||||
*
|
||||
* @param string the string to send
|
||||
*/
|
||||
public void writeString(final String string) {
|
||||
writeString(string, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the channel with the last received data
|
||||
*
|
||||
* @param channelId the channel to refresh
|
||||
* @param channelId the data to use
|
||||
*/
|
||||
private void refresh(final String channelId, final String data) {
|
||||
if (!isLinked(channelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channelId) {
|
||||
case STRING_CHANNEL:
|
||||
updateState(channelId, new StringType(data));
|
||||
break;
|
||||
case BINARY_CHANNEL:
|
||||
final StringBuilder sb = new StringBuilder("data:");
|
||||
sb.append(RawType.DEFAULT_MIME_TYPE).append(";base64,")
|
||||
.append(Base64.getEncoder().encodeToString(data.getBytes(charset)));
|
||||
updateState(channelId, new StringType(sb.toString()));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from the serial port and process the data
|
||||
*
|
||||
* @param sb the string builder to receive the data
|
||||
* @param firstAttempt indicates if this is the first read attempt without waiting
|
||||
*/
|
||||
private void receiveAndProcess(final StringBuilder sb, final boolean firstAttempt) {
|
||||
final InputStream inputStream = this.inputStream;
|
||||
|
||||
if (inputStream == null) {
|
||||
readerActive.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (firstAttempt || inputStream.available() > 0) {
|
||||
final byte[] readBuffer = new byte[20];
|
||||
|
||||
// read data from serial device
|
||||
while (inputStream.available() > 0) {
|
||||
final int bytes = inputStream.read(readBuffer);
|
||||
sb.append(new String(readBuffer, 0, bytes, charset));
|
||||
}
|
||||
|
||||
// Add wait states around reading the stream, so that interrupted transmissions
|
||||
// are merged
|
||||
if (readerActive.get()) {
|
||||
reader = scheduler.schedule(() -> receiveAndProcess(sb, false), 100, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
} else {
|
||||
final String result = sb.toString();
|
||||
|
||||
triggerChannel(TRIGGER_CHANNEL, CommonTriggerEvents.PRESSED);
|
||||
refresh(STRING_CHANNEL, result);
|
||||
refresh(BINARY_CHANNEL, result);
|
||||
|
||||
result.lines().forEach(l -> getThing().getThings().forEach(t -> {
|
||||
final SerialDeviceHandler device = (SerialDeviceHandler) t.getHandler();
|
||||
if (device != null) {
|
||||
device.handleData(l);
|
||||
}
|
||||
}));
|
||||
|
||||
lastValue = result;
|
||||
|
||||
if (readerActive.compareAndSet(true, false)) {
|
||||
// Check we haven't received more data while processing
|
||||
if (inputStream.available() > 0 && readerActive.compareAndSet(false, true)) {
|
||||
reader = scheduler.schedule(() -> receiveAndProcess(new StringBuilder(), true), 0,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
logger.debug("Error reading from serial port: {}", e.getMessage(), e);
|
||||
readerActive.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a string to the serial port.
|
||||
*
|
||||
* @param string the string to send
|
||||
* @param isRawType the string should be handled as a RawType
|
||||
*/
|
||||
private void writeString(final String string, final boolean isRawType) {
|
||||
final OutputStream outputStream = this.outputStream;
|
||||
|
||||
if (outputStream == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Writing '{}' to serial port {}", string, config.serialPort);
|
||||
|
||||
try {
|
||||
// write string to serial port
|
||||
if (isRawType) {
|
||||
final RawType rt = RawType.valueOf(string);
|
||||
outputStream.write(rt.getBytes());
|
||||
} else {
|
||||
outputStream.write(string.getBytes(charset));
|
||||
}
|
||||
|
||||
outputStream.flush();
|
||||
} catch (final IOException | IllegalArgumentException e) {
|
||||
logger.warn("Error writing '{}' to serial port {}: {}", string, config.serialPort, e.getMessage());
|
||||
}
|
||||
@Override
|
||||
protected String getLogPrefix() {
|
||||
return String.format("Serial port '%s'", config.serialPort);
|
||||
}
|
||||
}
|
||||
|
||||
+44
-17
@@ -14,6 +14,9 @@ package org.openhab.binding.serial.internal.handler;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
@@ -40,6 +43,7 @@ import org.openhab.core.types.RefreshType;
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Mike Major - Initial contribution
|
||||
* @author Roland Tapken - Added channel refresh
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SerialDeviceHandler extends BaseThingHandler {
|
||||
@@ -50,6 +54,8 @@ public class SerialDeviceHandler extends BaseThingHandler {
|
||||
|
||||
private final Map<ChannelUID, DeviceChannel> channels = new HashMap<>();
|
||||
|
||||
private final Map<ChannelUID, ScheduledFuture<?>> futures = new HashMap<>();
|
||||
|
||||
public SerialDeviceHandler(final Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
@@ -57,23 +63,16 @@ public class SerialDeviceHandler extends BaseThingHandler {
|
||||
@Override
|
||||
public void handleCommand(final ChannelUID channelUID, final Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
final String lastValue = this.lastValue;
|
||||
|
||||
if (lastValue != null) {
|
||||
final DeviceChannel channel = channels.get(channelUID);
|
||||
if (channel != null) {
|
||||
refresh(channelUID, channel, lastValue);
|
||||
}
|
||||
final DeviceChannel channel = channels.get(channelUID);
|
||||
if (channel != null) {
|
||||
refresh(channelUID, channel);
|
||||
}
|
||||
} else {
|
||||
final DeviceChannel channel = channels.get(channelUID);
|
||||
if (channel != null) {
|
||||
final Bridge bridge = getBridge();
|
||||
if (bridge != null) {
|
||||
final SerialBridgeHandler handler = (SerialBridgeHandler) bridge.getHandler();
|
||||
if (handler != null) {
|
||||
channel.mapCommand(command).ifPresent(value -> handler.writeString(value));
|
||||
}
|
||||
if (getBridge() instanceof Bridge bridge
|
||||
&& bridge.getHandler() instanceof CommonBridgeHandler handler) {
|
||||
channel.mapCommand(command).ifPresent(handler::writeString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,6 +99,13 @@ public class SerialDeviceHandler extends BaseThingHandler {
|
||||
type.getId());
|
||||
if (deviceChannel != null) {
|
||||
channels.put(c.getUID(), deviceChannel);
|
||||
|
||||
int delay = deviceChannel.getRefreshInterval();
|
||||
if (delay > 0) {
|
||||
ScheduledFuture<?> future = scheduler.scheduleWithFixedDelay(
|
||||
() -> refresh(c.getUID(), deviceChannel), delay, delay, TimeUnit.SECONDS);
|
||||
futures.put(c.getUID(), future);
|
||||
}
|
||||
}
|
||||
} catch (final IllegalArgumentException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
@@ -118,6 +124,9 @@ public class SerialDeviceHandler extends BaseThingHandler {
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
for (ScheduledFuture<?> future : this.futures.values()) {
|
||||
future.cancel(true);
|
||||
}
|
||||
channels.clear();
|
||||
lastValue = null;
|
||||
super.dispose();
|
||||
@@ -132,7 +141,11 @@ public class SerialDeviceHandler extends BaseThingHandler {
|
||||
final Pattern devicePattern = this.devicePattern;
|
||||
|
||||
if (devicePattern != null && devicePattern.matcher(data).matches()) {
|
||||
channels.forEach((channelUID, channel) -> refresh(channelUID, channel, data));
|
||||
channels.forEach((channelUID, channel) -> {
|
||||
if (isLinked(channelUID)) {
|
||||
channel.transformData(data).ifPresent(value -> updateState(channelUID, new StringType(value)));
|
||||
}
|
||||
});
|
||||
this.lastValue = data;
|
||||
}
|
||||
}
|
||||
@@ -152,13 +165,27 @@ public class SerialDeviceHandler extends BaseThingHandler {
|
||||
/**
|
||||
* Refreshes the channel with the last received data
|
||||
*
|
||||
* @param channelId the channel to refresh
|
||||
* @param channelUID the channel to refresh
|
||||
*/
|
||||
private void refresh(final ChannelUID channelUID, final DeviceChannel channel, final String data) {
|
||||
private void refresh(final ChannelUID channelUID, final DeviceChannel channel) {
|
||||
final String data = this.lastValue;
|
||||
if (!isLinked(channelUID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
channel.transformData(data).ifPresent(value -> updateState(channelUID, new StringType(value)));
|
||||
if (!channel.getRefreshValue().isBlank()) {
|
||||
if (getBridge() instanceof Bridge bridge && bridge.getHandler() instanceof CommonBridgeHandler handler) {
|
||||
Optional<String> value = channel.transformCommand(channel.getRefreshValue());
|
||||
if (value.isPresent()) {
|
||||
handler.writeString(value.get());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use last result line
|
||||
if (data != null) {
|
||||
channel.transformData(data).ifPresent(value -> updateState(channelUID, new StringType(value)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.serial.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Class describing the {@link TcpBridgeConfiguration} configuration
|
||||
*
|
||||
* @author Roland Tapken - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class TcpBridgeConfiguration extends CommonBridgeConfiguration {
|
||||
/**
|
||||
* IP address or hostname
|
||||
*/
|
||||
public String address = "";
|
||||
|
||||
/**
|
||||
* TCP Port
|
||||
*/
|
||||
public int port = 0;
|
||||
|
||||
/**
|
||||
* Socket timeout in seconds
|
||||
*/
|
||||
public int timeout = 0;
|
||||
|
||||
/**
|
||||
* Keep Alive
|
||||
*/
|
||||
public boolean keepAlive = false;
|
||||
|
||||
/**
|
||||
* Reconnection Interval in seconds
|
||||
*/
|
||||
public int reconnectInterval = 10;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TcpBridgeConfiguration [Address=" + address + ", Port=" + port + ", timeout=" + timeout + ", keepAlive="
|
||||
+ keepAlive + ", reconnectInterval=" + reconnectInterval + ", charset=" + charset + ", eolPattern="
|
||||
+ eolPattern + "]";
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.serial.internal.handler;
|
||||
|
||||
import static org.openhab.binding.serial.internal.SerialBindingConstants.*;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.CommonTriggerEvents;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
|
||||
/**
|
||||
* The {@link TcpBridgeHandler} is responsible for handling commands, which
|
||||
* are sent to one of the channels.
|
||||
*
|
||||
* @author Roland Tapken - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class TcpBridgeHandler extends CommonBridgeHandler {
|
||||
|
||||
/**
|
||||
* Since InputStream#read will block, we use our own instance of ScheduledThreadPoolExecutor
|
||||
* instead of OpenHab's default one to not block a background thread.
|
||||
*/
|
||||
private final ScheduledThreadPoolExecutor readSchedulerExcecutor = new ScheduledThreadPoolExecutor(1);
|
||||
|
||||
private TcpBridgeConfiguration config = new TcpBridgeConfiguration();
|
||||
private @Nullable Socket socket;
|
||||
private @Nullable ScheduledFuture<?> readScheduler;
|
||||
|
||||
public TcpBridgeHandler(final Bridge bridge) {
|
||||
super(bridge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(TcpBridgeConfiguration.class);
|
||||
if (!checkAndProcessConfiguration(config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String address = config.address;
|
||||
if (address.isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Address must be set");
|
||||
return;
|
||||
}
|
||||
|
||||
final int port = config.port;
|
||||
if (port <= 0) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set");
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize serial port
|
||||
try {
|
||||
Socket socket = new Socket();
|
||||
socket.connect(new InetSocketAddress(address, port), config.timeout * 1000);
|
||||
if (config.timeout > 0) {
|
||||
socket.setSoTimeout(config.timeout * 1000);
|
||||
}
|
||||
if (config.keepAlive) {
|
||||
socket.setKeepAlive(true);
|
||||
}
|
||||
|
||||
this.socket = socket;
|
||||
inputStream = new BufferedInputStream(socket.getInputStream());
|
||||
outputStream = socket.getOutputStream();
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
|
||||
// Since there is not a thing like a SerialPortEvent for Sockets
|
||||
// we have to trigger a read periodical.
|
||||
waitForData();
|
||||
} catch (final IllegalArgumentException ex) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, ex.getMessage());
|
||||
} catch (final IOException ex) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error");
|
||||
handleIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replacement for SerialBridgeHandler's SerialPortEventListener.
|
||||
* Checks for available data every 250ms.
|
||||
*/
|
||||
private void waitForData() {
|
||||
// If this is ever changed to values >= 1000,
|
||||
// this may interfere with tryToReconect. In this case
|
||||
// some kind of locking is required.
|
||||
int interval = 250;
|
||||
if (getThing().getStatus() == ThingStatus.ONLINE) {
|
||||
ScheduledFuture<?> readScheduler = this.readScheduler;
|
||||
if (readScheduler != null) {
|
||||
readScheduler.cancel(false);
|
||||
}
|
||||
|
||||
this.readScheduler = readSchedulerExcecutor.schedule(() -> {
|
||||
if (getThing().getStatus() == ThingStatus.ONLINE) {
|
||||
try {
|
||||
InputStream inputStream = this.inputStream;
|
||||
Socket socket = this.socket;
|
||||
if (inputStream != null && socket != null) {
|
||||
synchronized (inputStream) {
|
||||
inputStream.mark(2);
|
||||
// InputStream.available() does not recognise when a client has disconnected,
|
||||
// so we will use BufferedInputStream and cache one byte.
|
||||
int b = inputStream.read();
|
||||
if (b < 0) {
|
||||
throw new SocketException("Connection lost");
|
||||
}
|
||||
inputStream.reset();
|
||||
receiveAndProcessNow();
|
||||
}
|
||||
}
|
||||
|
||||
waitForData();
|
||||
} catch (IOException e) {
|
||||
handleIOException(e);
|
||||
}
|
||||
}
|
||||
}, interval, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processInput(String result) {
|
||||
if (isLinked(TRIGGER_CHANNEL)) {
|
||||
triggerChannel(TRIGGER_CHANNEL, CommonTriggerEvents.PRESSED);
|
||||
}
|
||||
if (isLinked(STRING_CHANNEL)) {
|
||||
refresh(STRING_CHANNEL, result);
|
||||
}
|
||||
if (isLinked(BINARY_CHANNEL)) {
|
||||
refresh(BINARY_CHANNEL, result);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
ScheduledFuture<?> readScheduler = this.readScheduler;
|
||||
this.readScheduler = null;
|
||||
if (readScheduler != null) {
|
||||
readScheduler.cancel(true);
|
||||
}
|
||||
|
||||
Socket socket = this.socket;
|
||||
this.socket = null;
|
||||
if (socket != null) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* TCP Connections do not reconnect automatically. In case of an IOException,
|
||||
* close the connection and try to re-connect.
|
||||
*/
|
||||
@Override
|
||||
protected void handleIOException(IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
|
||||
dispose();
|
||||
super.handleIOException(e);
|
||||
tryToReconnect();
|
||||
}
|
||||
|
||||
private void tryToReconnect() {
|
||||
int reconnectInterval = config.reconnectInterval;
|
||||
if (reconnectInterval > 0) {
|
||||
logger.info("Trying to reconnnect to {}:{} in {} seconds", this.config.address, this.config.port,
|
||||
reconnectInterval);
|
||||
scheduler.schedule(() -> {
|
||||
if (getThing().getStatus() == ThingStatus.OFFLINE) {
|
||||
// Will re-call tryToReconnect on failure
|
||||
initialize();
|
||||
}
|
||||
}, reconnectInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getLogPrefix() {
|
||||
return String.format("TCP socket '%s:%d'", config.address, config.port);
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.serial.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Class describing the {@link TcpServerBridgeConfiguration} configuration
|
||||
*
|
||||
* @author Roland Tapken - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class TcpServerBridgeConfiguration extends CommonBridgeConfiguration {
|
||||
/**
|
||||
* IP address or hostname
|
||||
*/
|
||||
public String bindAddress = "0.0.0.0";
|
||||
|
||||
/**
|
||||
* TCP Port
|
||||
*/
|
||||
public int port = 0;
|
||||
|
||||
/**
|
||||
* Socket timeout in seconds
|
||||
*/
|
||||
public int timeout = 0;
|
||||
|
||||
/**
|
||||
* Keep Alive
|
||||
*/
|
||||
public boolean keepAlive = false;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TcpServerBridgeConfiguration [BindAddress=" + bindAddress + ", Port=" + port + ", timeout=" + timeout
|
||||
+ ", keepAlive=" + keepAlive + ", charset=" + charset + ", eolPattern=" + eolPattern + "]";
|
||||
}
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.serial.internal.handler;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
|
||||
/**
|
||||
* The {@link TcpServerBridgeHandler} is responsible for handling commands, which
|
||||
* are sent to one of the channels.
|
||||
*
|
||||
* Like {@link TcpBridgeHandler}, but listens for an incoming connection on a defined
|
||||
* TCP port. Only one active connection per time is allowed.
|
||||
*
|
||||
* @author Roland Tapken - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class TcpServerBridgeHandler extends CommonBridgeHandler {
|
||||
|
||||
private TcpServerBridgeConfiguration config = new TcpServerBridgeConfiguration();
|
||||
|
||||
/**
|
||||
* Since ServerSocket#accept will block, we use our own instance of ScheduledThreadPoolExecutor
|
||||
* instead of OpenHab's default one to not block a background thread.
|
||||
*/
|
||||
private final ScheduledThreadPoolExecutor connectionSchedulerExcecutor = new ScheduledThreadPoolExecutor(1);
|
||||
|
||||
/**
|
||||
* Since InputStream#read will block, we use our own instance of ScheduledThreadPoolExecutor
|
||||
* instead of OpenHab's default one to not block a background thread.
|
||||
*/
|
||||
private final ScheduledThreadPoolExecutor readSchedulerExcecutor = new ScheduledThreadPoolExecutor(1);
|
||||
|
||||
private @Nullable ServerSocket server;
|
||||
private @Nullable Socket socket;
|
||||
private @Nullable ScheduledFuture<?> readScheduler;
|
||||
private @Nullable ScheduledFuture<?> connectionScheduler;
|
||||
|
||||
public TcpServerBridgeHandler(final Bridge bridge) {
|
||||
super(bridge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(TcpServerBridgeConfiguration.class);
|
||||
if (!checkAndProcessConfiguration(config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int port = config.port;
|
||||
if (port <= 0) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set");
|
||||
return;
|
||||
}
|
||||
|
||||
final String bindAddress = config.bindAddress;
|
||||
if (bindAddress.isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "BindAddress must be set");
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize serial port
|
||||
try {
|
||||
ServerSocket server = new ServerSocket();
|
||||
server.bind(new InetSocketAddress(bindAddress, port), 1);
|
||||
this.server = server;
|
||||
logger.info("Listening on TCP address {} port {}", bindAddress, port);
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
|
||||
// Wait for an incoming connection
|
||||
waitForConnection();
|
||||
|
||||
} catch (final IllegalArgumentException ex) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, ex.getMessage());
|
||||
} catch (final IOException ex) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error");
|
||||
handleIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void waitForConnection() {
|
||||
ServerSocket server = this.server;
|
||||
TcpServerBridgeConfiguration config = this.config;
|
||||
if (getThing().getStatus() == ThingStatus.ONLINE && server != null) {
|
||||
ScheduledFuture<?> acceptScheduler = this.connectionScheduler;
|
||||
if (acceptScheduler != null) {
|
||||
acceptScheduler.cancel(true);
|
||||
}
|
||||
|
||||
this.connectionScheduler = connectionSchedulerExcecutor.schedule(() -> {
|
||||
if (getThing().getStatus() == ThingStatus.ONLINE && server.equals(this.server)) {
|
||||
try {
|
||||
synchronized (server) {
|
||||
Socket socket = server.accept();
|
||||
if (!server.equals(this.server)) {
|
||||
// Drop this connection, it is not valid anymore
|
||||
logger.warn("Rejecting incoming connection from {}:{} (invalid)",
|
||||
socket.getInetAddress(), socket.getPort());
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.socket != null) {
|
||||
// Normally, this should not happen because the ServerSocket has been bound with
|
||||
// 'backlog=1' parameter. But it seems that this is only a recommendation to the
|
||||
// operating system and might be ignored, so we have to handle this.
|
||||
logger.warn("Rejecting incoming connection from {}:{} (socket already bound)",
|
||||
socket.getInetAddress(), socket.getPort());
|
||||
try {
|
||||
socket.shutdownInput();
|
||||
socket.shutdownOutput();
|
||||
socket.close();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
waitForConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Accepting incoming connection from {}:{}", socket.getInetAddress(),
|
||||
socket.getPort());
|
||||
socket.setKeepAlive(this.config.keepAlive);
|
||||
if (config.timeout > 0) {
|
||||
socket.setSoTimeout(config.timeout * 1000);
|
||||
}
|
||||
this.socket = socket;
|
||||
this.inputStream = new BufferedInputStream(socket.getInputStream());
|
||||
this.outputStream = socket.getOutputStream();
|
||||
}
|
||||
waitForData();
|
||||
waitForConnection();
|
||||
} catch (IOException e) {
|
||||
handleIOException(e);
|
||||
}
|
||||
}
|
||||
}, 100, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replacement for SerialBridgeHandler's SerialPortEventListener.
|
||||
* Checks for available data every 250ms.
|
||||
*/
|
||||
private void waitForData() {
|
||||
// If this is ever changed to values >= 1000,
|
||||
// this may interfere with tryToReconnect. In this case
|
||||
// some kind of locking is required.
|
||||
int interval = 250;
|
||||
if (getThing().getStatus() == ThingStatus.ONLINE) {
|
||||
ScheduledFuture<?> readScheduler = this.readScheduler;
|
||||
if (readScheduler != null) {
|
||||
readScheduler.cancel(false);
|
||||
}
|
||||
|
||||
this.readScheduler = readSchedulerExcecutor.schedule(() -> {
|
||||
if (getThing().getStatus() == ThingStatus.ONLINE) {
|
||||
try {
|
||||
InputStream inputStream = this.inputStream;
|
||||
if (inputStream != null) {
|
||||
synchronized (inputStream) {
|
||||
inputStream.mark(1);
|
||||
// InputStream.available() does not recognise when a client has disconnected,
|
||||
// so we will use BufferedInputStream and cache one byte.
|
||||
if (inputStream.read() < 0) {
|
||||
logger.info("{} connection lost", getLogPrefix());
|
||||
disposeSocket();
|
||||
return;
|
||||
}
|
||||
inputStream.reset();
|
||||
receiveAndProcessNow();
|
||||
}
|
||||
}
|
||||
|
||||
waitForData();
|
||||
} catch (IOException e) {
|
||||
handleIOException(e);
|
||||
}
|
||||
}
|
||||
}, interval, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private void disposeSocket() {
|
||||
// Dispose inputStream and outputStream and any active readers
|
||||
disposeReader();
|
||||
|
||||
ScheduledFuture<?> readScheduler = this.readScheduler;
|
||||
this.readScheduler = null;
|
||||
if (readScheduler != null) {
|
||||
readScheduler.cancel(true);
|
||||
}
|
||||
|
||||
Socket socket = this.socket;
|
||||
this.socket = null;
|
||||
if (socket != null) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
disposeSocket();
|
||||
|
||||
ScheduledFuture<?> connectionScheduler = this.connectionScheduler;
|
||||
this.connectionScheduler = null;
|
||||
if (connectionScheduler != null) {
|
||||
connectionScheduler.cancel(true);
|
||||
}
|
||||
|
||||
ServerSocket server = this.server;
|
||||
this.server = null;
|
||||
if (server != null) {
|
||||
try {
|
||||
server.close();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TCP Connections do not reconnect automatically. In case of an IOException,
|
||||
* close the connection and try to re-connect.
|
||||
*/
|
||||
@Override
|
||||
protected void handleIOException(IOException e) {
|
||||
Socket socket = this.socket;
|
||||
if (socket != null) {
|
||||
logger.warn("Connection to {}:{} failed with IOException: {}", socket.getInetAddress(), socket.getPort(),
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
// Unlike TcpBridgeHandler, this only disposes the current connection,
|
||||
// not the listening port itself.
|
||||
disposeSocket();
|
||||
super.handleIOException(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getLogPrefix() {
|
||||
return String.format("TCP Server '%s:%d'", config.bindAddress, config.port);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<type>binding</type>
|
||||
<name>Serial Binding</name>
|
||||
<description>This binding supports sending/receiving data to/from a serial port</description>
|
||||
<description>This binding supports sending/receiving data to/from a serial port or TCP connection</description>
|
||||
<connection>local</connection>
|
||||
|
||||
</addon:addon>
|
||||
|
||||
+54
-2
@@ -1,7 +1,7 @@
|
||||
# add-on
|
||||
|
||||
addon.serial.name = Serial Binding
|
||||
addon.serial.description = This binding supports sending/receiving data to/from a serial port
|
||||
addon.serial.description = This binding supports sending/receiving data to/from a serial port or TCP connection
|
||||
|
||||
# thing types
|
||||
|
||||
@@ -9,6 +9,10 @@ thing-type.serial.serialBridge.label = Serial Bridge
|
||||
thing-type.serial.serialBridge.description = Serial port which can send and receive data
|
||||
thing-type.serial.serialDevice.label = Serial Device
|
||||
thing-type.serial.serialDevice.description = Represents a device
|
||||
thing-type.serial.tcpBridge.label = TCP Bridge
|
||||
thing-type.serial.tcpBridge.description = TCP socket which can send and receive data
|
||||
thing-type.serial.tcpServerBridge.label = TCP Server Bridge
|
||||
thing-type.serial.tcpServerBridge.description = Like TCP bridge, but listens on a local port for incoming connections
|
||||
|
||||
# thing types config
|
||||
|
||||
@@ -22,13 +26,15 @@ thing-type.config.serial.serialBridge.baudRate.option.38400 = 38400
|
||||
thing-type.config.serial.serialBridge.baudRate.option.57600 = 57600
|
||||
thing-type.config.serial.serialBridge.baudRate.option.115200 = 115200
|
||||
thing-type.config.serial.serialBridge.charset.label = Charset
|
||||
thing-type.config.serial.serialBridge.charset.description = The charset to use for converting between bytes and string (e.g. UTF-8, ISO-8859-1)
|
||||
thing-type.config.serial.serialBridge.charset.description = The charset to use for converting between bytes and string (e.g. UTF-8, ISO-8859-1). Enter 'HEX' to convert binary data into hexadecimal strings, separated by space.
|
||||
thing-type.config.serial.serialBridge.dataBits.label = Data Bits
|
||||
thing-type.config.serial.serialBridge.dataBits.description = Set the data bits
|
||||
thing-type.config.serial.serialBridge.dataBits.option.5 = 5
|
||||
thing-type.config.serial.serialBridge.dataBits.option.6 = 6
|
||||
thing-type.config.serial.serialBridge.dataBits.option.7 = 7
|
||||
thing-type.config.serial.serialBridge.dataBits.option.8 = 8
|
||||
thing-type.config.serial.serialBridge.eolPattern.label = EOL Pattern
|
||||
thing-type.config.serial.serialBridge.eolPattern.description = Only for Charset = HEX: 'End of line' pattern as regular expression. For example, "0D 10" matches \r\n, "\\bFF" matches 0xFF. Please note that the matched pattern will be part of the result string.
|
||||
thing-type.config.serial.serialBridge.parity.label = Parity
|
||||
thing-type.config.serial.serialBridge.parity.description = Set the parity
|
||||
thing-type.config.serial.serialBridge.parity.option.N = N(one)
|
||||
@@ -45,6 +51,32 @@ thing-type.config.serial.serialBridge.stopBits.option.1.5 = 1.5
|
||||
thing-type.config.serial.serialBridge.stopBits.option.2 = 2
|
||||
thing-type.config.serial.serialDevice.patternMatch.label = Patern Match
|
||||
thing-type.config.serial.serialDevice.patternMatch.description = Regular expression used to identify device from received data (must match the whole line)
|
||||
thing-type.config.serial.tcpBridge.address.label = Address
|
||||
thing-type.config.serial.tcpBridge.address.description = The IP address or hostname
|
||||
thing-type.config.serial.tcpBridge.charset.label = Charset or HEX
|
||||
thing-type.config.serial.tcpBridge.charset.description = The charset to use for converting between bytes and string (e.g. UTF-8, ISO-8859-1). Enter 'HEX' to convert binary data into hexadecimal strings, separated by space.
|
||||
thing-type.config.serial.tcpBridge.eolPattern.label = EOL pattern
|
||||
thing-type.config.serial.tcpBridge.eolPattern.description = Only for Charset = HEX: 'End of line' pattern as regular expression. For example, "0D 10" matches \r\n, "\\bFF" matches 0xFF. Please note that the matched pattern will be part of the result string.
|
||||
thing-type.config.serial.tcpBridge.keepAlive.label = Keep-Alive
|
||||
thing-type.config.serial.tcpBridge.keepAlive.description = Send keep-alive
|
||||
thing-type.config.serial.tcpBridge.port.label = Port
|
||||
thing-type.config.serial.tcpBridge.port.description = TCP port to connect
|
||||
thing-type.config.serial.tcpBridge.reconnectInterval.label = Reconnect Interval
|
||||
thing-type.config.serial.tcpBridge.reconnectInterval.description = Reconnection interval in seconds after connection failure (0 = disabled)
|
||||
thing-type.config.serial.tcpBridge.timeout.label = Socket Timeout
|
||||
thing-type.config.serial.tcpBridge.timeout.description = Timeout in seconds
|
||||
thing-type.config.serial.tcpServerBridge.bindAddress.label = Address
|
||||
thing-type.config.serial.tcpServerBridge.bindAddress.description = The IP address to bind to (default: 0.0.0.0)
|
||||
thing-type.config.serial.tcpServerBridge.charset.label = Charset or HEX
|
||||
thing-type.config.serial.tcpServerBridge.charset.description = The charset to use for converting between bytes and string (e.g. UTF-8, ISO-8859-1). Enter 'HEX' to convert binary data into hexadecimal strings, separated by space.
|
||||
thing-type.config.serial.tcpServerBridge.eolPattern.label = EOL pattern
|
||||
thing-type.config.serial.tcpServerBridge.eolPattern.description = Only for Charset = HEX: 'End of line' pattern as regular expression. For example, "0D 10" matches \r\n, "\\bFF" matches 0xFF. Please note that the matched pattern will be part of the result string.
|
||||
thing-type.config.serial.tcpServerBridge.keepAlive.label = Keep-Alive
|
||||
thing-type.config.serial.tcpServerBridge.keepAlive.description = Send keep-alive
|
||||
thing-type.config.serial.tcpServerBridge.port.label = Port
|
||||
thing-type.config.serial.tcpServerBridge.port.description = Listening TCP port
|
||||
thing-type.config.serial.tcpServerBridge.timeout.label = Socket Timeout
|
||||
thing-type.config.serial.tcpServerBridge.timeout.description = Timeout in seconds
|
||||
|
||||
# channel types
|
||||
|
||||
@@ -77,12 +109,20 @@ channel-type.config.serial.dimmer.offValue.label = Off Value
|
||||
channel-type.config.serial.dimmer.offValue.description = Send this value when receiving an OFF command
|
||||
channel-type.config.serial.dimmer.onValue.label = On Value
|
||||
channel-type.config.serial.dimmer.onValue.description = Send this value when receiving an ON command
|
||||
channel-type.config.serial.dimmer.refreshInterval.label = Refresh Interval
|
||||
channel-type.config.serial.dimmer.refreshInterval.description = Interval for the automatic channel update in seconds. This value is only required if the peer has to send the “Refresh Value” command regularly in order to return the current properties. It is not required if the peer automatically forwards changed values to its clients.
|
||||
channel-type.config.serial.dimmer.refreshValue.label = Refresh Value
|
||||
channel-type.config.serial.dimmer.refreshValue.description = Send this value to receive the current state
|
||||
channel-type.config.serial.dimmer.stateTransformation.label = State Transformation
|
||||
channel-type.config.serial.dimmer.stateTransformation.description = Transformation used to convert device data to channel state, e.g. <code>REGEX(.*?STATE=(.*?);.*)</code>. Multiple transformations can be chained by listing each transformation on a separate line, or by concatenating them with "∩".
|
||||
channel-type.config.serial.number.commandFormat.label = Number Format
|
||||
channel-type.config.serial.number.commandFormat.description = Format string applied to the command, e.g. ID=671;VAL=%f
|
||||
channel-type.config.serial.number.commandTransformation.label = Command Transformation
|
||||
channel-type.config.serial.number.commandTransformation.description = Transformation used to convert command to device data, e.g. <code>JS(device.js)</code>. Multiple transformations can be chained by listing each transformation on a separate line, or by concatenating them with "∩".
|
||||
channel-type.config.serial.number.refreshInterval.label = Refresh Interval
|
||||
channel-type.config.serial.number.refreshInterval.description = Interval for the automatic channel update in seconds. This value is only required if the peer has to send the “Refresh Value” command regularly in order to return the current properties. It is not required if the peer automatically forwards changed values to its clients.
|
||||
channel-type.config.serial.number.refreshValue.label = Refresh Value
|
||||
channel-type.config.serial.number.refreshValue.description = Send this value to receive the current state
|
||||
channel-type.config.serial.number.stateTransformation.label = State Transformation
|
||||
channel-type.config.serial.number.stateTransformation.description = Transformation used to convert device data to channel state, e.g. <code>REGEX(.*?STATE=(.*?);.*)</code>. Multiple transformations can be chained by listing each transformation on a separate line, or by concatenating them with "∩".
|
||||
channel-type.config.serial.rollershutter.commandFormat.label = Percent Format
|
||||
@@ -91,6 +131,10 @@ channel-type.config.serial.rollershutter.commandTransformation.label = Command T
|
||||
channel-type.config.serial.rollershutter.commandTransformation.description = Transformation used to convert command to device data, e.g. <code>JS(device.js)</code>. Multiple transformations can be chained by listing each transformation on a separate line, or by concatenating them with "∩".
|
||||
channel-type.config.serial.rollershutter.downValue.label = Down Value
|
||||
channel-type.config.serial.rollershutter.downValue.description = Send this value when receiving a DOWN command
|
||||
channel-type.config.serial.rollershutter.refreshInterval.label = Refresh Interval
|
||||
channel-type.config.serial.rollershutter.refreshInterval.description = Interval for the automatic channel update in seconds. This value is only required if the peer has to send the “Refresh Value” command regularly in order to return the current properties. It is not required if the peer automatically forwards changed values to its clients.
|
||||
channel-type.config.serial.rollershutter.refreshValue.label = Refresh Value
|
||||
channel-type.config.serial.rollershutter.refreshValue.description = Send this value to receive the current state
|
||||
channel-type.config.serial.rollershutter.stateTransformation.label = State Transformation
|
||||
channel-type.config.serial.rollershutter.stateTransformation.description = Transformation used to convert device data to channel state, e.g. <code>REGEX(.*?STATE=(.*?);.*)</code>. Multiple transformations can be chained by listing each transformation on a separate line, or by concatenating them with "∩".
|
||||
channel-type.config.serial.rollershutter.stopValue.label = Stop Value
|
||||
@@ -101,6 +145,10 @@ channel-type.config.serial.string.commandFormat.label = String Format
|
||||
channel-type.config.serial.string.commandFormat.description = Format string applied to the command, e.g. ID=671;COMMAND=%s
|
||||
channel-type.config.serial.string.commandTransformation.label = Command Transformation
|
||||
channel-type.config.serial.string.commandTransformation.description = Transformation used to convert command to device data, e.g. <code>JS(device.js)</code>. Multiple transformations can be chained by listing each transformation on a separate line, or by concatenating them with "∩".
|
||||
channel-type.config.serial.string.refreshInterval.label = Refresh Interval
|
||||
channel-type.config.serial.string.refreshInterval.description = Interval for the automatic channel update in seconds. This value is only required if the peer has to send the “Refresh Value” command regularly in order to return the current properties. It is not required if the peer automatically forwards changed values to its clients.
|
||||
channel-type.config.serial.string.refreshValue.label = Refresh Value
|
||||
channel-type.config.serial.string.refreshValue.description = Send this value to receive the current state
|
||||
channel-type.config.serial.string.stateTransformation.label = State Transformation
|
||||
channel-type.config.serial.string.stateTransformation.description = Transformation used to convert device data to channel state, e.g. <code>REGEX(.*?STATE=(.*?);.*)</code>. Multiple transformations can be chained by listing each transformation on a separate line, or by concatenating them with "∩".
|
||||
channel-type.config.serial.switch.commandTransformation.label = Command Transformation
|
||||
@@ -109,5 +157,9 @@ channel-type.config.serial.switch.offValue.label = Off Value
|
||||
channel-type.config.serial.switch.offValue.description = Send this value when receiving an OFF command
|
||||
channel-type.config.serial.switch.onValue.label = On Value
|
||||
channel-type.config.serial.switch.onValue.description = Send this value when receiving an ON command
|
||||
channel-type.config.serial.switch.refreshInterval.label = Refresh Interval
|
||||
channel-type.config.serial.switch.refreshInterval.description = Interval for the automatic channel update in seconds. This value is only required if the peer has to send the “Refresh Value” command regularly in order to return the current properties. It is not required if the peer automatically forwards changed values to its clients.
|
||||
channel-type.config.serial.switch.refreshValue.label = Refresh Value
|
||||
channel-type.config.serial.switch.refreshValue.description = Send this value to receive the current state
|
||||
channel-type.config.serial.switch.stateTransformation.label = State Transformation
|
||||
channel-type.config.serial.switch.stateTransformation.description = Transformation used to convert device data to channel state, e.g. <code>REGEX(.*?STATE=(.*?);.*)</code>. Multiple transformations can be chained by listing each transformation on a separate line, or by concatenating them with "∩".
|
||||
|
||||
+154
-2
@@ -19,7 +19,6 @@
|
||||
<config-description>
|
||||
<parameter name="serialPort" type="text" required="true">
|
||||
<context>serial-port</context>
|
||||
<limitToOptions>false</limitToOptions>
|
||||
<label>Serial Port</label>
|
||||
<description>The serial port to use (e.g. Linux: /dev/ttyUSB0, Windows: COM1)</description>
|
||||
</parameter>
|
||||
@@ -78,7 +77,108 @@
|
||||
<parameter name="charset" type="text">
|
||||
<advanced>true</advanced>
|
||||
<label>Charset</label>
|
||||
<description>The charset to use for converting between bytes and string (e.g. UTF-8, ISO-8859-1)</description>
|
||||
<description>The charset to use for converting between bytes and string (e.g. UTF-8, ISO-8859-1). Enter 'HEX' to
|
||||
convert binary data into hexadecimal strings, separated by space.</description>
|
||||
</parameter>
|
||||
<parameter name="eolPattern" type="text">
|
||||
<advanced>true</advanced>
|
||||
<label>EOL Pattern</label>
|
||||
<description>Only for Charset = HEX: 'End of line' pattern as regular expression. For example, "0D 10" matches \r\n,
|
||||
"\\bFF" matches 0xFF. Please note that the matched pattern will be part of the result string.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
<bridge-type id="tcpBridge">
|
||||
<label>TCP Bridge</label>
|
||||
<description>TCP socket which can send and receive data</description>
|
||||
|
||||
<channels>
|
||||
<channel id="string" typeId="stringData"/>
|
||||
<channel id="binary" typeId="binaryData"/>
|
||||
<channel id="data" typeId="system.rawbutton"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="address" type="text" required="true">
|
||||
<context>network-address</context>
|
||||
<label>Address</label>
|
||||
<description>The IP address or hostname</description>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer" min="1" max="65535" required="true">
|
||||
<label>Port</label>
|
||||
<description>TCP port to connect</description>
|
||||
<limitToOptions>false</limitToOptions>
|
||||
</parameter>
|
||||
<parameter name="timeout" type="integer" min="0">
|
||||
<advanced>true</advanced>
|
||||
<label>Socket Timeout</label>
|
||||
<description>Timeout in seconds</description>
|
||||
</parameter>
|
||||
<parameter name="keepAlive" type="boolean">
|
||||
<advanced>true</advanced>
|
||||
<label>Keep-Alive</label>
|
||||
<description>Send keep-alive</description>
|
||||
</parameter>
|
||||
<parameter name="reconnectInterval" type="integer" min="0">
|
||||
<advanced>true</advanced>
|
||||
<label>Reconnect Interval</label>
|
||||
<description>Reconnection interval in seconds after connection failure (0 = disabled)</description>
|
||||
<default>10</default>
|
||||
</parameter>
|
||||
<parameter name="charset" type="text">
|
||||
<advanced>true</advanced>
|
||||
<label>Charset or HEX</label>
|
||||
<description>The charset to use for converting between bytes and string (e.g. UTF-8, ISO-8859-1). Enter 'HEX' to
|
||||
convert binary data into hexadecimal strings, separated by space.</description>
|
||||
</parameter>
|
||||
<parameter name="eolPattern" type="text">
|
||||
<advanced>true</advanced>
|
||||
<label>EOL pattern</label>
|
||||
<description>Only for Charset = HEX: 'End of line' pattern as regular expression. For example, "0D 10" matches \r\n,
|
||||
"\\bFF" matches 0xFF. Please note that the matched pattern will be part of the result string.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
<bridge-type id="tcpServerBridge">
|
||||
<label>TCP Server Bridge</label>
|
||||
<description>Like TCP bridge, but listens on a local port for incoming connections</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="port" type="integer" min="1" max="65535" required="true">
|
||||
<label>Port</label>
|
||||
<description>Listening TCP port</description>
|
||||
<limitToOptions>false</limitToOptions>
|
||||
</parameter>
|
||||
<parameter name="bindAddress" type="text">
|
||||
<context>network-address</context>
|
||||
<limitToOptions>false</limitToOptions>
|
||||
<label>Address</label>
|
||||
<default>0.0.0.0</default>
|
||||
<description>The IP address to bind to (default: 0.0.0.0)</description>
|
||||
</parameter>
|
||||
<parameter name="timeout" type="integer" min="0">
|
||||
<advanced>true</advanced>
|
||||
<label>Socket Timeout</label>
|
||||
<description>Timeout in seconds</description>
|
||||
</parameter>
|
||||
<parameter name="keepAlive" type="boolean">
|
||||
<advanced>true</advanced>
|
||||
<label>Keep-Alive</label>
|
||||
<description>Send keep-alive</description>
|
||||
</parameter>
|
||||
<parameter name="charset" type="text">
|
||||
<advanced>true</advanced>
|
||||
<label>Charset or HEX</label>
|
||||
<description>The charset to use for converting between bytes and string (e.g. UTF-8, ISO-8859-1). Enter 'HEX' to
|
||||
convert binary data into hexadecimal strings, separated by space.</description>
|
||||
</parameter>
|
||||
<parameter name="eolPattern" type="text">
|
||||
<advanced>true</advanced>
|
||||
<label>EOL pattern</label>
|
||||
<description>Only for Charset = HEX: 'End of line' pattern as regular expression. For example, "0D 10" matches \r\n,
|
||||
"\\bFF" matches 0xFF. Please note that the matched pattern will be part of the result string.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
@@ -86,6 +186,8 @@
|
||||
<thing-type id="serialDevice" extensible="string, number, dimmer, switch, rollershutter">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="serialBridge"/>
|
||||
<bridge-type-ref id="tcpBridge"/>
|
||||
<bridge-type-ref id="tcpServerBridge"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Serial Device</label>
|
||||
@@ -136,6 +238,16 @@
|
||||
Multiple transformations can be chained by listing each transformation on a separate line,
|
||||
or by concatenating them with "∩".]]></description>
|
||||
</parameter>
|
||||
<parameter name="refreshValue" type="text">
|
||||
<label>Refresh Value</label>
|
||||
<description>Send this value to receive the current state</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" min="0">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Interval for the automatic channel update in seconds. This value is only required if the peer has to
|
||||
send the “Refresh Value” command regularly in order to return the current properties. It is not required if the
|
||||
peer automatically forwards changed values to its clients.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
|
||||
@@ -162,6 +274,16 @@
|
||||
Multiple transformations can be chained by listing each transformation on a separate line,
|
||||
or by concatenating them with "∩".]]></description>
|
||||
</parameter>
|
||||
<parameter name="refreshValue" type="text">
|
||||
<label>Refresh Value</label>
|
||||
<description>Send this value to receive the current state</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" min="0">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Interval for the automatic channel update in seconds. This value is only required if the peer has to
|
||||
send the “Refresh Value” command regularly in order to return the current properties. It is not required if the
|
||||
peer automatically forwards changed values to its clients.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
|
||||
@@ -208,6 +330,16 @@
|
||||
Multiple transformations can be chained by listing each transformation on a separate line,
|
||||
or by concatenating them with "∩".]]></description>
|
||||
</parameter>
|
||||
<parameter name="refreshValue" type="text">
|
||||
<label>Refresh Value</label>
|
||||
<description>Send this value to receive the current state</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" min="0">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Interval for the automatic channel update in seconds. This value is only required if the peer has to
|
||||
send the “Refresh Value” command regularly in order to return the current properties. It is not required if the
|
||||
peer automatically forwards changed values to its clients.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
|
||||
@@ -242,6 +374,16 @@
|
||||
Multiple transformations can be chained by listing each transformation on a separate line,
|
||||
or by concatenating them with "∩".]]></description>
|
||||
</parameter>
|
||||
<parameter name="refreshValue" type="text">
|
||||
<label>Refresh Value</label>
|
||||
<description>Send this value to receive the current state</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" min="0">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Interval for the automatic channel update in seconds. This value is only required if the peer has to
|
||||
send the “Refresh Value” command regularly in order to return the current properties. It is not required if the
|
||||
peer automatically forwards changed values to its clients.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
|
||||
@@ -284,6 +426,16 @@
|
||||
Multiple transformations can be chained by listing each transformation on a separate line,
|
||||
or by concatenating them with "∩".]]></description>
|
||||
</parameter>
|
||||
<parameter name="refreshValue" type="text">
|
||||
<label>Refresh Value</label>
|
||||
<description>Send this value to receive the current state</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" min="0">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Interval for the automatic channel update in seconds. This value is only required if the peer has to
|
||||
send the “Refresh Value” command regularly in order to return the current properties. It is not required if the
|
||||
peer automatically forwards changed values to its clients.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.serial.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Provides an InputStream implementation that reads from a list of byte arrays.
|
||||
*
|
||||
* @author Roland Tapken - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ByteBufferInputStream extends InputStream {
|
||||
|
||||
private final LinkedList<byte[]> bufferQueue = new LinkedList<>();
|
||||
|
||||
private int available = 0;
|
||||
|
||||
private int pos = 0;
|
||||
|
||||
public void appendBuffer(byte[] bytes) {
|
||||
bufferQueue.add(bytes);
|
||||
available += bytes.length;
|
||||
}
|
||||
|
||||
public void appendString(String str, Charset charset) {
|
||||
appendBuffer(str.getBytes(charset));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (available <= 0) {
|
||||
throw new IOException("Nothing to read, on a socket input stream this would block");
|
||||
}
|
||||
|
||||
byte[] buffer = bufferQueue.get(0);
|
||||
int result = buffer[pos++];
|
||||
available -= 1;
|
||||
if (pos == buffer.length) {
|
||||
bufferQueue.remove(0);
|
||||
pos = 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return available;
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.serial.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.serial.internal.handler.SerialBridgeConfiguration;
|
||||
import org.openhab.binding.serial.internal.handler.SerialBridgeHandler;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
|
||||
/**
|
||||
* Provides a special implementation of CommonBridgeHandler that allows
|
||||
* access to the input- and output streams and changes the visibility
|
||||
* for #receiveAndProcess so that it can be used in the tests.
|
||||
*
|
||||
* @author Roland Tapken - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MockupBridgeHandler extends SerialBridgeHandler {
|
||||
|
||||
private final SerialBridgeConfiguration config;
|
||||
|
||||
MockupBridgeHandler(Bridge bridgeMock, SerialPortManager serialPortManager, SerialBridgeConfiguration config) {
|
||||
super(bridgeMock, serialPortManager);
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected <T> T getConfigAs(Class<T> configurationClass) {
|
||||
return (T) this.config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveAndProcess(final StringBuilder sb, final boolean firstAttempt) {
|
||||
super.receiveAndProcess(sb, firstAttempt);
|
||||
}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.serial.internal;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HexFormat;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.openhab.binding.serial.internal.handler.SerialBridgeConfiguration;
|
||||
import org.openhab.core.io.transport.serial.PortInUseException;
|
||||
import org.openhab.core.io.transport.serial.SerialPort;
|
||||
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
|
||||
import org.openhab.core.io.transport.serial.SerialPortManager;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
|
||||
/**
|
||||
* Implements some tests for the CommonBridgeHandler
|
||||
*
|
||||
* @author Roland Tapken - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SerialBindingHandlerTest {
|
||||
|
||||
private @Nullable MockupBridgeHandler handler;
|
||||
private @Nullable SerialBridgeConfiguration config;
|
||||
private @Nullable ByteBufferInputStream bin;
|
||||
private @Nullable ByteArrayOutputStream bos;
|
||||
private final ThingHandlerCallback callbackMock = Mockito.mock(ThingHandlerCallback.class);
|
||||
private final Bridge bridgeMock = Mockito.mock(Bridge.class);
|
||||
private final SerialPortIdentifier mockIdentifier = Mockito.mock(SerialPortIdentifier.class);
|
||||
private final SerialPort mockSerialPort = Mockito.mock(SerialPort.class);
|
||||
|
||||
private final SerialPortManager serialPortManager = new SerialPortManager() {
|
||||
@Override
|
||||
public @NonNull SerialPortIdentifier getIdentifier(final String name) {
|
||||
assertEquals("/dev/dummy-serial", name, "Expect the passed serial port name");
|
||||
return mockIdentifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Stream<SerialPortIdentifier> getIdentifiers() {
|
||||
Stream<SerialPortIdentifier> stream = Stream.empty();
|
||||
assertNotNull(stream);
|
||||
return stream;
|
||||
}
|
||||
};
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("null")
|
||||
public void setUp() throws PortInUseException, IOException {
|
||||
SerialBridgeConfiguration config = new SerialBridgeConfiguration();
|
||||
config.serialPort = "/dev/dummy-serial";
|
||||
this.config = config;
|
||||
this.handler = new MockupBridgeHandler(bridgeMock, serialPortManager, config);
|
||||
this.handler.setCallback(callbackMock);
|
||||
this.bin = new ByteBufferInputStream();
|
||||
this.bos = new ByteArrayOutputStream();
|
||||
resetMock();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
MockupBridgeHandler handler = this.handler;
|
||||
this.handler = null;
|
||||
if (handler != null) {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void resetMock() throws IOException, PortInUseException {
|
||||
reset(callbackMock, bridgeMock, mockSerialPort, mockIdentifier);
|
||||
doReturn(new ThingUID("serial:serialBridge:test")).when(bridgeMock).getUID();
|
||||
doReturn(mockSerialPort).when(mockIdentifier).open(anyString(), anyInt());
|
||||
doReturn(bin).when(mockSerialPort).getInputStream();
|
||||
doReturn(bos).when(mockSerialPort).getOutputStream();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initializeShouldCallTheCallback() throws PortInUseException, IOException {
|
||||
MockupBridgeHandler handler = this.handler;
|
||||
ThingHandlerCallback callbackMock = this.callbackMock;
|
||||
Bridge bridgeMock = this.bridgeMock;
|
||||
SerialBridgeConfiguration config = this.config;
|
||||
assertNotNull(handler);
|
||||
assertNotNull(callbackMock);
|
||||
assertNotNull(bridgeMock);
|
||||
assertNotNull(config);
|
||||
|
||||
handler.initialize();
|
||||
verify(callbackMock).statusUpdated(eq(bridgeMock), argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
|
||||
resetMock();
|
||||
config.charset = "UTF-8";
|
||||
handler.initialize();
|
||||
verify(callbackMock).statusUpdated(eq(bridgeMock), argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
|
||||
resetMock();
|
||||
config.charset = "foobar";
|
||||
handler.initialize();
|
||||
verify(callbackMock).statusUpdated(eq(bridgeMock), argThat(arg -> arg.getStatus().equals(ThingStatus.OFFLINE)));
|
||||
verify(callbackMock).statusUpdated(eq(bridgeMock),
|
||||
argThat(arg -> arg.getStatusDetail().equals(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR)));
|
||||
|
||||
resetMock();
|
||||
config.charset = "HEX";
|
||||
handler.initialize();
|
||||
verify(callbackMock).statusUpdated(eq(bridgeMock), argThat(arg -> arg.getStatus().equals(ThingStatus.OFFLINE)));
|
||||
verify(callbackMock).statusUpdated(eq(bridgeMock),
|
||||
argThat(arg -> arg.getStatusDetail().equals(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR)));
|
||||
|
||||
resetMock();
|
||||
config.eolPattern = "Invalid(Pattern";
|
||||
handler.initialize();
|
||||
verify(callbackMock).statusUpdated(eq(bridgeMock), argThat(arg -> arg.getStatus().equals(ThingStatus.OFFLINE)));
|
||||
verify(callbackMock).statusUpdated(eq(bridgeMock),
|
||||
argThat(arg -> arg.getStatusDetail().equals(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR)));
|
||||
|
||||
resetMock();
|
||||
config.eolPattern = "ValidPattern";
|
||||
handler.initialize();
|
||||
verify(callbackMock).statusUpdated(eq(bridgeMock), argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWriteUtf8String() {
|
||||
MockupBridgeHandler handler = this.handler;
|
||||
SerialBridgeConfiguration config = this.config;
|
||||
ByteArrayOutputStream bos = this.bos;
|
||||
assertNotNull(config);
|
||||
assertNotNull(handler);
|
||||
assertNotNull(bos);
|
||||
|
||||
config.charset = "UTF-8";
|
||||
handler.initialize();
|
||||
handler.writeString("föö");
|
||||
assertEquals("föö", bos.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWriteLatin1String() {
|
||||
MockupBridgeHandler handler = this.handler;
|
||||
SerialBridgeConfiguration config = this.config;
|
||||
ByteArrayOutputStream bos = this.bos;
|
||||
assertNotNull(config);
|
||||
assertNotNull(handler);
|
||||
assertNotNull(bos);
|
||||
|
||||
config.charset = "ISO-8859-1";
|
||||
handler.initialize();
|
||||
handler.writeString("föö");
|
||||
assertEquals("föö", bos.toString(StandardCharsets.ISO_8859_1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWriteHexString() {
|
||||
MockupBridgeHandler handler = this.handler;
|
||||
SerialBridgeConfiguration config = this.config;
|
||||
ByteArrayOutputStream bos = this.bos;
|
||||
assertNotNull(config);
|
||||
assertNotNull(handler);
|
||||
assertNotNull(bos);
|
||||
|
||||
config.charset = "HEX";
|
||||
config.eolPattern = "\\b0A";
|
||||
handler.initialize();
|
||||
|
||||
byte[] bytes = { 'T', 'E', 'S', 'T' };
|
||||
String str = HexFormat.of().withDelimiter(" ").formatHex(bytes);
|
||||
assertEquals(str, "54 45 53 54");
|
||||
handler.writeString(str);
|
||||
assertArrayEquals(bytes, bos.toByteArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReadUtf8String() {
|
||||
MockupBridgeHandler handler = this.handler;
|
||||
SerialBridgeConfiguration config = this.config;
|
||||
ByteBufferInputStream bin = this.bin;
|
||||
assertNotNull(config);
|
||||
assertNotNull(handler);
|
||||
assertNotNull(bin);
|
||||
|
||||
config.charset = "UTF-8";
|
||||
handler.initialize();
|
||||
bin.appendString("föö", StandardCharsets.UTF_8);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
handler.receiveAndProcess(sb, false);
|
||||
assertEquals("föö", sb.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReadLatin1String() {
|
||||
MockupBridgeHandler handler = this.handler;
|
||||
SerialBridgeConfiguration config = this.config;
|
||||
ByteBufferInputStream bin = this.bin;
|
||||
assertNotNull(config);
|
||||
assertNotNull(handler);
|
||||
assertNotNull(bin);
|
||||
|
||||
config.charset = "ISO-8859-1";
|
||||
handler.initialize();
|
||||
bin.appendString("föö", StandardCharsets.ISO_8859_1);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
handler.receiveAndProcess(sb, false);
|
||||
assertEquals("föö", sb.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReadHexString() {
|
||||
MockupBridgeHandler handler = this.handler;
|
||||
SerialBridgeConfiguration config = this.config;
|
||||
ByteArrayOutputStream bos = this.bos;
|
||||
ByteBufferInputStream bin = this.bin;
|
||||
assertNotNull(config);
|
||||
assertNotNull(handler);
|
||||
assertNotNull(bos);
|
||||
assertNotNull(bin);
|
||||
|
||||
config.charset = "HEX";
|
||||
config.eolPattern = "\\b0A";
|
||||
handler.initialize();
|
||||
bin.appendBuffer(new byte[] { 'T', 'E', 'S', 'T' });
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
handler.receiveAndProcess(sb, false);
|
||||
assertEquals("54 45 53 54", sb.toString());
|
||||
|
||||
// Test with newlines
|
||||
bin.appendBuffer(new byte[] { 'T', 'E', 'S', 'T', '1', '\n', 'T', 'E', 'S', 'T', '2' });
|
||||
sb = new StringBuilder();
|
||||
handler.receiveAndProcess(sb, false);
|
||||
assertEquals("54 45 53 54 31 0A\n54 45 53 54 32", sb.toString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user