2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.oceanic.internal.handler;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.OutputStream;
18 import java.text.SimpleDateFormat;
19 import java.util.Arrays;
20 import java.util.TooManyListenersException;
21 import java.util.stream.Collectors;
23 import org.apache.commons.lang3.StringUtils;
24 import org.openhab.binding.oceanic.internal.SerialOceanicBindingConfiguration;
25 import org.openhab.binding.oceanic.internal.Throttler;
26 import org.openhab.core.io.transport.serial.PortInUseException;
27 import org.openhab.core.io.transport.serial.SerialPort;
28 import org.openhab.core.io.transport.serial.SerialPortEvent;
29 import org.openhab.core.io.transport.serial.SerialPortEventListener;
30 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
31 import org.openhab.core.io.transport.serial.SerialPortManager;
32 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
40 * The {@link SerialOceanicThingHandler} implements {@link OceanicThingHandler} for an Oceanic water softener that is
41 * directly connected to a serial port of the openHAB host
43 * @author Karel Goderis - Initial contribution
45 public class SerialOceanicThingHandler extends OceanicThingHandler implements SerialPortEventListener {
47 private static final long REQUEST_TIMEOUT = 10000;
48 private static final int BAUD = 19200;
50 private final Logger logger = LoggerFactory.getLogger(SerialOceanicThingHandler.class);
52 private final SerialPortManager serialPortManager;
53 private SerialPort serialPort;
54 private InputStream inputStream;
55 private OutputStream outputStream;
56 private SerialPortReader readerThread;
58 public SerialOceanicThingHandler(Thing thing, SerialPortManager serialPortManager) {
60 this.serialPortManager = serialPortManager;
64 public void initialize() {
67 SerialOceanicBindingConfiguration config = getConfigAs(SerialOceanicBindingConfiguration.class);
69 if (serialPort == null && config.port != null) {
70 SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(config.port);
72 if (portIdentifier == null) {
73 String availablePorts = serialPortManager.getIdentifiers().map(id -> id.getName())
74 .collect(Collectors.joining(System.lineSeparator()));
75 String description = String.format("Serial port '%s' could not be found. Available ports are:%n%s",
76 config.port, availablePorts);
77 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, description);
82 logger.info("Connecting to the Oceanic water softener using {}.", config.port);
83 serialPort = portIdentifier.open(this.getThing().getUID().getBindingId(), 2000);
85 serialPort.setSerialPortParams(BAUD, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
86 SerialPort.PARITY_NONE);
87 serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
88 serialPort.enableReceiveThreshold(1);
89 serialPort.disableReceiveTimeout();
91 inputStream = serialPort.getInputStream();
92 outputStream = serialPort.getOutputStream();
94 serialPort.addEventListener(this);
95 serialPort.notifyOnDataAvailable(true);
97 readerThread = new SerialPortReader(inputStream);
100 updateStatus(ThingStatus.ONLINE);
102 } catch (PortInUseException portInUseException) {
103 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Port in use: " + config.port);
104 } catch (UnsupportedCommOperationException | IOException e) {
105 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Communication error");
106 } catch (TooManyListenersException e) {
107 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
108 "Too many listeners to serial port.");
114 public void dispose() {
115 if (readerThread != null) {
117 readerThread.interrupt();
119 } catch (InterruptedException e) {
120 logger.error("An exception occurred while interrupting the serial port reader thread : {}",
125 if (inputStream != null) {
128 } catch (IOException e) {
129 logger.debug("Error while closing the input stream: {}", e.getMessage());
132 if (outputStream != null) {
134 outputStream.close();
135 } catch (IOException e) {
136 logger.debug("Error while closing the output stream: {}", e.getMessage());
139 if (serialPort != null) {
152 protected String requestResponse(String commandAsString) {
153 synchronized (this) {
154 SerialOceanicBindingConfiguration config = getConfigAs(SerialOceanicBindingConfiguration.class);
156 Throttler.lock(config.port);
158 lastLineReceived = "";
159 String request = commandAsString + "\r";
160 String response = null;
163 if (logger.isTraceEnabled()) {
164 logger.trace("Requesting : {} ('{}')", request, request.getBytes());
166 outputStream.write(request.getBytes());
167 outputStream.flush();
168 } catch (IOException e) {
169 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
170 "Error writing '" + request + "' to serial port " + config.port + " : " + e.getMessage());
173 long timeStamp = System.currentTimeMillis();
174 while ("".equals(lastLineReceived)) {
177 } catch (InterruptedException e) {
178 logger.error("An exception occurred while putting the thread to sleep: {}", e.getMessage());
180 if (System.currentTimeMillis() - timeStamp > REQUEST_TIMEOUT) {
181 logger.warn("A timeout occurred while requesting data from the water softener");
182 readerThread.reset();
186 response = lastLineReceived;
188 Throttler.unlock(config.port);
195 public void serialEvent(SerialPortEvent serialPortEvent) {
196 if (logger.isTraceEnabled()) {
197 logger.trace("Received a serial port event : {}", serialPortEvent.getEventType());
201 public class SerialPortReader extends Thread {
203 private boolean interrupted = false;
204 private InputStream inputStream;
207 SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss,SSS");
209 public SerialPortReader(InputStream in) {
210 this.inputStream = in;
211 this.setName("SerialPortReader-" + getThing().getUID());
214 public void reset() {
215 logger.trace("Resetting the SerialPortReader");
220 public void interrupt() {
221 logger.trace("Interrupting the SerialPortReader");
228 logger.trace("Starting the serial port reader");
230 byte[] dataBuffer = new byte[bufferSize];
231 byte[] tmpData = new byte[bufferSize];
234 final byte lineFeed = (byte) '\n';
235 final byte carriageReturn = (byte) '\r';
236 final byte nullChar = (byte) '\0';
242 while (!interrupted) {
243 logger.trace("Reading the inputStream");
245 if ((len = inputStream.read(tmpData)) > -1) {
246 if (logger.isTraceEnabled()) {
247 StringBuilder sb = new StringBuilder();
248 for (int i = 0; i < len; i++) {
249 sb.append(String.format("%02X ", tmpData[i]));
251 logger.trace("Read {} bytes : {}", len, sb.toString());
253 for (int i = 0; i < len; i++) {
254 if (logger.isTraceEnabled()) {
255 logger.trace("Byte {} equals '{}' (hex '{}')", i, new String(new byte[] { tmpData[i] }),
256 String.format("%02X", tmpData[i]));
259 if (tmpData[i] != lineFeed && tmpData[i] != carriageReturn && tmpData[i] != nullChar) {
260 dataBuffer[index++] = tmpData[i];
261 if (logger.isTraceEnabled()) {
262 logger.trace("dataBuffer[{}] set to '{}'(hex '{}')", index - 1,
263 new String(new byte[] { dataBuffer[index - 1] }),
264 String.format("%02X", dataBuffer[index - 1]));
268 if (i > 0 && (tmpData[i] == lineFeed || tmpData[i] == carriageReturn
269 || tmpData[i] == nullChar)) {
271 if (logger.isTraceEnabled()) {
272 logger.trace("The resulting line is '{}'",
273 new String(Arrays.copyOf(dataBuffer, index)));
275 line = StringUtils.chomp(new String(Arrays.copyOf(dataBuffer, index)));
276 line = line.replace(",", ".");
280 lastLineReceived = line;
285 if (index == bufferSize) {
293 } catch (InterruptedException e) {
297 } catch (Exception e) {
298 logger.error("An exception occurred while reading serial port : {}", e.getMessage(), e);
299 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());