]> git.basschouten.com Git - openhab-addons.git/blob
73a7426fdcf550741628aff145948b4793c12aea
[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.freeathomesystem.internal.handler;
14
15 import java.io.IOException;
16 import java.io.StringReader;
17 import java.net.URI;
18 import java.net.URISyntaxException;
19 import java.util.ArrayList;
20 import java.util.Base64;
21 import java.util.Collection;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Locale;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.ExecutionException;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.TimeoutException;
31 import java.util.concurrent.atomic.AtomicBoolean;
32 import java.util.concurrent.atomic.AtomicInteger;
33 import java.util.concurrent.locks.Condition;
34 import java.util.concurrent.locks.Lock;
35 import java.util.concurrent.locks.ReentrantLock;
36
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.eclipse.jetty.client.HttpClient;
40 import org.eclipse.jetty.client.api.AuthenticationStore;
41 import org.eclipse.jetty.client.api.ContentResponse;
42 import org.eclipse.jetty.client.api.Request;
43 import org.eclipse.jetty.client.util.BasicAuthentication;
44 import org.eclipse.jetty.client.util.StringContentProvider;
45 import org.eclipse.jetty.http.HttpMethod;
46 import org.eclipse.jetty.http.HttpStatus;
47 import org.eclipse.jetty.util.thread.QueuedThreadPool;
48 import org.eclipse.jetty.websocket.api.Session;
49 import org.eclipse.jetty.websocket.api.StatusCode;
50 import org.eclipse.jetty.websocket.api.WebSocketListener;
51 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
52 import org.eclipse.jetty.websocket.client.WebSocketClient;
53 import org.openhab.binding.freeathomesystem.internal.FreeAtHomeSystemDiscoveryService;
54 import org.openhab.binding.freeathomesystem.internal.configuration.FreeAtHomeBridgeHandlerConfiguration;
55 import org.openhab.binding.freeathomesystem.internal.datamodel.FreeAtHomeDeviceDescription;
56 import org.openhab.binding.freeathomesystem.internal.util.FreeAtHomeHttpCommunicationException;
57 import org.openhab.core.thing.Bridge;
58 import org.openhab.core.thing.ChannelUID;
59 import org.openhab.core.thing.ThingStatus;
60 import org.openhab.core.thing.ThingStatusDetail;
61 import org.openhab.core.thing.binding.BaseBridgeHandler;
62 import org.openhab.core.thing.binding.ThingHandlerService;
63 import org.openhab.core.types.Command;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
66
67 import com.google.gson.JsonArray;
68 import com.google.gson.JsonElement;
69 import com.google.gson.JsonObject;
70 import com.google.gson.JsonParseException;
71 import com.google.gson.JsonParser;
72 import com.google.gson.stream.JsonReader;
73
74 /**
75  * The {@link FreeAtHomeBridgeHandler} is responsible for handling the free@home bridge and
76  * its main communication.
77  *
78  * @author Andras Uhrin - Initial contribution
79  *
80  */
81 @NonNullByDefault
82 public class FreeAtHomeBridgeHandler extends BaseBridgeHandler implements WebSocketListener {
83
84     private final Logger logger = LoggerFactory.getLogger(FreeAtHomeBridgeHandler.class);
85
86     private Map<String, FreeAtHomeDeviceHandler> mapEventListeners = new ConcurrentHashMap<>();
87
88     // Clients for the network communication
89     private HttpClient httpClient;
90     private @Nullable WebSocketClient websocketClient = null;
91     private FreeAtHomeWebsocketMonitorThread socketMonitor = new FreeAtHomeWebsocketMonitorThread();
92     private @Nullable QueuedThreadPool jettyThreadPool = null;
93     private volatile @Nullable Session websocketSession = null;
94
95     private String sysApUID = "00000000-0000-0000-0000-000000000000";
96     private String ipAddress = "";
97     private String username = "";
98     private String password = "";
99
100     private String baseUrl = "";
101
102     private String authField = "";
103
104     private Lock lock = new ReentrantLock();
105     private AtomicBoolean httpConnectionOK = new AtomicBoolean(false);
106     private Condition websocketSessionEstablished = lock.newCondition();
107
108     int numberOfComponents = 0;
109
110     private static final int BRIDGE_WEBSOCKET_RECONNECT_DELAY = 60;
111     private static final int BRIDGE_WEBSOCKET_TIMEOUT = 90;
112     private static final int BRIDGE_WEBSOCKET_KEEPALIVE = 50;
113     private static final String BRIDGE_URL_GETDEVICELIST = "/rest/devicelist";
114
115     public FreeAtHomeBridgeHandler(Bridge thing, HttpClient client) {
116         super(thing);
117
118         httpClient = client;
119     }
120
121     /**
122      * stub method for handlCommand
123      */
124     @Override
125     public void handleCommand(ChannelUID channelUID, Command command) {
126         logger.warn("Unknown handle command for the bridge - channellUID {}, command {}", channelUID, command);
127     }
128
129     @Override
130     public Collection<Class<? extends ThingHandlerService>> getServices() {
131         return List.of(FreeAtHomeSystemDiscoveryService.class);
132     }
133
134     /**
135      * Method to get the device list
136      */
137     public List<String> getDeviceDeviceList() throws FreeAtHomeHttpCommunicationException {
138         List<String> listOfComponentId = new ArrayList<String>();
139         boolean ret = false;
140
141         listOfComponentId.clear();
142
143         String url = baseUrl + BRIDGE_URL_GETDEVICELIST;
144
145         // Perform a simple GET and wait for the response.
146         try {
147             HttpClient client = httpClient;
148
149             Request req = client.newRequest(url);
150
151             if (req == null) {
152                 throw new FreeAtHomeHttpCommunicationException(0,
153                         "Invalid request object in getDeviceDeviceList with the URL [ " + url + " ]");
154             }
155
156             ContentResponse response = req.send();
157
158             // Get component List
159             String componentListString = new String(response.getContent());
160
161             JsonElement jsonTree = JsonParser.parseString(componentListString);
162
163             // check the output
164             if (!jsonTree.isJsonObject()) {
165                 throw new FreeAtHomeHttpCommunicationException(0,
166                         "Invalid jsonObject in getDeviceDeviceList with the URL [ " + url + " ]");
167             }
168
169             JsonObject jsonObject = jsonTree.getAsJsonObject();
170
171             // Get the main object
172             JsonElement listOfComponents = jsonObject.get(sysApUID);
173
174             if (listOfComponents == null) {
175                 throw new FreeAtHomeHttpCommunicationException(0,
176                         "Devices Section is missing in getDeviceDeviceList with the URL [ " + url + " ]");
177             }
178
179             JsonArray array = listOfComponents.getAsJsonArray();
180
181             this.numberOfComponents = array.size();
182
183             for (int i = 0; i < array.size(); i++) {
184                 JsonElement basicElement = array.get(i);
185
186                 listOfComponentId.add(basicElement.getAsString());
187             }
188
189             ret = true;
190         } catch (InterruptedException e) {
191             Thread.currentThread().interrupt();
192             logger.debug("Error to build up the Component list [ {} ]", e.getMessage());
193
194             throw new FreeAtHomeHttpCommunicationException(0,
195                     "Http communication interrupted [ " + e.getMessage() + " ]");
196         } catch (ExecutionException | TimeoutException e) {
197             logger.debug("Error to build up the Component list [ {} ]", e.getMessage());
198
199             throw new FreeAtHomeHttpCommunicationException(0,
200                     "Http communication interrupted in getDeviceList [ " + e.getMessage() + " ]");
201         }
202
203         // Scan finished but error. clear the list
204         if (!ret) {
205             listOfComponentId.clear();
206         }
207
208         return listOfComponentId;
209     }
210
211     /**
212      * Method to send http request to get the device description
213      */
214     public FreeAtHomeDeviceDescription getFreeatHomeDeviceDescription(String id)
215             throws FreeAtHomeHttpCommunicationException {
216         FreeAtHomeDeviceDescription device = new FreeAtHomeDeviceDescription();
217
218         String url = baseUrl + "/rest/device/" + sysApUID + "/" + id;
219         try {
220             HttpClient client = httpClient;
221             Request req = client.newRequest(url);
222
223             if (req == null) {
224                 throw new FreeAtHomeHttpCommunicationException(0,
225                         "Invalid request object in getDatapoint with the URL [ " + url + " ]");
226             }
227
228             ContentResponse response;
229             response = req.send();
230
231             // Get component List
232             String deviceString = new String(response.getContent());
233
234             JsonReader reader = new JsonReader(new StringReader(deviceString));
235             reader.setLenient(true);
236             JsonElement jsonTree = JsonParser.parseReader(reader);
237
238             if (!jsonTree.isJsonObject()) {
239                 throw new FreeAtHomeHttpCommunicationException(0,
240                         "No data is received by getDatapoint with the URL [ " + url + " ]");
241             }
242
243             if (!jsonTree.isJsonObject()) {
244                 throw new FreeAtHomeHttpCommunicationException(0,
245                         "Invalid jsonObject in getFreeatHomeDeviceDescription with the URL [ " + url + " ]");
246             }
247
248             // check the output
249             JsonObject jsonObject = jsonTree.getAsJsonObject();
250
251             if (!jsonObject.isJsonObject()) {
252                 throw new FreeAtHomeHttpCommunicationException(0,
253                         "Main jsonObject is invalid in getFreeatHomeDeviceDescription with the URL [ " + url + " ]");
254             }
255
256             jsonObject = jsonObject.getAsJsonObject(sysApUID);
257
258             if (!jsonObject.isJsonObject()) {
259                 throw new FreeAtHomeHttpCommunicationException(0,
260                         "jsonObject is invalid in getFreeatHomeDeviceDescription with the URL [ " + url + " ]");
261             }
262
263             jsonObject = jsonObject.getAsJsonObject("devices");
264
265             if (!jsonObject.isJsonObject()) {
266                 throw new FreeAtHomeHttpCommunicationException(0,
267                         "Devices Section is missing in getFreeatHomeDeviceDescription with the URL [ " + url + " ]");
268             }
269
270             device = new FreeAtHomeDeviceDescription(jsonObject, id);
271         } catch (InterruptedException e) {
272             Thread.currentThread().interrupt();
273             logger.debug("No communication possible to get device list - Communication interrupt [ {} ]",
274                     e.getMessage());
275
276             throw new FreeAtHomeHttpCommunicationException(0,
277                     "Http communication interrupted [ " + e.getMessage() + " ]");
278         } catch (ExecutionException | TimeoutException e) {
279             logger.debug("No communication possible to get device list - Communication interrupt [ {} ]",
280                     e.getMessage());
281
282             throw new FreeAtHomeHttpCommunicationException(0,
283                     "Http communication interrupted in getDeviceList [ " + e.getMessage() + " ]");
284         }
285
286         return device;
287     }
288
289     /**
290      * Method to get datapoint values for devices
291      */
292     public String getDatapoint(String deviceId, String channel, String datapoint)
293             throws FreeAtHomeHttpCommunicationException {
294         String url = baseUrl + "/rest/datapoint/" + sysApUID + "/" + deviceId + "." + channel + "." + datapoint;
295
296         try {
297             Request req = httpClient.newRequest(url);
298
299             logger.debug("Get datapoint by url: {}", url);
300
301             if (req == null) {
302                 throw new FreeAtHomeHttpCommunicationException(0,
303                         "Invalid request object in getDatapoint with the URL [ " + url + " ]");
304             }
305
306             ContentResponse response = req.send();
307
308             if (response.getStatus() != 200) {
309                 throw new FreeAtHomeHttpCommunicationException(response.getStatus(), response.getReason());
310             }
311
312             String deviceString = new String(response.getContent());
313
314             JsonReader reader = new JsonReader(new StringReader(deviceString));
315             reader.setLenient(true);
316             JsonElement jsonTree = JsonParser.parseReader(reader);
317
318             if (!jsonTree.isJsonObject()) {
319                 throw new FreeAtHomeHttpCommunicationException(0,
320                         "No data is received by getDatapoint with the URL [ " + url + " ]");
321             }
322
323             JsonObject jsonObject = jsonTree.getAsJsonObject();
324
325             jsonObject = jsonObject.getAsJsonObject(sysApUID);
326             JsonArray jsonValueArray = jsonObject.getAsJsonArray("values");
327
328             JsonElement element = jsonValueArray.get(0);
329             String value = element.getAsString();
330
331             if (value.isEmpty()) {
332                 value = "0";
333             }
334
335             return value;
336         } catch (InterruptedException e) {
337             Thread.currentThread().interrupt();
338             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
339                     "@text/comm-error.error-in-sysap-com");
340
341             throw new FreeAtHomeHttpCommunicationException(0,
342                     "Http communication interrupted [ " + e.getMessage() + " ]");
343         } catch (ExecutionException | TimeoutException e) {
344             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
345                     "@text/comm-error.error-in-sysap-com");
346
347             throw new FreeAtHomeHttpCommunicationException(0,
348                     "Http communication timout or execution interrupted [ " + e.getMessage() + " ]");
349         } catch (JsonParseException e) {
350             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
351                     "@text/comm-error.error-in-sysap-com");
352
353             throw new FreeAtHomeHttpCommunicationException(0,
354                     "Invalid JSON file is received by getDatapoint with the URL [ " + e.getMessage() + " ]");
355         }
356     }
357
358     /**
359      * Method to set datapoint values in channels
360      */
361     public boolean setDatapoint(String deviceId, String channel, String datapoint, String valueString)
362             throws FreeAtHomeHttpCommunicationException {
363         String url = baseUrl + "/rest/datapoint/" + sysApUID + "/" + deviceId + "." + channel + "." + datapoint;
364
365         try {
366             Request req = httpClient.newRequest(url);
367
368             if (req == null) {
369                 throw new FreeAtHomeHttpCommunicationException(0,
370                         "Invalid request object in getDatapoint with the URL [ " + url + " ]");
371             }
372
373             req.content(new StringContentProvider(valueString));
374             req.method(HttpMethod.PUT);
375
376             logger.debug("Set datapoint by url: {} value: {}", url, valueString);
377
378             ContentResponse response = req.send();
379
380             if (response.getStatus() != 200) {
381                 throw new FreeAtHomeHttpCommunicationException(response.getStatus(), response.getReason());
382             }
383         } catch (InterruptedException e) {
384             Thread.currentThread().interrupt();
385             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
386                     "@text/comm-error.error-in-sysap-com");
387
388             throw new FreeAtHomeHttpCommunicationException(0,
389                     "Http communication interrupted [ " + e.getMessage() + " ]");
390         } catch (ExecutionException | TimeoutException e) {
391             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
392                     "@text/comm-error.error-in-sysap-com");
393
394             throw new FreeAtHomeHttpCommunicationException(0,
395                     "Http communication interrupted [ " + e.getMessage() + " ]");
396         }
397
398         return true;
399     }
400
401     /**
402      * Method to process socket events
403      */
404     public void setDatapointOnWebsocketFeedback(String receivedText) {
405         JsonReader reader = new JsonReader(new StringReader(receivedText));
406         reader.setLenient(true);
407         JsonElement jsonTree = JsonParser.parseReader(reader);
408
409         // check the output
410         if (jsonTree.isJsonObject()) {
411             JsonObject jsonObject = jsonTree.getAsJsonObject();
412
413             jsonObject = jsonObject.getAsJsonObject(sysApUID);
414             jsonObject = jsonObject.getAsJsonObject("datapoints");
415
416             Set<String> keys = jsonObject.keySet();
417
418             Iterator<String> iter = keys.iterator();
419
420             while (iter.hasNext()) {
421                 String eventDatapointID = iter.next();
422
423                 JsonElement element = jsonObject.get(eventDatapointID);
424                 String value = element.getAsString();
425
426                 String[] parts = eventDatapointID.split("/");
427
428                 FreeAtHomeDeviceHandler deviceHandler = mapEventListeners.get(parts[0]);
429
430                 if (deviceHandler != null) {
431                     deviceHandler.onDeviceStateChanged(eventDatapointID, value);
432                 }
433
434                 logger.debug("Socket event processed: event-datapoint-ID {} value {}", eventDatapointID, value);
435             }
436         }
437     }
438
439     public void markDeviceRemovedOnWebsocketFeedback(String receivedText) {
440         JsonReader reader = new JsonReader(new StringReader(receivedText));
441         reader.setLenient(true);
442         JsonElement jsonTree = JsonParser.parseReader(reader);
443
444         // check the output
445         if (jsonTree.isJsonObject()) {
446             JsonObject jsonObject = jsonTree.getAsJsonObject();
447
448             jsonObject = jsonObject.getAsJsonObject(sysApUID);
449             JsonArray jsonArray = jsonObject.getAsJsonArray("devicesRemoved");
450
451             for (JsonElement element : jsonArray) {
452                 FreeAtHomeDeviceHandler deviceHandler = mapEventListeners.get(element.getAsString());
453
454                 if (deviceHandler != null) {
455                     deviceHandler.onDeviceRemoved();
456
457                     logger.debug("Device removal processed");
458                 }
459             }
460         }
461     }
462
463     public void registerDeviceStateListener(String deviceID, FreeAtHomeDeviceHandler deviceHandler) {
464         mapEventListeners.put(deviceID, deviceHandler);
465     }
466
467     public void unregisterDeviceStateListener(String deviceID) {
468         mapEventListeners.remove(deviceID);
469     }
470
471     /**
472      * Method to open Http connection
473      */
474     public boolean openHttpConnection() {
475         boolean ret = false;
476
477         try {
478             // Add authentication credentials.
479             AuthenticationStore auth = httpClient.getAuthenticationStore();
480
481             URI uri1 = new URI(baseUrl);
482             auth.addAuthenticationResult(new BasicAuthentication.BasicResult(uri1, username, password));
483
484             String url = baseUrl + BRIDGE_URL_GETDEVICELIST;
485
486             Request req = httpClient.newRequest(url);
487             ContentResponse res = req.send();
488
489             // check status
490             if (res.getStatus() == HttpStatus.OK_200) {
491                 // response OK
492                 httpConnectionOK.set(true);
493
494                 ret = true;
495
496                 logger.debug("HTTP connection to SysAP is OK");
497             } else {
498                 // response NOK, set error
499                 httpConnectionOK.set(false);
500
501                 ret = false;
502             }
503         } catch (URISyntaxException | InterruptedException | ExecutionException | TimeoutException ex) {
504             logger.debug("Initial HTTP connection to SysAP is not successful");
505
506             ret = false;
507         }
508
509         return ret;
510     }
511
512     /**
513      * Method to connect the websocket session
514      */
515     public boolean connectWebsocketSession() {
516         boolean ret = false;
517
518         URI uri = URI.create("ws://" + ipAddress + "/fhapi/v1/api/ws");
519
520         String authString = username + ":" + password;
521
522         // create base64 encoder
523         Base64.Encoder bas64Encoder = Base64.getEncoder();
524
525         // Encoding string using encoder object
526         String authStringEnc = bas64Encoder.encodeToString(authString.getBytes());
527
528         authField = "Basic " + authStringEnc;
529
530         WebSocketClient localWebsocketClient = websocketClient;
531
532         try {
533             // Start socket client
534             if (localWebsocketClient != null) {
535                 localWebsocketClient.setMaxTextMessageBufferSize(8 * 1024);
536                 localWebsocketClient.setMaxIdleTimeout(BRIDGE_WEBSOCKET_TIMEOUT * 60 * 1000);
537                 localWebsocketClient.setConnectTimeout(BRIDGE_WEBSOCKET_TIMEOUT * 60 * 1000);
538                 localWebsocketClient.start();
539                 ClientUpgradeRequest request = new ClientUpgradeRequest();
540                 request.setHeader("Authorization", authField);
541                 request.setTimeout(BRIDGE_WEBSOCKET_TIMEOUT, TimeUnit.MINUTES);
542                 localWebsocketClient.connect(this, uri, request);
543
544                 logger.debug("Websocket connection to SysAP is OK, timeout: {}", BRIDGE_WEBSOCKET_TIMEOUT);
545
546                 ret = true;
547             } else {
548                 ret = false;
549             }
550         } catch (Exception e) {
551             logger.debug("Error by opening Websocket connection [{}]", e.getMessage());
552
553             if (localWebsocketClient != null) {
554                 try {
555                     localWebsocketClient.stop();
556
557                     ret = false;
558                 } catch (Exception e1) {
559                     logger.debug("Error by opening Websocket connection [{}]", e1.getMessage());
560
561                     ret = false;
562                 }
563             } else {
564                 ret = false;
565             }
566         }
567
568         return ret;
569     }
570
571     /**
572      * Method to close the websocket connection
573      */
574     public void closeWebSocketConnection() {
575         socketMonitor.interrupt();
576
577         QueuedThreadPool localThreadPool = jettyThreadPool;
578
579         if (localThreadPool != null) {
580             try {
581                 localThreadPool.stop();
582             } catch (Exception e1) {
583                 logger.debug("Error by closing Websocket connection [{}]", e1.getMessage());
584             }
585             jettyThreadPool = null;
586         }
587
588         WebSocketClient localWebSocketClient = websocketClient;
589
590         if (localWebSocketClient != null) {
591             try {
592                 localWebSocketClient.stop();
593             } catch (Exception e2) {
594                 logger.debug("Error by closing Websocket connection [{}]", e2.getMessage());
595             }
596             websocketClient = null;
597         }
598     }
599
600     /**
601      * Method to open the websocket connection
602      */
603     public boolean openWebSocketConnection() {
604         boolean ret = false;
605
606         QueuedThreadPool localThreadPool = jettyThreadPool;
607
608         if (localThreadPool == null) {
609             jettyThreadPool = new QueuedThreadPool();
610
611             localThreadPool = jettyThreadPool;
612
613             if (localThreadPool != null) {
614                 localThreadPool.setName(FreeAtHomeBridgeHandler.class.getSimpleName());
615                 localThreadPool.setDaemon(true);
616                 localThreadPool.setStopTimeout(0);
617
618                 ret = true;
619             }
620         }
621
622         WebSocketClient localWebSocketClient = websocketClient;
623
624         if (localWebSocketClient == null) {
625             websocketClient = new WebSocketClient(httpClient);
626
627             localWebSocketClient = websocketClient;
628
629             if (localWebSocketClient != null) {
630                 localWebSocketClient.setExecutor(jettyThreadPool);
631
632                 socketMonitor.start();
633
634                 ret = true;
635             } else {
636                 ret = false;
637             }
638         } else {
639             ret = true;
640         }
641
642         return ret;
643     }
644
645     /**
646      * Method to initialize the bridge
647      */
648     @Override
649     public void initialize() {
650         httpConnectionOK.set(false);
651
652         // load configuration
653         FreeAtHomeBridgeHandlerConfiguration locConfig = getConfigAs(FreeAtHomeBridgeHandlerConfiguration.class);
654
655         ipAddress = locConfig.ipAddress;
656         if (ipAddress.isBlank()) {
657             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
658                     "@text/conf-error.ip-address-missing");
659             return;
660         }
661
662         password = locConfig.password;
663         if (password.isBlank()) {
664             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
665                     "@text/conf-error.password-missing");
666             return;
667         }
668
669         username = locConfig.username;
670         if (username.isBlank()) {
671             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
672                     "@text/conf-error.username-missing");
673             return;
674         }
675
676         // build base URL
677         baseUrl = "http://" + ipAddress + "/fhapi/v1/api";
678
679         updateStatus(ThingStatus.UNKNOWN);
680
681         scheduler.execute(() -> {
682             boolean thingReachable = true;
683
684             // Open Http connection
685             if (!openHttpConnection()) {
686                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
687                         "@text/comm-error.http-wrongpass-or-ip");
688
689                 thingReachable = false;
690             }
691
692             // Open the websocket connection for immediate status updates
693             if (!openWebSocketConnection()) {
694                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
695                         "@text/comm-error.not-able-open-websocketconnection");
696
697                 thingReachable = false;
698             }
699
700             if (thingReachable) {
701                 updateStatus(ThingStatus.ONLINE);
702             }
703         });
704     }
705
706     /**
707      * Method to dispose
708      */
709     @Override
710     public void dispose() {
711         // let run out the thread
712         socketMonitor.interrupt();
713
714         closeWebSocketConnection();
715     }
716
717     /**
718      * Thread that maintains connection via Websocket.
719      */
720     private class FreeAtHomeWebsocketMonitorThread extends Thread {
721
722         // initial delay to initiate connection
723         private AtomicInteger reconnectDelay = new AtomicInteger();
724
725         public FreeAtHomeWebsocketMonitorThread() {
726         }
727
728         @Override
729         public void run() {
730             // set initial connect delay to 0
731             reconnectDelay.set(0);
732
733             try {
734                 while (!isInterrupted()) {
735                     if (httpConnectionOK.get()) {
736                         if (connectSession()) {
737                             while (isSocketConnectionAlive()) {
738                                 TimeUnit.SECONDS.sleep(BRIDGE_WEBSOCKET_KEEPALIVE);
739
740                                 logger.debug("Sending keep-alive message {}", System.currentTimeMillis());
741                                 sendWebsocketKeepAliveMessage("keep-alive");
742                             }
743                         }
744                         logger.debug("Socket connection closed");
745                         reconnectDelay.set(BRIDGE_WEBSOCKET_RECONNECT_DELAY);
746                     } else {
747                         TimeUnit.SECONDS.sleep(BRIDGE_WEBSOCKET_RECONNECT_DELAY);
748                     }
749                 }
750             } catch (InterruptedException e) {
751                 Thread.currentThread().interrupt();
752
753                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
754                         "@text/comm-error.general-websocket-issue");
755             } catch (IOException e) {
756                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
757                         "@text/comm-error.websocket-keep-alive-error");
758             }
759         }
760
761         private boolean connectSession() throws InterruptedException {
762             int delay = reconnectDelay.get();
763
764             if (delay > 0) {
765                 logger.debug("Delaying (re)connect request by {} seconds.", reconnectDelay);
766                 TimeUnit.SECONDS.sleep(delay);
767             }
768
769             logger.debug("Server connecting to websocket");
770
771             if (!connectWebsocketSession()) {
772                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
773                         "@text/comm-error.general-websocket-issue");
774
775                 reconnectDelay.set(BRIDGE_WEBSOCKET_RECONNECT_DELAY);
776
777                 return false;
778             }
779
780             if (websocketSession == null) {
781                 lock.lock();
782                 try {
783                     websocketSessionEstablished.await();
784                 } finally {
785                     lock.unlock();
786                 }
787             }
788
789             return true;
790         }
791     }
792
793     /**
794      * Send keep-alive message to SysAp
795      */
796     public void sendWebsocketKeepAliveMessage(String message) throws IOException {
797         Session localSession = websocketSession;
798
799         if (localSession != null) {
800             localSession.getRemote().sendString(message);
801         }
802     }
803
804     /**
805      * Get socket alive state
806      * 
807      * @throws InterruptedException
808      */
809     public boolean isSocketConnectionAlive() throws InterruptedException {
810         Session localSession = websocketSession;
811
812         return (localSession != null) ? localSession.isOpen() : false;
813     }
814
815     /**
816      * Socket closed. Report the state
817      */
818     @Override
819     public void onWebSocketClose(int statusCode, @Nullable String reason) {
820         websocketSession = null;
821         logger.debug("Socket Closed: [ {} ] {}", statusCode, reason);
822     }
823
824     /**
825      * Socket connected. store the session for later use
826      */
827     @Override
828     public void onWebSocketConnect(@Nullable Session session) {
829         Session localSession = session;
830
831         if (localSession != null) {
832             websocketSession = localSession;
833
834             localSession.setIdleTimeout(-1);
835
836             logger.debug("Socket Connected - Timeout {} - sesson: {}", localSession.getIdleTimeout(), session);
837         } else {
838             logger.debug("Socket Connected - Timeout (invalid) - sesson: (invalid)");
839         }
840
841         lock.lock();
842         try {
843             websocketSessionEstablished.signal();
844         } finally {
845             lock.unlock();
846         }
847     }
848
849     /**
850      * Error caused. Report the state
851      */
852     @Override
853     public void onWebSocketError(@Nullable Throwable cause) {
854         websocketSession = null;
855
856         if (cause != null) {
857             logger.debug("Socket Error: {}", cause.getLocalizedMessage());
858         } else {
859             logger.debug("Socket Error: unknown");
860         }
861     }
862
863     /**
864      * Binary message received. It shall not happen with the free@home SysAp
865      */
866     @Override
867     @NonNullByDefault({})
868     public void onWebSocketBinary(byte[] payload, int offset, int len) {
869         logger.debug("Binary message received via websocket");
870     }
871
872     /**
873      * Text message received. Processing will be started
874      */
875     @Override
876     public void onWebSocketText(@Nullable String message) {
877         if (message != null) {
878             if (message.toLowerCase(Locale.US).contains("bye")) {
879                 Session localSession = websocketSession;
880
881                 if (localSession != null) {
882                     localSession.close(StatusCode.NORMAL, "Thanks");
883                 }
884
885                 logger.debug("Websocket connection closed: {} ", message);
886             } else {
887                 logger.debug("Received websocket text: {} ", message);
888
889                 setDatapointOnWebsocketFeedback(message);
890
891                 markDeviceRemovedOnWebsocketFeedback(message);
892             }
893         } else {
894             logger.debug("Invalid message string");
895         }
896     }
897 }