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