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