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.net.Socket;
19 import java.net.UnknownHostException;
20 import java.util.Arrays;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.apache.commons.lang3.StringUtils;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.oceanic.internal.NetworkOceanicBindingConfiguration;
28 import org.openhab.binding.oceanic.internal.Throttler;
29 import org.openhab.core.thing.Thing;
30 import org.openhab.core.thing.ThingStatus;
31 import org.openhab.core.thing.ThingStatusDetail;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * The {@link NetworkOceanicThingHandler} implements {@link OceanicThingHandler} for an Oceanic water softener that is
37 * reached using a socat TCP proxy
39 * @author Karel Goderis - Initial contribution
42 public class NetworkOceanicThingHandler extends OceanicThingHandler {
44 private static final int REQUEST_TIMEOUT = 3000;
45 private static final int RECONNECT_INTERVAL = 15;
47 private final Logger logger = LoggerFactory.getLogger(NetworkOceanicThingHandler.class);
49 private @Nullable Socket socket;
50 private @Nullable InputStream inputStream;
51 private @Nullable OutputStream outputStream;
52 protected @Nullable ScheduledFuture<?> reconnectJob;
54 public NetworkOceanicThingHandler(Thing thing) {
59 public void initialize() {
62 NetworkOceanicBindingConfiguration config = getConfigAs(NetworkOceanicBindingConfiguration.class);
65 socket = new Socket(config.ipAddress, config.portNumber);
66 socket.setSoTimeout(REQUEST_TIMEOUT);
67 outputStream = socket.getOutputStream();
68 inputStream = socket.getInputStream();
69 updateStatus(ThingStatus.ONLINE);
70 } catch (UnknownHostException e) {
71 logger.error("An exception occurred while resolving host {}:{} : '{}'", config.ipAddress, config.portNumber,
73 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
74 "Could not resolve host " + config.ipAddress + ": " + e.getMessage());
75 } catch (IOException e) {
76 logger.debug("An exception occurred while connecting to host {}:{} : '{}'", config.ipAddress,
77 config.portNumber, e.getMessage());
78 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
79 "Could not connect to host " + config.ipAddress + ": " + e.getMessage());
80 reconnectJob = scheduler.schedule(reconnectRunnable, RECONNECT_INTERVAL, TimeUnit.SECONDS);
85 public void dispose() {
86 NetworkOceanicBindingConfiguration config = getConfigAs(NetworkOceanicBindingConfiguration.class);
91 } catch (IOException e) {
92 logger.error("An exception occurred while disconnecting to host {}:{} : '{}'", config.ipAddress,
93 config.portNumber, e.getMessage());
105 protected @Nullable String requestResponse(String commandAsString) {
106 synchronized (this) {
107 if (getThing().getStatus() == ThingStatus.ONLINE) {
108 NetworkOceanicBindingConfiguration config = getConfigAs(NetworkOceanicBindingConfiguration.class);
109 Throttler.lock(config.ipAddress);
111 String request = commandAsString + "\r";
113 byte[] dataBuffer = new byte[bufferSize];
114 byte[] tmpData = new byte[bufferSize];
118 boolean sequenceFound = false;
120 final byte lineFeed = (byte) '\n';
121 final byte carriageReturn = (byte) '\r';
122 final byte nullChar = (byte) '\0';
123 final byte eChar = (byte) 'E';
124 final byte rChar = (byte) 'R';
127 logger.debug("Sending request '{}'", request);
129 outputStream.write(request.getBytes());
130 outputStream.flush();
133 if ((len = inputStream.read(tmpData)) > -1) {
134 if (logger.isTraceEnabled()) {
135 StringBuilder sb = new StringBuilder();
136 for (int i = 0; i < len; i++) {
137 sb.append(String.format("%02X ", tmpData[i]));
139 logger.trace("Read {} bytes : {}", len, sb.toString());
142 for (int i = 0; i < len; i++) {
143 if (logger.isTraceEnabled()) {
144 logger.trace("Byte {} equals '{}' (hex '{}')", i,
145 new String(new byte[] { tmpData[i] }), String.format("%02X", tmpData[i]));
148 if (tmpData[i] == nullChar && !sequenceFound) {
149 sequenceFound = true;
150 logger.trace("Start of sequence found");
153 if (sequenceFound && tmpData[i] != lineFeed && tmpData[i] != carriageReturn
154 && tmpData[i] != nullChar) {
155 dataBuffer[index++] = tmpData[i];
156 if (logger.isTraceEnabled()) {
157 logger.trace("dataBuffer[{}] set to '{}'(hex '{}')", index - 1,
158 new String(new byte[] { dataBuffer[index - 1] }),
159 String.format("%02X", dataBuffer[index - 1]));
163 if (sequenceFound && i >= 2) {
164 if (tmpData[i - 2] == eChar && tmpData[i - 1] == rChar && tmpData[i] == rChar) {
165 // Received ERR from the device.
170 if (sequenceFound && i > 0
171 && (tmpData[i - 1] != carriageReturn && tmpData[i] == nullChar)) {
173 // Ignore trash received
174 if (logger.isTraceEnabled()) {
175 StringBuilder sb = new StringBuilder();
176 for (int j = 0; j < i; j++) {
177 sb.append(String.format("%02X ", tmpData[j]));
179 logger.trace("Ingoring {} bytes : {}", i, sb);
183 if (sequenceFound && (tmpData[i] == carriageReturn)) {
185 line = new String(Arrays.copyOf(dataBuffer, index));
186 logger.debug("Received response '{}'", line);
187 line = StringUtils.chomp(line);
188 line = line.replace(",", ".");
196 if (index == bufferSize) {
203 } catch (IOException e) {
204 logger.debug("An exception occurred while quering host {}:{} : '{}'", config.ipAddress,
205 config.portNumber, e.getMessage(), e);
206 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
207 reconnectJob = scheduler.schedule(reconnectRunnable, RECONNECT_INTERVAL, TimeUnit.SECONDS);
209 Throttler.unlock(config.ipAddress);
216 private Runnable reconnectRunnable = () -> {