]> git.basschouten.com Git - openhab-addons.git/blob
5b0cb829a543ad80d8b666418e775e45c7f53ac1
[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         updateStatus(ThingStatus.UNKNOWN);
113     }
114
115     @Override
116     public void dispose() {
117         logger.debug("Disposing shade handler for shade {}", shadeId);
118         isDisposing = true;
119         ScheduledFuture<?> future = refreshPositionFuture;
120         if (future != null) {
121             future.cancel(true);
122         }
123         refreshPositionFuture = null;
124         future = refreshSignalFuture;
125         if (future != null) {
126             future.cancel(true);
127         }
128         refreshSignalFuture = null;
129         future = refreshBatteryLevelFuture;
130         if (future != null) {
131             future.cancel(true);
132         }
133         refreshBatteryLevelFuture = null;
134         capabilities = null;
135     }
136
137     @Override
138     public void handleCommand(ChannelUID channelUID, Command command) {
139         String channelId = channelUID.getId();
140
141         if (RefreshType.REFRESH == command) {
142             switch (channelId) {
143                 case CHANNEL_SHADE_POSITION:
144                 case CHANNEL_SHADE_SECONDARY_POSITION:
145                 case CHANNEL_SHADE_VANE:
146                     requestRefreshShadePosition();
147                     break;
148                 case CHANNEL_SHADE_LOW_BATTERY:
149                 case CHANNEL_SHADE_BATTERY_LEVEL:
150                 case CHANNEL_SHADE_BATTERY_VOLTAGE:
151                     requestRefreshShadeBatteryLevel();
152                     break;
153                 case CHANNEL_SHADE_SIGNAL_STRENGTH:
154                     requestRefreshShadeSurvey();
155                     break;
156             }
157             return;
158         }
159
160         HDPowerViewHubHandler bridge = getBridgeHandler();
161         if (bridge == null) {
162             logger.warn("Missing bridge handler");
163             return;
164         }
165         HDPowerViewWebTargets webTargets = bridge.getWebTargets();
166         if (webTargets == null) {
167             logger.warn("Web targets not initialized");
168             return;
169         }
170         try {
171             handleShadeCommand(channelId, command, webTargets, shadeId);
172         } catch (HubInvalidResponseException e) {
173             Throwable cause = e.getCause();
174             if (cause == null) {
175                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
176             } else {
177                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
178             }
179         } catch (HubMaintenanceException e) {
180             // exceptions are logged in HDPowerViewWebTargets
181         } catch (HubShadeTimeoutException e) {
182             logger.warn("Shade {} timeout when sending command {}", shadeId, command);
183         } catch (HubException e) {
184             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
185             // for any ongoing requests. Logging this would only cause confusion.
186             if (!isDisposing) {
187                 logger.warn("Unexpected error: {}", e.getMessage());
188             }
189         }
190     }
191
192     private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
193             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
194             HubShadeTimeoutException {
195         switch (channelId) {
196             case CHANNEL_SHADE_POSITION:
197                 if (command instanceof PercentType) {
198                     moveShade(PRIMARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
199                 } else if (command instanceof UpDownType) {
200                     moveShade(PRIMARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
201                 } else if (command instanceof StopMoveType) {
202                     if (StopMoveType.STOP == command) {
203                         stopShade(webTargets, shadeId);
204                     } else {
205                         logger.warn("Unexpected StopMoveType command");
206                     }
207                 }
208                 break;
209
210             case CHANNEL_SHADE_VANE:
211                 if (command instanceof PercentType) {
212                     moveShade(VANE_TILT_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
213                 } else if (command instanceof OnOffType) {
214                     moveShade(VANE_TILT_POSITION, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
215                 }
216                 break;
217
218             case CHANNEL_SHADE_SECONDARY_POSITION:
219                 if (command instanceof PercentType) {
220                     moveShade(SECONDARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
221                 } else if (command instanceof UpDownType) {
222                     moveShade(SECONDARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
223                 } else if (command instanceof StopMoveType) {
224                     if (StopMoveType.STOP == command) {
225                         stopShade(webTargets, shadeId);
226                     } else {
227                         logger.warn("Unexpected StopMoveType command");
228                     }
229                 }
230                 break;
231
232             case CHANNEL_SHADE_COMMAND:
233                 if (command instanceof StringType) {
234                     if (COMMAND_IDENTIFY.equals(((StringType) command).toString())) {
235                         logger.debug("Identify shade {}", shadeId);
236                         identifyShade(webTargets, shadeId);
237                     } else if (COMMAND_CALIBRATE.equals(((StringType) command).toString())) {
238                         logger.debug("Calibrate shade {}", shadeId);
239                         calibrateShade(webTargets, shadeId);
240                     }
241                 } else {
242                     logger.warn("Unsupported command: {}. Supported commands are: " + COMMAND_CALIBRATE, command);
243                 }
244                 break;
245         }
246     }
247
248     /**
249      * Update the state of the channels based on the ShadeData provided.
250      *
251      * @param shadeData the ShadeData to be used.
252      */
253     protected void onReceiveUpdate(ShadeData shadeData) {
254         updateStatus(ThingStatus.ONLINE);
255         updateCapabilities(shadeData);
256         updateSoftProperties(shadeData);
257         updateFirmwareProperties(shadeData);
258         ShadePosition shadePosition = shadeData.positions;
259         if (shadePosition != null) {
260             updatePositionStates(shadePosition);
261         }
262         updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
263         updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
264     }
265
266     private void updateCapabilities(ShadeData shade) {
267         if (capabilities != null) {
268             // Already cached.
269             return;
270         }
271         Integer value = shade.capabilities;
272         if (value != null) {
273             int valueAsInt = value.intValue();
274             logger.debug("Caching capabilities {} for shade {}", valueAsInt, shade.id);
275             capabilities = db.getCapabilities(valueAsInt);
276         } else {
277             logger.debug("Capabilities not included in shade response");
278         }
279     }
280
281     private Capabilities getCapabilitiesOrDefault() {
282         Capabilities capabilities = this.capabilities;
283         if (capabilities == null) {
284             return new Capabilities();
285         }
286         return capabilities;
287     }
288
289     /**
290      * Update the Thing's properties based on the contents of the provided ShadeData.
291      *
292      * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
293      * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
294      *
295      * @param shadeData
296      */
297     private void updateSoftProperties(ShadeData shadeData) {
298         final Map<String, String> properties = getThing().getProperties();
299         boolean propChanged = false;
300
301         // update 'type' property
302         final int type = shadeData.type;
303         String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
304         String propOldVal = properties.getOrDefault(propKey, "");
305         String propNewVal = db.getType(type).toString();
306         if (!propNewVal.equals(propOldVal)) {
307             propChanged = true;
308             getThing().setProperty(propKey, propNewVal);
309             if ((type > 0) && !db.isTypeInDatabase(type)) {
310                 db.logTypeNotInDatabase(type);
311             }
312         }
313
314         // update 'capabilities' property
315         final Integer temp = shadeData.capabilities;
316         final int capabilitiesVal = temp != null ? temp.intValue() : -1;
317         Capabilities capabilities = db.getCapabilities(capabilitiesVal);
318         propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
319         propOldVal = properties.getOrDefault(propKey, "");
320         propNewVal = capabilities.toString();
321         if (!propNewVal.equals(propOldVal)) {
322             propChanged = true;
323             getThing().setProperty(propKey, propNewVal);
324             if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) {
325                 db.logCapabilitiesNotInDatabase(type, capabilitiesVal);
326             }
327         }
328
329         if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type)
330                 && (capabilitiesVal != db.getType(type).getCapabilities())) {
331             db.logCapabilitiesMismatch(type, capabilitiesVal);
332         }
333     }
334
335     private void updateFirmwareProperties(ShadeData shadeData) {
336         Map<String, String> properties = editProperties();
337         Firmware shadeFirmware = shadeData.firmware;
338         Firmware motorFirmware = shadeData.motor;
339         if (shadeFirmware != null) {
340             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
341         }
342         if (motorFirmware != null) {
343             properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
344         }
345         updateProperties(properties);
346     }
347
348     /**
349      * After a hard refresh, update the Thing's properties based on the contents of the provided ShadeData.
350      *
351      * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
352      * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
353      * potential need to add support for that type resp. capabilities.
354      *
355      * @param shadeData
356      */
357     private void updateHardProperties(ShadeData shadeData) {
358         final ShadePosition positions = shadeData.positions;
359         if (positions == null) {
360             return;
361         }
362         Capabilities capabilities = getCapabilitiesOrDefault();
363         final Map<String, String> properties = getThing().getProperties();
364
365         // update 'secondary rail detected' property
366         String propKey = HDPowerViewBindingConstants.PROPERTY_SECONDARY_RAIL_DETECTED;
367         String propOldVal = properties.getOrDefault(propKey, "");
368         boolean propNewBool = positions.secondaryRailDetected();
369         String propNewVal = String.valueOf(propNewBool);
370         if (!propNewVal.equals(propOldVal)) {
371             getThing().setProperty(propKey, propNewVal);
372             if (propNewBool != capabilities.supportsSecondary()) {
373                 db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), propNewBool);
374             }
375         }
376
377         // update 'tilt anywhere detected' property
378         propKey = HDPowerViewBindingConstants.PROPERTY_TILT_ANYWHERE_DETECTED;
379         propOldVal = properties.getOrDefault(propKey, "");
380         propNewBool = positions.tiltAnywhereDetected();
381         propNewVal = String.valueOf(propNewBool);
382         if (!propNewVal.equals(propOldVal)) {
383             getThing().setProperty(propKey, propNewVal);
384             if (propNewBool != capabilities.supportsTiltAnywhere()) {
385                 db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), propNewBool);
386             }
387         }
388     }
389
390     private void updatePositionStates(ShadePosition shadePos) {
391         Capabilities capabilities = this.capabilities;
392         if (capabilities == null) {
393             logger.debug("The 'shadeCapabilities' field has not yet been initialized");
394             updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
395             updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
396             updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
397             return;
398         }
399         updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_POSITION));
400         updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_POSITION));
401         updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_POSITION));
402     }
403
404     private void updateBatteryStates(int batteryStatus, double batteryStrength) {
405         updateBatteryLevelStates(batteryStatus);
406         updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
407                 batteryStrength > 0 ? new QuantityType<>(batteryStrength / 10, Units.VOLT) : UnDefType.UNDEF);
408     }
409
410     private void updateBatteryLevelStates(int batteryStatus) {
411         int mappedValue;
412         switch (batteryStatus) {
413             case 1: // Low
414                 mappedValue = 10;
415                 break;
416             case 2: // Medium
417                 mappedValue = 50;
418                 break;
419             case 3: // High
420             case 4: // Plugged in
421                 mappedValue = 100;
422                 break;
423             default: // No status available (0) or invalid
424                 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
425                 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
426                 return;
427         }
428         updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
429         updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
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                     updateHardProperties(shadeData);
535                     break;
536                 case SURVEY:
537                     Survey survey = webTargets.getShadeSurvey(shadeId);
538                     if (survey.surveyData != null) {
539                         logger.debug("Survey response for shade {}: {}", survey.shadeId, survey.toString());
540                     } else {
541                         logger.warn("No response from shade {} survey", shadeId);
542                     }
543                     break;
544                 case BATTERY_LEVEL:
545                     shadeData = webTargets.refreshShadeBatteryLevel(shadeId);
546                     updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
547                     break;
548                 default:
549                     throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
550             }
551         } catch (HubInvalidResponseException e) {
552             Throwable cause = e.getCause();
553             if (cause == null) {
554                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
555             } else {
556                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
557             }
558         } catch (HubMaintenanceException e) {
559             // exceptions are logged in HDPowerViewWebTargets
560         } catch (HubShadeTimeoutException e) {
561             logger.info("Shade {} wireless refresh time out", shadeId);
562         } catch (HubException e) {
563             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
564             // for any ongoing requests. Logging this would only cause confusion.
565             if (!isDisposing) {
566                 logger.warn("Unexpected error: {}", e.getMessage());
567             }
568         }
569     }
570 }