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.Objects;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
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.openhab.core.util.StringUtils;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * The {@link NetworkOceanicThingHandler} implements {@link OceanicThingHandler} for an Oceanic water softener that is
38 * reached using a socat TCP proxy
40 * @author Karel Goderis - Initial contribution
43 public class NetworkOceanicThingHandler extends OceanicThingHandler {
45 private static final int REQUEST_TIMEOUT = 3000;
46 private static final int RECONNECT_INTERVAL = 15;
48 private final Logger logger = LoggerFactory.getLogger(NetworkOceanicThingHandler.class);
50 private @Nullable Socket socket;
51 private @Nullable InputStream inputStream;
52 private @Nullable OutputStream outputStream;
53 protected @Nullable ScheduledFuture<?> reconnectJob;
55 public NetworkOceanicThingHandler(Thing thing) {
60 public void initialize() {
63 NetworkOceanicBindingConfiguration config = getConfigAs(NetworkOceanicBindingConfiguration.class);
66 Socket socket = new Socket(config.ipAddress, config.portNumber);
68 socket.setSoTimeout(REQUEST_TIMEOUT);
69 outputStream = socket.getOutputStream();
70 inputStream = socket.getInputStream();
71 updateStatus(ThingStatus.ONLINE);
72 } catch (UnknownHostException e) {
73 logger.error("An exception occurred while resolving host {}:{} : '{}'", config.ipAddress, config.portNumber,
75 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
76 "Could not resolve host " + config.ipAddress + ": " + e.getMessage());
77 } catch (IOException e) {
78 logger.debug("An exception occurred while connecting to host {}:{} : '{}'", config.ipAddress,
79 config.portNumber, e.getMessage());
80 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
81 "Could not connect to host " + config.ipAddress + ": " + e.getMessage());
82 reconnectJob = scheduler.schedule(reconnectRunnable, RECONNECT_INTERVAL, TimeUnit.SECONDS);
87 public void dispose() {
88 NetworkOceanicBindingConfiguration config = getConfigAs(NetworkOceanicBindingConfiguration.class);
89 Socket socket = this.socket;
93 } catch (IOException e) {
94 logger.error("An exception occurred while disconnecting to host {}:{} : '{}'", config.ipAddress,
95 config.portNumber, e.getMessage());
107 protected @Nullable String requestResponse(String commandAsString) {
108 synchronized (this) {
109 if (getThing().getStatus() == ThingStatus.ONLINE) {
110 NetworkOceanicBindingConfiguration config = getConfigAs(NetworkOceanicBindingConfiguration.class);
111 Throttler.lock(config.ipAddress);
113 String request = commandAsString + "\r";
115 byte[] dataBuffer = new byte[bufferSize];
116 byte[] tmpData = new byte[bufferSize];
120 boolean sequenceFound = false;
122 final byte lineFeed = (byte) '\n';
123 final byte carriageReturn = (byte) '\r';
124 final byte nullChar = (byte) '\0';
125 final byte eChar = (byte) 'E';
126 final byte rChar = (byte) 'R';
129 logger.debug("Sending request '{}'", request);
131 outputStream.write(request.getBytes());
132 outputStream.flush();
135 if ((len = inputStream.read(tmpData)) > -1) {
136 if (logger.isTraceEnabled()) {
137 StringBuilder sb = new StringBuilder();
138 for (int i = 0; i < len; i++) {
139 sb.append(String.format("%02X ", tmpData[i]));
141 logger.trace("Read {} bytes : {}", len, sb.toString());
144 for (int i = 0; i < len; i++) {
145 if (logger.isTraceEnabled()) {
146 logger.trace("Byte {} equals '{}' (hex '{}')", i,
147 new String(new byte[] { tmpData[i] }), String.format("%02X", tmpData[i]));
150 if (tmpData[i] == nullChar && !sequenceFound) {
151 sequenceFound = true;
152 logger.trace("Start of sequence found");
155 if (sequenceFound && tmpData[i] != lineFeed && tmpData[i] != carriageReturn
156 && tmpData[i] != nullChar) {
157 dataBuffer[index++] = tmpData[i];
158 if (logger.isTraceEnabled()) {
159 logger.trace("dataBuffer[{}] set to '{}'(hex '{}')", index - 1,
160 new String(new byte[] { dataBuffer[index - 1] }),
161 String.format("%02X", dataBuffer[index - 1]));
165 if (sequenceFound && i >= 2) {
166 if (tmpData[i - 2] == eChar && tmpData[i - 1] == rChar && tmpData[i] == rChar) {
167 // Received ERR from the device.
172 if (sequenceFound && i > 0
173 && (tmpData[i - 1] != carriageReturn && tmpData[i] == nullChar)) {
175 // Ignore trash received
176 if (logger.isTraceEnabled()) {
177 StringBuilder sb = new StringBuilder();
178 for (int j = 0; j < i; j++) {
179 sb.append(String.format("%02X ", tmpData[j]));
181 logger.trace("Ingoring {} bytes : {}", i, sb);
185 if (sequenceFound && (tmpData[i] == carriageReturn)) {
187 line = new String(Arrays.copyOf(dataBuffer, index));
188 logger.debug("Received response '{}'", line);
189 line = Objects.requireNonNull(StringUtils.chomp(line));
190 line = line.replace(",", ".");
198 if (index == bufferSize) {
204 } catch (IOException e) {
205 logger.debug("An exception occurred while quering host {}:{} : '{}'", config.ipAddress,
206 config.portNumber, e.getMessage(), e);
207 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
208 reconnectJob = scheduler.schedule(reconnectRunnable, RECONNECT_INTERVAL, TimeUnit.SECONDS);
210 Throttler.unlock(config.ipAddress);
217 private Runnable reconnectRunnable = () -> {