]> git.basschouten.com Git - openhab-addons.git/blob
787d98c8e9ca60ab2884a473557dac4383f8ea2e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.dto.CoordinateSystem.*;
17
18 import java.util.ArrayList;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.StringJoiner;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import javax.ws.rs.NotSupportedException;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
31 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
32 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
33 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
34 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
35 import org.openhab.binding.hdpowerview.internal.dto.BatteryKind;
36 import org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem;
37 import org.openhab.binding.hdpowerview.internal.dto.Firmware;
38 import org.openhab.binding.hdpowerview.internal.dto.ShadeData;
39 import org.openhab.binding.hdpowerview.internal.dto.ShadePosition;
40 import org.openhab.binding.hdpowerview.internal.dto.SurveyData;
41 import org.openhab.binding.hdpowerview.internal.exceptions.HubException;
42 import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException;
43 import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException;
44 import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
45 import org.openhab.binding.hdpowerview.internal.exceptions.HubShadeTimeoutException;
46 import org.openhab.core.library.types.DecimalType;
47 import org.openhab.core.library.types.OnOffType;
48 import org.openhab.core.library.types.PercentType;
49 import org.openhab.core.library.types.QuantityType;
50 import org.openhab.core.library.types.StopMoveType;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.library.types.UpDownType;
53 import org.openhab.core.library.unit.Units;
54 import org.openhab.core.thing.Bridge;
55 import org.openhab.core.thing.Channel;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.types.Command;
61 import org.openhab.core.types.RefreshType;
62 import org.openhab.core.types.UnDefType;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66 /**
67  * Handles commands for an HD PowerView Shade
68  *
69  * @author Andy Lintner - Initial contribution
70  * @author Andrew Fiddian-Green - Added support for secondary rail positions
71  */
72 @NonNullByDefault
73 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
74
75     private enum RefreshKind {
76         POSITION,
77         SURVEY,
78         BATTERY_LEVEL
79     }
80
81     private static final String COMMAND_CALIBRATE = "CALIBRATE";
82     private static final String COMMAND_IDENTIFY = "IDENTIFY";
83
84     private static final String DETECTED_SECONDARY_RAIL = "secondaryRailDetected";
85     private static final String DETECTED_TILT_ANYWHERE = "tiltAnywhereDetected";
86     private static final ShadeCapabilitiesDatabase DB = new ShadeCapabilitiesDatabase();
87
88     private final Map<String, String> detectedCapabilities = new HashMap<>();
89     private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
90
91     private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
92     private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
93     private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
94     private @Nullable Capabilities capabilities;
95     private int shadeId;
96     private boolean isDisposing;
97
98     public HDPowerViewShadeHandler(Thing thing) {
99         super(thing);
100     }
101
102     @Override
103     public void initialize() {
104         isDisposing = false;
105         shadeId = getConfigAs(HDPowerViewShadeConfiguration.class).id;
106         logger.debug("Initializing shade handler for shade {}", shadeId);
107         Bridge bridge = getBridge();
108         if (bridge == null) {
109             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
110                     "@text/offline.conf-error.invalid-bridge-handler");
111             return;
112         }
113         updateStatus(ThingStatus.UNKNOWN);
114     }
115
116     @Override
117     public void dispose() {
118         logger.debug("Disposing shade handler for shade {}", shadeId);
119         isDisposing = true;
120         ScheduledFuture<?> future = refreshPositionFuture;
121         if (future != null) {
122             future.cancel(true);
123         }
124         refreshPositionFuture = null;
125         future = refreshSignalFuture;
126         if (future != null) {
127             future.cancel(true);
128         }
129         refreshSignalFuture = null;
130         future = refreshBatteryLevelFuture;
131         if (future != null) {
132             future.cancel(true);
133         }
134         refreshBatteryLevelFuture = null;
135         capabilities = null;
136     }
137
138     @Override
139     public void handleCommand(ChannelUID channelUID, Command command) {
140         String channelId = channelUID.getId();
141
142         if (RefreshType.REFRESH == command) {
143             switch (channelId) {
144                 case CHANNEL_SHADE_POSITION:
145                 case CHANNEL_SHADE_SECONDARY_POSITION:
146                 case CHANNEL_SHADE_VANE:
147                     requestRefreshShadePosition();
148                     break;
149                 case CHANNEL_SHADE_LOW_BATTERY:
150                 case CHANNEL_SHADE_BATTERY_LEVEL:
151                 case CHANNEL_SHADE_BATTERY_VOLTAGE:
152                     requestRefreshShadeBatteryLevel();
153                     break;
154                 case CHANNEL_SHADE_SIGNAL_STRENGTH:
155                 case CHANNEL_SHADE_HUB_RSSI:
156                 case CHANNEL_SHADE_REPEATER_RSSI:
157                     requestRefreshShadeSurvey();
158                     break;
159             }
160             return;
161         }
162
163         HDPowerViewHubHandler bridge = getBridgeHandler();
164         if (bridge == null) {
165             logger.warn("Missing bridge handler");
166             return;
167         }
168         HDPowerViewWebTargets webTargets = bridge.getWebTargets();
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 percentCommand) {
197                     moveShade(PRIMARY_POSITION, percentCommand.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 percentCommand) {
211                     moveShade(VANE_TILT_POSITION, percentCommand.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 percentCommand) {
219                     moveShade(SECONDARY_POSITION, percentCommand.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 stringCommand) {
233                     if (COMMAND_IDENTIFY.equals(stringCommand.toString())) {
234                         logger.debug("Identify shade {}", shadeId);
235                         identifyShade(webTargets, shadeId);
236                     } else if (COMMAND_CALIBRATE.equals(stringCommand.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         updateDynamicChannels(capabilities, shade);
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         Capabilities capabilities = DB.getCapabilities(shadeData.capabilities);
316         final int capabilitiesVal = capabilities.getValue();
317         propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
318         propOldVal = properties.getOrDefault(propKey, "");
319         propNewVal = capabilities.toString();
320         if (!propNewVal.equals(propOldVal)) {
321             propChanged = true;
322             getThing().setProperty(propKey, propNewVal);
323             if ((capabilitiesVal >= 0) && !DB.isCapabilitiesInDatabase(capabilitiesVal)) {
324                 DB.logCapabilitiesNotInDatabase(type, capabilitiesVal);
325             }
326         }
327
328         if (propChanged && DB.isCapabilitiesInDatabase(capabilitiesVal) && DB.isTypeInDatabase(type)
329                 && (capabilitiesVal != DB.getType(type).getCapabilities()) && (shadeData.capabilities != null)) {
330             DB.logCapabilitiesMismatch(type, capabilitiesVal);
331         }
332     }
333
334     private void updateFirmwareProperties(ShadeData shadeData) {
335         Map<String, String> properties = editProperties();
336         Firmware shadeFirmware = shadeData.firmware;
337         Firmware motorFirmware = shadeData.motor;
338         if (shadeFirmware != null) {
339             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
340         }
341         if (motorFirmware != null) {
342             properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
343         }
344         updateProperties(properties);
345     }
346
347     /**
348      * After a hard refresh, update the Thing's detected capabilities based on the contents of the provided ShadeData.
349      *
350      * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
351      * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
352      * potential need to add support for that type resp. capabilities.
353      *
354      * @param shadeData
355      */
356     private void updateDetectedCapabilities(ShadeData shadeData) {
357         final ShadePosition positions = shadeData.positions;
358         if (positions == null) {
359             return;
360         }
361         Capabilities capabilities = getCapabilitiesOrDefault();
362
363         // update 'secondary rail' detected capability
364         String capsKey = DETECTED_SECONDARY_RAIL;
365         String capsOldVal = detectedCapabilities.getOrDefault(capsKey, "");
366         boolean capsNewBool = positions.secondaryRailDetected();
367         String capsNewVal = String.valueOf(capsNewBool);
368         if (!capsNewVal.equals(capsOldVal)) {
369             detectedCapabilities.put(capsKey, capsNewVal);
370             if (capsNewBool != capabilities.supportsSecondary()) {
371                 DB.logPropertyMismatch(capsKey, shadeData.type, capabilities.getValue(), capsNewBool);
372             }
373         }
374
375         // update 'tilt anywhere' detected capability
376         capsKey = DETECTED_TILT_ANYWHERE;
377         capsOldVal = detectedCapabilities.getOrDefault(capsKey, "");
378         capsNewBool = positions.tiltAnywhereDetected();
379         capsNewVal = String.valueOf(capsNewBool);
380         if (!capsNewVal.equals(capsOldVal)) {
381             detectedCapabilities.put(capsKey, capsNewVal);
382             if (capsNewBool != capabilities.supportsTiltAnywhere()) {
383                 DB.logPropertyMismatch(capsKey, shadeData.type, capabilities.getValue(), capsNewBool);
384             }
385         }
386     }
387
388     private void updatePositionStates(ShadePosition shadePos) {
389         Capabilities capabilities = this.capabilities;
390         if (capabilities == null) {
391             logger.debug("The 'shadeCapabilities' field has not yet been initialized");
392             updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
393             updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
394             updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
395             return;
396         }
397         updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_POSITION));
398         updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_POSITION));
399         updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_POSITION));
400     }
401
402     private void updateBatteryStates(int batteryStatus, double batteryStrength) {
403         updateBatteryLevelStates(batteryStatus);
404         updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
405                 batteryStrength > 0 ? new QuantityType<>(batteryStrength / 10, Units.VOLT) : UnDefType.UNDEF);
406     }
407
408     private void updateBatteryLevelStates(int batteryStatus) {
409         int mappedValue;
410         switch (batteryStatus) {
411             case 1: // Low
412                 mappedValue = 10;
413                 break;
414             case 2: // Medium
415                 mappedValue = 50;
416                 break;
417             case 3: // High
418             case 4: // Plugged in
419                 mappedValue = 100;
420                 break;
421             default: // No status available (0) or invalid
422                 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
423                 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
424                 return;
425         }
426         updateState(CHANNEL_SHADE_LOW_BATTERY, OnOffType.from(batteryStatus == 1));
427         updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
428     }
429
430     private void updateSignalStrengthState(int signalStrength) {
431         updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(signalStrength));
432     }
433
434     private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId)
435             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
436             HubShadeTimeoutException {
437         ShadePosition newPosition = null;
438         // (try to) read the positions from the hub
439         ShadeData shadeData = webTargets.getShade(shadeId);
440         updateCapabilities(shadeData);
441         newPosition = shadeData.positions;
442         // if no positions returned, then create a new position
443         if (newPosition == null) {
444             newPosition = new ShadePosition();
445         }
446         Capabilities capabilities = getCapabilitiesOrDefault();
447         // set the new position value, and write the positions to the hub
448         shadeData = webTargets.moveShade(shadeId, newPosition.setPosition(capabilities, coordSys, newPercent));
449         updateShadePositions(shadeData);
450     }
451
452     private void stopShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
453             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
454         updateShadePositions(webTargets.stopShade(shadeId));
455         // Positions in response from stop motion is not updated to to actual positions yet,
456         // so we need to request hard refresh.
457         requestRefreshShadePosition();
458     }
459
460     private void identifyShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
461             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
462         updateShadePositions(webTargets.jogShade(shadeId));
463     }
464
465     private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
466             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
467         updateShadePositions(webTargets.calibrateShade(shadeId));
468     }
469
470     private void updateShadePositions(ShadeData shadeData) {
471         ShadePosition shadePosition = shadeData.positions;
472         if (shadePosition == null) {
473             return;
474         }
475         updateCapabilities(shadeData);
476         updatePositionStates(shadePosition);
477     }
478
479     /**
480      * Request that the shade shall undergo a 'hard' refresh for querying its current position
481      */
482     protected synchronized void requestRefreshShadePosition() {
483         if (refreshPositionFuture == null) {
484             refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
485         }
486     }
487
488     /**
489      * Request that the shade shall undergo a 'hard' refresh for querying its survey data
490      */
491     protected synchronized void requestRefreshShadeSurvey() {
492         if (refreshSignalFuture == null) {
493             refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
494         }
495     }
496
497     /**
498      * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
499      */
500     protected synchronized void requestRefreshShadeBatteryLevel() {
501         if (refreshBatteryLevelFuture == null) {
502             refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
503         }
504     }
505
506     private void doRefreshShadePosition() {
507         this.doRefreshShade(RefreshKind.POSITION);
508         refreshPositionFuture = null;
509     }
510
511     private void doRefreshShadeSignal() {
512         this.doRefreshShade(RefreshKind.SURVEY);
513         refreshSignalFuture = null;
514     }
515
516     private void doRefreshShadeBatteryLevel() {
517         this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
518         refreshBatteryLevelFuture = null;
519     }
520
521     private void doRefreshShade(RefreshKind kind) {
522         try {
523             HDPowerViewHubHandler bridge;
524             if ((bridge = getBridgeHandler()) == null) {
525                 throw new HubProcessingException("Missing bridge handler");
526             }
527             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
528             ShadeData shadeData;
529             switch (kind) {
530                 case POSITION:
531                     shadeData = webTargets.refreshShadePosition(shadeId);
532                     updateShadePositions(shadeData);
533                     updateDetectedCapabilities(shadeData);
534                     break;
535                 case SURVEY:
536                     List<SurveyData> surveyData = webTargets.getShadeSurvey(shadeId);
537                     if (!surveyData.isEmpty()) {
538                         if (logger.isDebugEnabled()) {
539                             StringJoiner joiner = new StringJoiner(", ");
540                             surveyData.forEach(data -> joiner.add(data.toString()));
541                             logger.debug("Survey response for shade {}: {}", shadeId, joiner.toString());
542                         }
543
544                         int hubRssi = Integer.MAX_VALUE;
545                         int repeaterRssi = Integer.MAX_VALUE;
546                         for (SurveyData survey : surveyData) {
547                             if (survey.neighborId == 0) {
548                                 hubRssi = survey.rssi;
549                             } else {
550                                 repeaterRssi = survey.rssi;
551                             }
552                         }
553                         updateState(CHANNEL_SHADE_HUB_RSSI, hubRssi == Integer.MAX_VALUE ? UnDefType.UNDEF
554                                 : new QuantityType<>(hubRssi, Units.DECIBEL_MILLIWATTS));
555                         updateState(CHANNEL_SHADE_REPEATER_RSSI, repeaterRssi == Integer.MAX_VALUE ? UnDefType.UNDEF
556                                 : new QuantityType<>(repeaterRssi, Units.DECIBEL_MILLIWATTS));
557
558                         shadeData = webTargets.getShade(shadeId);
559                         updateSignalStrengthState(shadeData.signalStrength);
560                     } else {
561                         logger.info("No data from shade {} survey", shadeId);
562                         /*
563                          * Setting signal strength channel to UNDEF here would be reverted on next poll,
564                          * since signal strength is part of shade response. So leaving current value,
565                          * even though refreshing the value failed.
566                          */
567                         updateState(CHANNEL_SHADE_HUB_RSSI, UnDefType.UNDEF);
568                         updateState(CHANNEL_SHADE_REPEATER_RSSI, UnDefType.UNDEF);
569                     }
570                     break;
571                 case BATTERY_LEVEL:
572                     shadeData = webTargets.refreshShadeBatteryLevel(shadeId);
573                     updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
574                     break;
575                 default:
576                     throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
577             }
578         } catch (HubInvalidResponseException e) {
579             Throwable cause = e.getCause();
580             if (cause == null) {
581                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
582             } else {
583                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
584             }
585             // Survey calls are unreliable and often returns "{}" as payload. For repeater RSSI tracking to be useful,
586             // we need to reset channels also in this case.
587             if (kind == RefreshKind.SURVEY) {
588                 updateState(CHANNEL_SHADE_HUB_RSSI, UnDefType.UNDEF);
589                 updateState(CHANNEL_SHADE_REPEATER_RSSI, UnDefType.UNDEF);
590             }
591         } catch (HubMaintenanceException e) {
592             // exceptions are logged in HDPowerViewWebTargets
593         } catch (HubShadeTimeoutException e) {
594             logger.info("Shade {} wireless refresh time out", shadeId);
595         } catch (HubException e) {
596             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
597             // for any ongoing requests. Logging this would only cause confusion.
598             if (!isDisposing) {
599                 logger.warn("Unexpected error: {}", e.getMessage());
600             }
601         }
602     }
603
604     /**
605      * If the given channel exists in the thing, but is NOT required in the thing, then add it to a list of channels to
606      * be removed. Or if the channel does NOT exist in the thing, but is required in the thing, then log a warning.
607      *
608      * @param removeList the list of channels to be removed from the thing.
609      * @param channelId the id of the channel to be (eventually) removed.
610      * @param channelRequired true if the thing requires this channel.
611      */
612     private void removeListProcessChannel(List<Channel> removeList, String channelId, boolean channelRequired) {
613         Channel channel = thing.getChannel(channelId);
614         if (!channelRequired && channel != null) {
615             removeList.add(channel);
616         } else if (channelRequired && channel == null) {
617             logger.warn("Shade {} does not have a '{}' channel => please reinitialize the thing", shadeId, channelId);
618         }
619     }
620
621     /**
622      * Remove previously statically created channels if the shade does not support them or they are not relevant.
623      *
624      * @param capabilities the capabilities of the shade.
625      * @param shade the shade data.
626      */
627     private void updateDynamicChannels(Capabilities capabilities, ShadeData shade) {
628         List<Channel> removeList = new ArrayList<>();
629
630         removeListProcessChannel(removeList, CHANNEL_SHADE_POSITION, capabilities.supportsPrimary());
631
632         removeListProcessChannel(removeList, CHANNEL_SHADE_SECONDARY_POSITION,
633                 capabilities.supportsSecondary() || capabilities.supportsSecondaryOverlapped());
634
635         removeListProcessChannel(removeList, CHANNEL_SHADE_VANE,
636                 capabilities.supportsTiltAnywhere() || capabilities.supportsTiltOnClosed());
637
638         boolean batteryChannelsRequired = shade.getBatteryKind() != BatteryKind.HARDWIRED_POWER_SUPPLY;
639         removeListProcessChannel(removeList, CHANNEL_SHADE_BATTERY_LEVEL, batteryChannelsRequired);
640         removeListProcessChannel(removeList, CHANNEL_SHADE_LOW_BATTERY, batteryChannelsRequired);
641
642         if (!removeList.isEmpty()) {
643             if (logger.isDebugEnabled()) {
644                 StringJoiner joiner = new StringJoiner(", ");
645                 removeList.forEach(c -> joiner.add(c.getUID().getId()));
646                 logger.debug("Removing unsupported channels for {}: {}", shadeId, joiner.toString());
647             }
648             updateThing(editThing().withoutChannels(removeList).build());
649         }
650     }
651 }