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