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) {
71 SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(config.port);
73 if (portIdentifier == null) {
74 String availablePorts = serialPortManager.getIdentifiers().map(id -> id.getName())
75 .collect(Collectors.joining(System.lineSeparator()));
76 String description = String.format("Serial port '%s' could not be found. Available ports are:%n%s",
77 config.port, availablePorts);
78 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, description);
83 logger.info("Connecting to the Oceanic water softener using {}.", config.port);
84 serialPort = portIdentifier.open(this.getThing().getUID().getBindingId(), 2000);
86 serialPort.setSerialPortParams(BAUD, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
87 SerialPort.PARITY_NONE);
88 serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
89 serialPort.enableReceiveThreshold(1);
90 serialPort.disableReceiveTimeout();
92 inputStream = serialPort.getInputStream();
93 outputStream = serialPort.getOutputStream();
95 serialPort.addEventListener(this);
96 serialPort.notifyOnDataAvailable(true);
98 readerThread = new SerialPortReader(inputStream);
101 updateStatus(ThingStatus.ONLINE);
103 } catch (PortInUseException portInUseException) {
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Port in use: " + config.port);
105 } catch (UnsupportedCommOperationException | IOException e) {
106 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Communication error");
107 } catch (TooManyListenersException e) {
108 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
109 "Too many listeners to serial port.");
115 public void dispose() {
116 if (readerThread != null) {
118 readerThread.interrupt();
120 } catch (InterruptedException e) {
121 logger.error("An exception occurred while interrupting the serial port reader thread : {}",
126 if (inputStream != null) {
129 } catch (IOException e) {
130 logger.debug("Error while closing the input stream: {}", e.getMessage());
133 if (outputStream != null) {
135 outputStream.close();
136 } catch (IOException e) {
137 logger.debug("Error while closing the output stream: {}", e.getMessage());
140 if (serialPort != null) {
153 protected String requestResponse(String commandAsString) {
154 synchronized (this) {
155 SerialOceanicBindingConfiguration config = getConfigAs(SerialOceanicBindingConfiguration.class);
157 Throttler.lock(config.port);
159 lastLineReceived = "";
160 String request = commandAsString + "\r";
161 String response = null;
164 if (logger.isTraceEnabled()) {
165 logger.trace("Requesting : {} ('{}')", request, request.getBytes());
167 outputStream.write(request.getBytes());
168 outputStream.flush();
169 } catch (IOException e) {
170 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
171 "Error writing '" + request + "' to serial port " + config.port + " : " + e.getMessage());
174 long timeStamp = System.currentTimeMillis();
175 while (lastLineReceived.equals("")) {
178 } catch (InterruptedException e) {
179 logger.error("An exception occurred while putting the thread to sleep: {}", e.getMessage());
181 if (System.currentTimeMillis() - timeStamp > REQUEST_TIMEOUT) {
182 logger.warn("A timeout occurred while requesting data from the water softener");
183 readerThread.reset();
187 response = lastLineReceived;
189 Throttler.unlock(config.port);
196 public void serialEvent(SerialPortEvent serialPortEvent) {
197 if (logger.isTraceEnabled()) {
198 logger.trace("Received a serial port event : {}", serialPortEvent.getEventType());
202 public class SerialPortReader extends Thread {
204 private boolean interrupted = false;
205 private InputStream inputStream;
208 SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss,SSS");
210 public SerialPortReader(InputStream in) {
211 this.inputStream = in;
212 this.setName("SerialPortReader-" + getThing().getUID());
215 public void reset() {
216 logger.trace("Resetting the SerialPortReader");
221 public void interrupt() {
222 logger.trace("Interrupting the SerialPortReader");
229 logger.trace("Starting the serial port reader");
231 byte[] dataBuffer = new byte[bufferSize];
232 byte[] tmpData = new byte[bufferSize];
235 final byte lineFeed = (byte) '\n';
236 final byte carriageReturn = (byte) '\r';
237 final byte nullChar = (byte) '\0';
243 while (!interrupted) {
244 logger.trace("Reading the inputStream");
246 if ((len = inputStream.read(tmpData)) > -1) {
247 if (logger.isTraceEnabled()) {
248 StringBuilder sb = new StringBuilder();
249 for (int i = 0; i < len; i++) {
250 sb.append(String.format("%02X ", tmpData[i]));
252 logger.trace("Read {} bytes : {}", len, sb.toString());
254 for (int i = 0; i < len; i++) {
255 if (logger.isTraceEnabled()) {
256 logger.trace("Byte {} equals '{}' (hex '{}')", i, new String(new byte[] { tmpData[i] }),
257 String.format("%02X", tmpData[i]));
260 if (tmpData[i] != lineFeed && tmpData[i] != carriageReturn && tmpData[i] != nullChar) {
261 dataBuffer[index++] = tmpData[i];
262 if (logger.isTraceEnabled()) {
263 logger.trace("dataBuffer[{}] set to '{}'(hex '{}')", index - 1,
264 new String(new byte[] { dataBuffer[index - 1] }),
265 String.format("%02X", dataBuffer[index - 1]));
269 if (i > 0 && (tmpData[i] == lineFeed || tmpData[i] == carriageReturn
270 || tmpData[i] == nullChar)) {
272 if (logger.isTraceEnabled()) {
273 logger.trace("The resulting line is '{}'",
274 new String(Arrays.copyOf(dataBuffer, index)));
276 line = StringUtils.chomp(new String(Arrays.copyOf(dataBuffer, index)));
277 line = line.replace(",", ".");
281 lastLineReceived = line;
286 if (index == bufferSize) {
294 } catch (InterruptedException e) {
298 } catch (Exception e) {
299 logger.error("An exception occurred while reading serial port : {}", e.getMessage(), e);
300 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());