]> git.basschouten.com Git - openhab-addons.git/blob
aa6b9d58dedc0c91a669c1f213f21a91b39b48a4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.loxone.internal;
14
15 import java.io.IOException;
16 import java.net.InetAddress;
17 import java.net.URI;
18 import java.net.UnknownHostException;
19 import java.time.Duration;
20 import java.time.Instant;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.concurrent.BlockingQueue;
28 import java.util.concurrent.LinkedBlockingQueue;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.atomic.AtomicBoolean;
31 import java.util.concurrent.atomic.AtomicInteger;
32 import java.util.concurrent.locks.Lock;
33 import java.util.concurrent.locks.ReentrantLock;
34
35 import org.eclipse.jetty.util.thread.QueuedThreadPool;
36 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
37 import org.eclipse.jetty.websocket.client.WebSocketClient;
38 import org.openhab.binding.loxone.internal.controls.LxControl;
39 import org.openhab.binding.loxone.internal.types.LxConfig;
40 import org.openhab.binding.loxone.internal.types.LxConfig.LxServerInfo;
41 import org.openhab.binding.loxone.internal.types.LxErrorCode;
42 import org.openhab.binding.loxone.internal.types.LxResponse;
43 import org.openhab.binding.loxone.internal.types.LxState;
44 import org.openhab.binding.loxone.internal.types.LxStateUpdate;
45 import org.openhab.binding.loxone.internal.types.LxUuid;
46 import org.openhab.core.config.core.Configuration;
47 import org.openhab.core.thing.Channel;
48 import org.openhab.core.thing.ChannelUID;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.ThingStatus;
51 import org.openhab.core.thing.ThingStatusDetail;
52 import org.openhab.core.thing.ThingTypeUID;
53 import org.openhab.core.thing.ThingUID;
54 import org.openhab.core.thing.binding.BaseThingHandler;
55 import org.openhab.core.thing.binding.builder.ThingBuilder;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.openhab.core.types.State;
59 import org.openhab.core.types.StateDescription;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63 import com.google.gson.Gson;
64 import com.google.gson.GsonBuilder;
65
66 /**
67  * Representation of a Loxone Miniserver. It is an openHAB {@link Thing}, which is used to communicate with
68  * objects (controls) configured in the Miniserver over channels.
69  *
70  * @author Pawel Pieczul - Initial contribution
71  */
72 public class LxServerHandler extends BaseThingHandler implements LxServerHandlerApi {
73
74     private static final String SOCKET_URL = "/ws/rfc6455";
75     private static final String CMD_CFG_API = "jdev/cfg/api";
76
77     private static final Gson GSON;
78
79     private LxBindingConfiguration bindingConfig;
80     private InetAddress host;
81
82     // initial delay to initiate connection
83     private AtomicInteger reconnectDelay = new AtomicInteger();
84
85     // Map of state UUID to a map of control UUID and state objects
86     // State with a unique UUID can be configured in many controls and each control can even have a different name of
87     // the state. It must be ensured that updates received for this state UUID are passed to all controls that have this
88     // state UUID configured.
89     private Map<LxUuid, Map<LxUuid, LxState>> states = new HashMap<>();
90
91     private LxWebSocket socket;
92     private WebSocketClient wsClient;
93
94     private int debugId = 0;
95     private Thread monitorThread;
96     private final Lock threadLock = new ReentrantLock();
97     private AtomicBoolean sessionActive = new AtomicBoolean(false);
98
99     // Data structures
100     private final Map<LxUuid, LxControl> controls = new HashMap<>();
101     private final Map<ChannelUID, LxControl> channels = new HashMap<>();
102     private final BlockingQueue<LxStateUpdate> stateUpdateQueue = new LinkedBlockingQueue<>();
103
104     private LxDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
105     private final Logger logger = LoggerFactory.getLogger(LxServerHandler.class);
106     private static AtomicInteger staticDebugId = new AtomicInteger(1);
107
108     static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
109             .singleton(LxBindingConstants.THING_TYPE_MINISERVER);
110
111     private QueuedThreadPool jettyThreadPool;
112
113     static {
114         GsonBuilder builder = new GsonBuilder();
115         builder.registerTypeAdapter(LxUuid.class, LxUuid.DESERIALIZER);
116         builder.registerTypeAdapter(LxControl.class, LxControl.DESERIALIZER);
117         GSON = builder.create();
118     }
119
120     /**
121      * Create {@link LxServerHandler} object
122      *
123      * @param thing Thing object that creates the handler
124      * @param provider state description provider service
125      */
126     public LxServerHandler(Thing thing, LxDynamicStateDescriptionProvider provider) {
127         super(thing);
128         logger.debug("[{}] Constructing thing object", debugId);
129         if (provider != null) {
130             dynamicStateDescriptionProvider = provider;
131         } else {
132             logger.warn("Dynamic state description provider is null");
133         }
134     }
135
136     /*
137      * Methods from BaseThingHandler
138      */
139
140     @Override
141     public void handleCommand(ChannelUID channelUID, Command command) {
142         if (command instanceof RefreshType) {
143             updateChannelState(channelUID);
144             return;
145         }
146         try {
147             LxControl control = channels.get(channelUID);
148             if (control != null) {
149                 control.handleCommand(channelUID, command);
150             } else {
151                 logger.error("[{}] Received command {} for unknown control.", debugId, command);
152             }
153         } catch (IOException e) {
154             setOffline(LxErrorCode.COMMUNICATION_ERROR, e.getMessage());
155         }
156     }
157
158     @Override
159     public void channelLinked(ChannelUID channelUID) {
160         logger.debug("[{}] Channel linked: {}", debugId, channelUID.getAsString());
161         updateChannelState(channelUID);
162     }
163
164     @Override
165     public void initialize() {
166         threadLock.lock();
167         try {
168             debugId = staticDebugId.getAndIncrement();
169
170             logger.debug("[{}] Initializing thing instance", debugId);
171             bindingConfig = getConfig().as(LxBindingConfiguration.class);
172             try {
173                 this.host = InetAddress.getByName(bindingConfig.host);
174             } catch (UnknownHostException e) {
175                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unknown host");
176                 return;
177             }
178             reconnectDelay.set(bindingConfig.firstConDelay);
179
180             jettyThreadPool = new QueuedThreadPool();
181             jettyThreadPool.setName(LxServerHandler.class.getSimpleName() + "-" + debugId);
182             jettyThreadPool.setDaemon(true);
183
184             socket = new LxWebSocket(debugId, this, bindingConfig, host);
185             wsClient = new WebSocketClient();
186             wsClient.setExecutor(jettyThreadPool);
187             if (debugId > 1) {
188                 reconnectDelay.set(0);
189             }
190             if (monitorThread == null) {
191                 monitorThread = new LxServerThread(debugId);
192                 monitorThread.start();
193             }
194         } finally {
195             threadLock.unlock();
196         }
197     }
198
199     @Override
200     public void dispose() {
201         logger.debug("[{}] Disposing of thing", debugId);
202         Thread thread;
203         threadLock.lock();
204         try {
205             sessionActive.set(false);
206             stateUpdateQueue.clear();
207             thread = monitorThread;
208             if (monitorThread != null) {
209                 monitorThread.interrupt();
210                 monitorThread = null;
211             }
212             clearConfiguration();
213         } finally {
214             threadLock.unlock();
215         }
216         if (thread != null) {
217             try {
218                 thread.join(5000);
219             } catch (InterruptedException e) {
220                 logger.warn("[{}] Waiting for thread termination interrupted.", debugId);
221             }
222         }
223     }
224
225     /*
226      * Public methods that are called by {@link LxControl} child classes
227      */
228
229     /*
230      * (non-Javadoc)
231      *
232      * @see org.openhab.binding.loxone.internal.LxServerHandlerApi#sendAction(org.openhab.binding.loxone.internal.types.
233      * LxUuid, java.lang.String)
234      */
235     @Override
236     public void sendAction(LxUuid id, String operation) throws IOException {
237         socket.sendAction(id, operation);
238     }
239
240     /*
241      * (non-Javadoc)
242      *
243      * @see
244      * org.openhab.binding.loxone.internal.LxServerHandlerApi#addControl(org.openhab.binding.loxone.internal.controls.
245      * LxControl)
246      */
247     @Override
248     public void addControl(LxControl control) {
249         addControlStructures(control);
250         addThingChannels(control.getChannelsWithSubcontrols(), false);
251     }
252
253     /*
254      * (non-Javadoc)
255      *
256      * @see
257      * org.openhab.binding.loxone.internal.LxServerHandlerApi#removeControl(org.openhab.binding.loxone.internal.controls
258      * .LxControl)
259      */
260     @Override
261     public void removeControl(LxControl control) {
262         logger.debug("[{}] Removing control: {}", debugId, control.getName());
263         control.getSubControls().values().forEach(subControl -> removeControl(subControl));
264         LxUuid controlUuid = control.getUuid();
265         control.getStates().values().forEach(state -> {
266             LxUuid stateUuid = state.getUuid();
267             Map<LxUuid, LxState> perUuid = states.get(stateUuid);
268             if (perUuid != null) {
269                 perUuid.remove(controlUuid);
270                 if (perUuid.isEmpty()) {
271                     states.remove(stateUuid);
272                 }
273             }
274         });
275
276         ThingBuilder builder = editThing();
277         control.getChannels().forEach(channel -> {
278             ChannelUID id = channel.getUID();
279             builder.withoutChannel(id);
280             dynamicStateDescriptionProvider.removeDescription(id);
281             channels.remove(id);
282         });
283         updateThing(builder.build());
284         controls.remove(controlUuid);
285     }
286
287     /*
288      * (non-Javadoc)
289      *
290      * @see org.openhab.binding.loxone.internal.LxServerHandlerApi#setChannelState(org.openhab.core.thing.
291      * ChannelUID, org.openhab.core.types.State)
292      */
293     @Override
294     public void setChannelState(ChannelUID channelId, State state) {
295         updateState(channelId, state);
296     }
297
298     /*
299      * (non-Javadoc)
300      *
301      * @see
302      * org.openhab.binding.loxone.internal.LxServerHandlerApi#setChannelStateDescription(org.openhab.core.
303      * thing.ChannelUID, org.openhab.core.types.StateDescription)
304      */
305     @Override
306     public void setChannelStateDescription(ChannelUID channelId, StateDescription description) {
307         logger.debug("[{}] State description update for channel {}", debugId, channelId);
308         dynamicStateDescriptionProvider.setDescription(channelId, description);
309     }
310
311     /*
312      * Public methods called by {@link LxWsSecurity} child classes.
313      */
314
315     /*
316      * (non-Javadoc)
317      *
318      * @see org.openhab.binding.loxone.internal.LxServerHandlerApi#getSetting(java.lang.String)
319      */
320     @Override
321     public String getSetting(String name) {
322         Object value = getConfig().get(name);
323         return (value instanceof String) ? (String) value : null;
324     }
325
326     /*
327      * (non-Javadoc)
328      *
329      * @see org.openhab.binding.loxone.internal.LxServerHandlerApi#setSettings(java.util.Map)
330      */
331     @Override
332     public void setSettings(Map<String, String> properties) {
333         Configuration config = getConfig();
334         properties.forEach((name, value) -> config.put(name, value));
335         updateConfiguration(config);
336     }
337
338     /*
339      * (non-Javadoc)
340      *
341      * @see org.openhab.binding.loxone.internal.LxServerHandlerApi#getGson()
342      */
343     @Override
344     public Gson getGson() {
345         return GSON;
346     }
347
348     /*
349      * (non-Javadoc)
350      *
351      * @see org.openhab.binding.loxone.internal.LxServerHandlerApi#getThingId()
352      */
353     @Override
354     public ThingUID getThingId() {
355         return getThing().getUID();
356     }
357
358     /*
359      * Methods called by {@link LxWebSocket} class.
360      */
361
362     /**
363      * Dispose of all objects created from the Miniserver configuration.
364      */
365     void clearConfiguration() {
366         controls.clear();
367         channels.clear();
368         states.clear();
369         dynamicStateDescriptionProvider.removeAllDescriptions();
370     }
371
372     /**
373      * Sets a new configuration received from the Miniserver and creates all required channels.
374      *
375      * @param config Miniserver's configuration
376      */
377     void setMiniserverConfig(LxConfig config) {
378         logger.debug("[{}] Setting configuration from Miniserver", debugId);
379
380         if (config.msInfo == null) {
381             logger.warn("[{}] missing global configuration msInfo on Loxone", debugId);
382             config.msInfo = config.new LxServerInfo();
383         }
384         Thing thing = getThing();
385         LxServerInfo info = config.msInfo;
386         thing.setProperty(LxBindingConstants.MINISERVER_PROPERTY_MINISERVER_NAME, buildName(info.msName));
387         thing.setProperty(LxBindingConstants.MINISERVER_PROPERTY_PROJECT_NAME, buildName(info.projectName));
388         thing.setProperty(LxBindingConstants.MINISERVER_PROPERTY_CLOUD_ADDRESS, buildName(info.remoteUrl));
389         thing.setProperty(LxBindingConstants.MINISERVER_PROPERTY_PHYSICAL_LOCATION, buildName(info.location));
390         thing.setProperty(Thing.PROPERTY_FIRMWARE_VERSION, buildName(info.swVersion));
391         thing.setProperty(Thing.PROPERTY_SERIAL_NUMBER, buildName(info.serialNr));
392         thing.setProperty(Thing.PROPERTY_MAC_ADDRESS, buildName(info.macAddress));
393
394         List<Channel> list = new ArrayList<>();
395         if (config.controls != null) {
396             logger.trace("[{}] creating control structures.", debugId);
397             config.controls.values().forEach(ctrl -> {
398                 addControlStructures(ctrl);
399                 list.addAll(ctrl.getChannelsWithSubcontrols());
400             });
401         } else {
402             logger.warn("[{}] no controls received in Miniserver configuration.", debugId);
403         }
404         addThingChannels(list, true);
405         updateStatus(ThingStatus.ONLINE);
406     }
407
408     /**
409      * Set thing status to offline and start attempts to establish a new connection to the Miniserver after a delay
410      * depending of the reason for going offline.
411      *
412      * @param code error code
413      * @param reason reason for going offline
414      */
415     void setOffline(LxErrorCode code, String reason) {
416         logger.debug("[{}] set offline code={} reason={}", debugId, code, reason);
417         switch (code) {
418             case TOO_MANY_FAILED_LOGIN_ATTEMPTS:
419                 // assume credentials are wrong, do not re-attempt connections any time soon
420                 // expect a new instance will have to be initialized with corrected configuration
421                 reconnectDelay.set(60 * 60 * 24 * 7);
422                 updateStatusToOffline(ThingStatusDetail.CONFIGURATION_ERROR,
423                         "Too many failed login attempts - stopped trying");
424                 break;
425             case USER_UNAUTHORIZED:
426                 reconnectDelay.set(bindingConfig.userErrorDelay);
427                 updateStatusToOffline(ThingStatusDetail.CONFIGURATION_ERROR,
428                         reason != null ? reason : "User authentication error (invalid user name or password)");
429                 break;
430             case USER_AUTHENTICATION_TIMEOUT:
431                 updateStatusToOffline(ThingStatusDetail.COMMUNICATION_ERROR, "User authentication timeout");
432                 break;
433             case COMMUNICATION_ERROR:
434                 reconnectDelay.set(bindingConfig.comErrorDelay);
435                 String text = "Error communicating with Miniserver";
436                 if (reason != null) {
437                     text += " (" + reason + ")";
438                 }
439                 updateStatusToOffline(ThingStatusDetail.COMMUNICATION_ERROR, text);
440                 break;
441             case INTERNAL_ERROR:
442                 updateStatusToOffline(ThingStatusDetail.CONFIGURATION_ERROR,
443                         reason != null ? "Internal error (" + reason + ")" : "Internal error");
444                 break;
445             case WEBSOCKET_IDLE_TIMEOUT:
446                 logger.warn("Idle timeout from Loxone Miniserver - adjust keepalive settings");
447                 updateStatusToOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Timeout due to no activity");
448                 break;
449             case ERROR_CODE_MISSING:
450                 logger.warn("No error code available from the Miniserver");
451                 updateStatusToOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Unknown reason - error code missing");
452                 break;
453             default:
454                 updateStatusToOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Unknown reason");
455                 break;
456         }
457         sessionActive.set(false);
458     }
459
460     /**
461      * Put a new state update event to the queue for processing and signal thread to process it
462      *
463      * @param uuid state uuid (null indicates websocket session should be closed)
464      * @param value new state value
465      */
466     void queueStateUpdate(LxUuid uuid, Object value) {
467         stateUpdateQueue.add(new LxStateUpdate(uuid, value));
468     }
469
470     /**
471      * Update to the new value of a state received from Miniserver. This method will go through all instances of this
472      * state UUID and update their value, which will trigger corresponding control state update method in each control
473      * that has this state.
474      *
475      * @param update Miniserver's update event
476      */
477     private void updateStateValue(LxStateUpdate update) {
478         Map<LxUuid, LxState> perStateUuid = states.get(update.getUuid());
479         if (perStateUuid != null) {
480             perStateUuid.forEach((controlUuid, state) -> {
481                 state.setStateValue(update.getValue());
482             });
483         }
484     }
485
486     /**
487      * Add a new control, its states, subcontrols and channels to the handler structures.
488      * Handler maintains maps of all controls (main controls + subcontrols), all channels for all controls and all
489      * states to match received openHAB commands and state updates from the Miniserver. States also contain links to
490      * possibly multiple control objects, as many controls can share the same state with the same state uuid.
491      * To create channels, {@link LxServerHandler#addThingChannels} method should be called separately. This allows
492      * creation of all channels for all controls with a single thing update.
493      *
494      * @param control a created control object to be added
495      */
496     private void addControlStructures(LxControl control) {
497         LxUuid uuid = control.getUuid();
498         logger.debug("[{}] Adding control to handler: {}, {}", debugId, uuid, control.getName());
499         control.getStates().values().forEach(state -> {
500             Map<LxUuid, LxState> perUuid = states.get(state.getUuid());
501             if (perUuid == null) {
502                 perUuid = new HashMap<>();
503                 states.put(state.getUuid(), perUuid);
504             }
505             perUuid.put(uuid, state);
506         });
507         controls.put(control.getUuid(), control);
508         control.getChannels().forEach(channel -> channels.put(channel.getUID(), control));
509         control.getSubControls().values().forEach(subControl -> addControlStructures(subControl));
510     }
511
512     /**
513      * Adds channels to the thing, to make them available to the framework and user.
514      * This method will sort the channels according to their label.
515      * It is expected that input list contains no duplicate channel IDs.
516      *
517      * @param newChannels a list of channels to add to the thing
518      * @param purge if true, old channels will be removed, otherwise merged
519      */
520     private void addThingChannels(List<Channel> newChannels, boolean purge) {
521         List<Channel> channels = newChannels;
522         if (!purge) {
523             channels.addAll(getThing().getChannels());
524         }
525         channels.sort((c1, c2) -> {
526             String label1 = c1.getLabel();
527             String label2 = c2.getLabel();
528             if (label1 != null && label2 != null) {
529                 return label1.compareTo(label2);
530             } else if (label1 == null && label2 != null) {
531                 return 1;
532             } else if (label1 != null && label2 == null) {
533                 return -1;
534             } else {
535                 return 0;
536             }
537         });
538         ThingBuilder builder = editThing();
539         builder.withChannels(channels);
540         updateThing(builder.build());
541     }
542
543     /**
544      * Connect the websocket.
545      * Attempts to connect to the websocket on a remote Miniserver. If a connection is established, a
546      * {@link LxWebSocket#onConnect} method will be called from a parallel websocket thread.
547      *
548      * @return true if connection request initiated correctly, false if not
549      */
550     private boolean connect() {
551         logger.debug("[{}] connect() websocket", debugId);
552         /*
553          * Try to read CfgApi structure from the miniserver. It contains serial number and firmware version. If it can't
554          * be read this is not a fatal issue, we will assume most recent version running.
555          */
556         String message = socket.httpGet(CMD_CFG_API);
557         if (message != null) {
558             LxResponse resp = socket.getResponse(message);
559             if (resp != null) {
560                 socket.setFwVersion(GSON.fromJson(resp.getValueAsString(), LxResponse.LxResponseCfgApi.class).version);
561             }
562         } else {
563             logger.debug("[{}] Http get failed for API config request.", debugId);
564         }
565
566         try {
567             wsClient.start();
568
569             // Following the PR github.com/eclipse/smarthome/pull/6636
570             // without this zero timeout, jetty will wait 30 seconds for stopping the client to eventually fail
571             // with the timeout it is immediate and all threads end correctly
572             jettyThreadPool.setStopTimeout(0);
573             URI target = new URI("ws://" + host.getHostAddress() + ":" + bindingConfig.port + SOCKET_URL);
574             ClientUpgradeRequest request = new ClientUpgradeRequest();
575             request.setSubProtocols("remotecontrol");
576
577             socket.startResponseTimeout();
578             logger.debug("[{}] Connecting to server : {} ", debugId, target);
579             wsClient.connect(socket, target, request);
580             return true;
581         } catch (Exception e) {
582             logger.debug("[{}] Error starting websocket client: {}", debugId, e.getMessage());
583             try {
584                 wsClient.stop();
585             } catch (Exception e2) {
586                 logger.debug("[{}] Error stopping websocket client: {}", debugId, e2.getMessage());
587             }
588             return false;
589         }
590     }
591
592     /*
593      * Private methods
594      */
595
596     /**
597      * Disconnect websocket session - initiated from this end.
598      *
599      * @param code error code for disconnecting the websocket
600      * @param reason reason for disconnecting the websocket
601      */
602     private void disconnect(LxErrorCode code, String reason) {
603         logger.debug("[{}] disconnect the websocket: {}, {}", debugId, code, reason);
604         socket.disconnect(code, reason);
605         try {
606             logger.debug("[{}] client stop", debugId);
607             wsClient.stop();
608             logger.debug("[{}] client stopped", debugId);
609         } catch (Exception e) {
610             logger.debug("[{}] Exception disconnecting the websocket: ", e.getMessage());
611         }
612     }
613
614     /**
615      * Thread that maintains connection to the Miniserver.
616      * It will periodically attempt to connect and if failed, wait a configured amount of time.
617      * If connection succeeds, it will sleep until the session is terminated. Then it will wait and try to reconnect
618      * again.
619      *
620      * @author Pawel Pieczul - initial contribution
621      *
622      */
623     private class LxServerThread extends Thread {
624         private int debugId = 0;
625         private long elapsed = 0;
626         private Instant lastKeepAlive;
627
628         LxServerThread(int id) {
629             debugId = id;
630         }
631
632         @Override
633         public void run() {
634             logger.debug("[{}] Thread starting", debugId);
635             try {
636                 while (!isInterrupted()) {
637                     sessionActive.set(connectSession());
638                     processStateUpdates();
639                 }
640             } catch (InterruptedException e) {
641                 logger.debug("[{}] Thread interrupted", debugId);
642             }
643             disconnect(LxErrorCode.OK, "Thing is going down.");
644             logger.debug("[{}] Thread ending", debugId);
645         }
646
647         private boolean connectSession() throws InterruptedException {
648             int delay = reconnectDelay.get();
649             if (delay > 0) {
650                 logger.debug("[{}] Delaying connect request by {} seconds.", debugId, reconnectDelay);
651                 TimeUnit.SECONDS.sleep(delay);
652             }
653             logger.debug("[{}] Server connecting to websocket", debugId);
654             if (!connect()) {
655                 updateStatusToOffline(ThingStatusDetail.COMMUNICATION_ERROR,
656                         "Failed to connect to Miniserver's WebSocket");
657                 reconnectDelay.set(bindingConfig.connectErrDelay);
658                 return false;
659             }
660             lastKeepAlive = Instant.now();
661             return true;
662         }
663
664         private void processStateUpdates() throws InterruptedException {
665             while (sessionActive.get()) {
666                 logger.debug("[{}] Sleeping for {} seconds.", debugId, bindingConfig.keepAlivePeriod - elapsed);
667                 LxStateUpdate update = stateUpdateQueue.poll(bindingConfig.keepAlivePeriod - elapsed, TimeUnit.SECONDS);
668                 elapsed = Duration.between(lastKeepAlive, Instant.now()).getSeconds();
669                 if (update == null || elapsed >= bindingConfig.keepAlivePeriod) {
670                     sendKeepAlive();
671                     elapsed = 0;
672                 }
673                 if (update != null) {
674                     updateStateValue(update);
675                 }
676             }
677         }
678
679         private void sendKeepAlive() {
680             socket.sendKeepAlive();
681             lastKeepAlive = Instant.now();
682             elapsed = 0;
683         }
684     }
685
686     /**
687      * Updates the thing status to offline, if it is not already offline. This will preserve he first reason of going
688      * offline in case there were multiple reasons.
689      *
690      * @param code error code
691      * @param reason reason for going offline
692      */
693     private void updateStatusToOffline(ThingStatusDetail code, String reason) {
694         ThingStatus status = getThing().getStatus();
695         if (status == ThingStatus.OFFLINE) {
696             logger.debug("[{}] received offline request with code {}, but thing already offline.", debugId, code);
697         } else {
698             updateStatus(ThingStatus.OFFLINE, code, reason);
699         }
700     }
701
702     /**
703      * Updates an actual state of a channel.
704      * Determines control for the channel and retrieves the state from the control.
705      *
706      * @param channelId channel ID to update its state
707      */
708     private void updateChannelState(ChannelUID channelId) {
709         LxControl control = channels.get(channelId);
710         if (control != null) {
711             State state = control.getChannelState(channelId);
712             if (state != null) {
713                 updateState(channelId, state);
714             }
715         } else {
716             logger.error("[{}] Received state update request for unknown control (channelId={}).", debugId, channelId);
717         }
718     }
719
720     /**
721      * Check and convert null string to empty string.
722      *
723      * @param name string to check
724      * @return string guaranteed to be not null
725      */
726     private String buildName(String name) {
727         if (name == null) {
728             return "";
729         }
730         return name;
731     }
732 }