]> git.basschouten.com Git - openhab-addons.git/blob
35be15d3a9a5af20a31e97287756337bd8c9b714
[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         if (webTargets == null) {
166             logger.warn("Web targets not initialized");
167             return;
168         }
169         try {
170             handleShadeCommand(channelId, command, webTargets, shadeId);
171         } catch (HubInvalidResponseException e) {
172             Throwable cause = e.getCause();
173             if (cause == null) {
174                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
175             } else {
176                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
177             }
178         } catch (HubMaintenanceException e) {
179             // exceptions are logged in HDPowerViewWebTargets
180         } catch (HubShadeTimeoutException e) {
181             logger.warn("Shade {} timeout when sending command {}", shadeId, command);
182         } catch (HubException e) {
183             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
184             // for any ongoing requests. Logging this would only cause confusion.
185             if (!isDisposing) {
186                 logger.warn("Unexpected error: {}", e.getMessage());
187             }
188         }
189     }
190
191     private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
192             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
193             HubShadeTimeoutException {
194         switch (channelId) {
195             case CHANNEL_SHADE_POSITION:
196                 if (command instanceof PercentType) {
197                     moveShade(PRIMARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
198                 } else if (command instanceof UpDownType) {
199                     moveShade(PRIMARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
200                 } else if (command instanceof StopMoveType) {
201                     if (StopMoveType.STOP == command) {
202                         stopShade(webTargets, shadeId);
203                     } else {
204                         logger.warn("Unexpected StopMoveType command");
205                     }
206                 }
207                 break;
208
209             case CHANNEL_SHADE_VANE:
210                 if (command instanceof PercentType) {
211                     moveShade(VANE_TILT_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
212                 } else if (command instanceof OnOffType) {
213                     moveShade(VANE_TILT_POSITION, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
214                 }
215                 break;
216
217             case CHANNEL_SHADE_SECONDARY_POSITION:
218                 if (command instanceof PercentType) {
219                     moveShade(SECONDARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
220                 } else if (command instanceof UpDownType) {
221                     moveShade(SECONDARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
222                 } else if (command instanceof StopMoveType) {
223                     if (StopMoveType.STOP == command) {
224                         stopShade(webTargets, shadeId);
225                     } else {
226                         logger.warn("Unexpected StopMoveType command");
227                     }
228                 }
229                 break;
230
231             case CHANNEL_SHADE_COMMAND:
232                 if (command instanceof StringType) {
233                     if (COMMAND_IDENTIFY.equals(((StringType) command).toString())) {
234                         logger.debug("Identify shade {}", shadeId);
235                         identifyShade(webTargets, shadeId);
236                     } else if (COMMAND_CALIBRATE.equals(((StringType) command).toString())) {
237                         logger.debug("Calibrate shade {}", shadeId);
238                         calibrateShade(webTargets, shadeId);
239                     }
240                 } else {
241                     logger.warn("Unsupported command: {}. Supported commands are: " + COMMAND_CALIBRATE, command);
242                 }
243                 break;
244         }
245     }
246
247     /**
248      * Update the state of the channels based on the ShadeData provided.
249      *
250      * @param shadeData the ShadeData to be used.
251      */
252     protected void onReceiveUpdate(ShadeData shadeData) {
253         updateStatus(ThingStatus.ONLINE);
254         updateCapabilities(shadeData);
255         updateSoftProperties(shadeData);
256         updateFirmwareProperties(shadeData);
257         ShadePosition shadePosition = shadeData.positions;
258         if (shadePosition != null) {
259             updatePositionStates(shadePosition);
260         }
261         updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
262         updateSignalStrengthState(shadeData.signalStrength);
263     }
264
265     private void updateCapabilities(ShadeData shade) {
266         if (capabilities != null) {
267             // Already cached.
268             return;
269         }
270         Capabilities capabilities = db.getCapabilities(shade.type, shade.capabilities);
271         if (capabilities.getValue() < 0) {
272             logger.debug("Unable to set capabilities for shade {}", shade.id);
273             return;
274         }
275         logger.debug("Caching capabilities {} for shade {}", capabilities.getValue(), shade.id);
276         this.capabilities = capabilities;
277     }
278
279     private Capabilities getCapabilitiesOrDefault() {
280         Capabilities capabilities = this.capabilities;
281         if (capabilities == null) {
282             return new Capabilities();
283         }
284         return capabilities;
285     }
286
287     /**
288      * Update the Thing's properties based on the contents of the provided ShadeData.
289      *
290      * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
291      * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
292      *
293      * @param shadeData
294      */
295     private void updateSoftProperties(ShadeData shadeData) {
296         final Map<String, String> properties = getThing().getProperties();
297         boolean propChanged = false;
298
299         // update 'type' property
300         final int type = shadeData.type;
301         String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
302         String propOldVal = properties.getOrDefault(propKey, "");
303         String propNewVal = db.getType(type).toString();
304         if (!propNewVal.equals(propOldVal)) {
305             propChanged = true;
306             getThing().setProperty(propKey, propNewVal);
307             if ((type > 0) && !db.isTypeInDatabase(type)) {
308                 db.logTypeNotInDatabase(type);
309             }
310         }
311
312         // update 'capabilities' property
313         Capabilities capabilities = db.getCapabilities(shadeData.capabilities);
314         final int capabilitiesVal = capabilities.getValue();
315         propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
316         propOldVal = properties.getOrDefault(propKey, "");
317         propNewVal = capabilities.toString();
318         if (!propNewVal.equals(propOldVal)) {
319             propChanged = true;
320             getThing().setProperty(propKey, propNewVal);
321             if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) {
322                 db.logCapabilitiesNotInDatabase(type, capabilitiesVal);
323             }
324         }
325
326         if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type)
327                 && (capabilitiesVal != db.getType(type).getCapabilities())) {
328             db.logCapabilitiesMismatch(type, capabilitiesVal);
329         }
330     }
331
332     private void updateFirmwareProperties(ShadeData shadeData) {
333         Map<String, String> properties = editProperties();
334         Firmware shadeFirmware = shadeData.firmware;
335         Firmware motorFirmware = shadeData.motor;
336         if (shadeFirmware != null) {
337             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
338         }
339         if (motorFirmware != null) {
340             properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
341         }
342         updateProperties(properties);
343     }
344
345     /**
346      * After a hard refresh, update the Thing's detected capabilities based on the contents of the provided ShadeData.
347      *
348      * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
349      * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
350      * potential need to add support for that type resp. capabilities.
351      *
352      * @param shadeData
353      */
354     private void updateDetectedCapabilities(ShadeData shadeData) {
355         final ShadePosition positions = shadeData.positions;
356         if (positions == null) {
357             return;
358         }
359         Capabilities capabilities = getCapabilitiesOrDefault();
360
361         // update 'secondary rail' detected capability
362         String capsKey = DETECTED_SECONDARY_RAIL;
363         String capsOldVal = detectedCapabilities.getOrDefault(capsKey, "");
364         boolean capsNewBool = positions.secondaryRailDetected();
365         String capsNewVal = String.valueOf(capsNewBool);
366         if (!capsNewVal.equals(capsOldVal)) {
367             detectedCapabilities.put(capsKey, capsNewVal);
368             if (capsNewBool != capabilities.supportsSecondary()) {
369                 db.logPropertyMismatch(capsKey, shadeData.type, capabilities.getValue(), capsNewBool);
370             }
371         }
372
373         // update 'tilt anywhere' detected capability
374         capsKey = DETECTED_TILT_ANYWHERE;
375         capsOldVal = detectedCapabilities.getOrDefault(capsKey, "");
376         capsNewBool = positions.tiltAnywhereDetected();
377         capsNewVal = String.valueOf(capsNewBool);
378         if (!capsNewVal.equals(capsOldVal)) {
379             detectedCapabilities.put(capsKey, capsNewVal);
380             if (capsNewBool != capabilities.supportsTiltAnywhere()) {
381                 db.logPropertyMismatch(capsKey, shadeData.type, capabilities.getValue(), capsNewBool);
382             }
383         }
384     }
385
386     private void updatePositionStates(ShadePosition shadePos) {
387         Capabilities capabilities = this.capabilities;
388         if (capabilities == null) {
389             logger.debug("The 'shadeCapabilities' field has not yet been initialized");
390             updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
391             updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
392             updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
393             return;
394         }
395         updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_POSITION));
396         updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_POSITION));
397         updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_POSITION));
398     }
399
400     private void updateBatteryStates(int batteryStatus, double batteryStrength) {
401         updateBatteryLevelStates(batteryStatus);
402         updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
403                 batteryStrength > 0 ? new QuantityType<>(batteryStrength / 10, Units.VOLT) : UnDefType.UNDEF);
404     }
405
406     private void updateBatteryLevelStates(int batteryStatus) {
407         int mappedValue;
408         switch (batteryStatus) {
409             case 1: // Low
410                 mappedValue = 10;
411                 break;
412             case 2: // Medium
413                 mappedValue = 50;
414                 break;
415             case 3: // High
416             case 4: // Plugged in
417                 mappedValue = 100;
418                 break;
419             default: // No status available (0) or invalid
420                 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
421                 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
422                 return;
423         }
424         updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
425         updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
426     }
427
428     private void updateSignalStrengthState(int signalStrength) {
429         updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(signalStrength));
430     }
431
432     private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId)
433             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
434             HubShadeTimeoutException {
435         ShadePosition newPosition = null;
436         // (try to) read the positions from the hub
437         ShadeData shadeData = webTargets.getShade(shadeId);
438         updateCapabilities(shadeData);
439         newPosition = shadeData.positions;
440         // if no positions returned, then create a new position
441         if (newPosition == null) {
442             newPosition = new ShadePosition();
443         }
444         Capabilities capabilities = getCapabilitiesOrDefault();
445         // set the new position value, and write the positions to the hub
446         shadeData = webTargets.moveShade(shadeId, newPosition.setPosition(capabilities, coordSys, newPercent));
447         updateShadePositions(shadeData);
448     }
449
450     private void stopShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
451             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
452         updateShadePositions(webTargets.stopShade(shadeId));
453         // Positions in response from stop motion is not updated to to actual positions yet,
454         // so we need to request hard refresh.
455         requestRefreshShadePosition();
456     }
457
458     private void identifyShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
459             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
460         updateShadePositions(webTargets.jogShade(shadeId));
461     }
462
463     private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
464             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
465         updateShadePositions(webTargets.calibrateShade(shadeId));
466     }
467
468     private void updateShadePositions(ShadeData shadeData) {
469         ShadePosition shadePosition = shadeData.positions;
470         if (shadePosition == null) {
471             return;
472         }
473         updateCapabilities(shadeData);
474         updatePositionStates(shadePosition);
475     }
476
477     /**
478      * Request that the shade shall undergo a 'hard' refresh for querying its current position
479      */
480     protected synchronized void requestRefreshShadePosition() {
481         if (refreshPositionFuture == null) {
482             refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
483         }
484     }
485
486     /**
487      * Request that the shade shall undergo a 'hard' refresh for querying its survey data
488      */
489     protected synchronized void requestRefreshShadeSurvey() {
490         if (refreshSignalFuture == null) {
491             refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
492         }
493     }
494
495     /**
496      * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
497      */
498     protected synchronized void requestRefreshShadeBatteryLevel() {
499         if (refreshBatteryLevelFuture == null) {
500             refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
501         }
502     }
503
504     private void doRefreshShadePosition() {
505         this.doRefreshShade(RefreshKind.POSITION);
506         refreshPositionFuture = null;
507     }
508
509     private void doRefreshShadeSignal() {
510         this.doRefreshShade(RefreshKind.SURVEY);
511         refreshSignalFuture = null;
512     }
513
514     private void doRefreshShadeBatteryLevel() {
515         this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
516         refreshBatteryLevelFuture = null;
517     }
518
519     private void doRefreshShade(RefreshKind kind) {
520         try {
521             HDPowerViewHubHandler bridge;
522             if ((bridge = getBridgeHandler()) == null) {
523                 throw new HubProcessingException("Missing bridge handler");
524             }
525             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
526             if (webTargets == null) {
527                 throw new HubProcessingException("Web targets not initialized");
528             }
529             ShadeData shadeData;
530             switch (kind) {
531                 case POSITION:
532                     shadeData = webTargets.refreshShadePosition(shadeId);
533                     updateShadePositions(shadeData);
534                     updateDetectedCapabilities(shadeData);
535                     break;
536                 case SURVEY:
537                     List<SurveyData> surveyData = webTargets.getShadeSurvey(shadeId);
538                     if (!surveyData.isEmpty()) {
539                         if (logger.isDebugEnabled()) {
540                             StringJoiner joiner = new StringJoiner(", ");
541                             surveyData.forEach(data -> joiner.add(data.toString()));
542                             logger.debug("Survey response for shade {}: {}", shadeId, joiner.toString());
543                         }
544                         shadeData = webTargets.getShade(shadeId);
545                         updateSignalStrengthState(shadeData.signalStrength);
546                     } else {
547                         logger.info("No data from shade {} survey", shadeId);
548                         /*
549                          * Setting channel to UNDEF here would be reverted on next poll, since
550                          * signal strength is part of shade response. So leaving current value,
551                          * even though refreshing the value failed.
552                          */
553                     }
554                     break;
555                 case BATTERY_LEVEL:
556                     shadeData = webTargets.refreshShadeBatteryLevel(shadeId);
557                     updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
558                     break;
559                 default:
560                     throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
561             }
562         } catch (HubInvalidResponseException e) {
563             Throwable cause = e.getCause();
564             if (cause == null) {
565                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
566             } else {
567                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
568             }
569         } catch (HubMaintenanceException e) {
570             // exceptions are logged in HDPowerViewWebTargets
571         } catch (HubShadeTimeoutException e) {
572             logger.info("Shade {} wireless refresh time out", shadeId);
573         } catch (HubException e) {
574             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
575             // for any ongoing requests. Logging this would only cause confusion.
576             if (!isDisposing) {
577                 logger.warn("Unexpected error: {}", e.getMessage());
578             }
579         }
580     }
581 }