]> git.basschouten.com Git - openhab-addons.git/blob
fb0d76e643da5176fb692cd5b1b50b53a7784ea8
[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.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.api.CoordinateSystem;
33 import org.openhab.binding.hdpowerview.internal.api.Firmware;
34 import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
35 import org.openhab.binding.hdpowerview.internal.api.SurveyData;
36 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
37 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
38 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
39 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
40 import org.openhab.binding.hdpowerview.internal.exceptions.HubException;
41 import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException;
42 import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException;
43 import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
44 import org.openhab.binding.hdpowerview.internal.exceptions.HubShadeTimeoutException;
45 import org.openhab.core.library.types.DecimalType;
46 import org.openhab.core.library.types.OnOffType;
47 import org.openhab.core.library.types.PercentType;
48 import org.openhab.core.library.types.QuantityType;
49 import org.openhab.core.library.types.StopMoveType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.library.types.UpDownType;
52 import org.openhab.core.library.unit.Units;
53 import org.openhab.core.thing.Bridge;
54 import org.openhab.core.thing.Channel;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.thing.Thing;
57 import org.openhab.core.thing.ThingStatus;
58 import org.openhab.core.thing.ThingStatusDetail;
59 import org.openhab.core.types.Command;
60 import org.openhab.core.types.RefreshType;
61 import org.openhab.core.types.UnDefType;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 /**
66  * Handles commands for an HD PowerView Shade
67  *
68  * @author Andy Lintner - Initial contribution
69  * @author Andrew Fiddian-Green - Added support for secondary rail positions
70  */
71 @NonNullByDefault
72 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
73
74     private enum RefreshKind {
75         POSITION,
76         SURVEY,
77         BATTERY_LEVEL
78     }
79
80     private static final String COMMAND_CALIBRATE = "CALIBRATE";
81     private static final String COMMAND_IDENTIFY = "IDENTIFY";
82
83     private static final String DETECTED_SECONDARY_RAIL = "secondaryRailDetected";
84     private static final String DETECTED_TILT_ANYWHERE = "tiltAnywhereDetected";
85     private static final ShadeCapabilitiesDatabase DB = new ShadeCapabilitiesDatabase();
86
87     private final Map<String, String> detectedCapabilities = new HashMap<>();
88     private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
89
90     private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
91     private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
92     private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
93     private @Nullable Capabilities capabilities;
94     private int shadeId;
95     private boolean isDisposing;
96
97     public HDPowerViewShadeHandler(Thing thing) {
98         super(thing);
99     }
100
101     @Override
102     public void initialize() {
103         isDisposing = false;
104         shadeId = getConfigAs(HDPowerViewShadeConfiguration.class).id;
105         logger.debug("Initializing shade handler for shade {}", shadeId);
106         Bridge bridge = getBridge();
107         if (bridge == null) {
108             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
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                 case CHANNEL_SHADE_HUB_RSSI:
155                 case CHANNEL_SHADE_REPEATER_RSSI:
156                     requestRefreshShadeSurvey();
157                     break;
158             }
159             return;
160         }
161
162         HDPowerViewHubHandler bridge = getBridgeHandler();
163         if (bridge == null) {
164             logger.warn("Missing bridge handler");
165             return;
166         }
167         HDPowerViewWebTargets webTargets = bridge.getWebTargets();
168         try {
169             handleShadeCommand(channelId, command, webTargets, shadeId);
170         } catch (HubInvalidResponseException e) {
171             Throwable cause = e.getCause();
172             if (cause == null) {
173                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
174             } else {
175                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
176             }
177         } catch (HubMaintenanceException e) {
178             // exceptions are logged in HDPowerViewWebTargets
179         } catch (HubShadeTimeoutException e) {
180             logger.warn("Shade {} timeout when sending command {}", shadeId, command);
181         } catch (HubException e) {
182             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
183             // for any ongoing requests. Logging this would only cause confusion.
184             if (!isDisposing) {
185                 logger.warn("Unexpected error: {}", e.getMessage());
186             }
187         }
188     }
189
190     private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
191             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
192             HubShadeTimeoutException {
193         switch (channelId) {
194             case CHANNEL_SHADE_POSITION:
195                 if (command instanceof PercentType) {
196                     moveShade(PRIMARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
197                 } else if (command instanceof UpDownType) {
198                     moveShade(PRIMARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
199                 } else if (command instanceof StopMoveType) {
200                     if (StopMoveType.STOP == command) {
201                         stopShade(webTargets, shadeId);
202                     } else {
203                         logger.warn("Unexpected StopMoveType command");
204                     }
205                 }
206                 break;
207
208             case CHANNEL_SHADE_VANE:
209                 if (command instanceof PercentType) {
210                     moveShade(VANE_TILT_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
211                 } else if (command instanceof OnOffType) {
212                     moveShade(VANE_TILT_POSITION, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
213                 }
214                 break;
215
216             case CHANNEL_SHADE_SECONDARY_POSITION:
217                 if (command instanceof PercentType) {
218                     moveShade(SECONDARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
219                 } else if (command instanceof UpDownType) {
220                     moveShade(SECONDARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
221                 } else if (command instanceof StopMoveType) {
222                     if (StopMoveType.STOP == command) {
223                         stopShade(webTargets, shadeId);
224                     } else {
225                         logger.warn("Unexpected StopMoveType command");
226                     }
227                 }
228                 break;
229
230             case CHANNEL_SHADE_COMMAND:
231                 if (command instanceof StringType) {
232                     if (COMMAND_IDENTIFY.equals(((StringType) command).toString())) {
233                         logger.debug("Identify shade {}", shadeId);
234                         identifyShade(webTargets, shadeId);
235                     } else if (COMMAND_CALIBRATE.equals(((StringType) command).toString())) {
236                         logger.debug("Calibrate shade {}", shadeId);
237                         calibrateShade(webTargets, shadeId);
238                     }
239                 } else {
240                     logger.warn("Unsupported command: {}. Supported commands are: " + COMMAND_CALIBRATE, command);
241                 }
242                 break;
243         }
244     }
245
246     /**
247      * Update the state of the channels based on the ShadeData provided.
248      *
249      * @param shadeData the ShadeData to be used.
250      */
251     protected void onReceiveUpdate(ShadeData shadeData) {
252         updateStatus(ThingStatus.ONLINE);
253         updateCapabilities(shadeData);
254         updateSoftProperties(shadeData);
255         updateFirmwareProperties(shadeData);
256         ShadePosition shadePosition = shadeData.positions;
257         if (shadePosition != null) {
258             updatePositionStates(shadePosition);
259         }
260         updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
261         updateSignalStrengthState(shadeData.signalStrength);
262     }
263
264     private void updateCapabilities(ShadeData shade) {
265         if (capabilities != null) {
266             // Already cached.
267             return;
268         }
269         Capabilities capabilities = DB.getCapabilities(shade.type, shade.capabilities);
270         if (capabilities.getValue() < 0) {
271             logger.debug("Unable to set capabilities for shade {}", shade.id);
272             return;
273         }
274         logger.debug("Caching capabilities {} for shade {}", capabilities.getValue(), shade.id);
275         this.capabilities = capabilities;
276
277         updateDynamicChannels(capabilities);
278     }
279
280     private Capabilities getCapabilitiesOrDefault() {
281         Capabilities capabilities = this.capabilities;
282         if (capabilities == null) {
283             return new Capabilities();
284         }
285         return capabilities;
286     }
287
288     /**
289      * Update the Thing's properties based on the contents of the provided ShadeData.
290      *
291      * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
292      * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
293      *
294      * @param shadeData
295      */
296     private void updateSoftProperties(ShadeData shadeData) {
297         final Map<String, String> properties = getThing().getProperties();
298         boolean propChanged = false;
299
300         // update 'type' property
301         final int type = shadeData.type;
302         String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
303         String propOldVal = properties.getOrDefault(propKey, "");
304         String propNewVal = DB.getType(type).toString();
305         if (!propNewVal.equals(propOldVal)) {
306             propChanged = true;
307             getThing().setProperty(propKey, propNewVal);
308             if ((type > 0) && !DB.isTypeInDatabase(type)) {
309                 DB.logTypeNotInDatabase(type);
310             }
311         }
312
313         // update 'capabilities' property
314         Capabilities capabilities = DB.getCapabilities(shadeData.capabilities);
315         final int capabilitiesVal = capabilities.getValue();
316         propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
317         propOldVal = properties.getOrDefault(propKey, "");
318         propNewVal = capabilities.toString();
319         if (!propNewVal.equals(propOldVal)) {
320             propChanged = true;
321             getThing().setProperty(propKey, propNewVal);
322             if ((capabilitiesVal >= 0) && !DB.isCapabilitiesInDatabase(capabilitiesVal)) {
323                 DB.logCapabilitiesNotInDatabase(type, capabilitiesVal);
324             }
325         }
326
327         if (propChanged && DB.isCapabilitiesInDatabase(capabilitiesVal) && DB.isTypeInDatabase(type)
328                 && (capabilitiesVal != DB.getType(type).getCapabilities()) && (shadeData.capabilities != null)) {
329             DB.logCapabilitiesMismatch(type, capabilitiesVal);
330         }
331     }
332
333     private void updateFirmwareProperties(ShadeData shadeData) {
334         Map<String, String> properties = editProperties();
335         Firmware shadeFirmware = shadeData.firmware;
336         Firmware motorFirmware = shadeData.motor;
337         if (shadeFirmware != null) {
338             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
339         }
340         if (motorFirmware != null) {
341             properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
342         }
343         updateProperties(properties);
344     }
345
346     /**
347      * After a hard refresh, update the Thing's detected capabilities based on the contents of the provided ShadeData.
348      *
349      * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
350      * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
351      * potential need to add support for that type resp. capabilities.
352      *
353      * @param shadeData
354      */
355     private void updateDetectedCapabilities(ShadeData shadeData) {
356         final ShadePosition positions = shadeData.positions;
357         if (positions == null) {
358             return;
359         }
360         Capabilities capabilities = getCapabilitiesOrDefault();
361
362         // update 'secondary rail' detected capability
363         String capsKey = DETECTED_SECONDARY_RAIL;
364         String capsOldVal = detectedCapabilities.getOrDefault(capsKey, "");
365         boolean capsNewBool = positions.secondaryRailDetected();
366         String capsNewVal = String.valueOf(capsNewBool);
367         if (!capsNewVal.equals(capsOldVal)) {
368             detectedCapabilities.put(capsKey, capsNewVal);
369             if (capsNewBool != capabilities.supportsSecondary()) {
370                 DB.logPropertyMismatch(capsKey, shadeData.type, capabilities.getValue(), capsNewBool);
371             }
372         }
373
374         // update 'tilt anywhere' detected capability
375         capsKey = DETECTED_TILT_ANYWHERE;
376         capsOldVal = detectedCapabilities.getOrDefault(capsKey, "");
377         capsNewBool = positions.tiltAnywhereDetected();
378         capsNewVal = String.valueOf(capsNewBool);
379         if (!capsNewVal.equals(capsOldVal)) {
380             detectedCapabilities.put(capsKey, capsNewVal);
381             if (capsNewBool != capabilities.supportsTiltAnywhere()) {
382                 DB.logPropertyMismatch(capsKey, shadeData.type, capabilities.getValue(), capsNewBool);
383             }
384         }
385     }
386
387     private void updatePositionStates(ShadePosition shadePos) {
388         Capabilities capabilities = this.capabilities;
389         if (capabilities == null) {
390             logger.debug("The 'shadeCapabilities' field has not yet been initialized");
391             updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
392             updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
393             updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
394             return;
395         }
396         updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_POSITION));
397         updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_POSITION));
398         updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_POSITION));
399     }
400
401     private void updateBatteryStates(int batteryStatus, double batteryStrength) {
402         updateBatteryLevelStates(batteryStatus);
403         updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
404                 batteryStrength > 0 ? new QuantityType<>(batteryStrength / 10, Units.VOLT) : UnDefType.UNDEF);
405     }
406
407     private void updateBatteryLevelStates(int batteryStatus) {
408         int mappedValue;
409         switch (batteryStatus) {
410             case 1: // Low
411                 mappedValue = 10;
412                 break;
413             case 2: // Medium
414                 mappedValue = 50;
415                 break;
416             case 3: // High
417             case 4: // Plugged in
418                 mappedValue = 100;
419                 break;
420             default: // No status available (0) or invalid
421                 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
422                 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
423                 return;
424         }
425         updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
426         updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
427     }
428
429     private void updateSignalStrengthState(int signalStrength) {
430         updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(signalStrength));
431     }
432
433     private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId)
434             throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
435             HubShadeTimeoutException {
436         ShadePosition newPosition = null;
437         // (try to) read the positions from the hub
438         ShadeData shadeData = webTargets.getShade(shadeId);
439         updateCapabilities(shadeData);
440         newPosition = shadeData.positions;
441         // if no positions returned, then create a new position
442         if (newPosition == null) {
443             newPosition = new ShadePosition();
444         }
445         Capabilities capabilities = getCapabilitiesOrDefault();
446         // set the new position value, and write the positions to the hub
447         shadeData = webTargets.moveShade(shadeId, newPosition.setPosition(capabilities, coordSys, newPercent));
448         updateShadePositions(shadeData);
449     }
450
451     private void stopShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
452             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
453         updateShadePositions(webTargets.stopShade(shadeId));
454         // Positions in response from stop motion is not updated to to actual positions yet,
455         // so we need to request hard refresh.
456         requestRefreshShadePosition();
457     }
458
459     private void identifyShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
460             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
461         updateShadePositions(webTargets.jogShade(shadeId));
462     }
463
464     private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
465             HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
466         updateShadePositions(webTargets.calibrateShade(shadeId));
467     }
468
469     private void updateShadePositions(ShadeData shadeData) {
470         ShadePosition shadePosition = shadeData.positions;
471         if (shadePosition == null) {
472             return;
473         }
474         updateCapabilities(shadeData);
475         updatePositionStates(shadePosition);
476     }
477
478     /**
479      * Request that the shade shall undergo a 'hard' refresh for querying its current position
480      */
481     protected synchronized void requestRefreshShadePosition() {
482         if (refreshPositionFuture == null) {
483             refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
484         }
485     }
486
487     /**
488      * Request that the shade shall undergo a 'hard' refresh for querying its survey data
489      */
490     protected synchronized void requestRefreshShadeSurvey() {
491         if (refreshSignalFuture == null) {
492             refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
493         }
494     }
495
496     /**
497      * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
498      */
499     protected synchronized void requestRefreshShadeBatteryLevel() {
500         if (refreshBatteryLevelFuture == null) {
501             refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
502         }
503     }
504
505     private void doRefreshShadePosition() {
506         this.doRefreshShade(RefreshKind.POSITION);
507         refreshPositionFuture = null;
508     }
509
510     private void doRefreshShadeSignal() {
511         this.doRefreshShade(RefreshKind.SURVEY);
512         refreshSignalFuture = null;
513     }
514
515     private void doRefreshShadeBatteryLevel() {
516         this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
517         refreshBatteryLevelFuture = null;
518     }
519
520     private void doRefreshShade(RefreshKind kind) {
521         try {
522             HDPowerViewHubHandler bridge;
523             if ((bridge = getBridgeHandler()) == null) {
524                 throw new HubProcessingException("Missing bridge handler");
525             }
526             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
527             ShadeData shadeData;
528             switch (kind) {
529                 case POSITION:
530                     shadeData = webTargets.refreshShadePosition(shadeId);
531                     updateShadePositions(shadeData);
532                     updateDetectedCapabilities(shadeData);
533                     break;
534                 case SURVEY:
535                     List<SurveyData> surveyData = webTargets.getShadeSurvey(shadeId);
536                     if (!surveyData.isEmpty()) {
537                         if (logger.isDebugEnabled()) {
538                             StringJoiner joiner = new StringJoiner(", ");
539                             surveyData.forEach(data -> joiner.add(data.toString()));
540                             logger.debug("Survey response for shade {}: {}", shadeId, joiner.toString());
541                         }
542
543                         int hubRssi = Integer.MAX_VALUE;
544                         int repeaterRssi = Integer.MAX_VALUE;
545                         for (SurveyData survey : surveyData) {
546                             if (survey.neighborId == 0) {
547                                 hubRssi = survey.rssi;
548                             } else {
549                                 repeaterRssi = survey.rssi;
550                             }
551                         }
552                         updateState(CHANNEL_SHADE_HUB_RSSI, hubRssi == Integer.MAX_VALUE ? UnDefType.UNDEF
553                                 : new QuantityType<>(hubRssi, Units.DECIBEL_MILLIWATTS));
554                         updateState(CHANNEL_SHADE_REPEATER_RSSI, repeaterRssi == Integer.MAX_VALUE ? UnDefType.UNDEF
555                                 : new QuantityType<>(repeaterRssi, Units.DECIBEL_MILLIWATTS));
556
557                         shadeData = webTargets.getShade(shadeId);
558                         updateSignalStrengthState(shadeData.signalStrength);
559                     } else {
560                         logger.info("No data from shade {} survey", shadeId);
561                         /*
562                          * Setting signal strength channel to UNDEF here would be reverted on next poll,
563                          * since signal strength is part of shade response. So leaving current value,
564                          * even though refreshing the value failed.
565                          */
566                         updateState(CHANNEL_SHADE_HUB_RSSI, UnDefType.UNDEF);
567                         updateState(CHANNEL_SHADE_REPEATER_RSSI, UnDefType.UNDEF);
568                     }
569                     break;
570                 case BATTERY_LEVEL:
571                     shadeData = webTargets.refreshShadeBatteryLevel(shadeId);
572                     updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
573                     break;
574                 default:
575                     throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
576             }
577         } catch (HubInvalidResponseException e) {
578             Throwable cause = e.getCause();
579             if (cause == null) {
580                 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
581             } else {
582                 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
583             }
584             // Survey calls are unreliable and often returns "{}" as payload. For repeater RSSI tracking to be useful,
585             // we need to reset channels also in this case.
586             if (kind == RefreshKind.SURVEY) {
587                 updateState(CHANNEL_SHADE_HUB_RSSI, UnDefType.UNDEF);
588                 updateState(CHANNEL_SHADE_REPEATER_RSSI, UnDefType.UNDEF);
589             }
590         } catch (HubMaintenanceException e) {
591             // exceptions are logged in HDPowerViewWebTargets
592         } catch (HubShadeTimeoutException e) {
593             logger.info("Shade {} wireless refresh time out", shadeId);
594         } catch (HubException e) {
595             // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
596             // for any ongoing requests. Logging this would only cause confusion.
597             if (!isDisposing) {
598                 logger.warn("Unexpected error: {}", e.getMessage());
599             }
600         }
601     }
602
603     /**
604      * If the given channel exists in the thing, but is NOT required in the thing, then add it to a list of channels to
605      * be removed. Or if the channel does NOT exist in the thing, but is required in the thing, then log a warning.
606      *
607      * @param removeList the list of channels to be removed from the thing.
608      * @param channelId the id of the channel to be (eventually) removed.
609      * @param channelRequired true if the thing requires this channel.
610      */
611     private void removeListProcessChannel(List<Channel> removeList, String channelId, boolean channelRequired) {
612         Channel channel = thing.getChannel(channelId);
613         if (!channelRequired && channel != null) {
614             removeList.add(channel);
615         } else if (channelRequired && channel == null) {
616             logger.warn("Shade {} does not have a '{}' channel => please reinitialize the thing", shadeId, channelId);
617         }
618     }
619
620     /**
621      * Remove previously statically created channels if the shade does not support them.
622      */
623     private void updateDynamicChannels(Capabilities capabilities) {
624         List<Channel> removeList = new ArrayList<>();
625
626         removeListProcessChannel(removeList, CHANNEL_SHADE_POSITION, capabilities.supportsPrimary());
627
628         removeListProcessChannel(removeList, CHANNEL_SHADE_SECONDARY_POSITION,
629                 capabilities.supportsSecondary() || capabilities.supportsSecondaryOverlapped());
630
631         removeListProcessChannel(removeList, CHANNEL_SHADE_VANE,
632                 capabilities.supportsTiltAnywhere() || capabilities.supportsTiltOnClosed());
633
634         if (!removeList.isEmpty()) {
635             if (logger.isDebugEnabled()) {
636                 StringJoiner joiner = new StringJoiner(", ");
637                 removeList.forEach(c -> joiner.add(c.getUID().getId()));
638                 logger.debug("Removing unsupported channels for {}: {}", shadeId, joiner.toString());
639             }
640             updateThing(editThing().withoutChannels(removeList).build());
641         }
642     }
643 }