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