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