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