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