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