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