2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.velux.internal.handler;
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;
21 import java.util.Map.Entry;
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;
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;
86 * <B>Common interaction with the </B><I>Velux</I><B> bridge.</B>
88 * It implements the communication between <B>OpenHAB</B> and the <I>Velux</I> Bridge:
90 * <LI><B>OpenHAB</B> Event Bus → <I>Velux</I> <B>bridge</B>
92 * Sending commands and value updates.</LI>
95 * <LI><I>Velux</I> <B>bridge</B> → <B>OpenHAB</B>:
97 * Retrieving information by sending a Refresh command.</LI>
100 * Entry point for this class is the method
101 * {@link VeluxBridgeHandler#handleCommand handleCommand}.
103 * @author Guenther Schreiner - Initial contribution.
106 public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements VeluxBridgeInstance, VeluxBridgeProvider {
109 * timeout to ensure that the binding shutdown will not block and stall the shutdown of OH itself
111 private static final int COMMUNICATION_TASK_MAX_WAIT_SECS = 10;
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
118 private static final String LOCK_MODIFIER = "velux.ipaddr.";
120 private final Logger logger = LoggerFactory.getLogger(VeluxBridgeHandler.class);
125 * Scheduler for continuous refresh by scheduleWithFixedDelay.
127 private @Nullable ScheduledFuture<?> refreshSchedulerJob = null;
130 * Counter of refresh invocations by {@link refreshSchedulerJob}.
132 private int refreshCounter = 0;
135 * Dedicated task executor for the long-running bridge communication tasks.
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
141 private @Nullable ExecutorService communicationsJobExecutor = null;
142 private @Nullable NamedThreadFactory threadFactory = null;
144 private VeluxBridge myJsonBridge = new JsonVeluxBridge(this);
145 private VeluxBridge mySlipBridge = new SlipVeluxBridge(this);
146 private boolean disposing = false;
149 * **************************************
150 * ***** Default visibility Objects *****
153 public VeluxBridge thisBridge = myJsonBridge;
154 public BridgeParameters bridgeParameters = new BridgeParameters();
155 public Localization localization;
158 * Mapping from ChannelUID to class Thing2VeluxActuator, which return Velux device information, probably cached.
160 public final Map<ChannelUID, Thing2VeluxActuator> channel2VeluxActuator = new ConcurrentHashMap<>();
163 * Information retrieved by {@link VeluxBinding#VeluxBinding}.
165 private VeluxBridgeConfiguration veluxBridgeConfiguration = new VeluxBridgeConfiguration();
167 private Duration offlineDelay = Duration.ofMinutes(5);
168 private int initializeRetriesDone = 0;
171 * ************************
172 * ***** Constructors *****
175 public VeluxBridgeHandler(final Bridge bridge, Localization localization) {
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());
186 * Set of information retrieved from the bridge/gateway:
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>
197 public class BridgeParameters {
198 /** Information retrieved by {@link VeluxBridgeActuators#getProducts} */
199 public VeluxBridgeActuators actuators = new VeluxBridgeActuators();
201 /** Information retrieved by {@link org.openhab.binding.velux.internal.bridge.VeluxBridgeScenes#getScenes} */
202 VeluxBridgeScenes scenes = new VeluxBridgeScenes();
204 /** Information retrieved by {@link VeluxBridgeDeviceStatus#retrieve} */
205 VeluxBridgeDeviceStatus.Channel gateway = new VeluxBridgeDeviceStatus().getChannel();
207 /** Information retrieved by {@link VeluxBridgeGetFirmware#retrieve} */
208 VeluxBridgeGetFirmware.Channel firmware = new VeluxBridgeGetFirmware().getChannel();
210 /** Information retrieved by {@link VeluxBridgeLANConfig#retrieve} */
211 VeluxBridgeLANConfig.Channel lanConfig = new VeluxBridgeLANConfig().getChannel();
213 /** Information retrieved by {@link VeluxBridgeWLANConfig#retrieve} */
214 VeluxBridgeWLANConfig.Channel wlanConfig = new VeluxBridgeWLANConfig().getChannel();
220 * Provide the ThingType for a given Channel.
222 * Separated into this private method to deal with the deprecated method.
225 * @param channelUID for type {@link ChannelUID}.
226 * @return thingTypeUID of type {@link ThingTypeUID}.
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]);
233 logger.warn("thingTypeUIDOf({}) failed.", channelUID);
234 return new ThingTypeUID(VeluxBindingConstants.BINDING_ID, VeluxBindingConstants.UNKNOWN_THING_TYPE_ID);
237 // Objects and Methods for interface VeluxBridgeInstance
240 * Information retrieved by ...
243 public VeluxBridgeConfiguration veluxBridgeConfiguration() {
244 return veluxBridgeConfiguration;
248 * Information retrieved by {@link VeluxBridgeActuators#getProducts}
251 public VeluxExistingProducts existingProducts() {
252 return bridgeParameters.actuators.getChannel().existingProducts;
256 * Information retrieved by {@link VeluxBridgeScenes#getScenes}
259 public VeluxExistingScenes existingScenes() {
260 return bridgeParameters.scenes.getChannel().existingScenes;
263 // Objects and Methods for interface VeluxBridgeProvider *****
266 public boolean bridgeCommunicate(BridgeCommunicationProtocol communication) {
267 logger.warn("bridgeCommunicate() called. Should never be called (as implemented by protocol-specific layers).");
272 public @Nullable BridgeAPI bridgeAPI() {
273 logger.warn("bridgeAPI() called. Should never be called (as implemented by protocol-specific layers).");
277 // Provisioning/Deprovisioning methods *****
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);
284 // take care of unusual situations...
285 if (scheduler.isShutdown()) {
286 logger.warn("initialize(): scheduler is shutdown, aborting initialization.");
290 logger.trace("initialize(): initialize bridge configuration parameters.");
291 veluxBridgeConfiguration = new VeluxBinding(getConfigAs(VeluxBridgeConfiguration.class)).checked();
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.
301 offlineDelay = Duration.ofMillis(
302 ((long) Math.pow(2, veluxBridgeConfiguration.retries + 1) - 1) * veluxBridgeConfiguration.refreshMSecs);
304 initializeRetriesDone = 0;
306 scheduler.execute(() -> {
308 initializeSchedulerJob();
313 * Various initialisation actions to be executed on a background thread
315 private void initializeSchedulerJob() {
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
320 synchronized (LOCK_MODIFIER.concat(veluxBridgeConfiguration.ipAddress).intern()) {
321 logger.trace("initializeSchedulerJob(): adopt new bridge configuration parameters.");
322 bridgeParamsUpdated();
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);
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);
341 VeluxHandlerFactory.refreshBindingInfo();
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());
352 public void dispose() {
353 scheduler.submit(() -> {
355 disposeSchedulerJob();
360 * Various disposal actions to be executed on a background thread
362 private void disposeSchedulerJob() {
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
367 synchronized (LOCK_MODIFIER.concat(veluxBridgeConfiguration.ipAddress).intern()) {
369 * cancel the regular refresh polling job
371 ScheduledFuture<?> refreshSchedulerJob = this.refreshSchedulerJob;
372 if (refreshSchedulerJob != null) {
373 logger.trace("disposeSchedulerJob(): cancel the refresh polling job.");
374 refreshSchedulerJob.cancel(false);
377 ExecutorService commsJobExecutor = this.communicationsJobExecutor;
378 if (commsJobExecutor != null) {
379 this.communicationsJobExecutor = null;
380 logger.trace("disposeSchedulerJob(): cancel any other scheduled jobs.");
382 * remove un-started communication tasks from the execution queue; and stop accepting more tasks
384 commsJobExecutor.shutdownNow();
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
390 if (thisBridge.lastCommunicationOk()) {
392 if (!commsJobExecutor.awaitTermination(COMMUNICATION_TASK_MAX_WAIT_SECS, TimeUnit.SECONDS)) {
393 logger.warn("disposeSchedulerJob(): unexpected awaitTermination() timeout.");
395 } catch (InterruptedException e) {
396 logger.warn("disposeSchedulerJob(): unexpected exception awaitTermination() '{}'.",
403 * if the last bridge communication was OK, deactivate HSM to prevent queueing more HSM events
405 if (thisBridge.lastCommunicationOk()
406 && (new VeluxBridgeSetHouseStatusMonitor().modifyHSM(thisBridge, false))) {
407 logger.trace("disposeSchedulerJob(): HSM deactivated.");
411 * finally clean up everything else
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());
423 * NOTE: It takes care by calling {@link #handleCommand} with the REFRESH command, that every used channel is
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);
434 logger.trace("channelLinked({}) doing nothing as Thing is not online.", channelUID.getAsString());
439 public void channelUnlinked(ChannelUID channelUID) {
440 logger.trace("channelUnlinked({}) called.", channelUID.getAsString());
443 // Reconfiguration methods
445 private void bridgeParamsUpdated() {
446 logger.debug("bridgeParamsUpdated() called.");
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;
455 if (mySlipBridge.supportedProtocols.contains(veluxBridgeConfiguration.protocol)) {
456 logger.debug("bridgeParamsUpdated(): choosing SLIP as communication method.");
457 thisBridge = mySlipBridge;
458 validBridgeFound = true;
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.");
468 InetAddress bridgeAddress = InetAddress.getByName(veluxBridgeConfiguration.ipAddress);
469 if (!bridgeAddress.isReachable(veluxBridgeConfiguration.timeoutMsecs)) {
470 throw new IOException();
472 } catch (IOException e) {
473 logger.debug("bridgeParamsUpdated(): Bridge ip address not reachable.");
474 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
477 logger.trace("bridgeParamsUpdated(): Trying to authenticate towards bridge.");
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.");
488 logger.trace("bridgeParamsUpdated(): Querying bridge state.");
489 bridgeParameters.gateway = new VeluxBridgeDeviceStatus().retrieve(thisBridge);
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"));
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.");
505 logger.warn("Activation of House-Status-Monitoring failed (might lead to a lack of status updates).");
509 updateDynamicChannels();
511 veluxBridgeConfiguration.hasChanged = false;
512 logger.debug("Velux veluxBridge is online, now.");
513 updateStatus(ThingStatus.ONLINE);
514 logger.trace("bridgeParamsUpdated() successfully finished.");
517 // Continuous synchronization methods
519 private synchronized void refreshSchedulerJob() {
520 logger.debug("refreshSchedulerJob() initiated by {} starting cycle {}.", Thread.currentThread(),
522 logger.trace("refreshSchedulerJob(): processing of possible HSM messages.");
524 // Background execution of bridge related I/O
525 submitCommunicationsJob(() -> {
526 getHouseStatusCommsJob();
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);
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);
546 logger.debug("refreshSchedulerJob() initiated by {} finished cycle {}.", Thread.currentThread(),
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();
557 logger.trace("getHouseStatusCommsJob(): => GetHouseStatus() => no updates");
559 logger.trace("getHouseStatusCommsJob() initiated by {} has finished.", Thread.currentThread());
563 * In case of recognized changes in the real world, the method will
564 * update the corresponding states via openHAB event bus.
566 private void syncChannelsWithProducts() {
567 if (!bridgeParameters.actuators.getChannel().existingProducts.isDirty()) {
568 logger.trace("syncChannelsWithProducts(): no existing products with changed parameters.");
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 (!channel2VeluxActuator.containsKey(channelUID)) {
578 logger.trace("syncChannelsWithProducts(): channel {} not found.", channelUID);
581 Thing2VeluxActuator actuator = channel2VeluxActuator.get(channelUID);
582 if (actuator == null || !actuator.isKnown()) {
583 logger.trace("syncChannelsWithProducts(): channel {} not registered on bridge.", channelUID);
586 ProductBridgeIndex channelPbi = actuator.getProductBridgeIndex();
587 if (!channelPbi.equals(productPbi)) {
591 VeluxProductPosition position;
592 if (channelUID.getId().equals(VeluxBindingConstants.CHANNEL_VANE_POSITION)) {
594 position = new VeluxProductPosition(product.getVanePosition());
596 // Handle value inversion
597 isInverted = actuator.isInverted();
598 logger.trace("syncChannelsWithProducts(): isInverted is {}.", isInverted);
599 position = new VeluxProductPosition(product.getDisplayPosition());
601 if (position.isValid()) {
602 PercentType positionAsPercent = position.getPositionAsPercentType(isInverted);
603 logger.debug("syncChannelsWithProducts(): updating channel {} to position {}%.", channelUID,
605 updateState(channelUID, positionAsPercent);
608 logger.trace("syncChannelsWithProducts(): updating channel {} to 'UNDEFINED'.", channelUID);
609 updateState(channelUID, UnDefType.UNDEF);
613 logger.trace("syncChannelsWithProducts(): resetting dirty flag.");
614 bridgeParameters.actuators.getChannel().existingProducts.resetDirtyFlag();
615 logger.trace("syncChannelsWithProducts() done.");
618 // Processing of openHAB events
621 public void handleCommand(ChannelUID channelUID, Command command) {
622 logger.trace("handleCommand({}): command {} on channel {} will be scheduled.", Thread.currentThread(), command,
623 channelUID.getAsString());
624 logger.debug("handleCommand({},{}) called.", channelUID.getAsString(), command);
626 // Background execution of bridge related I/O
627 submitCommunicationsJob(() -> {
628 handleCommandCommsJob(channelUID, command);
630 logger.trace("handleCommand({}) done.", Thread.currentThread());
634 * Normally called by {@link #handleCommand} to handle a command for a given channel with possibly long execution
637 * <B>NOTE:</B> This method is to be called as separated thread to ensure proper openHAB framework in parallel.
640 * @param channelUID the {@link ChannelUID} of the channel to which the command was sent,
641 * @param command the {@link Command}.
643 private synchronized void handleCommandCommsJob(ChannelUID channelUID, Command command) {
644 logger.trace("handleCommandCommsJob({}): command {} on channel {}.", Thread.currentThread(), command,
645 channelUID.getAsString());
646 logger.debug("handleCommandCommsJob({},{}) called.", channelUID.getAsString(), command);
649 * ===========================================================
653 if (veluxBridgeConfiguration.isProtocolTraceEnabled) {
654 Threads.findDeadlocked();
657 String channelId = channelUID.getId();
658 State newState = null;
659 String itemName = channelUID.getAsString();
660 VeluxItemType itemType = VeluxItemType.getByThingAndChannel(thingTypeUIDOf(channelUID), channelUID.getId());
662 if (itemType == VeluxItemType.UNKNOWN) {
663 logger.warn("{} Cannot determine type of Channel {}, ignoring command {}.",
664 VeluxBindingConstants.LOGGING_CONTACT, channelUID, command);
665 logger.trace("handleCommandCommsJob() aborting.");
670 if (!channel2VeluxActuator.containsKey(channelUID)) {
671 channel2VeluxActuator.put(channelUID, new Thing2VeluxActuator(this, channelUID));
674 if (veluxBridgeConfiguration.hasChanged) {
675 logger.trace("handleCommandCommsJob(): work on updated bridge configuration parameters.");
676 bridgeParamsUpdated();
679 syncChannelsWithProducts();
681 if (command instanceof RefreshType) {
683 * ===========================================================
686 logger.trace("handleCommandCommsJob(): work on refresh.");
687 if (!itemType.isReadable()) {
688 logger.debug("handleCommandCommsJob(): received a Refresh command for a non-readable item.");
690 logger.trace("handleCommandCommsJob(): refreshing item {} (type {}).", itemName, itemType);
691 try { // expecting an IllegalArgumentException for unknown Velux device
695 newState = ChannelBridgeStatus.handleRefresh(channelUID, channelId, this);
697 case BRIDGE_DOWNTIME:
698 newState = new DecimalType(
699 thisBridge.lastCommunication() - thisBridge.lastSuccessfulCommunication());
701 case BRIDGE_FIRMWARE:
702 newState = ChannelBridgeFirmware.handleRefresh(channelUID, channelId, this);
705 // delete legacy property name entry (if any) and fall through
706 ThingProperty.setValue(this, VeluxBridgeConfiguration.BRIDGE_IPADDRESS, null);
707 case BRIDGE_SUBNETMASK:
708 case BRIDGE_DEFAULTGW:
710 newState = ChannelBridgeLANconfig.handleRefresh(channelUID, channelId, this);
712 case BRIDGE_WLANSSID:
713 case BRIDGE_WLANPASSWORD:
714 newState = ChannelBridgeWLANconfig.handleRefresh(channelUID, channelId, this);
717 newState = ChannelBridgeScenes.handleRefresh(channelUID, channelId, this);
719 case BRIDGE_PRODUCTS:
720 newState = ChannelBridgeProducts.handleRefresh(channelUID, channelId, this);
723 newState = ChannelBridgeCheck.handleRefresh(channelUID, channelId, this);
726 case ACTUATOR_POSITION:
728 case ROLLERSHUTTER_POSITION:
729 case WINDOW_POSITION:
730 case ROLLERSHUTTER_VANE_POSITION:
731 newState = ChannelActuatorPosition.handleRefresh(channelUID, channelId, this);
733 case ACTUATOR_LIMIT_MINIMUM:
734 case ROLLERSHUTTER_LIMIT_MINIMUM:
735 case WINDOW_LIMIT_MINIMUM:
736 // note: the empty string ("") below is intentional
737 newState = ChannelActuatorLimitation.handleRefresh(channelUID, "", this);
739 case ACTUATOR_LIMIT_MAXIMUM:
740 case ROLLERSHUTTER_LIMIT_MAXIMUM:
741 case WINDOW_LIMIT_MAXIMUM:
742 newState = ChannelActuatorLimitation.handleRefresh(channelUID, channelId, this);
745 // VirtualShutter channels
746 case VSHUTTER_POSITION:
747 newState = ChannelVShutterPosition.handleRefresh(channelUID, channelId, this);
751 logger.warn("{} Cannot handle REFRESH on channel {} as it is of type {}.",
752 VeluxBindingConstants.LOGGING_CONTACT, itemName, channelId);
754 } catch (IllegalArgumentException e) {
755 logger.warn("Cannot handle REFRESH on channel {} as it isn't (yet) known to the bridge.", itemName);
757 if (newState != null) {
758 if (itemType.isChannel()) {
759 logger.debug("handleCommandCommsJob(): updating channel {} to {}.", channelUID, newState);
760 updateState(channelUID, newState);
761 } else if (itemType.isProperty()) {
762 // if property value is 'unknown', null it completely
763 String val = newState.toString();
764 if (VeluxBindingConstants.UNKNOWN.equals(val)) {
767 logger.debug("handleCommandCommsJob(): updating property {} to {}.", channelUID, val);
768 ThingProperty.setValue(this, itemType.getIdentifier(), val);
771 logger.warn("handleCommandCommsJob({},{}): updating of item {} (type {}) failed.",
772 channelUID.getAsString(), command, itemName, itemType);
777 * ===========================================================
780 logger.trace("handleCommandCommsJob(): working on item {} (type {}) with COMMAND {}.", itemName, itemType,
782 Command newValue = null;
783 try { // expecting an IllegalArgumentException for unknown Velux device
787 if (command == OnOffType.ON) {
788 logger.trace("handleCommandCommsJob(): about to reload informations from veluxBridge.");
789 bridgeParamsUpdated();
791 logger.trace("handleCommandCommsJob(): ignoring OFF command.");
794 case BRIDGE_DO_DETECTION:
795 ChannelBridgeDoDetection.handleCommand(channelUID, channelId, command, this);
800 ChannelSceneAction.handleCommand(channelUID, channelId, command, this);
804 * NOTA BENE: Setting of a scene silent mode is no longer supported via the KLF API (i.e. the
805 * GW_SET_NODE_VELOCITY_REQ/CFM command set is no longer supported in the API), so the binding can
806 * no longer explicitly support a Channel with such a function. Therefore the silent mode Channel
807 * type was removed from the binding implementation.
809 * By contrast scene actions can still be called with a silent mode argument, so a silent mode
810 * Configuration Parameter has been introduced as a means for the user to set this argument.
812 * Strictly speaking the following case statement will now never be called, so in theory it,
813 * AND ALL THE CLASSES BEHIND, could be deleted from the binding CODE BASE. But out of prudence
814 * it is retained anyway 'just in case'.
816 case SCENE_SILENTMODE:
817 ChannelSceneSilentmode.handleCommand(channelUID, channelId, command, this);
821 case ACTUATOR_POSITION:
823 case ROLLERSHUTTER_POSITION:
824 case WINDOW_POSITION:
825 case ROLLERSHUTTER_VANE_POSITION:
826 newValue = ChannelActuatorPosition.handleCommand(channelUID, channelId, command, this);
828 case ACTUATOR_LIMIT_MINIMUM:
829 case ROLLERSHUTTER_LIMIT_MINIMUM:
830 case WINDOW_LIMIT_MINIMUM:
831 ChannelActuatorLimitation.handleCommand(channelUID, channelId, command, this);
833 case ACTUATOR_LIMIT_MAXIMUM:
834 case ROLLERSHUTTER_LIMIT_MAXIMUM:
835 case WINDOW_LIMIT_MAXIMUM:
836 ChannelActuatorLimitation.handleCommand(channelUID, channelId, command, this);
839 // VirtualShutter channels
840 case VSHUTTER_POSITION:
841 newValue = ChannelVShutterPosition.handleCommand(channelUID, channelId, command, this);
845 logger.warn("{} Cannot handle command {} on channel {} (type {}).",
846 VeluxBindingConstants.LOGGING_CONTACT, command, itemName, itemType);
848 } catch (IllegalArgumentException e) {
849 logger.warn("Cannot handle command on channel {} as it isn't (yet) known to the bridge.", itemName);
851 if (newValue != null) {
852 postCommand(channelUID, newValue);
856 Instant lastCommunication = Instant.ofEpochMilli(thisBridge.lastCommunication());
857 Instant lastSuccessfulCommunication = Instant.ofEpochMilli(thisBridge.lastSuccessfulCommunication());
858 boolean lastCommunicationSucceeded = lastSuccessfulCommunication.equals(lastCommunication);
859 ThingStatus thingStatus = getThing().getStatus();
861 if (lastCommunicationSucceeded) {
862 if (thingStatus == ThingStatus.OFFLINE || thingStatus == ThingStatus.UNKNOWN) {
863 updateStatus(ThingStatus.ONLINE);
866 if ((thingStatus == ThingStatus.ONLINE || thingStatus == ThingStatus.UNKNOWN)
867 && lastSuccessfulCommunication.plus(offlineDelay).isBefore(lastCommunication)) {
868 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
872 ThingProperty.setValue(this, VeluxBindingConstants.PROPERTY_BRIDGE_TIMESTAMP_ATTEMPT,
873 lastCommunication.toString());
874 ThingProperty.setValue(this, VeluxBindingConstants.PROPERTY_BRIDGE_TIMESTAMP_SUCCESS,
875 lastSuccessfulCommunication.toString());
877 logger.trace("handleCommandCommsJob({}) done.", Thread.currentThread());
881 * Register the exported actions
884 public Collection<Class<? extends ThingHandlerService>> getServices() {
885 return Set.of(VeluxActions.class);
889 * Exported method (called by an OpenHAB Rules Action) to issue a reboot command to the hub.
891 * @return true if the command could be issued
893 public boolean rebootBridge() {
894 logger.trace("runReboot() called on {}", getThing().getUID());
895 RunReboot bcp = thisBridge.bridgeAPI().runReboot();
897 // background execution of reboot process
898 submitCommunicationsJob(() -> {
899 if (thisBridge.bridgeCommunicate(bcp)) {
900 logger.info("Reboot command {}successfully sent to {}", bcp.isCommunicationSuccessful() ? "" : "un",
901 getThing().getUID());
910 * Exported method (called by an OpenHAB Rules Action) to move an actuator relative to its current position
912 * @param nodeId the node to be moved
913 * @param relativePercent relative position change to the current position (-100% <= relativePercent <= +100%)
914 * @return true if the command could be issued
916 public boolean moveRelative(int nodeId, int relativePercent) {
917 logger.trace("moveRelative() called on {}", getThing().getUID());
918 RunProductCommand bcp = thisBridge.bridgeAPI().runProductCommand();
920 // background execution of moveRelative
921 submitCommunicationsJob(() -> {
923 bcp.setNodeIdAndParameters(nodeId,
924 new VeluxProductPosition(new PercentType(Math.abs(relativePercent))).overridePositionType(
925 relativePercent > 0 ? PositionType.OFFSET_POSITIVE : PositionType.OFFSET_NEGATIVE),
927 if (thisBridge.bridgeCommunicate(bcp)) {
928 logger.trace("moveRelative() command {}successfully sent to {}",
929 bcp.isCommunicationSuccessful() ? "" : "un", getThing().getUID());
939 * If necessary initialise the communications job executor. Then check if the executor is shut down. And if it is
940 * not shut down, then submit the given communications job for execution.
942 private void submitCommunicationsJob(Runnable communicationsJob) {
943 ExecutorService commsJobExecutor = this.communicationsJobExecutor;
944 if (commsJobExecutor == null) {
945 commsJobExecutor = this.communicationsJobExecutor = Executors.newSingleThreadExecutor(getThreadFactory());
947 if (!commsJobExecutor.isShutdown()) {
948 commsJobExecutor.execute(communicationsJob);
953 * If necessary initialise the thread factory and return it
955 * @return the thread factory
957 public NamedThreadFactory getThreadFactory() {
958 NamedThreadFactory threadFactory = this.threadFactory;
959 if (threadFactory == null) {
960 threadFactory = new NamedThreadFactory(getThing().getUID().getAsString());
962 return threadFactory;
966 * Indicates if the bridge thing is being disposed.
968 * @return true if the bridge thing is being disposed.
970 public boolean isDisposing() {
975 * Exported method (called by an OpenHAB Rules Action) to simultaneously move the shade main position and the vane
978 * @param node the node index in the bridge.
979 * @param mainPosition the desired main position.
980 * @param vanePosition the desired vane position.
981 * @return true if the command could be issued.
983 public Boolean moveMainAndVane(ProductBridgeIndex node, PercentType mainPosition, PercentType vanePosition) {
984 logger.trace("moveMainAndVane() called on {}", getThing().getUID());
985 RunProductCommand bcp = thisBridge.bridgeAPI().runProductCommand();
987 VeluxProduct product = existingProducts().get(node).clone();
988 FunctionalParameters functionalParameters = null;
989 if (product.supportsVanePosition()) {
990 int vanePos = new VeluxProductPosition(vanePosition).getPositionAsVeluxType();
991 product.setVanePosition(vanePos);
992 functionalParameters = product.getFunctionalParameters();
994 VeluxProductPosition mainPos = new VeluxProductPosition(mainPosition);
995 bcp.setNodeIdAndParameters(node.toInt(), mainPos, functionalParameters);
996 submitCommunicationsJob(() -> {
997 if (thisBridge.bridgeCommunicate(bcp)) {
998 logger.trace("moveMainAndVane() command {}successfully sent to {}",
999 bcp.isCommunicationSuccessful() ? "" : "un", getThing().getUID());
1008 * Get the bridge product index for a given thing name.
1010 * @param thingName the thing name
1011 * @return the bridge product index or ProductBridgeIndex.UNKNOWN if not found.
1013 public ProductBridgeIndex getProductBridgeIndex(String thingName) {
1014 for (Entry<ChannelUID, Thing2VeluxActuator> entry : channel2VeluxActuator.entrySet()) {
1015 if (thingName.equals(entry.getKey().getThingUID().getAsString())) {
1016 return entry.getValue().getProductBridgeIndex();
1019 return ProductBridgeIndex.UNKNOWN;
1023 * Ask all things in the hub to initialise their dynamic vane position channel if they support it.
1025 private void updateDynamicChannels() {
1026 getThing().getThings().stream().forEach(thing -> {
1027 ThingHandler thingHandler = thing.getHandler();
1028 if (thingHandler instanceof VeluxHandler) {
1029 ((VeluxHandler) thingHandler).updateDynamicChannels(this);