2 * Copyright (c) 2010-2024 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.insteon.internal.driver.hub;
15 import java.io.BufferedInputStream;
16 import java.io.ByteArrayOutputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.OutputStream;
20 import java.net.HttpURLConnection;
22 import java.nio.ByteBuffer;
23 import java.nio.charset.StandardCharsets;
24 import java.util.Base64;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.insteon.internal.InsteonBindingConstants;
29 import org.openhab.binding.insteon.internal.driver.IOStream;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
34 * Implements IOStream for a Hub 2014 device
36 * @author Daniel Pfrommer - Initial contribution
37 * @author Rob Nielsen - Port to openHAB 2 insteon binding
41 public class HubIOStream extends IOStream implements Runnable {
42 private final Logger logger = LoggerFactory.getLogger(HubIOStream.class);
44 private static final String BS_START = "<BS>";
45 private static final String BS_END = "</BS>";
47 /** time between polls (in milliseconds */
48 private int pollTime = 1000;
50 private String baseUrl;
51 private @Nullable String auth = null;
53 private @Nullable Thread pollThread = null;
55 // index of the last byte we have read in the buffer
56 private int bufferIdx = -1;
58 private boolean polling;
61 * Constructor for HubIOStream
63 * @param host host name of hub device
64 * @param port port to connect to
65 * @param pollTime time between polls (in milliseconds)
66 * @param user hub user name
67 * @param pass hub password
69 public HubIOStream(String host, int port, int pollTime, @Nullable String user, @Nullable String pass) {
70 this.pollTime = pollTime;
72 StringBuilder s = new StringBuilder();
76 s.append(":").append(port);
78 baseUrl = s.toString();
80 if (user != null && pass != null) {
81 auth = "Basic " + Base64.getEncoder().encodeToString((user + ":" + pass).getBytes(StandardCharsets.UTF_8));
86 public boolean open() {
89 } catch (IOException e) {
90 logger.warn("open failed: {}", e.getMessage());
94 in = new HubInputStream();
95 out = new HubOutputStream();
98 pollThread = new Thread(this);
99 setParamsAndStart(pollThread);
104 private void setParamsAndStart(@Nullable Thread thread) {
105 if (thread != null) {
106 thread.setName("OH-binding-" + InsteonBindingConstants.BINDING_ID + "-hubPoller");
107 thread.setDaemon(true);
113 public void close() {
116 if (pollThread != null) {
120 InputStream in = this.in;
124 } catch (IOException e) {
125 logger.warn("failed to close input stream", e);
130 OutputStream out = this.out;
134 } catch (IOException e) {
135 logger.warn("failed to close output stream", e);
142 * Fetches the latest status buffer from the Hub
144 * @return string with status buffer
145 * @throws IOException
147 private synchronized String bufferStatus() throws IOException {
148 String result = getURL("/buffstatus.xml");
150 int start = result.indexOf(BS_START);
152 throw new IOException("malformed bufferstatus.xml");
154 start += BS_START.length();
156 int end = result.indexOf(BS_END, start);
158 throw new IOException("malformed bufferstatus.xml");
161 return result.substring(start, end).trim();
165 * Sends command to Hub to clear the status buffer
167 * @throws IOException
169 private synchronized void clearBuffer() throws IOException {
170 logger.trace("clearing buffer");
176 * Sends Insteon message (byte array) as a readable ascii string to the Hub
178 * @param msg byte array representing the Insteon message
179 * @throws IOException in case of I/O error
181 public synchronized void write(ByteBuffer msg) throws IOException {
182 poll(); // fetch the status buffer before we send out commands
184 StringBuilder b = new StringBuilder();
185 while (msg.remaining() > 0) {
186 b.append(String.format("%02x", msg.get()));
188 String hexMSG = b.toString();
189 logger.trace("writing a message");
190 getURL("/3?" + hexMSG + "=I=3");
195 * Polls the Hub web interface to fetch the status buffer
197 * @throws IOException if something goes wrong with I/O
199 public synchronized void poll() throws IOException {
200 String buffer = bufferStatus(); // fetch via http call
201 logger.trace("poll: {}", buffer);
203 // The Hub maintains a ring buffer where the last two digits (in hex!) represent
204 // the position of the last byte read.
206 String data = buffer.substring(0, buffer.length() - 2); // pure data w/o index pointer
210 nIdx = Integer.parseInt(buffer.substring(buffer.length() - 2, buffer.length()), 16);
211 } catch (NumberFormatException e) {
213 logger.warn("invalid buffer size received in line: {}", buffer);
217 if (bufferIdx == -1) {
218 // this is the first call or first call after error, no need for buffer copying
220 return; // XXX why return here????
223 if (allZeros(data)) {
224 logger.trace("skip cleared buffer");
229 StringBuilder msg = new StringBuilder();
230 if (nIdx < bufferIdx) {
231 String msgStart = data.substring(bufferIdx, data.length());
232 String msgEnd = data.substring(0, nIdx);
233 if (allZeros(msgStart)) {
234 logger.trace("discard cleared buffer wrap around msg start");
238 msg.append(msgStart + msgEnd);
239 logger.trace("wrap around: copying new data on: {}", msg.toString());
241 msg.append(data.substring(bufferIdx, nIdx));
242 logger.trace("no wrap: appending new data: {}", msg.toString());
244 if (msg.length() != 0) {
245 ByteBuffer buf = ByteBuffer.wrap(hexStringToByteArray(msg.toString()));
246 InputStream in = this.in;
248 ((HubInputStream) in).handle(buf);
250 logger.warn("in is null");
256 private boolean allZeros(String s) {
257 return "0".repeat(s.length()).equals(s);
261 * Helper method to fetch url from http server
263 * @param resource the url
264 * @return contents returned by http server
265 * @throws IOException
267 private String getURL(String resource) throws IOException {
268 String url = baseUrl + resource;
270 HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
272 connection.setConnectTimeout(30000);
273 connection.setReadTimeout(10000);
274 connection.setUseCaches(false);
275 connection.setDoInput(true);
276 connection.setDoOutput(false);
278 connection.setRequestProperty("Authorization", auth);
281 logger.debug("getting {}", url);
283 int responseCode = connection.getResponseCode();
284 if (responseCode != 200) {
285 if (responseCode == 401) {
287 "Bad username or password. See the label on the bottom of the hub for the correct login information.");
288 throw new IOException("login credentials are incorrect");
290 String message = url + " failed with the response code: " + responseCode;
291 logger.warn(message);
292 throw new IOException(message);
296 return getData(connection.getInputStream());
298 connection.disconnect();
302 private String getData(InputStream is) throws IOException {
303 BufferedInputStream bis = new BufferedInputStream(is);
305 ByteArrayOutputStream baos = new ByteArrayOutputStream();
306 byte[] buffer = new byte[1024];
308 while ((length = bis.read(buffer)) != -1) {
309 baos.write(buffer, 0, length);
312 String s = baos.toString();
320 * Entry point for thread
327 } catch (IOException e) {
328 logger.warn("got exception while polling: {}", e.toString());
331 Thread.sleep(pollTime);
332 } catch (InterruptedException e) {
339 * Helper function to convert an ascii hex string (received from hub)
342 * @param s string received from hub
343 * @return simple byte array
345 public static byte[] hexStringToByteArray(String s) {
346 int len = s.length();
347 byte[] bytes = new byte[len / 2];
348 for (int i = 0; i < len; i += 2) {
349 bytes[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
356 * Implements an InputStream for the Hub 2014
358 * @author Daniel Pfrommer - Initial contribution
361 public class HubInputStream extends InputStream {
363 // A buffer to keep bytes while we are waiting for the inputstream to read
364 private ReadByteBuffer buffer = new ReadByteBuffer(1024);
366 public HubInputStream() {
369 public void handle(ByteBuffer b) throws IOException {
370 // Make sure we cleanup as much space as possible
371 buffer.makeCompact();
372 buffer.add(b.array());
376 public int read() throws IOException {
381 public int read(byte @Nullable [] b, int off, int len) throws IOException {
382 return buffer.get(b, off, len);
386 public void close() throws IOException {
392 * Implements an OutputStream for the Hub 2014
394 * @author Daniel Pfrommer - Initial contribution
397 public class HubOutputStream extends OutputStream {
398 private ByteArrayOutputStream out = new ByteArrayOutputStream();
401 public void write(int b) {
407 public void write(byte @Nullable [] b, int off, int len) {
408 out.write(b, off, len);
412 private void flushBuffer() {
413 ByteBuffer buffer = ByteBuffer.wrap(out.toByteArray());
415 HubIOStream.this.write(buffer);
416 } catch (IOException e) {
417 logger.warn("failed to write to hub: {}", e.toString());