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