]> git.basschouten.com Git - openhab-addons.git/blob
ab544cadc51d14b0be724136c8dbfa83ca352a49
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.InsteonBindingConstants;
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 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         setParamsAndStart(pollThread);
100
101         return true;
102     }
103
104     private void setParamsAndStart(@Nullable Thread thread) {
105         if (thread != null) {
106             thread.setName("OH-binding-" + InsteonBindingConstants.BINDING_ID + "-hubPoller");
107             thread.setDaemon(true);
108             thread.start();
109         }
110     }
111
112     @Override
113     public void close() {
114         polling = false;
115
116         if (pollThread != null) {
117             pollThread = null;
118         }
119
120         InputStream in = this.in;
121         if (in != null) {
122             try {
123                 in.close();
124             } catch (IOException e) {
125                 logger.warn("failed to close input stream", e);
126             }
127             this.in = null;
128         }
129
130         OutputStream out = this.out;
131         if (out != null) {
132             try {
133                 out.close();
134             } catch (IOException e) {
135                 logger.warn("failed to close output stream", e);
136             }
137             this.out = null;
138         }
139     }
140
141     /**
142      * Fetches the latest status buffer from the Hub
143      *
144      * @return string with status buffer
145      * @throws IOException
146      */
147     private synchronized String bufferStatus() throws IOException {
148         String result = getURL("/buffstatus.xml");
149
150         int start = result.indexOf(BS_START);
151         if (start == -1) {
152             throw new IOException("malformed bufferstatus.xml");
153         }
154         start += BS_START.length();
155
156         int end = result.indexOf(BS_END, start);
157         if (end == -1) {
158             throw new IOException("malformed bufferstatus.xml");
159         }
160
161         return result.substring(start, end).trim();
162     }
163
164     /**
165      * Sends command to Hub to clear the status buffer
166      *
167      * @throws IOException
168      */
169     private synchronized void clearBuffer() throws IOException {
170         logger.trace("clearing buffer");
171         getURL("/1?XB=M=1");
172         bufferIdx = 0;
173     }
174
175     /**
176      * Sends Insteon message (byte array) as a readable ascii string to the Hub
177      *
178      * @param msg byte array representing the Insteon message
179      * @throws IOException in case of I/O error
180      */
181     public synchronized void write(ByteBuffer msg) throws IOException {
182         poll(); // fetch the status buffer before we send out commands
183
184         StringBuilder b = new StringBuilder();
185         while (msg.remaining() > 0) {
186             b.append(String.format("%02x", msg.get()));
187         }
188         String hexMSG = b.toString();
189         logger.trace("writing a message");
190         getURL("/3?" + hexMSG + "=I=3");
191         bufferIdx = 0;
192     }
193
194     /**
195      * Polls the Hub web interface to fetch the status buffer
196      *
197      * @throws IOException if something goes wrong with I/O
198      */
199     public synchronized void poll() throws IOException {
200         String buffer = bufferStatus(); // fetch via http call
201         logger.trace("poll: {}", buffer);
202         //
203         // The Hub maintains a ring buffer where the last two digits (in hex!) represent
204         // the position of the last byte read.
205         //
206         String data = buffer.substring(0, buffer.length() - 2); // pure data w/o index pointer
207
208         int nIdx = -1;
209         try {
210             nIdx = Integer.parseInt(buffer.substring(buffer.length() - 2, buffer.length()), 16);
211         } catch (NumberFormatException e) {
212             bufferIdx = -1;
213             logger.warn("invalid buffer size received in line: {}", buffer);
214             return;
215         }
216
217         if (bufferIdx == -1) {
218             // this is the first call or first call after error, no need for buffer copying
219             bufferIdx = nIdx;
220             return; // XXX why return here????
221         }
222
223         if (allZeros(data)) {
224             logger.trace("skip cleared buffer");
225             bufferIdx = 0;
226             return;
227         }
228
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");
235                 msgStart = "";
236             }
237
238             msg.append(msgStart + msgEnd);
239             logger.trace("wrap around: copying new data on: {}", msg.toString());
240         } else {
241             msg.append(data.substring(bufferIdx, nIdx));
242             logger.trace("no wrap:      appending new data: {}", msg.toString());
243         }
244         if (msg.length() != 0) {
245             ByteBuffer buf = ByteBuffer.wrap(hexStringToByteArray(msg.toString()));
246             InputStream in = this.in;
247             if (in != null) {
248                 ((HubInputStream) in).handle(buf);
249             } else {
250                 logger.warn("in is null");
251             }
252         }
253         bufferIdx = nIdx;
254     }
255
256     private boolean allZeros(String s) {
257         return "0".repeat(s.length()).equals(s);
258     }
259
260     /**
261      * Helper method to fetch url from http server
262      *
263      * @param resource the url
264      * @return contents returned by http server
265      * @throws IOException
266      */
267     private String getURL(String resource) throws IOException {
268         String url = baseUrl + resource;
269
270         HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
271         try {
272             connection.setConnectTimeout(30000);
273             connection.setReadTimeout(10000);
274             connection.setUseCaches(false);
275             connection.setDoInput(true);
276             connection.setDoOutput(false);
277             if (auth != null) {
278                 connection.setRequestProperty("Authorization", auth);
279             }
280
281             logger.debug("getting {}", url);
282
283             int responseCode = connection.getResponseCode();
284             if (responseCode != 200) {
285                 if (responseCode == 401) {
286                     logger.warn(
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");
289                 } else {
290                     String message = url + " failed with the response code: " + responseCode;
291                     logger.warn(message);
292                     throw new IOException(message);
293                 }
294             }
295
296             return getData(connection.getInputStream());
297         } finally {
298             connection.disconnect();
299         }
300     }
301
302     private String getData(InputStream is) throws IOException {
303         BufferedInputStream bis = new BufferedInputStream(is);
304         try {
305             ByteArrayOutputStream baos = new ByteArrayOutputStream();
306             byte[] buffer = new byte[1024];
307             int length = 0;
308             while ((length = bis.read(buffer)) != -1) {
309                 baos.write(buffer, 0, length);
310             }
311
312             String s = baos.toString();
313             return s;
314         } finally {
315             bis.close();
316         }
317     }
318
319     /**
320      * Entry point for thread
321      */
322     @Override
323     public void run() {
324         while (polling) {
325             try {
326                 poll();
327             } catch (IOException e) {
328                 logger.warn("got exception while polling: {}", e.toString());
329             }
330             try {
331                 Thread.sleep(pollTime);
332             } catch (InterruptedException e) {
333                 break;
334             }
335         }
336     }
337
338     /**
339      * Helper function to convert an ascii hex string (received from hub)
340      * into a byte array
341      *
342      * @param s string received from hub
343      * @return simple byte array
344      */
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));
350         }
351
352         return bytes;
353     }
354
355     /**
356      * Implements an InputStream for the Hub 2014
357      *
358      * @author Daniel Pfrommer - Initial contribution
359      *
360      */
361     public class HubInputStream extends InputStream {
362
363         // A buffer to keep bytes while we are waiting for the inputstream to read
364         private ReadByteBuffer buffer = new ReadByteBuffer(1024);
365
366         public HubInputStream() {
367         }
368
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());
373         }
374
375         @Override
376         public int read() throws IOException {
377             return buffer.get();
378         }
379
380         @Override
381         public int read(byte @Nullable [] b, int off, int len) throws IOException {
382             return buffer.get(b, off, len);
383         }
384
385         @Override
386         public void close() throws IOException {
387             buffer.done();
388         }
389     }
390
391     /**
392      * Implements an OutputStream for the Hub 2014
393      *
394      * @author Daniel Pfrommer - Initial contribution
395      *
396      */
397     public class HubOutputStream extends OutputStream {
398         private ByteArrayOutputStream out = new ByteArrayOutputStream();
399
400         @Override
401         public void write(int b) {
402             out.write(b);
403             flushBuffer();
404         }
405
406         @Override
407         public void write(byte @Nullable [] b, int off, int len) {
408             out.write(b, off, len);
409             flushBuffer();
410         }
411
412         private void flushBuffer() {
413             ByteBuffer buffer = ByteBuffer.wrap(out.toByteArray());
414             try {
415                 HubIOStream.this.write(buffer);
416             } catch (IOException e) {
417                 logger.warn("failed to write to hub: {}", e.toString());
418             }
419             out.reset();
420         }
421     }
422 }