2 * Copyright (c) 2010-2021 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.driver.IOStream;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
33 * Implements IOStream for a Hub 2014 device
35 * @author Daniel Pfrommer - Initial contribution
36 * @author Rob Nielsen - Port to openHAB 2 insteon binding
40 public class HubIOStream extends IOStream implements Runnable {
41 private final Logger logger = LoggerFactory.getLogger(HubIOStream.class);
43 private static final String BS_START = "<BS>";
44 private static final String BS_END = "</BS>";
46 /** time between polls (in milliseconds */
47 private int pollTime = 1000;
49 private String baseUrl;
50 private @Nullable String auth = null;
52 private @Nullable Thread pollThread = null;
54 // index of the last byte we have read in the buffer
55 private int bufferIdx = -1;
57 private boolean polling;
60 * Constructor for HubIOStream
62 * @param host host name of hub device
63 * @param port port to connect to
64 * @param pollTime time between polls (in milliseconds)
65 * @param user hub user name
66 * @param pass hub password
68 public HubIOStream(String host, int port, int pollTime, @Nullable String user, @Nullable String pass) {
69 this.pollTime = pollTime;
71 StringBuilder s = new StringBuilder();
75 s.append(":").append(port);
77 baseUrl = s.toString();
79 if (user != null && pass != null) {
80 auth = "Basic " + Base64.getEncoder().encodeToString((user + ":" + pass).getBytes(StandardCharsets.UTF_8));
85 public boolean open() {
88 } catch (IOException e) {
89 logger.warn("open failed: {}", e.getMessage());
93 in = new HubInputStream();
94 out = new HubOutputStream();
97 pollThread = new Thread(this);
98 setParamsAndStart(pollThread);
103 private void setParamsAndStart(@Nullable Thread thread) {
104 if (thread != null) {
105 thread.setName("Insteon Hub Poller");
106 thread.setDaemon(true);
112 public void close() {
115 if (pollThread != null) {
119 InputStream in = this.in;
123 } catch (IOException e) {
124 logger.warn("failed to close input stream", e);
129 OutputStream out = this.out;
133 } catch (IOException e) {
134 logger.warn("failed to close output stream", e);
141 * Fetches the latest status buffer from the Hub
143 * @return string with status buffer
144 * @throws IOException
146 private synchronized String bufferStatus() throws IOException {
147 String result = getURL("/buffstatus.xml");
149 int start = result.indexOf(BS_START);
151 throw new IOException("malformed bufferstatus.xml");
153 start += BS_START.length();
155 int end = result.indexOf(BS_END, start);
157 throw new IOException("malformed bufferstatus.xml");
160 return result.substring(start, end).trim();
164 * Sends command to Hub to clear the status buffer
166 * @throws IOException
168 private synchronized void clearBuffer() throws IOException {
169 logger.trace("clearing buffer");
175 * Sends Insteon message (byte array) as a readable ascii string to the Hub
177 * @param msg byte array representing the Insteon message
178 * @throws IOException in case of I/O error
180 public synchronized void write(ByteBuffer msg) throws IOException {
181 poll(); // fetch the status buffer before we send out commands
183 StringBuilder b = new StringBuilder();
184 while (msg.remaining() > 0) {
185 b.append(String.format("%02x", msg.get()));
187 String hexMSG = b.toString();
188 logger.trace("writing a message");
189 getURL("/3?" + hexMSG + "=I=3");
194 * Polls the Hub web interface to fetch the status buffer
196 * @throws IOException if something goes wrong with I/O
198 public synchronized void poll() throws IOException {
199 String buffer = bufferStatus(); // fetch via http call
200 logger.trace("poll: {}", buffer);
202 // The Hub maintains a ring buffer where the last two digits (in hex!) represent
203 // the position of the last byte read.
205 String data = buffer.substring(0, buffer.length() - 2); // pure data w/o index pointer
209 nIdx = Integer.parseInt(buffer.substring(buffer.length() - 2, buffer.length()), 16);
210 } catch (NumberFormatException e) {
212 logger.warn("invalid buffer size received in line: {}", buffer);
216 if (bufferIdx == -1) {
217 // this is the first call or first call after error, no need for buffer copying
219 return; // XXX why return here????
222 if (allZeros(data)) {
223 logger.trace("skip cleared buffer");
228 StringBuilder msg = new StringBuilder();
229 if (nIdx < bufferIdx) {
230 String msgStart = data.substring(bufferIdx, data.length());
231 String msgEnd = data.substring(0, nIdx);
232 if (allZeros(msgStart)) {
233 logger.trace("discard cleared buffer wrap around msg start");
237 msg.append(msgStart + msgEnd);
238 logger.trace("wrap around: copying new data on: {}", msg.toString());
240 msg.append(data.substring(bufferIdx, nIdx));
241 logger.trace("no wrap: appending new data: {}", msg.toString());
243 if (msg.length() != 0) {
244 ByteBuffer buf = ByteBuffer.wrap(hexStringToByteArray(msg.toString()));
245 InputStream in = this.in;
247 ((HubInputStream) in).handle(buf);
249 logger.warn("in is null");
255 private boolean allZeros(String s) {
256 return "0".repeat(s.length()).equals(s);
260 * Helper method to fetch url from http server
262 * @param resource the url
263 * @return contents returned by http server
264 * @throws IOException
266 private String getURL(String resource) throws IOException {
267 String url = baseUrl + resource;
269 HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
271 connection.setConnectTimeout(30000);
272 connection.setUseCaches(false);
273 connection.setDoInput(true);
274 connection.setDoOutput(false);
276 connection.setRequestProperty("Authorization", auth);
279 logger.debug("getting {}", url);
281 int responseCode = connection.getResponseCode();
282 if (responseCode != 200) {
283 if (responseCode == 401) {
285 "Bad username or password. See the label on the bottom of the hub for the correct login information.");
286 throw new IOException("login credentials are incorrect");
288 String message = url + " failed with the response code: " + responseCode;
289 logger.warn(message);
290 throw new IOException(message);
294 return getData(connection.getInputStream());
296 connection.disconnect();
300 private String getData(InputStream is) throws IOException {
301 BufferedInputStream bis = new BufferedInputStream(is);
303 ByteArrayOutputStream baos = new ByteArrayOutputStream();
304 byte[] buffer = new byte[1024];
306 while ((length = bis.read(buffer)) != -1) {
307 baos.write(buffer, 0, length);
310 String s = baos.toString();
318 * Entry point for thread
325 } catch (IOException e) {
326 logger.warn("got exception while polling: {}", e.toString());
329 Thread.sleep(pollTime);
330 } catch (InterruptedException e) {
337 * Helper function to convert an ascii hex string (received from hub)
340 * @param s string received from hub
341 * @return simple byte array
343 public static byte[] hexStringToByteArray(String s) {
344 int len = s.length();
345 byte[] bytes = new byte[len / 2];
346 for (int i = 0; i < len; i += 2) {
347 bytes[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
354 * Implements an InputStream for the Hub 2014
356 * @author Daniel Pfrommer - Initial contribution
359 public class HubInputStream extends InputStream {
361 // A buffer to keep bytes while we are waiting for the inputstream to read
362 private ReadByteBuffer buffer = new ReadByteBuffer(1024);
364 public HubInputStream() {
367 public void handle(ByteBuffer b) throws IOException {
368 // Make sure we cleanup as much space as possible
369 buffer.makeCompact();
370 buffer.add(b.array());
374 public int read() throws IOException {
379 public int read(byte @Nullable [] b, int off, int len) throws IOException {
380 return buffer.get(b, off, len);
384 public void close() throws IOException {
390 * Implements an OutputStream for the Hub 2014
392 * @author Daniel Pfrommer - Initial contribution
395 public class HubOutputStream extends OutputStream {
396 private ByteArrayOutputStream out = new ByteArrayOutputStream();
399 public void write(int b) {
405 public void write(byte @Nullable [] b, int off, int len) {
406 out.write(b, off, len);
410 private void flushBuffer() {
411 ByteBuffer buffer = ByteBuffer.wrap(out.toByteArray());
413 HubIOStream.this.write(buffer);
414 } catch (IOException e) {
415 logger.warn("failed to write to hub: {}", e.toString());