]> git.basschouten.com Git - openhab-addons.git/blob
e7013f55f0960fd8a76d589968cfa3a476dc3db9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.io.openhabcloud.internal;
14
15 import java.io.IOException;
16 import java.net.MalformedURLException;
17 import java.net.URI;
18 import java.net.URISyntaxException;
19 import java.net.URL;
20 import java.net.URLEncoder;
21 import java.nio.charset.StandardCharsets;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.concurrent.ConcurrentHashMap;
27
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.client.api.Request;
31 import org.eclipse.jetty.client.util.BytesContentProvider;
32 import org.eclipse.jetty.http.HttpField;
33 import org.eclipse.jetty.http.HttpFields;
34 import org.eclipse.jetty.http.HttpMethod;
35 import org.eclipse.jetty.http.HttpStatus;
36 import org.eclipse.jetty.util.BufferUtil;
37 import org.eclipse.jetty.util.URIUtil;
38 import org.json.JSONException;
39 import org.json.JSONObject;
40 import org.openhab.core.OpenHAB;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 import io.socket.backo.Backoff;
45 import io.socket.client.IO;
46 import io.socket.client.IO.Options;
47 import io.socket.client.Manager;
48 import io.socket.client.Socket;
49 import io.socket.emitter.Emitter;
50 import io.socket.engineio.client.Transport;
51 import io.socket.parser.Packet;
52 import io.socket.parser.Parser;
53 import io.socket.thread.EventThread;
54 import okhttp3.OkHttpClient.Builder;
55 import okhttp3.logging.HttpLoggingInterceptor;
56 import okhttp3.logging.HttpLoggingInterceptor.Level;
57
58 /**
59  * This class provides communication between openHAB and the openHAB Cloud service.
60  * It also implements async http proxy for serving requests from user to
61  * openHAB through the openHAB Cloud. It uses Socket.IO connection to connect to
62  * the openHAB Cloud service and Jetty Http client to send local http requests to
63  * openHAB.
64  *
65  * @author Victor Belov - Initial contribution
66  * @author Kai Kreuzer - migrated code to new Jetty client and ESH APIs
67  */
68 public class CloudClient {
69     /*
70      * Logger for this class
71      */
72     private final Logger logger = LoggerFactory.getLogger(CloudClient.class);
73
74     /*
75      * This variable holds base URL for the openHAB Cloud connections
76      */
77     private final String baseURL;
78
79     /*
80      * This variable holds openHAB's UUID for authenticating and connecting to the openHAB Cloud
81      */
82     private final String uuid;
83
84     /*
85      * This variable holds openHAB's secret for authenticating and connecting to the openHAB Cloud
86      */
87     private final String secret;
88
89     /*
90      * This variable holds local openHAB's base URL for connecting to the local openHAB instance
91      */
92     private final String localBaseUrl;
93
94     /*
95      * This variable holds instance of Jetty HTTP client to make requests to local openHAB
96      */
97     private final HttpClient jettyClient;
98
99     /*
100      * This map holds HTTP requests to local openHAB which are currently running
101      */
102     private final Map<Integer, Request> runningRequests = new ConcurrentHashMap<>();
103
104     /*
105      * This variable indicates if connection to the openHAB Cloud is currently in an established state
106      */
107     private boolean isConnected;
108
109     /*
110      * This variable holds version of local openHAB
111      */
112     private String openHABVersion;
113
114     /*
115      * This variable holds instance of Socket.IO client class which provides communication
116      * with the openHAB Cloud
117      */
118     private Socket socket;
119
120     /*
121      * The protocol of the openHAB-cloud URL.
122      */
123     private String protocol = "https";
124
125     /*
126      * This variable holds instance of CloudClientListener which provides callbacks to communicate
127      * certain events from the openHAB Cloud back to openHAB
128      */
129     private CloudClientListener listener;
130     private boolean remoteAccessEnabled;
131     private Set<String> exposedItems;
132
133     /**
134      * Back-off strategy for reconnecting when manual reconnection is needed
135      */
136     private final Backoff reconnectBackoff = new Backoff();
137
138     /**
139      * Constructor of CloudClient
140      *
141      * @param uuid openHAB's UUID to connect to the openHAB Cloud
142      * @param secret openHAB's Secret to connect to the openHAB Cloud
143      * @param remoteAccessEnabled Allow the openHAB Cloud to be used as a remote proxy
144      * @param exposedItems Items that are made available to apps connected to the openHAB Cloud
145      */
146     public CloudClient(HttpClient httpClient, String uuid, String secret, String baseURL, String localBaseUrl,
147             boolean remoteAccessEnabled, Set<String> exposedItems) {
148         this.uuid = uuid;
149         this.secret = secret;
150         this.baseURL = baseURL;
151         this.localBaseUrl = localBaseUrl;
152         this.remoteAccessEnabled = remoteAccessEnabled;
153         this.exposedItems = exposedItems;
154         this.jettyClient = httpClient;
155         reconnectBackoff.setMin(1000);
156         reconnectBackoff.setMax(30_000);
157         reconnectBackoff.setJitter(0.5);
158     }
159
160     /**
161      * Connect to the openHAB Cloud
162      */
163
164     public void connect() {
165         try {
166             Options options = new Options();
167             if (logger.isTraceEnabled()) {
168                 // When trace level logging is enabled, we activate further logging of HTTP calls
169                 // of the Socket.IO library
170                 Builder okHttpBuilder = new Builder();
171                 HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
172                 loggingInterceptor.setLevel(Level.BASIC);
173                 okHttpBuilder.addInterceptor(loggingInterceptor);
174                 okHttpBuilder.addNetworkInterceptor(loggingInterceptor);
175                 options.callFactory = okHttpBuilder.build();
176                 options.webSocketFactory = okHttpBuilder.build();
177             }
178             socket = IO.socket(baseURL, options);
179             URL parsed = new URL(baseURL);
180             protocol = parsed.getProtocol();
181         } catch (URISyntaxException e) {
182             logger.error("Error creating Socket.IO: {}", e.getMessage());
183             return;
184         } catch (MalformedURLException e) {
185             logger.error("Error parsing baseURL to get protocol, assuming https. Error: {}", e.getMessage());
186             return;
187         }
188         //
189         // socket manager events
190         //
191         socket.io()//
192                 .on(Manager.EVENT_TRANSPORT, args -> {
193                     logger.trace("Manager.EVENT_TRANSPORT");
194                     Transport transport = (Transport) args[0];
195                     transport.on(Transport.EVENT_REQUEST_HEADERS, new Emitter.Listener() {
196                         @Override
197                         public void call(Object... args) {
198                             logger.trace("Transport.EVENT_REQUEST_HEADERS");
199                             @SuppressWarnings("unchecked")
200                             Map<String, List<String>> headers = (Map<String, List<String>>) args[0];
201                             headers.put("uuid", List.of(uuid));
202                             headers.put("secret", List.of(secret));
203                             headers.put("openhabversion", List.of(OpenHAB.getVersion()));
204                             headers.put("clientversion", List.of(CloudService.clientVersion));
205                             headers.put("remoteaccess", List.of(((Boolean) remoteAccessEnabled).toString()));
206                         }
207                     });
208                 })//
209                 .on(Manager.EVENT_CONNECT_ERROR, args -> {
210                     if (args.length > 0) {
211                         if (args[0] instanceof Exception) {
212                             Exception e = (Exception) args[0];
213                             logger.debug(
214                                     "Error connecting to the openHAB Cloud instance: {} {}. Should reconnect automatically.",
215                                     e.getClass().getSimpleName(), e.getMessage());
216                         } else {
217                             logger.debug(
218                                     "Error connecting to the openHAB Cloud instance: {}. Should reconnect automatically.",
219                                     args[0]);
220                         }
221                     } else {
222                         logger.debug("Error connecting to the openHAB Cloud instance. Should reconnect automatically.");
223                     }
224                 })//
225                 .on(Manager.EVENT_OPEN, args -> logger.debug("Socket.IO OPEN"))//
226                 .on(Manager.EVENT_CLOSE, args -> logger.debug("Socket.IO CLOSE: {}", args[0]))//
227                 .on(Manager.EVENT_PACKET, args -> {
228                     int packetTypeIndex = -1;
229                     String type = "<unexpected packet type>";
230                     if (args.length == 1 && args[0] instanceof Packet<?>) {
231                         packetTypeIndex = ((Packet<?>) args[0]).type;
232
233                         if (packetTypeIndex < Parser.types.length) {
234                             type = Parser.types[packetTypeIndex];
235                         } else {
236                             type = "<unknown type>";
237                         }
238                     }
239                     logger.trace("Socket.IO Packet: {} ({})", type, packetTypeIndex);
240                 })//
241         ;
242
243         //
244         // socket events
245         //
246         socket.on(Socket.EVENT_CONNECT, args -> {
247             logger.debug("Socket.IO connected");
248             isConnected = true;
249             onConnect();
250         })//
251                 .on(Socket.EVENT_CONNECTING, args -> logger.debug("Socket.IO connecting"))//
252                 .on(Socket.EVENT_RECONNECTING, args -> logger.debug("Socket.IO re-connecting (attempt {})", args[0]))//
253                 .on(Socket.EVENT_RECONNECT,
254                         args -> logger.debug("Socket.IO re-connected successfully (attempt {})", args[0]))//
255                 .on(Socket.EVENT_RECONNECT_ERROR, args -> {
256                     if (args[0] instanceof Exception) {
257                         Exception e = (Exception) args[0];
258                         logger.debug("Socket.IO re-connect attempt error: {} {}", e.getClass().getSimpleName(),
259                                 e.getMessage());
260                     } else {
261                         logger.debug("Socket.IO re-connect attempt error: {}", args[0]);
262                     }
263                 })//
264                 .on(Socket.EVENT_RECONNECT_FAILED,
265                         args -> logger.debug("Socket.IO re-connect attempts failed. Stopping reconnection."))//
266                 .on(Socket.EVENT_DISCONNECT, args -> {
267                     if (args.length > 0) {
268                         logger.warn("Socket.IO disconnected: {}", args[0]);
269                     } else {
270                         logger.warn("Socket.IO disconnected");
271                     }
272                     isConnected = false;
273                     onDisconnect();
274                 })//
275                 .on(Socket.EVENT_ERROR, args -> {
276                     if (CloudClient.this.socket.connected()) {
277                         if (args.length > 0) {
278                             if (args[0] instanceof Exception) {
279                                 Exception e = (Exception) args[0];
280                                 logger.warn("Error during communication: {} {}", e.getClass().getSimpleName(),
281                                         e.getMessage());
282                             } else {
283                                 logger.warn("Error during communication: {}", args[0]);
284                             }
285                         } else {
286                             logger.warn("Error during communication");
287                         }
288                     } else {
289                         // We are not connected currently, manual reconnection is needed to keep trying to
290                         // (re-)establish
291                         // connection.
292                         //
293                         // Socket.IO 1.x java client: 'error' event is emitted from Socket on connection errors that
294                         // are not
295                         // retried, but also with error that are automatically retried. If we
296                         //
297                         // Note how this is different in Socket.IO 2.x java client, Socket emits 'connect_error'
298                         // event.
299                         // OBS: Don't get confused with Socket IO 2.x docs online, in 1.x connect_error is emitted
300                         // also on
301                         // errors that are retried by the library automatically!
302                         long delay = reconnectBackoff.duration();
303                         // Try reconnecting on connection errors
304                         if (args.length > 0) {
305                             if (args[0] instanceof Exception) {
306                                 Exception e = (Exception) args[0];
307                                 logger.warn(
308                                         "Error connecting to the openHAB Cloud instance: {} {}. Reconnecting after {} ms.",
309                                         e.getClass().getSimpleName(), e.getMessage(), delay);
310                             } else {
311                                 logger.warn(
312                                         "Error connecting to the openHAB Cloud instance: {}. Reconnecting after {} ms.",
313                                         args[0], delay);
314                             }
315                         } else {
316                             logger.warn("Error connecting to the openHAB Cloud instance. Reconnecting.");
317                         }
318                         socket.close();
319                         sleepSocketIO(delay);
320                         socket.connect();
321                     }
322                 })//
323
324                 .on(Socket.EVENT_PING, args -> logger.debug("Socket.IO ping"))//
325                 .on(Socket.EVENT_PONG, args -> logger.debug("Socket.IO pong: {} ms", args[0]))//
326                 .on("request", args -> onEvent("request", (JSONObject) args[0]))//
327                 .on("cancel", args -> onEvent("cancel", (JSONObject) args[0]))//
328                 .on("command", args -> onEvent("command", (JSONObject) args[0]))//
329         ;
330         socket.connect();
331     }
332
333     /**
334      * Callback method for socket.io client which is called when connection is established
335      */
336
337     public void onConnect() {
338         logger.info("Connected to the openHAB Cloud service (UUID = {}, base URL = {})", censored(this.uuid),
339                 this.localBaseUrl);
340         reconnectBackoff.reset();
341         isConnected = true;
342     }
343
344     /**
345      * Callback method for socket.io client which is called when disconnect occurs
346      */
347
348     public void onDisconnect() {
349         logger.info("Disconnected from the openHAB Cloud service (UUID = {}, base URL = {})", censored(this.uuid),
350                 this.localBaseUrl);
351         isConnected = false;
352         // And clean up the list of running requests
353         runningRequests.clear();
354     }
355
356     /**
357      * Callback method for socket.io client which is called when a message is received
358      */
359
360     public void onEvent(String event, JSONObject data) {
361         logger.debug("on(): {}", event);
362         if ("command".equals(event)) {
363             handleCommandEvent(data);
364             return;
365         }
366         if (remoteAccessEnabled) {
367             if ("request".equals(event)) {
368                 handleRequestEvent(data);
369             } else if ("cancel".equals(event)) {
370                 handleCancelEvent(data);
371             } else {
372                 logger.warn("Unsupported event from openHAB Cloud: {}", event);
373             }
374         }
375     }
376
377     private void handleRequestEvent(JSONObject data) {
378         try {
379             // Get unique request Id
380             int requestId = data.getInt("id");
381             logger.debug("Got request {}", requestId);
382             // Get request path
383             String requestPath = data.getString("path");
384             logger.debug("Path {}", requestPath);
385             // Get request method
386             String requestMethod = data.getString("method");
387             logger.debug("Method {}", requestMethod);
388             // Get JSONObject for request headers
389             JSONObject requestHeadersJson = data.getJSONObject("headers");
390             logger.debug("Headers: {}", requestHeadersJson.toString());
391             // Get request body
392             String requestBody = data.getString("body");
393             logger.trace("Body {}", requestBody);
394             // Get JSONObject for request query parameters
395             JSONObject requestQueryJson = data.getJSONObject("query");
396             logger.debug("Query {}", requestQueryJson.toString());
397             // Create URI builder with base request URI of openHAB and path from request
398             String newPath = URIUtil.addPaths(localBaseUrl, requestPath);
399             Iterator<String> queryIterator = requestQueryJson.keys();
400             // Add query parameters to URI builder, if any
401             newPath += "?";
402             while (queryIterator.hasNext()) {
403                 String queryName = queryIterator.next();
404                 newPath += queryName;
405                 newPath += "=";
406                 newPath += URLEncoder.encode(requestQueryJson.getString(queryName), "UTF-8");
407                 if (queryIterator.hasNext()) {
408                     newPath += "&";
409                 }
410             }
411             // Finally get the future request URI
412             URI requestUri = new URI(newPath);
413             // All preparations which are common for different methods are done
414             // Now perform the request to openHAB
415             // If method is GET
416             logger.debug("Request method is {}", requestMethod);
417             Request request = jettyClient.newRequest(requestUri);
418             setRequestHeaders(request, requestHeadersJson);
419             String proto = protocol;
420             if (data.has("protocol")) {
421                 proto = data.getString("protocol");
422             }
423             request.header("X-Forwarded-Proto", proto);
424             HttpMethod method = HttpMethod.fromString(requestMethod);
425             if (method == null) {
426                 logger.debug("Unsupported request method {}", requestMethod);
427                 return;
428             }
429             request.method(method);
430             if (!requestBody.isEmpty()) {
431                 request.content(new BytesContentProvider(requestBody.getBytes()));
432             }
433
434             request.onResponseHeaders(response -> {
435                 logger.debug("onHeaders {}", requestId);
436                 JSONObject responseJson = new JSONObject();
437                 try {
438                     responseJson.put("id", requestId);
439                     responseJson.put("headers", getJSONHeaders(response.getHeaders()));
440                     responseJson.put("responseStatusCode", response.getStatus());
441                     responseJson.put("responseStatusText", "OK");
442                     socket.emit("responseHeader", responseJson);
443                     logger.trace("Sent headers to request {}", requestId);
444                     logger.trace("{}", responseJson.toString());
445                 } catch (JSONException e) {
446                     logger.debug("{}", e.getMessage());
447                 }
448             }).onResponseContent((theResponse, content) -> {
449                 logger.debug("onResponseContent: {}, content size {}", requestId, String.valueOf(content.remaining()));
450                 JSONObject responseJson = new JSONObject();
451                 try {
452                     responseJson.put("id", requestId);
453                     responseJson.put("body", BufferUtil.toArray(content));
454                     if (logger.isTraceEnabled()) {
455                         logger.trace("{}", StandardCharsets.UTF_8.decode(content).toString());
456                     }
457                     socket.emit("responseContentBinary", responseJson);
458                     logger.trace("Sent content to request {}", requestId);
459                 } catch (JSONException e) {
460                     logger.debug("{}", e.getMessage());
461                 }
462             }).onRequestFailure((origRequest, failure) -> {
463                 logger.debug("onRequestFailure: {},  {}", requestId, failure.getMessage());
464                 JSONObject responseJson = new JSONObject();
465                 try {
466                     responseJson.put("id", requestId);
467                     responseJson.put("responseStatusText", "openHAB connection error: " + failure.getMessage());
468                     socket.emit("responseError", responseJson);
469                 } catch (JSONException e) {
470                     logger.debug("{}", e.getMessage());
471                 }
472             }).send(result -> {
473                 logger.debug("onComplete: {}", requestId);
474                 // Remove this request from list of running requests
475                 runningRequests.remove(requestId);
476                 if ((result != null && result.isFailed())
477                         && (result.getResponse() != null && result.getResponse().getStatus() != HttpStatus.OK_200)) {
478                     if (result.getFailure() != null) {
479                         logger.debug("Jetty request {} failed: {}", requestId, result.getFailure().getMessage());
480                     }
481                     if (result.getRequestFailure() != null) {
482                         logger.debug("Request Failure: {}", result.getRequestFailure().getMessage());
483                     }
484                     if (result.getResponseFailure() != null) {
485                         logger.debug("Response Failure: {}", result.getResponseFailure().getMessage());
486                     }
487                 }
488                 JSONObject responseJson = new JSONObject();
489                 try {
490                     responseJson.put("id", requestId);
491                     socket.emit("responseFinished", responseJson);
492                     logger.debug("Finished responding to request {}", requestId);
493                 } catch (JSONException e) {
494                     logger.debug("{}", e.getMessage());
495                 }
496             });
497
498             // If successfully submitted request to http client, add it to the list of currently
499             // running requests to be able to cancel it if needed
500             runningRequests.put(requestId, request);
501         } catch (JSONException | IOException | URISyntaxException e) {
502             logger.debug("{}", e.getMessage());
503         }
504     }
505
506     private void setRequestHeaders(Request request, JSONObject requestHeadersJson) {
507         Iterator<String> headersIterator = requestHeadersJson.keys();
508         // Convert JSONObject of headers into Header ArrayList
509         while (headersIterator.hasNext()) {
510             String headerName = headersIterator.next();
511             String headerValue;
512             try {
513                 headerValue = requestHeadersJson.getString(headerName);
514                 logger.debug("Jetty set header {} = {}", headerName, headerValue);
515                 if (!headerName.equalsIgnoreCase("Content-Length")) {
516                     request.header(headerName, headerValue);
517                 }
518             } catch (JSONException e) {
519                 logger.warn("Error processing request headers: {}", e.getMessage());
520             }
521         }
522     }
523
524     private void handleCancelEvent(JSONObject data) {
525         try {
526             int requestId = data.getInt("id");
527             logger.debug("Received cancel for request {}", requestId);
528             // Find and abort running request
529             Request request = runningRequests.get(requestId);
530             if (request != null) {
531                 request.abort(new InterruptedException());
532                 runningRequests.remove(requestId);
533             }
534         } catch (JSONException e) {
535             logger.debug("{}", e.getMessage());
536         }
537     }
538
539     private void handleCommandEvent(JSONObject data) {
540         String itemName = data.getString("item");
541         if (exposedItems.contains(itemName)) {
542             try {
543                 logger.debug("Received command {} for item {}.", data.getString("command"), itemName);
544                 if (this.listener != null) {
545                     this.listener.sendCommand(itemName, data.getString("command"));
546                 }
547             } catch (JSONException e) {
548                 logger.debug("{}", e.getMessage());
549             }
550         } else {
551             logger.warn("Received command from openHAB Cloud for item '{}', which is not exposed.", itemName);
552         }
553     }
554
555     /**
556      * This method sends notification to the openHAB Cloud
557      *
558      * @param userId openHAB Cloud user id
559      * @param message notification message text
560      * @param icon name of the icon for this notification
561      * @param severity severity name for this notification
562      */
563     public void sendNotification(String userId, String message, @Nullable String icon, @Nullable String severity) {
564         if (isConnected()) {
565             JSONObject notificationMessage = new JSONObject();
566             try {
567                 notificationMessage.put("userId", userId);
568                 notificationMessage.put("message", message);
569                 notificationMessage.put("icon", icon);
570                 notificationMessage.put("severity", severity);
571                 socket.emit("notification", notificationMessage);
572             } catch (JSONException e) {
573                 logger.debug("{}", e.getMessage());
574             }
575         } else {
576             logger.debug("No connection, notification is not sent");
577         }
578     }
579
580     /**
581      * This method sends log notification to the openHAB Cloud
582      *
583      * @param message notification message text
584      * @param icon name of the icon for this notification
585      * @param severity severity name for this notification
586      */
587     public void sendLogNotification(String message, @Nullable String icon, @Nullable String severity) {
588         if (isConnected()) {
589             JSONObject notificationMessage = new JSONObject();
590             try {
591                 notificationMessage.put("message", message);
592                 notificationMessage.put("icon", icon);
593                 notificationMessage.put("severity", severity);
594                 socket.emit("lognotification", notificationMessage);
595             } catch (JSONException e) {
596                 logger.debug("{}", e.getMessage());
597             }
598         } else {
599             logger.debug("No connection, notification is not sent");
600         }
601     }
602
603     /**
604      * This method sends broadcast notification to the openHAB Cloud
605      *
606      * @param message notification message text
607      * @param icon name of the icon for this notification
608      * @param severity severity name for this notification
609      */
610     public void sendBroadcastNotification(String message, @Nullable String icon, @Nullable String severity) {
611         if (isConnected()) {
612             JSONObject notificationMessage = new JSONObject();
613             try {
614                 notificationMessage.put("message", message);
615                 notificationMessage.put("icon", icon);
616                 notificationMessage.put("severity", severity);
617                 socket.emit("broadcastnotification", notificationMessage);
618             } catch (JSONException e) {
619                 logger.debug("{}", e.getMessage());
620             }
621         } else {
622             logger.debug("No connection, notification is not sent");
623         }
624     }
625
626     /**
627      * Send item update to openHAB Cloud
628      *
629      * @param itemName the name of the item
630      * @param itemState updated item state
631      *
632      */
633     public void sendItemUpdate(String itemName, String itemState) {
634         if (isConnected()) {
635             logger.debug("Sending update '{}' for item '{}'", itemState, itemName);
636             JSONObject itemUpdateMessage = new JSONObject();
637             try {
638                 itemUpdateMessage.put("itemName", itemName);
639                 itemUpdateMessage.put("itemStatus", itemState);
640                 socket.emit("itemupdate", itemUpdateMessage);
641             } catch (JSONException e) {
642                 logger.debug("{}", e.getMessage());
643             }
644         } else {
645             logger.debug("No connection, Item update is not sent");
646         }
647     }
648
649     /**
650      * Returns true if openHAB Cloud connection is active
651      */
652     public boolean isConnected() {
653         return isConnected;
654     }
655
656     /**
657      * Disconnect from openHAB Cloud
658      */
659     public void shutdown() {
660         logger.info("Shutting down openHAB Cloud service connection");
661         socket.disconnect();
662     }
663
664     public String getOpenHABVersion() {
665         return openHABVersion;
666     }
667
668     public void setOpenHABVersion(String openHABVersion) {
669         this.openHABVersion = openHABVersion;
670     }
671
672     public void setListener(CloudClientListener listener) {
673         this.listener = listener;
674     }
675
676     private JSONObject getJSONHeaders(HttpFields httpFields) {
677         JSONObject headersJSON = new JSONObject();
678         try {
679             for (HttpField field : httpFields) {
680                 headersJSON.put(field.getName(), field.getValue());
681             }
682         } catch (JSONException e) {
683             logger.warn("Error forming response headers: {}", e.getMessage());
684         }
685         return headersJSON;
686     }
687
688     private void sleepSocketIO(long delay) {
689         EventThread.exec(() -> {
690             try {
691                 Thread.sleep(delay);
692             } catch (InterruptedException e) {
693
694             }
695         });
696     }
697
698     private static String censored(String secret) {
699         if (secret.length() < 4) {
700             return "*******";
701         }
702         return secret.substring(0, 2) + "..." + secret.substring(secret.length() - 2, secret.length());
703     }
704 }