]> git.basschouten.com Git - openhab-addons.git/blob
36a51d1482b186d868c8f162af0f09a49e6f5b9c
[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.hdpowerview.internal.handler;
14
15 import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
16 import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
17
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.StringJoiner;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24
25 import javax.ws.rs.NotSupportedException;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
30 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
31 import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem;
32 import org.openhab.binding.hdpowerview.internal.api.Firmware;
33 import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
34 import org.openhab.binding.hdpowerview.internal.api.SurveyData;
35 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
36 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
37 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
38 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
39 import org.openhab.binding.hdpowerview.internal.exceptions.HubException;
40 import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException;
41 import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException;
42 import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
43 import org.openhab.binding.hdpowerview.internal.exceptions.HubShadeTimeoutException;
44 import org.openhab.core.library.types.DecimalType;
45 import org.openhab.core.library.types.OnOffType;
46 import org.openhab.core.library.types.PercentType;
47 import org.openhab.core.library.types.QuantityType;
48 import org.openhab.core.library.types.StopMoveType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.library.types.UpDownType;
51 import org.openhab.core.library.unit.Units;
52 import org.openhab.core.thing.Bridge;
53 import org.openhab.core.thing.ChannelUID;
54 import org.openhab.core.thing.Thing;
55 import org.openhab.core.thing.ThingStatus;
56 import org.openhab.core.thing.ThingStatusDetail;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.RefreshType;
59 import org.openhab.core.types.UnDefType;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63 /**
64  * Handles commands for an HD PowerView Shade
65  *
66  * @author Andy Lintner - Initial contribution
67  * @author Andrew Fiddian-Green - Added support for secondary rail positions
68  */
69 @NonNullByDefault
70 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
71
72     private enum RefreshKind {
73         POSITION,
74         SURVEY,
75         BATTERY_LEVEL
76     }
77
78     private static final String COMMAND_CALIBRATE = "CALIBRATE";
79     private static final String COMMAND_IDENTIFY = "IDENTIFY";
80
81     private static final String DETECTED_SECONDARY_RAIL = "secondaryRailDetected";
82     private static final String DETECTED_TILT_ANYWHERE = "tiltAnywhereDetected";
83     private final Map<String, String> detectedCapabilities = new HashMap<>();
84
85     private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
86     private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
87
88     private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
89     private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
90     private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
91     private @Nullable Capabilities capabilities;
92     private int shadeId;
93     private boolean isDisposing;
94
95     public HDPowerViewShadeHandler(Thing thing) {
96         super(thing);
97     }
98
99     @Override
100     public void initialize() {
101         isDisposing = false;
102         shadeId = getConfigAs(HDPowerViewShadeConfiguration.class).id;
103         logger.debug("Initializing shade handler for shade {}", shadeId);
104         Bridge bridge = getBridge();
105         if (bridge == null) {
106             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
107                     "@text/offline.conf-error.invalid-bridge-handler");
108             return;
109         }
110
111         updateStatus(ThingStatus.UNKNOWN);
112     }
113
114     @Override
115     public void dispose() {
116         logger.debug("Disposing shade handler for shade {}", shadeId);
117         isDisposing = true;
118         ScheduledFuture<?> future = refreshPositionFuture;
119         if (future != null) {
120             future.cancel(true);
121         }
122         refreshPositionFuture = null;
123         future = refreshSignalFuture;
124         if (future != null) {
125             future.cancel(true);
126         }
127         refreshSignalFuture = null;
128         future = refreshBatteryLevelFuture;
129         if (future != null) {
130             future.cancel(true);
131         }
132         refreshBatteryLevelFuture = null;
133         capabilities = null;
134     }
135
136     @Override
137     public void handleCommand(ChannelUID channelUID, Command command) {
138         String channelId = channelUID.getId();
139
140         if (RefreshType.REFRESH == command) {
141             switch (channelId) {
142                 case CHANNEL_SHADE_POSITION:
143                 case CHANNEL_SHADE_SECONDARY_POSITION:
144                 case CHANNEL_SHADE_VANE:
145                     requestRefreshShadePosition();
146                     break;
147                 case CHANNEL_SHADE_LOW_BATTERY:
148                 case CHANNEL_SHADE_BATTERY_LEVEL:
149                 case CHANNEL_SHADE_BATTERY_VOLTAGE:
150                     requestRefreshShadeBatteryLevel();
151                     break;
152                 case CHANNEL_SHADE_SIGNAL_STRENGTH:
153                     requestRefreshShadeSurvey();
154                     break;
155             }
156             return;
157         }
158
159         HDPowerViewHubHandler bridge = getBridgeHandler();
160         if (bridge == null) {
161             logger.warn("Missing bridge handler");
162             return;
163         }
164         HDPowerViewWebTargets webTargets = bridge.getWebTargets();
165         try {
166             handleShadeCommand(channelId, command, webTargets, shadeId);
167         } catch (HubInvalidResponseException e) {
168             Throwable cause = e.getCause();
169             if (cause == null) {
170                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
171             } else {
172                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
173             }
174         } catch (HubMaintenanceException e) {
175             // exceptions are logged in HDPowerViewWebTargets
176         } catch (HubShadeTimeoutException e) {
177             logger.warn("Shade {} timeout when sending command {}", shadeId, command);
178         } catch (HubException e) {
179             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
180             // for any ongoing requests. Logging this would only cause confusion.
181             if (!isDisposing) {
182                 logger.warn("Unexpected error: {}", e.getMessage());
183             }
184         }
185     }
186
187     private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
188             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
189             HubShadeTimeoutException {
190         switch (channelId) {
191             case CHANNEL_SHADE_POSITION:
192                 if (command instanceof PercentType) {
193                     moveShade(PRIMARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
194                 } else if (command instanceof UpDownType) {
195                     moveShade(PRIMARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
196                 } else if (command instanceof StopMoveType) {
197                     if (StopMoveType.STOP == command) {
198                         stopShade(webTargets, shadeId);
199                     } else {
200                         logger.warn("Unexpected StopMoveType command");
201                     }
202                 }
203                 break;
204
205             case CHANNEL_SHADE_VANE:
206                 if (command instanceof PercentType) {
207                     moveShade(VANE_TILT_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
208                 } else if (command instanceof OnOffType) {
209                     moveShade(VANE_TILT_POSITION, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
210                 }
211                 break;
212
213             case CHANNEL_SHADE_SECONDARY_POSITION:
214                 if (command instanceof PercentType) {
215                     moveShade(SECONDARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
216                 } else if (command instanceof UpDownType) {
217                     moveShade(SECONDARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
218                 } else if (command instanceof StopMoveType) {
219                     if (StopMoveType.STOP == command) {
220                         stopShade(webTargets, shadeId);
221                     } else {
222                         logger.warn("Unexpected StopMoveType command");
223                     }
224                 }
225                 break;
226
227             case CHANNEL_SHADE_COMMAND:
228                 if (command instanceof StringType) {
229                     if (COMMAND_IDENTIFY.equals(((StringType) command).toString())) {
230                         logger.debug("Identify shade {}", shadeId);
231                         identifyShade(webTargets, shadeId);
232                     } else if (COMMAND_CALIBRATE.equals(((StringType) command).toString())) {
233                         logger.debug("Calibrate shade {}", shadeId);
234                         calibrateShade(webTargets, shadeId);
235                     }
236                 } else {
237                     logger.warn("Unsupported command: {}. Supported commands are: " + COMMAND_CALIBRATE, command);
238                 }
239                 break;
240         }
241     }
242
243     /**
244      * Update the state of the channels based on the ShadeData provided.
245      *
246      * @param shadeData the ShadeData to be used.
247      */
248     protected void onReceiveUpdate(ShadeData shadeData) {
249         updateStatus(ThingStatus.ONLINE);
250         updateCapabilities(shadeData);
251         updateSoftProperties(shadeData);
252         updateFirmwareProperties(shadeData);
253         ShadePosition shadePosition = shadeData.positions;
254         if (shadePosition != null) {
255             updatePositionStates(shadePosition);
256         }
257         updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
258         updateSignalStrengthState(shadeData.signalStrength);
259     }
260
261     private void updateCapabilities(ShadeData shade) {
262         if (capabilities != null) {
263             // Already cached.
264             return;
265         }
266         Capabilities capabilities = db.getCapabilities(shade.type, shade.capabilities);
267         if (capabilities.getValue() < 0) {
268             logger.debug("Unable to set capabilities for shade {}", shade.id);
269             return;
270         }
271         logger.debug("Caching capabilities {} for shade {}", capabilities.getValue(), shade.id);
272         this.capabilities = capabilities;
273     }
274
275     private Capabilities getCapabilitiesOrDefault() {
276         Capabilities capabilities = this.capabilities;
277         if (capabilities == null) {
278             return new Capabilities();
279         }
280         return capabilities;
281     }
282
283     /**
284      * Update the Thing's properties based on the contents of the provided ShadeData.
285      *
286      * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
287      * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
288      *
289      * @param shadeData
290      */
291     private void updateSoftProperties(ShadeData shadeData) {
292         final Map<String, String> properties = getThing().getProperties();
293         boolean propChanged = false;
294
295         // update 'type' property
296         final int type = shadeData.type;
297         String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
298         String propOldVal = properties.getOrDefault(propKey, "");
299         String propNewVal = db.getType(type).toString();
300         if (!propNewVal.equals(propOldVal)) {
301             propChanged = true;
302             getThing().setProperty(propKey, propNewVal);
303             if ((type > 0) && !db.isTypeInDatabase(type)) {
304                 db.logTypeNotInDatabase(type);
305             }
306         }
307
308         // update 'capabilities' property
309         Capabilities capabilities = db.getCapabilities(shadeData.capabilities);
310         final int capabilitiesVal = capabilities.getValue();
311         propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
312         propOldVal = properties.getOrDefault(propKey, "");
313         propNewVal = capabilities.toString();
314         if (!propNewVal.equals(propOldVal)) {
315             propChanged = true;
316             getThing().setProperty(propKey, propNewVal);
317             if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) {
318                 db.logCapabilitiesNotInDatabase(type, capabilitiesVal);
319             }
320         }
321
322         if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type)
323                 && (capabilitiesVal != db.getType(type).getCapabilities())) {
324             db.logCapabilitiesMismatch(type, capabilitiesVal);
325         }
326     }
327
328     private void updateFirmwareProperties(ShadeData shadeData) {
329         Map<String, String> properties = editProperties();
330         Firmware shadeFirmware = shadeData.firmware;
331         Firmware motorFirmware = shadeData.motor;
332         if (shadeFirmware != null) {
333             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
334         }
335         if (motorFirmware != null) {
336             properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
337         }
338         updateProperties(properties);
339     }
340
341     /**
342      * After a hard refresh, update the Thing's detected capabilities based on the contents of the provided ShadeData.
343      *
344      * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
345      * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
346      * potential need to add support for that type resp. capabilities.
347      *
348      * @param shadeData
349      */
350     private void updateDetectedCapabilities(ShadeData shadeData) {
351         final ShadePosition positions = shadeData.positions;
352         if (positions == null) {
353             return;
354         }
355         Capabilities capabilities = getCapabilitiesOrDefault();
356
357         // update 'secondary rail' detected capability
358         String capsKey = DETECTED_SECONDARY_RAIL;
359         String capsOldVal = detectedCapabilities.getOrDefault(capsKey, "");
360         boolean capsNewBool = positions.secondaryRailDetected();
361         String capsNewVal = String.valueOf(capsNewBool);
362         if (!capsNewVal.equals(capsOldVal)) {
363             detectedCapabilities.put(capsKey, capsNewVal);
364             if (capsNewBool != capabilities.supportsSecondary()) {
365                 db.logPropertyMismatch(capsKey, shadeData.type, capabilities.getValue(), capsNewBool);
366             }
367         }
368
369         // update 'tilt anywhere' detected capability
370         capsKey = DETECTED_TILT_ANYWHERE;
371         capsOldVal = detectedCapabilities.getOrDefault(capsKey, "");
372         capsNewBool = positions.tiltAnywhereDetected();
373         capsNewVal = String.valueOf(capsNewBool);
374         if (!capsNewVal.equals(capsOldVal)) {
375             detectedCapabilities.put(capsKey, capsNewVal);
376             if (capsNewBool != capabilities.supportsTiltAnywhere()) {
377                 db.logPropertyMismatch(capsKey, shadeData.type, capabilities.getValue(), capsNewBool);
378             }
379         }
380     }
381
382     private void updatePositionStates(ShadePosition shadePos) {
383         Capabilities capabilities = this.capabilities;
384         if (capabilities == null) {
385             logger.debug("The 'shadeCapabilities' field has not yet been initialized");
386             updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
387             updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
388             updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
389             return;
390         }
391         updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_POSITION));
392         updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_POSITION));
393         updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_POSITION));
394     }
395
396     private void updateBatteryStates(int batteryStatus, double batteryStrength) {
397         updateBatteryLevelStates(batteryStatus);
398         updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
399                 batteryStrength > 0 ? new QuantityType<>(batteryStrength / 10, Units.VOLT) : UnDefType.UNDEF);
400     }
401
402     private void updateBatteryLevelStates(int batteryStatus) {
403         int mappedValue;
404         switch (batteryStatus) {
405             case 1: // Low
406                 mappedValue = 10;
407                 break;
408             case 2: // Medium
409                 mappedValue = 50;
410                 break;
411             case 3: // High
412             case 4: // Plugged in
413                 mappedValue = 100;
414                 break;
415             default: // No status available (0) or invalid
416                 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
417                 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
418                 return;
419         }
420         updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
421         updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
422     }
423
424     private void updateSignalStrengthState(int signalStrength) {
425         updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(signalStrength));
426     }
427
428     private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId)
429             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
430             HubShadeTimeoutException {
431         ShadePosition newPosition = null;
432         // (try to) read the positions from the hub
433         ShadeData shadeData = webTargets.getShade(shadeId);
434         updateCapabilities(shadeData);
435         newPosition = shadeData.positions;
436         // if no positions returned, then create a new position
437         if (newPosition == null) {
438             newPosition = new ShadePosition();
439         }
440         Capabilities capabilities = getCapabilitiesOrDefault();
441         // set the new position value, and write the positions to the hub
442         shadeData = webTargets.moveShade(shadeId, newPosition.setPosition(capabilities, coordSys, newPercent));
443         updateShadePositions(shadeData);
444     }
445
446     private void stopShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
447             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
448         updateShadePositions(webTargets.stopShade(shadeId));
449         // Positions in response from stop motion is not updated to to actual positions yet,
450         // so we need to request hard refresh.
451         requestRefreshShadePosition();
452     }
453
454     private void identifyShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
455             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
456         updateShadePositions(webTargets.jogShade(shadeId));
457     }
458
459     private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
460             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
461         updateShadePositions(webTargets.calibrateShade(shadeId));
462     }
463
464     private void updateShadePositions(ShadeData shadeData) {
465         ShadePosition shadePosition = shadeData.positions;
466         if (shadePosition == null) {
467             return;
468         }
469         updateCapabilities(shadeData);
470         updatePositionStates(shadePosition);
471     }
472
473     /**
474      * Request that the shade shall undergo a 'hard' refresh for querying its current position
475      */
476     protected synchronized void requestRefreshShadePosition() {
477         if (refreshPositionFuture == null) {
478             refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
479         }
480     }
481
482     /**
483      * Request that the shade shall undergo a 'hard' refresh for querying its survey data
484      */
485     protected synchronized void requestRefreshShadeSurvey() {
486         if (refreshSignalFuture == null) {
487             refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
488         }
489     }
490
491     /**
492      * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
493      */
494     protected synchronized void requestRefreshShadeBatteryLevel() {
495         if (refreshBatteryLevelFuture == null) {
496             refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
497         }
498     }
499
500     private void doRefreshShadePosition() {
501         this.doRefreshShade(RefreshKind.POSITION);
502         refreshPositionFuture = null;
503     }
504
505     private void doRefreshShadeSignal() {
506         this.doRefreshShade(RefreshKind.SURVEY);
507         refreshSignalFuture = null;
508     }
509
510     private void doRefreshShadeBatteryLevel() {
511         this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
512         refreshBatteryLevelFuture = null;
513     }
514
515     private void doRefreshShade(RefreshKind kind) {
516         try {
517             HDPowerViewHubHandler bridge;
518             if ((bridge = getBridgeHandler()) == null) {
519                 throw new HubProcessingException("Missing bridge handler");
520             }
521             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
522             ShadeData shadeData;
523             switch (kind) {
524                 case POSITION:
525                     shadeData = webTargets.refreshShadePosition(shadeId);
526                     updateShadePositions(shadeData);
527                     updateDetectedCapabilities(shadeData);
528                     break;
529                 case SURVEY:
530                     List<SurveyData> surveyData = webTargets.getShadeSurvey(shadeId);
531                     if (!surveyData.isEmpty()) {
532                         if (logger.isDebugEnabled()) {
533                             StringJoiner joiner = new StringJoiner(", ");
534                             surveyData.forEach(data -> joiner.add(data.toString()));
535                             logger.debug("Survey response for shade {}: {}", shadeId, joiner.toString());
536                         }
537                         shadeData = webTargets.getShade(shadeId);
538                         updateSignalStrengthState(shadeData.signalStrength);
539                     } else {
540                         logger.info("No data from shade {} survey", shadeId);
541                         /*
542                          * Setting channel to UNDEF here would be reverted on next poll, since
543                          * signal strength is part of shade response. So leaving current value,
544                          * even though refreshing the value failed.
545                          */
546                     }
547                     break;
548                 case BATTERY_LEVEL:
549                     shadeData = webTargets.refreshShadeBatteryLevel(shadeId);
550                     updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
551                     break;
552                 default:
553                     throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
554             }
555         } catch (HubInvalidResponseException e) {
556             Throwable cause = e.getCause();
557             if (cause == null) {
558                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
559             } else {
560                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
561             }
562         } catch (HubMaintenanceException e) {
563             // exceptions are logged in HDPowerViewWebTargets
564         } catch (HubShadeTimeoutException e) {
565             logger.info("Shade {} wireless refresh time out", shadeId);
566         } catch (HubException e) {
567             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
568             // for any ongoing requests. Logging this would only cause confusion.
569             if (!isDisposing) {
570                 logger.warn("Unexpected error: {}", e.getMessage());
571             }
572         }
573     }
574 }