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