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