]> git.basschouten.com Git - openhab-addons.git/blob
80a46c9658e764dcc1d8698e5204057eca6cbe1a
[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.velux.internal.handler;
14
15 import java.time.Duration;
16 import java.time.Instant;
17 import java.util.Collection;
18 import java.util.Map;
19 import java.util.Map.Entry;
20 import java.util.Set;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.ExecutorService;
23 import java.util.concurrent.Executors;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.velux.internal.VeluxBinding;
30 import org.openhab.binding.velux.internal.VeluxBindingConstants;
31 import org.openhab.binding.velux.internal.VeluxItemType;
32 import org.openhab.binding.velux.internal.action.VeluxActions;
33 import org.openhab.binding.velux.internal.bridge.VeluxBridge;
34 import org.openhab.binding.velux.internal.bridge.VeluxBridgeActuators;
35 import org.openhab.binding.velux.internal.bridge.VeluxBridgeDeviceStatus;
36 import org.openhab.binding.velux.internal.bridge.VeluxBridgeGetFirmware;
37 import org.openhab.binding.velux.internal.bridge.VeluxBridgeGetHouseStatus;
38 import org.openhab.binding.velux.internal.bridge.VeluxBridgeInstance;
39 import org.openhab.binding.velux.internal.bridge.VeluxBridgeLANConfig;
40 import org.openhab.binding.velux.internal.bridge.VeluxBridgeProvider;
41 import org.openhab.binding.velux.internal.bridge.VeluxBridgeScenes;
42 import org.openhab.binding.velux.internal.bridge.VeluxBridgeSetHouseStatusMonitor;
43 import org.openhab.binding.velux.internal.bridge.VeluxBridgeWLANConfig;
44 import org.openhab.binding.velux.internal.bridge.common.BridgeAPI;
45 import org.openhab.binding.velux.internal.bridge.common.BridgeCommunicationProtocol;
46 import org.openhab.binding.velux.internal.bridge.common.RunProductCommand;
47 import org.openhab.binding.velux.internal.bridge.common.RunReboot;
48 import org.openhab.binding.velux.internal.bridge.json.JsonVeluxBridge;
49 import org.openhab.binding.velux.internal.bridge.slip.FunctionalParameters;
50 import org.openhab.binding.velux.internal.bridge.slip.SlipVeluxBridge;
51 import org.openhab.binding.velux.internal.config.VeluxBridgeConfiguration;
52 import org.openhab.binding.velux.internal.development.Threads;
53 import org.openhab.binding.velux.internal.factory.VeluxHandlerFactory;
54 import org.openhab.binding.velux.internal.handler.utils.ExtendedBaseBridgeHandler;
55 import org.openhab.binding.velux.internal.handler.utils.Thing2VeluxActuator;
56 import org.openhab.binding.velux.internal.handler.utils.ThingProperty;
57 import org.openhab.binding.velux.internal.things.VeluxExistingProducts;
58 import org.openhab.binding.velux.internal.things.VeluxExistingScenes;
59 import org.openhab.binding.velux.internal.things.VeluxProduct;
60 import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex;
61 import org.openhab.binding.velux.internal.things.VeluxProductPosition;
62 import org.openhab.binding.velux.internal.things.VeluxProductPosition.PositionType;
63 import org.openhab.binding.velux.internal.utils.Localization;
64 import org.openhab.core.common.AbstractUID;
65 import org.openhab.core.common.NamedThreadFactory;
66 import org.openhab.core.library.types.DecimalType;
67 import org.openhab.core.library.types.OnOffType;
68 import org.openhab.core.library.types.PercentType;
69 import org.openhab.core.thing.Bridge;
70 import org.openhab.core.thing.ChannelUID;
71 import org.openhab.core.thing.ThingStatus;
72 import org.openhab.core.thing.ThingStatusDetail;
73 import org.openhab.core.thing.ThingTypeUID;
74 import org.openhab.core.thing.binding.ThingHandler;
75 import org.openhab.core.thing.binding.ThingHandlerService;
76 import org.openhab.core.types.Command;
77 import org.openhab.core.types.RefreshType;
78 import org.openhab.core.types.State;
79 import org.openhab.core.types.UnDefType;
80 import org.slf4j.Logger;
81 import org.slf4j.LoggerFactory;
82
83 /**
84  * <B>Common interaction with the </B><I>Velux</I><B> bridge.</B>
85  * <P>
86  * It implements the communication between <B>OpenHAB</B> and the <I>Velux</I> Bridge:
87  * <UL>
88  * <LI><B>OpenHAB</B> Event Bus &rarr; <I>Velux</I> <B>bridge</B>
89  * <P>
90  * Sending commands and value updates.</LI>
91  * </UL>
92  * <UL>
93  * <LI><I>Velux</I> <B>bridge</B> &rarr; <B>OpenHAB</B>:
94  * <P>
95  * Retrieving information by sending a Refresh command.</LI>
96  * </UL>
97  * <P>
98  * Entry point for this class is the method
99  * {@link VeluxBridgeHandler#handleCommand handleCommand}.
100  *
101  * @author Guenther Schreiner - Initial contribution.
102  */
103 @NonNullByDefault
104 public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements VeluxBridgeInstance, VeluxBridgeProvider {
105
106     /*
107      * timeout to ensure that the binding shutdown will not block and stall the shutdown of OH itself
108      */
109     private static final int COMMUNICATION_TASK_MAX_WAIT_SECS = 10;
110
111     /*
112      * a modifier string to avoid the (small) risk of other tasks (outside this binding) locking on the same ip address
113      * Strings.intern() object
114      *
115      */
116     private static final String LOCK_MODIFIER = "velux.ipaddr.";
117
118     private final Logger logger = LoggerFactory.getLogger(VeluxBridgeHandler.class);
119
120     // Class internal
121
122     /**
123      * Scheduler for continuous refresh by scheduleWithFixedDelay.
124      */
125     private @Nullable ScheduledFuture<?> refreshSchedulerJob = null;
126
127     /**
128      * Counter of refresh invocations by {@link refreshSchedulerJob}.
129      */
130     private int refreshCounter = 0;
131
132     /**
133      * Dedicated task executor for the long-running bridge communication tasks.
134      *
135      * Note: there is no point in using multi threaded thread-pool here, since all the submitted (Runnable) tasks are
136      * anyway forced to go through the same serial pipeline, because they all call the same class level "synchronized"
137      * method to actually communicate with the KLF bridge via its one single TCP socket connection
138      */
139     private @Nullable ExecutorService communicationsJobExecutor = null;
140     private @Nullable NamedThreadFactory threadFactory = null;
141
142     private VeluxBridge myJsonBridge = new JsonVeluxBridge(this);
143     private VeluxBridge mySlipBridge = new SlipVeluxBridge(this);
144     private boolean disposing = false;
145
146     /*
147      * **************************************
148      * ***** Default visibility Objects *****
149      */
150
151     public VeluxBridge thisBridge = myJsonBridge;
152     public BridgeParameters bridgeParameters = new BridgeParameters();
153     public Localization localization;
154
155     /**
156      * Mapping from ChannelUID to class Thing2VeluxActuator, which return Velux device information, probably cached.
157      */
158     public final Map<ChannelUID, Thing2VeluxActuator> channel2VeluxActuator = new ConcurrentHashMap<>();
159
160     /**
161      * Information retrieved by {@link VeluxBinding#VeluxBinding}.
162      */
163     private VeluxBridgeConfiguration veluxBridgeConfiguration = new VeluxBridgeConfiguration();
164
165     private Duration offlineDelay = Duration.ofMinutes(5);
166
167     /*
168      * ************************
169      * ***** Constructors *****
170      */
171
172     public VeluxBridgeHandler(final Bridge bridge, Localization localization) {
173         super(bridge);
174         logger.trace("VeluxBridgeHandler(constructor with bridge={}, localization={}) called.", bridge, localization);
175         this.localization = localization;
176         logger.debug("Creating a VeluxBridgeHandler for thing '{}'.", getThing().getUID());
177     }
178
179     // Private classes
180
181     /**
182      * <P>
183      * Set of information retrieved from the bridge/gateway:
184      * </P>
185      * <UL>
186      * <LI>{@link #actuators} - Already known actuators,</LI>
187      * <LI>{@link #scenes} - Already on the gateway defined scenes,</LI>
188      * <LI>{@link #gateway} - Current status of the gateway status,</LI>
189      * <LI>{@link #firmware} - Information about the gateway firmware revision,</LI>
190      * <LI>{@link #lanConfig} - Information about the gateway configuration,</LI>
191      * <LI>{@link #wlanConfig} - Information about the gateway configuration.</LI>
192      * </UL>
193      */
194     public class BridgeParameters {
195         /** Information retrieved by {@link VeluxBridgeActuators#getProducts} */
196         public VeluxBridgeActuators actuators = new VeluxBridgeActuators();
197
198         /** Information retrieved by {@link org.openhab.binding.velux.internal.bridge.VeluxBridgeScenes#getScenes} */
199         VeluxBridgeScenes scenes = new VeluxBridgeScenes();
200
201         /** Information retrieved by {@link VeluxBridgeDeviceStatus#retrieve} */
202         VeluxBridgeDeviceStatus.Channel gateway = new VeluxBridgeDeviceStatus().getChannel();
203
204         /** Information retrieved by {@link VeluxBridgeGetFirmware#retrieve} */
205         VeluxBridgeGetFirmware.Channel firmware = new VeluxBridgeGetFirmware().getChannel();
206
207         /** Information retrieved by {@link VeluxBridgeLANConfig#retrieve} */
208         VeluxBridgeLANConfig.Channel lanConfig = new VeluxBridgeLANConfig().getChannel();
209
210         /** Information retrieved by {@link VeluxBridgeWLANConfig#retrieve} */
211         VeluxBridgeWLANConfig.Channel wlanConfig = new VeluxBridgeWLANConfig().getChannel();
212     }
213
214     // Private methods
215
216     /**
217      * Provide the ThingType for a given Channel.
218      * <P>
219      * Separated into this private method to deal with the deprecated method.
220      * </P>
221      *
222      * @param channelUID for type {@link ChannelUID}.
223      * @return thingTypeUID of type {@link ThingTypeUID}.
224      */
225     public ThingTypeUID thingTypeUIDOf(ChannelUID channelUID) {
226         String[] segments = channelUID.getAsString().split(AbstractUID.SEPARATOR);
227         if (segments.length > 1) {
228             return new ThingTypeUID(segments[0], segments[1]);
229         }
230         logger.warn("thingTypeUIDOf({}) failed.", channelUID);
231         return new ThingTypeUID(VeluxBindingConstants.BINDING_ID, VeluxBindingConstants.UNKNOWN_THING_TYPE_ID);
232     }
233
234     // Objects and Methods for interface VeluxBridgeInstance
235
236     /**
237      * Information retrieved by ...
238      */
239     @Override
240     public VeluxBridgeConfiguration veluxBridgeConfiguration() {
241         return veluxBridgeConfiguration;
242     };
243
244     /**
245      * Information retrieved by {@link VeluxBridgeActuators#getProducts}
246      */
247     @Override
248     public VeluxExistingProducts existingProducts() {
249         return bridgeParameters.actuators.getChannel().existingProducts;
250     };
251
252     /**
253      * Information retrieved by {@link VeluxBridgeScenes#getScenes}
254      */
255     @Override
256     public VeluxExistingScenes existingScenes() {
257         return bridgeParameters.scenes.getChannel().existingScenes;
258     }
259
260     // Objects and Methods for interface VeluxBridgeProvider *****
261
262     @Override
263     public boolean bridgeCommunicate(BridgeCommunicationProtocol communication) {
264         logger.warn("bridgeCommunicate() called. Should never be called (as implemented by protocol-specific layers).");
265         return false;
266     }
267
268     @Override
269     public @Nullable BridgeAPI bridgeAPI() {
270         logger.warn("bridgeAPI() called. Should never be called (as implemented by protocol-specific layers).");
271         return null;
272     }
273
274     // Provisioning/Deprovisioning methods *****
275
276     @Override
277     public void initialize() {
278         // set the thing status to UNKNOWN temporarily and let the background task decide the real status
279         updateStatus(ThingStatus.UNKNOWN);
280
281         // take care of unusual situations...
282         if (scheduler.isShutdown()) {
283             logger.warn("initialize(): scheduler is shutdown, aborting initialization.");
284             return;
285         }
286
287         logger.trace("initialize(): initialize bridge configuration parameters.");
288         veluxBridgeConfiguration = new VeluxBinding(getConfigAs(VeluxBridgeConfiguration.class)).checked();
289
290         /*
291          * When a binding call to the hub fails with a communication error, it will retry the call for a maximum of
292          * veluxBridgeConfiguration.retries times, where the interval between retry attempts increases on each attempt
293          * calculated as veluxBridgeConfiguration.refreshMSecs * 2^retry (i.e. 1, 2, 4, 8, 16, 32 etc.) so a complete
294          * retry series takes (veluxBridgeConfiguration.refreshMSecs * ((2^(veluxBridgeConfiguration.retries + 1)) - 1)
295          * milliseconds. So we have to let this full retry series to have been tried (and failed), before we consider
296          * the thing to be actually offline.
297          */
298         offlineDelay = Duration.ofMillis(
299                 ((long) Math.pow(2, veluxBridgeConfiguration.retries + 1) - 1) * veluxBridgeConfiguration.refreshMSecs);
300
301         scheduler.execute(() -> {
302             disposing = false;
303             initializeSchedulerJob();
304         });
305     }
306
307     /**
308      * Various initialisation actions to be executed on a background thread
309      */
310     private void initializeSchedulerJob() {
311         /*
312          * synchronize disposeSchedulerJob() and initializeSchedulerJob() based an IP address Strings.intern() object to
313          * prevent overlap of initialization and disposal communications towards the same physical bridge
314          */
315         synchronized (LOCK_MODIFIER.concat(veluxBridgeConfiguration.ipAddress).intern()) {
316             logger.trace("initializeSchedulerJob(): adopt new bridge configuration parameters.");
317             bridgeParamsUpdated();
318
319             long mSecs = veluxBridgeConfiguration.refreshMSecs;
320             logger.trace("initializeSchedulerJob(): scheduling refresh at {} milliseconds.", mSecs);
321             refreshSchedulerJob = scheduler.scheduleWithFixedDelay(() -> {
322                 refreshSchedulerJob();
323             }, mSecs, mSecs, TimeUnit.MILLISECONDS);
324
325             VeluxHandlerFactory.refreshBindingInfo();
326
327             if (logger.isDebugEnabled()) {
328                 logger.debug("Velux Bridge '{}' is initialized (with {} scenes and {} actuators).", getThing().getUID(),
329                         bridgeParameters.scenes.getChannel().existingScenes.getNoMembers(),
330                         bridgeParameters.actuators.getChannel().existingProducts.getNoMembers());
331             }
332         }
333     }
334
335     @Override
336     public void dispose() {
337         scheduler.submit(() -> {
338             disposing = true;
339             disposeSchedulerJob();
340         });
341     }
342
343     /**
344      * Various disposal actions to be executed on a background thread
345      */
346     private void disposeSchedulerJob() {
347         /*
348          * synchronize disposeSchedulerJob() and initializeSchedulerJob() based an IP address Strings.intern() object to
349          * prevent overlap of initialization and disposal communications towards the same physical bridge
350          */
351         synchronized (LOCK_MODIFIER.concat(veluxBridgeConfiguration.ipAddress).intern()) {
352             /*
353              * cancel the regular refresh polling job
354              */
355             ScheduledFuture<?> refreshSchedulerJob = this.refreshSchedulerJob;
356             if (refreshSchedulerJob != null) {
357                 logger.trace("disposeSchedulerJob(): cancel the refresh polling job.");
358                 refreshSchedulerJob.cancel(false);
359             }
360
361             ExecutorService commsJobExecutor = this.communicationsJobExecutor;
362             if (commsJobExecutor != null) {
363                 this.communicationsJobExecutor = null;
364                 logger.trace("disposeSchedulerJob(): cancel any other scheduled jobs.");
365                 /*
366                  * remove un-started communication tasks from the execution queue; and stop accepting more tasks
367                  */
368                 commsJobExecutor.shutdownNow();
369                 /*
370                  * if the last bridge communication was OK, wait for already started task(s) to complete (so the bridge
371                  * won't lock up); but to prevent stalling the OH shutdown process, time out after
372                  * MAX_COMMUNICATION_TASK_WAIT_TIME_SECS
373                  */
374                 if (thisBridge.lastCommunicationOk()) {
375                     try {
376                         if (!commsJobExecutor.awaitTermination(COMMUNICATION_TASK_MAX_WAIT_SECS, TimeUnit.SECONDS)) {
377                             logger.warn("disposeSchedulerJob(): unexpected awaitTermination() timeout.");
378                         }
379                     } catch (InterruptedException e) {
380                         logger.warn("disposeSchedulerJob(): unexpected exception awaitTermination() '{}'.",
381                                 e.getMessage());
382                     }
383                 }
384             }
385
386             /*
387              * if the last bridge communication was OK, deactivate HSM to prevent queueing more HSM events
388              */
389             if (thisBridge.lastCommunicationOk()
390                     && (new VeluxBridgeSetHouseStatusMonitor().modifyHSM(thisBridge, false))) {
391                 logger.trace("disposeSchedulerJob(): HSM deactivated.");
392             }
393
394             /*
395              * finally clean up everything else
396              */
397             logger.trace("disposeSchedulerJob(): shut down JSON connection interface.");
398             myJsonBridge.shutdown();
399             logger.trace("disposeSchedulerJob(): shut down SLIP connection interface.");
400             mySlipBridge.shutdown();
401             VeluxHandlerFactory.refreshBindingInfo();
402             logger.debug("Velux Bridge '{}' is shut down.", getThing().getUID());
403         }
404     }
405
406     /**
407      * NOTE: It takes care by calling {@link #handleCommand} with the REFRESH command, that every used channel is
408      * initialized.
409      */
410     @Override
411     public void channelLinked(ChannelUID channelUID) {
412         if (thing.getStatus() == ThingStatus.ONLINE) {
413             channel2VeluxActuator.put(channelUID, new Thing2VeluxActuator(this, channelUID));
414             logger.trace("channelLinked({}) refreshing channel value with help of handleCommand as Thing is online.",
415                     channelUID.getAsString());
416             handleCommand(channelUID, RefreshType.REFRESH);
417         } else {
418             logger.trace("channelLinked({}) doing nothing as Thing is not online.", channelUID.getAsString());
419         }
420     }
421
422     @Override
423     public void channelUnlinked(ChannelUID channelUID) {
424         logger.trace("channelUnlinked({}) called.", channelUID.getAsString());
425     }
426
427     // Reconfiguration methods
428
429     private void bridgeParamsUpdated() {
430         logger.debug("bridgeParamsUpdated() called.");
431
432         // Determine the appropriate bridge communication channel
433         boolean validBridgeFound = false;
434         if (myJsonBridge.supportedProtocols.contains(veluxBridgeConfiguration.protocol)) {
435             logger.debug("bridgeParamsUpdated(): choosing JSON as communication method.");
436             thisBridge = myJsonBridge;
437             validBridgeFound = true;
438         }
439         if (mySlipBridge.supportedProtocols.contains(veluxBridgeConfiguration.protocol)) {
440             logger.debug("bridgeParamsUpdated(): choosing SLIP as communication method.");
441             thisBridge = mySlipBridge;
442             validBridgeFound = true;
443         }
444         if (!validBridgeFound) {
445             logger.debug("No valid protocol selected, aborting this {} binding.", VeluxBindingConstants.BINDING_ID);
446             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
447                     "@text/runtime.bridge-offline-no-valid-bridgeProtocol-selected");
448             logger.trace("bridgeParamsUpdated() done.");
449             return;
450         }
451
452         logger.trace("bridgeParamsUpdated(): Trying to authenticate towards bridge.");
453
454         if (!thisBridge.bridgeLogin()) {
455             logger.warn("{} bridge login sequence failed; expecting bridge is OFFLINE.",
456                     VeluxBindingConstants.BINDING_ID);
457             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
458                     "@text/runtime.bridge-offline-login-sequence-failed");
459             logger.trace("bridgeParamsUpdated() done.");
460             return;
461         }
462
463         logger.trace("bridgeParamsUpdated(): Querying bridge state.");
464         bridgeParameters.gateway = new VeluxBridgeDeviceStatus().retrieve(thisBridge);
465
466         logger.trace("bridgeParamsUpdated(): Fetching existing scenes.");
467         bridgeParameters.scenes.getScenes(thisBridge);
468         logger.debug("Found Velux scenes:\n\t{}",
469                 bridgeParameters.scenes.getChannel().existingScenes.toString(false, "\n\t"));
470         logger.trace("bridgeParamsUpdated(): Fetching existing actuators/products.");
471         bridgeParameters.actuators.getProducts(thisBridge);
472         logger.debug("Found Velux actuators:\n\t{}",
473                 bridgeParameters.actuators.getChannel().existingProducts.toString(false, "\n\t"));
474
475         if (thisBridge.bridgeAPI().setHouseStatusMonitor() != null) {
476             logger.trace("bridgeParamsUpdated(): Activating HouseStatusMonitor.");
477             if (new VeluxBridgeSetHouseStatusMonitor().modifyHSM(thisBridge, true)) {
478                 logger.trace("bridgeParamsUpdated(): HSM activated.");
479             } else {
480                 logger.warn("Activation of House-Status-Monitoring failed (might lead to a lack of status updates).");
481             }
482         }
483
484         updateDynamicChannels();
485
486         veluxBridgeConfiguration.hasChanged = false;
487         logger.debug("Velux veluxBridge is online, now.");
488         updateStatus(ThingStatus.ONLINE);
489         logger.trace("bridgeParamsUpdated() successfully finished.");
490     }
491
492     // Continuous synchronization methods
493
494     private synchronized void refreshSchedulerJob() {
495         logger.debug("refreshSchedulerJob() initiated by {} starting cycle {}.", Thread.currentThread(),
496                 refreshCounter);
497         logger.trace("refreshSchedulerJob(): processing of possible HSM messages.");
498
499         // Background execution of bridge related I/O
500         submitCommunicationsJob(() -> {
501             getHouseStatusCommsJob();
502         });
503
504         logger.trace(
505                 "refreshSchedulerJob(): loop through all (child things and bridge) linked channels needing a refresh");
506         for (ChannelUID channelUID : BridgeChannels.getAllLinkedChannelUIDs(this)) {
507             if (VeluxItemType.isToBeRefreshedNow(refreshCounter, thingTypeUIDOf(channelUID), channelUID.getId())) {
508                 logger.trace("refreshSchedulerJob(): refreshing channel {}.", channelUID);
509                 handleCommand(channelUID, RefreshType.REFRESH);
510             }
511         }
512
513         logger.trace("refreshSchedulerJob(): loop through properties needing a refresh");
514         for (VeluxItemType veluxItem : VeluxItemType.getPropertyEntriesByThing(getThing().getThingTypeUID())) {
515             if (VeluxItemType.isToBeRefreshedNow(refreshCounter, getThing().getThingTypeUID(),
516                     veluxItem.getIdentifier())) {
517                 logger.trace("refreshSchedulerJob(): refreshing property {}.", veluxItem.getIdentifier());
518                 handleCommand(new ChannelUID(getThing().getUID(), veluxItem.getIdentifier()), RefreshType.REFRESH);
519             }
520         }
521         logger.debug("refreshSchedulerJob() initiated by {} finished cycle {}.", Thread.currentThread(),
522                 refreshCounter);
523         refreshCounter++;
524     }
525
526     private void getHouseStatusCommsJob() {
527         logger.trace("getHouseStatusCommsJob() initiated by {} will process HouseStatus.", Thread.currentThread());
528         if (new VeluxBridgeGetHouseStatus().evaluateState(thisBridge)) {
529             logger.trace("getHouseStatusCommsJob(): => GetHouseStatus() => updates received => synchronizing");
530             syncChannelsWithProducts();
531         } else {
532             logger.trace("getHouseStatusCommsJob(): => GetHouseStatus() => no updates");
533         }
534         logger.trace("getHouseStatusCommsJob() initiated by {} has finished.", Thread.currentThread());
535     }
536
537     /**
538      * In case of recognized changes in the real world, the method will
539      * update the corresponding states via openHAB event bus.
540      */
541     private void syncChannelsWithProducts() {
542         if (!bridgeParameters.actuators.getChannel().existingProducts.isDirty()) {
543             logger.trace("syncChannelsWithProducts(): no existing products with changed parameters.");
544             return;
545         }
546         logger.trace("syncChannelsWithProducts(): there are some existing products with changed parameters.");
547         for (VeluxProduct product : bridgeParameters.actuators.getChannel().existingProducts.valuesOfModified()) {
548             logger.trace("syncChannelsWithProducts(): actuator {} has changed values.", product.getProductName());
549             ProductBridgeIndex productPbi = product.getBridgeProductIndex();
550             logger.trace("syncChannelsWithProducts(): bridge index is {}.", productPbi);
551             for (ChannelUID channelUID : BridgeChannels.getAllLinkedChannelUIDs(this)) {
552                 if (!channel2VeluxActuator.containsKey(channelUID)) {
553                     logger.trace("syncChannelsWithProducts(): channel {} not found.", channelUID);
554                     continue;
555                 }
556                 Thing2VeluxActuator actuator = channel2VeluxActuator.get(channelUID);
557                 if (actuator == null || !actuator.isKnown()) {
558                     logger.trace("syncChannelsWithProducts(): channel {} not registered on bridge.", channelUID);
559                     continue;
560                 }
561                 ProductBridgeIndex channelPbi = actuator.getProductBridgeIndex();
562                 if (!channelPbi.equals(productPbi)) {
563                     continue;
564                 }
565                 boolean isInverted;
566                 VeluxProductPosition position;
567                 if (channelUID.getId().equals(VeluxBindingConstants.CHANNEL_VANE_POSITION)) {
568                     isInverted = false;
569                     position = new VeluxProductPosition(product.getVanePosition());
570                 } else {
571                     // Handle value inversion
572                     isInverted = actuator.isInverted();
573                     logger.trace("syncChannelsWithProducts(): isInverted is {}.", isInverted);
574                     position = new VeluxProductPosition(product.getDisplayPosition());
575                 }
576                 if (position.isValid()) {
577                     PercentType positionAsPercent = position.getPositionAsPercentType(isInverted);
578                     logger.debug("syncChannelsWithProducts(): updating channel {} to position {}%.", channelUID,
579                             positionAsPercent);
580                     updateState(channelUID, positionAsPercent);
581                     continue;
582                 }
583                 logger.trace("syncChannelsWithProducts(): updating channel {} to 'UNDEFINED'.", channelUID);
584                 updateState(channelUID, UnDefType.UNDEF);
585                 continue;
586             }
587         }
588         logger.trace("syncChannelsWithProducts(): resetting dirty flag.");
589         bridgeParameters.actuators.getChannel().existingProducts.resetDirtyFlag();
590         logger.trace("syncChannelsWithProducts() done.");
591     }
592
593     // Processing of openHAB events
594
595     @Override
596     public void handleCommand(ChannelUID channelUID, Command command) {
597         logger.trace("handleCommand({}): command {} on channel {} will be scheduled.", Thread.currentThread(), command,
598                 channelUID.getAsString());
599         logger.debug("handleCommand({},{}) called.", channelUID.getAsString(), command);
600
601         // Background execution of bridge related I/O
602         submitCommunicationsJob(() -> {
603             handleCommandCommsJob(channelUID, command);
604         });
605         logger.trace("handleCommand({}) done.", Thread.currentThread());
606     }
607
608     /**
609      * Normally called by {@link #handleCommand} to handle a command for a given channel with possibly long execution
610      * time.
611      * <p>
612      * <B>NOTE:</B> This method is to be called as separated thread to ensure proper openHAB framework in parallel.
613      * <p>
614      *
615      * @param channelUID the {@link ChannelUID} of the channel to which the command was sent,
616      * @param command the {@link Command}.
617      */
618     private synchronized void handleCommandCommsJob(ChannelUID channelUID, Command command) {
619         logger.trace("handleCommandCommsJob({}): command {} on channel {}.", Thread.currentThread(), command,
620                 channelUID.getAsString());
621         logger.debug("handleCommandCommsJob({},{}) called.", channelUID.getAsString(), command);
622
623         /*
624          * ===========================================================
625          * Common part
626          */
627
628         if (veluxBridgeConfiguration.isProtocolTraceEnabled) {
629             Threads.findDeadlocked();
630         }
631
632         String channelId = channelUID.getId();
633         State newState = null;
634         String itemName = channelUID.getAsString();
635         VeluxItemType itemType = VeluxItemType.getByThingAndChannel(thingTypeUIDOf(channelUID), channelUID.getId());
636
637         if (itemType == VeluxItemType.UNKNOWN) {
638             logger.warn("{} Cannot determine type of Channel {}, ignoring command {}.",
639                     VeluxBindingConstants.LOGGING_CONTACT, channelUID, command);
640             logger.trace("handleCommandCommsJob() aborting.");
641             return;
642         }
643
644         // Build cache
645         if (!channel2VeluxActuator.containsKey(channelUID)) {
646             channel2VeluxActuator.put(channelUID, new Thing2VeluxActuator(this, channelUID));
647         }
648
649         if (veluxBridgeConfiguration.hasChanged) {
650             logger.trace("handleCommandCommsJob(): work on updated bridge configuration parameters.");
651             bridgeParamsUpdated();
652         }
653
654         syncChannelsWithProducts();
655
656         if (command instanceof RefreshType) {
657             /*
658              * ===========================================================
659              * Refresh part
660              */
661             logger.trace("handleCommandCommsJob(): work on refresh.");
662             if (!itemType.isReadable()) {
663                 logger.debug("handleCommandCommsJob(): received a Refresh command for a non-readable item.");
664             } else {
665                 logger.trace("handleCommandCommsJob(): refreshing item {} (type {}).", itemName, itemType);
666                 try { // expecting an IllegalArgumentException for unknown Velux device
667                     switch (itemType) {
668                         // Bridge channels
669                         case BRIDGE_STATUS:
670                             newState = ChannelBridgeStatus.handleRefresh(channelUID, channelId, this);
671                             break;
672                         case BRIDGE_DOWNTIME:
673                             newState = new DecimalType(
674                                     thisBridge.lastCommunication() - thisBridge.lastSuccessfulCommunication());
675                             break;
676                         case BRIDGE_FIRMWARE:
677                             newState = ChannelBridgeFirmware.handleRefresh(channelUID, channelId, this);
678                             break;
679                         case BRIDGE_ADDRESS:
680                             // delete legacy property name entry (if any) and fall through
681                             ThingProperty.setValue(this, VeluxBridgeConfiguration.BRIDGE_IPADDRESS, null);
682                         case BRIDGE_SUBNETMASK:
683                         case BRIDGE_DEFAULTGW:
684                         case BRIDGE_DHCP:
685                             newState = ChannelBridgeLANconfig.handleRefresh(channelUID, channelId, this);
686                             break;
687                         case BRIDGE_WLANSSID:
688                         case BRIDGE_WLANPASSWORD:
689                             newState = ChannelBridgeWLANconfig.handleRefresh(channelUID, channelId, this);
690                             break;
691                         case BRIDGE_SCENES:
692                             newState = ChannelBridgeScenes.handleRefresh(channelUID, channelId, this);
693                             break;
694                         case BRIDGE_PRODUCTS:
695                             newState = ChannelBridgeProducts.handleRefresh(channelUID, channelId, this);
696                             break;
697                         case BRIDGE_CHECK:
698                             newState = ChannelBridgeCheck.handleRefresh(channelUID, channelId, this);
699                             break;
700                         // Actuator channels
701                         case ACTUATOR_POSITION:
702                         case ACTUATOR_STATE:
703                         case ROLLERSHUTTER_POSITION:
704                         case WINDOW_POSITION:
705                         case ROLLERSHUTTER_VANE_POSITION:
706                             newState = ChannelActuatorPosition.handleRefresh(channelUID, channelId, this);
707                             break;
708                         case ACTUATOR_LIMIT_MINIMUM:
709                         case ROLLERSHUTTER_LIMIT_MINIMUM:
710                         case WINDOW_LIMIT_MINIMUM:
711                             // note: the empty string ("") below is intentional
712                             newState = ChannelActuatorLimitation.handleRefresh(channelUID, "", this);
713                             break;
714                         case ACTUATOR_LIMIT_MAXIMUM:
715                         case ROLLERSHUTTER_LIMIT_MAXIMUM:
716                         case WINDOW_LIMIT_MAXIMUM:
717                             newState = ChannelActuatorLimitation.handleRefresh(channelUID, channelId, this);
718                             break;
719
720                         // VirtualShutter channels
721                         case VSHUTTER_POSITION:
722                             newState = ChannelVShutterPosition.handleRefresh(channelUID, channelId, this);
723                             break;
724
725                         default:
726                             logger.warn("{} Cannot handle REFRESH on channel {} as it is of type {}.",
727                                     VeluxBindingConstants.LOGGING_CONTACT, itemName, channelId);
728                     }
729                 } catch (IllegalArgumentException e) {
730                     logger.warn("Cannot handle REFRESH on channel {} as it isn't (yet) known to the bridge.", itemName);
731                 }
732                 if (newState != null) {
733                     if (itemType.isChannel()) {
734                         logger.debug("handleCommandCommsJob(): updating channel {} to {}.", channelUID, newState);
735                         updateState(channelUID, newState);
736                     } else if (itemType.isProperty()) {
737                         // if property value is 'unknown', null it completely
738                         String val = newState.toString();
739                         if (VeluxBindingConstants.UNKNOWN.equals(val)) {
740                             val = null;
741                         }
742                         logger.debug("handleCommandCommsJob(): updating property {} to {}.", channelUID, val);
743                         ThingProperty.setValue(this, itemType.getIdentifier(), val);
744                     }
745                 } else {
746                     logger.warn("handleCommandCommsJob({},{}): updating of item {} (type {}) failed.",
747                             channelUID.getAsString(), command, itemName, itemType);
748                 }
749             }
750         } else {
751             /*
752              * ===========================================================
753              * Modification part
754              */
755             logger.trace("handleCommandCommsJob(): working on item {} (type {}) with COMMAND {}.", itemName, itemType,
756                     command);
757             Command newValue = null;
758             try { // expecting an IllegalArgumentException for unknown Velux device
759                 switch (itemType) {
760                     // Bridge channels
761                     case BRIDGE_RELOAD:
762                         if (command == OnOffType.ON) {
763                             logger.trace("handleCommandCommsJob(): about to reload informations from veluxBridge.");
764                             bridgeParamsUpdated();
765                         } else {
766                             logger.trace("handleCommandCommsJob(): ignoring OFF command.");
767                         }
768                         break;
769                     case BRIDGE_DO_DETECTION:
770                         ChannelBridgeDoDetection.handleCommand(channelUID, channelId, command, this);
771                         break;
772
773                     // Scene channels
774                     case SCENE_ACTION:
775                         ChannelSceneAction.handleCommand(channelUID, channelId, command, this);
776                         break;
777
778                     /*
779                      * NOTA BENE: Setting of a scene silent mode is no longer supported via the KLF API (i.e. the
780                      * GW_SET_NODE_VELOCITY_REQ/CFM command set is no longer supported in the API), so the binding can
781                      * no longer explicitly support a Channel with such a function. Therefore the silent mode Channel
782                      * type was removed from the binding implementation.
783                      *
784                      * By contrast scene actions can still be called with a silent mode argument, so a silent mode
785                      * Configuration Parameter has been introduced as a means for the user to set this argument.
786                      *
787                      * Strictly speaking the following case statement will now never be called, so in theory it,
788                      * AND ALL THE CLASSES BEHIND, could be deleted from the binding CODE BASE. But out of prudence
789                      * it is retained anyway 'just in case'.
790                      */
791                     case SCENE_SILENTMODE:
792                         ChannelSceneSilentmode.handleCommand(channelUID, channelId, command, this);
793                         break;
794
795                     // Actuator channels
796                     case ACTUATOR_POSITION:
797                     case ACTUATOR_STATE:
798                     case ROLLERSHUTTER_POSITION:
799                     case WINDOW_POSITION:
800                     case ROLLERSHUTTER_VANE_POSITION:
801                         newValue = ChannelActuatorPosition.handleCommand(channelUID, channelId, command, this);
802                         break;
803                     case ACTUATOR_LIMIT_MINIMUM:
804                     case ROLLERSHUTTER_LIMIT_MINIMUM:
805                     case WINDOW_LIMIT_MINIMUM:
806                         ChannelActuatorLimitation.handleCommand(channelUID, channelId, command, this);
807                         break;
808                     case ACTUATOR_LIMIT_MAXIMUM:
809                     case ROLLERSHUTTER_LIMIT_MAXIMUM:
810                     case WINDOW_LIMIT_MAXIMUM:
811                         ChannelActuatorLimitation.handleCommand(channelUID, channelId, command, this);
812                         break;
813
814                     // VirtualShutter channels
815                     case VSHUTTER_POSITION:
816                         newValue = ChannelVShutterPosition.handleCommand(channelUID, channelId, command, this);
817                         break;
818
819                     default:
820                         logger.warn("{} Cannot handle command {} on channel {} (type {}).",
821                                 VeluxBindingConstants.LOGGING_CONTACT, command, itemName, itemType);
822                 }
823             } catch (IllegalArgumentException e) {
824                 logger.warn("Cannot handle command on channel {} as it isn't (yet) known to the bridge.", itemName);
825             }
826             if (newValue != null) {
827                 postCommand(channelUID, newValue);
828             }
829         }
830
831         Instant lastCommunication = Instant.ofEpochMilli(thisBridge.lastCommunication());
832         Instant lastSuccessfulCommunication = Instant.ofEpochMilli(thisBridge.lastSuccessfulCommunication());
833         boolean lastCommunicationSucceeded = lastSuccessfulCommunication.equals(lastCommunication);
834         ThingStatus thingStatus = getThing().getStatus();
835
836         if (lastCommunicationSucceeded) {
837             if (thingStatus == ThingStatus.OFFLINE || thingStatus == ThingStatus.UNKNOWN) {
838                 updateStatus(ThingStatus.ONLINE);
839             }
840         } else {
841             if ((thingStatus == ThingStatus.ONLINE || thingStatus == ThingStatus.UNKNOWN)
842                     && lastSuccessfulCommunication.plus(offlineDelay).isBefore(lastCommunication)) {
843                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
844             }
845         }
846
847         ThingProperty.setValue(this, VeluxBindingConstants.PROPERTY_BRIDGE_TIMESTAMP_ATTEMPT,
848                 lastCommunication.toString());
849         ThingProperty.setValue(this, VeluxBindingConstants.PROPERTY_BRIDGE_TIMESTAMP_SUCCESS,
850                 lastSuccessfulCommunication.toString());
851
852         logger.trace("handleCommandCommsJob({}) done.", Thread.currentThread());
853     }
854
855     /**
856      * Register the exported actions
857      */
858     @Override
859     public Collection<Class<? extends ThingHandlerService>> getServices() {
860         return Set.of(VeluxActions.class);
861     }
862
863     /**
864      * Exported method (called by an OpenHAB Rules Action) to issue a reboot command to the hub.
865      *
866      * @return true if the command could be issued
867      */
868     public boolean rebootBridge() {
869         logger.trace("runReboot() called on {}", getThing().getUID());
870         RunReboot bcp = thisBridge.bridgeAPI().runReboot();
871         if (bcp != null) {
872             // background execution of reboot process
873             submitCommunicationsJob(() -> {
874                 if (thisBridge.bridgeCommunicate(bcp)) {
875                     logger.info("Reboot command {}successfully sent to {}", bcp.isCommunicationSuccessful() ? "" : "un",
876                             getThing().getUID());
877                 }
878             });
879             return true;
880         }
881         return false;
882     }
883
884     /**
885      * Exported method (called by an OpenHAB Rules Action) to move an actuator relative to its current position
886      *
887      * @param nodeId the node to be moved
888      * @param relativePercent relative position change to the current position (-100% <= relativePercent <= +100%)
889      * @return true if the command could be issued
890      */
891     public boolean moveRelative(int nodeId, int relativePercent) {
892         logger.trace("moveRelative() called on {}", getThing().getUID());
893         RunProductCommand bcp = thisBridge.bridgeAPI().runProductCommand();
894         if (bcp != null) {
895             // background execution of moveRelative
896             submitCommunicationsJob(() -> {
897                 synchronized (bcp) {
898                     bcp.setNodeIdAndParameters(nodeId,
899                             new VeluxProductPosition(new PercentType(Math.abs(relativePercent))).overridePositionType(
900                                     relativePercent > 0 ? PositionType.OFFSET_POSITIVE : PositionType.OFFSET_NEGATIVE),
901                             null);
902                     if (thisBridge.bridgeCommunicate(bcp)) {
903                         logger.trace("moveRelative() command {}successfully sent to {}",
904                                 bcp.isCommunicationSuccessful() ? "" : "un", getThing().getUID());
905                     }
906                 }
907             });
908             return true;
909         }
910         return false;
911     }
912
913     /**
914      * If necessary initialise the communications job executor. Then check if the executor is shut down. And if it is
915      * not shut down, then submit the given communications job for execution.
916      */
917     private void submitCommunicationsJob(Runnable communicationsJob) {
918         ExecutorService commsJobExecutor = this.communicationsJobExecutor;
919         if (commsJobExecutor == null) {
920             commsJobExecutor = this.communicationsJobExecutor = Executors.newSingleThreadExecutor(getThreadFactory());
921         }
922         if (!commsJobExecutor.isShutdown()) {
923             commsJobExecutor.execute(communicationsJob);
924         }
925     }
926
927     /**
928      * If necessary initialise the thread factory and return it
929      *
930      * @return the thread factory
931      */
932     public NamedThreadFactory getThreadFactory() {
933         NamedThreadFactory threadFactory = this.threadFactory;
934         if (threadFactory == null) {
935             threadFactory = new NamedThreadFactory(getThing().getUID().getAsString());
936         }
937         return threadFactory;
938     }
939
940     /**
941      * Indicates if the bridge thing is being disposed.
942      *
943      * @return true if the bridge thing is being disposed.
944      */
945     public boolean isDisposing() {
946         return disposing;
947     }
948
949     /**
950      * Exported method (called by an OpenHAB Rules Action) to simultaneously move the shade main position and the vane
951      * position.
952      *
953      * @param node the node index in the bridge.
954      * @param mainPosition the desired main position.
955      * @param vanePosition the desired vane position.
956      * @return true if the command could be issued.
957      */
958     public Boolean moveMainAndVane(ProductBridgeIndex node, PercentType mainPosition, PercentType vanePosition) {
959         logger.trace("moveMainAndVane() called on {}", getThing().getUID());
960         RunProductCommand bcp = thisBridge.bridgeAPI().runProductCommand();
961         if (bcp != null) {
962             VeluxProduct product = existingProducts().get(node).clone();
963             FunctionalParameters functionalParameters = null;
964             if (product.supportsVanePosition()) {
965                 int vanePos = new VeluxProductPosition(vanePosition).getPositionAsVeluxType();
966                 product.setVanePosition(vanePos);
967                 functionalParameters = product.getFunctionalParameters();
968             }
969             VeluxProductPosition mainPos = new VeluxProductPosition(mainPosition);
970             bcp.setNodeIdAndParameters(node.toInt(), mainPos, functionalParameters);
971             submitCommunicationsJob(() -> {
972                 if (thisBridge.bridgeCommunicate(bcp)) {
973                     logger.trace("moveMainAndVane() command {}successfully sent to {}",
974                             bcp.isCommunicationSuccessful() ? "" : "un", getThing().getUID());
975                 }
976             });
977             return true;
978         }
979         return false;
980     }
981
982     /**
983      * Get the bridge product index for a given thing name.
984      *
985      * @param thingName the thing name
986      * @return the bridge product index or ProductBridgeIndex.UNKNOWN if not found.
987      */
988     public ProductBridgeIndex getProductBridgeIndex(String thingName) {
989         for (Entry<ChannelUID, Thing2VeluxActuator> entry : channel2VeluxActuator.entrySet()) {
990             if (thingName.equals(entry.getKey().getThingUID().getAsString())) {
991                 return entry.getValue().getProductBridgeIndex();
992             }
993         }
994         return ProductBridgeIndex.UNKNOWN;
995     }
996
997     /**
998      * Ask all things in the hub to initialise their dynamic vane position channel if they support it.
999      */
1000     private void updateDynamicChannels() {
1001         getThing().getThings().stream().forEach(thing -> {
1002             ThingHandler thingHandler = thing.getHandler();
1003             if (thingHandler instanceof VeluxHandler) {
1004                 ((VeluxHandler) thingHandler).updateDynamicChannels(this);
1005             }
1006         });
1007     }
1008 }