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