]> git.basschouten.com Git - openhab-addons.git/blob
83b3f7a60d7d88ac1046f9936857f9c5176bbafa
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.insteon.internal.driver.hub;
14
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;
21 import java.net.URL;
22 import java.nio.ByteBuffer;
23 import java.nio.charset.StandardCharsets;
24 import java.util.Base64;
25
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;
31
32 /**
33  * Implements IOStream for a Hub 2014 device
34  *
35  * @author Daniel Pfrommer - Initial contribution
36  * @author Rob Nielsen - Port to openHAB 2 insteon binding
37  *
38  */
39 @NonNullByDefault
40 public class HubIOStream extends IOStream implements Runnable {
41     private final Logger logger = LoggerFactory.getLogger(HubIOStream.class);
42
43     private static final String BS_START = "<BS>";
44     private static final String BS_END = "</BS>";
45
46     /** time between polls (in milliseconds */
47     private int pollTime = 1000;
48
49     private String baseUrl;
50     private @Nullable String auth = null;
51
52     private @Nullable Thread pollThread = null;
53
54     // index of the last byte we have read in the buffer
55     private int bufferIdx = -1;
56
57     private boolean polling;
58
59     /**
60      * Constructor for HubIOStream
61      *
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
67      */
68     public HubIOStream(String host, int port, int pollTime, @Nullable String user, @Nullable String pass) {
69         this.pollTime = pollTime;
70
71         StringBuilder s = new StringBuilder();
72         s.append("http://");
73         s.append(host);
74         if (port != -1) {
75             s.append(":").append(port);
76         }
77         baseUrl = s.toString();
78
79         if (user != null && pass != null) {
80             auth = "Basic " + Base64.getEncoder().encodeToString((user + ":" + pass).getBytes(StandardCharsets.UTF_8));
81         }
82     }
83
84     @Override
85     public boolean open() {
86         try {
87             clearBuffer();
88         } catch (IOException e) {
89             logger.warn("open failed: {}", e.getMessage());
90             return false;
91         }
92
93         in = new HubInputStream();
94         out = new HubOutputStream();
95
96         polling = true;
97         pollThread = new Thread(this);
98         setParamsAndStart(pollThread);
99
100         return true;
101     }
102
103     private void setParamsAndStart(@Nullable Thread thread) {
104         if (thread != null) {
105             thread.setName("Insteon Hub Poller");
106             thread.setDaemon(true);
107             thread.start();
108         }
109     }
110
111     @Override
112     public void close() {
113         polling = false;
114
115         if (pollThread != null) {
116             pollThread = null;
117         }
118
119         InputStream in = this.in;
120         if (in != null) {
121             try {
122                 in.close();
123             } catch (IOException e) {
124                 logger.warn("failed to close input stream", e);
125             }
126             this.in = null;
127         }
128
129         OutputStream out = this.out;
130         if (out != null) {
131             try {
132                 out.close();
133             } catch (IOException e) {
134                 logger.warn("failed to close output stream", e);
135             }
136             this.out = null;
137         }
138     }
139
140     /**
141      * Fetches the latest status buffer from the Hub
142      *
143      * @return string with status buffer
144      * @throws IOException
145      */
146     private synchronized String bufferStatus() throws IOException {
147         String result = getURL("/buffstatus.xml");
148
149         int start = result.indexOf(BS_START);
150         if (start == -1) {
151             throw new IOException("malformed bufferstatus.xml");
152         }
153         start += BS_START.length();
154
155         int end = result.indexOf(BS_END, start);
156         if (end == -1) {
157             throw new IOException("malformed bufferstatus.xml");
158         }
159
160         return result.substring(start, end).trim();
161     }
162
163     /**
164      * Sends command to Hub to clear the status buffer
165      *
166      * @throws IOException
167      */
168     private synchronized void clearBuffer() throws IOException {
169         logger.trace("clearing buffer");
170         getURL("/1?XB=M=1");
171         bufferIdx = 0;
172     }
173
174     /**
175      * Sends Insteon message (byte array) as a readable ascii string to the Hub
176      *
177      * @param msg byte array representing the Insteon message
178      * @throws IOException in case of I/O error
179      */
180     public synchronized void write(ByteBuffer msg) throws IOException {
181         poll(); // fetch the status buffer before we send out commands
182
183         StringBuilder b = new StringBuilder();
184         while (msg.remaining() > 0) {
185             b.append(String.format("%02x", msg.get()));
186         }
187         String hexMSG = b.toString();
188         logger.trace("writing a message");
189         getURL("/3?" + hexMSG + "=I=3");
190         bufferIdx = 0;
191     }
192
193     /**
194      * Polls the Hub web interface to fetch the status buffer
195      *
196      * @throws IOException if something goes wrong with I/O
197      */
198     public synchronized void poll() throws IOException {
199         String buffer = bufferStatus(); // fetch via http call
200         logger.trace("poll: {}", buffer);
201         //
202         // The Hub maintains a ring buffer where the last two digits (in hex!) represent
203         // the position of the last byte read.
204         //
205         String data = buffer.substring(0, buffer.length() - 2); // pure data w/o index pointer
206
207         int nIdx = -1;
208         try {
209             nIdx = Integer.parseInt(buffer.substring(buffer.length() - 2, buffer.length()), 16);
210         } catch (NumberFormatException e) {
211             bufferIdx = -1;
212             logger.warn("invalid buffer size received in line: {}", buffer);
213             return;
214         }
215
216         if (bufferIdx == -1) {
217             // this is the first call or first call after error, no need for buffer copying
218             bufferIdx = nIdx;
219             return; // XXX why return here????
220         }
221
222         if (allZeros(data)) {
223             logger.trace("skip cleared buffer");
224             bufferIdx = 0;
225             return;
226         }
227
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");
234                 msgStart = "";
235             }
236
237             msg.append(msgStart + msgEnd);
238             logger.trace("wrap around: copying new data on: {}", msg.toString());
239         } else {
240             msg.append(data.substring(bufferIdx, nIdx));
241             logger.trace("no wrap:      appending new data: {}", msg.toString());
242         }
243         if (msg.length() != 0) {
244             ByteBuffer buf = ByteBuffer.wrap(hexStringToByteArray(msg.toString()));
245             InputStream in = this.in;
246             if (in != null) {
247                 ((HubInputStream) in).handle(buf);
248             } else {
249                 logger.warn("in is null");
250             }
251         }
252         bufferIdx = nIdx;
253     }
254
255     private boolean allZeros(String s) {
256         return "0".repeat(s.length()).equals(s);
257     }
258
259     /**
260      * Helper method to fetch url from http server
261      *
262      * @param resource the url
263      * @return contents returned by http server
264      * @throws IOException
265      */
266     private String getURL(String resource) throws IOException {
267         String url = baseUrl + resource;
268
269         HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
270         try {
271             connection.setConnectTimeout(30000);
272             connection.setUseCaches(false);
273             connection.setDoInput(true);
274             connection.setDoOutput(false);
275             if (auth != null) {
276                 connection.setRequestProperty("Authorization", auth);
277             }
278
279             logger.debug("getting {}", url);
280
281             int responseCode = connection.getResponseCode();
282             if (responseCode != 200) {
283                 if (responseCode == 401) {
284                     logger.warn(
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");
287                 } else {
288                     String message = url + " failed with the response code: " + responseCode;
289                     logger.warn(message);
290                     throw new IOException(message);
291                 }
292             }
293
294             return getData(connection.getInputStream());
295         } finally {
296             connection.disconnect();
297         }
298     }
299
300     private String getData(InputStream is) throws IOException {
301         BufferedInputStream bis = new BufferedInputStream(is);
302         try {
303             ByteArrayOutputStream baos = new ByteArrayOutputStream();
304             byte[] buffer = new byte[1024];
305             int length = 0;
306             while ((length = bis.read(buffer)) != -1) {
307                 baos.write(buffer, 0, length);
308             }
309
310             String s = baos.toString();
311             return s;
312         } finally {
313             bis.close();
314         }
315     }
316
317     /**
318      * Entry point for thread
319      */
320     @Override
321     public void run() {
322         while (polling) {
323             try {
324                 poll();
325             } catch (IOException e) {
326                 logger.warn("got exception while polling: {}", e.toString());
327             }
328             try {
329                 Thread.sleep(pollTime);
330             } catch (InterruptedException e) {
331                 break;
332             }
333         }
334     }
335
336     /**
337      * Helper function to convert an ascii hex string (received from hub)
338      * into a byte array
339      *
340      * @param s string received from hub
341      * @return simple byte array
342      */
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));
348         }
349
350         return bytes;
351     }
352
353     /**
354      * Implements an InputStream for the Hub 2014
355      *
356      * @author Daniel Pfrommer - Initial contribution
357      *
358      */
359     public class HubInputStream extends InputStream {
360
361         // A buffer to keep bytes while we are waiting for the inputstream to read
362         private ReadByteBuffer buffer = new ReadByteBuffer(1024);
363
364         public HubInputStream() {
365         }
366
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());
371         }
372
373         @Override
374         public int read() throws IOException {
375             return buffer.get();
376         }
377
378         @Override
379         public int read(byte @Nullable [] b, int off, int len) throws IOException {
380             return buffer.get(b, off, len);
381         }
382
383         @Override
384         public void close() throws IOException {
385             buffer.done();
386         }
387     }
388
389     /**
390      * Implements an OutputStream for the Hub 2014
391      *
392      * @author Daniel Pfrommer - Initial contribution
393      *
394      */
395     public class HubOutputStream extends OutputStream {
396         private ByteArrayOutputStream out = new ByteArrayOutputStream();
397
398         @Override
399         public void write(int b) {
400             out.write(b);
401             flushBuffer();
402         }
403
404         @Override
405         public void write(byte @Nullable [] b, int off, int len) {
406             out.write(b, off, len);
407             flushBuffer();
408         }
409
410         private void flushBuffer() {
411             ByteBuffer buffer = ByteBuffer.wrap(out.toByteArray());
412             try {
413                 HubIOStream.this.write(buffer);
414             } catch (IOException e) {
415                 logger.warn("failed to write to hub: {}", e.toString());
416             }
417             out.reset();
418         }
419     }
420 }