2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.hdpowerview.internal.handler;
15 import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
16 import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
18 import java.util.ArrayList;
19 import java.util.HashMap;
20 import java.util.List;
22 import java.util.StringJoiner;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import javax.ws.rs.NotSupportedException;
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;
66 * Handles commands for an HD PowerView Shade
68 * @author Andy Lintner - Initial contribution
69 * @author Andrew Fiddian-Green - Added support for secondary rail positions
72 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
74 private enum RefreshKind {
80 private static final String COMMAND_CALIBRATE = "CALIBRATE";
81 private static final String COMMAND_IDENTIFY = "IDENTIFY";
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();
87 private final Map<String, String> detectedCapabilities = new HashMap<>();
88 private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
90 private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
91 private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
92 private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
93 private @Nullable Capabilities capabilities;
95 private boolean isDisposing;
97 public HDPowerViewShadeHandler(Thing thing) {
102 public void initialize() {
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");
112 updateStatus(ThingStatus.UNKNOWN);
116 public void dispose() {
117 logger.debug("Disposing shade handler for shade {}", shadeId);
119 ScheduledFuture<?> future = refreshPositionFuture;
120 if (future != null) {
123 refreshPositionFuture = null;
124 future = refreshSignalFuture;
125 if (future != null) {
128 refreshSignalFuture = null;
129 future = refreshBatteryLevelFuture;
130 if (future != null) {
133 refreshBatteryLevelFuture = null;
138 public void handleCommand(ChannelUID channelUID, Command command) {
139 String channelId = channelUID.getId();
141 if (RefreshType.REFRESH == command) {
143 case CHANNEL_SHADE_POSITION:
144 case CHANNEL_SHADE_SECONDARY_POSITION:
145 case CHANNEL_SHADE_VANE:
146 requestRefreshShadePosition();
148 case CHANNEL_SHADE_LOW_BATTERY:
149 case CHANNEL_SHADE_BATTERY_LEVEL:
150 case CHANNEL_SHADE_BATTERY_VOLTAGE:
151 requestRefreshShadeBatteryLevel();
153 case CHANNEL_SHADE_SIGNAL_STRENGTH:
154 case CHANNEL_SHADE_HUB_RSSI:
155 case CHANNEL_SHADE_REPEATER_RSSI:
156 requestRefreshShadeSurvey();
162 HDPowerViewHubHandler bridge = getBridgeHandler();
163 if (bridge == null) {
164 logger.warn("Missing bridge handler");
167 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
169 handleShadeCommand(channelId, command, webTargets, shadeId);
170 } catch (HubInvalidResponseException e) {
171 Throwable cause = e.getCause();
173 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
175 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
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.
185 logger.warn("Unexpected error: {}", e.getMessage());
190 private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
191 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
192 HubShadeTimeoutException {
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);
203 logger.warn("Unexpected StopMoveType command");
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);
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);
225 logger.warn("Unexpected StopMoveType command");
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);
240 logger.warn("Unsupported command: {}. Supported commands are: " + COMMAND_CALIBRATE, command);
247 * Update the state of the channels based on the ShadeData provided.
249 * @param shadeData the ShadeData to be used.
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);
260 updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
261 updateSignalStrengthState(shadeData.signalStrength);
264 private void updateCapabilities(ShadeData shade) {
265 if (capabilities != null) {
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);
274 logger.debug("Caching capabilities {} for shade {}", capabilities.getValue(), shade.id);
275 this.capabilities = capabilities;
277 updateDynamicChannels(capabilities);
280 private Capabilities getCapabilitiesOrDefault() {
281 Capabilities capabilities = this.capabilities;
282 if (capabilities == null) {
283 return new Capabilities();
289 * Update the Thing's properties based on the contents of the provided ShadeData.
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.
296 private void updateSoftProperties(ShadeData shadeData) {
297 final Map<String, String> properties = getThing().getProperties();
298 boolean propChanged = false;
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)) {
307 getThing().setProperty(propKey, propNewVal);
308 if ((type > 0) && !DB.isTypeInDatabase(type)) {
309 DB.logTypeNotInDatabase(type);
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)) {
321 getThing().setProperty(propKey, propNewVal);
322 if ((capabilitiesVal >= 0) && !DB.isCapabilitiesInDatabase(capabilitiesVal)) {
323 DB.logCapabilitiesNotInDatabase(type, capabilitiesVal);
327 if (propChanged && DB.isCapabilitiesInDatabase(capabilitiesVal) && DB.isTypeInDatabase(type)
328 && (capabilitiesVal != DB.getType(type).getCapabilities()) && (shadeData.capabilities != null)) {
329 DB.logCapabilitiesMismatch(type, capabilitiesVal);
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());
340 if (motorFirmware != null) {
341 properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
343 updateProperties(properties);
347 * After a hard refresh, update the Thing's detected capabilities based on the contents of the provided ShadeData.
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.
355 private void updateDetectedCapabilities(ShadeData shadeData) {
356 final ShadePosition positions = shadeData.positions;
357 if (positions == null) {
360 Capabilities capabilities = getCapabilitiesOrDefault();
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);
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);
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);
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));
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);
407 private void updateBatteryLevelStates(int batteryStatus) {
409 switch (batteryStatus) {
417 case 4: // Plugged in
420 default: // No status available (0) or invalid
421 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
422 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
425 updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
426 updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
429 private void updateSignalStrengthState(int signalStrength) {
430 updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(signalStrength));
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();
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);
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();
459 private void identifyShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
460 HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
461 updateShadePositions(webTargets.jogShade(shadeId));
464 private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
465 HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
466 updateShadePositions(webTargets.calibrateShade(shadeId));
469 private void updateShadePositions(ShadeData shadeData) {
470 ShadePosition shadePosition = shadeData.positions;
471 if (shadePosition == null) {
474 updateCapabilities(shadeData);
475 updatePositionStates(shadePosition);
479 * Request that the shade shall undergo a 'hard' refresh for querying its current position
481 protected synchronized void requestRefreshShadePosition() {
482 if (refreshPositionFuture == null) {
483 refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
488 * Request that the shade shall undergo a 'hard' refresh for querying its survey data
490 protected synchronized void requestRefreshShadeSurvey() {
491 if (refreshSignalFuture == null) {
492 refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
497 * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
499 protected synchronized void requestRefreshShadeBatteryLevel() {
500 if (refreshBatteryLevelFuture == null) {
501 refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
505 private void doRefreshShadePosition() {
506 this.doRefreshShade(RefreshKind.POSITION);
507 refreshPositionFuture = null;
510 private void doRefreshShadeSignal() {
511 this.doRefreshShade(RefreshKind.SURVEY);
512 refreshSignalFuture = null;
515 private void doRefreshShadeBatteryLevel() {
516 this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
517 refreshBatteryLevelFuture = null;
520 private void doRefreshShade(RefreshKind kind) {
522 HDPowerViewHubHandler bridge;
523 if ((bridge = getBridgeHandler()) == null) {
524 throw new HubProcessingException("Missing bridge handler");
526 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
530 shadeData = webTargets.refreshShadePosition(shadeId);
531 updateShadePositions(shadeData);
532 updateDetectedCapabilities(shadeData);
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());
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;
549 repeaterRssi = survey.rssi;
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));
557 shadeData = webTargets.getShade(shadeId);
558 updateSignalStrengthState(shadeData.signalStrength);
560 logger.info("No data from shade {} survey", shadeId);
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.
566 updateState(CHANNEL_SHADE_HUB_RSSI, UnDefType.UNDEF);
567 updateState(CHANNEL_SHADE_REPEATER_RSSI, UnDefType.UNDEF);
571 shadeData = webTargets.refreshShadeBatteryLevel(shadeId);
572 updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
575 throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
577 } catch (HubInvalidResponseException e) {
578 Throwable cause = e.getCause();
580 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
582 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
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);
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.
598 logger.warn("Unexpected error: {}", e.getMessage());
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.
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.
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);
621 * Remove previously statically created channels if the shade does not support them.
623 private void updateDynamicChannels(Capabilities capabilities) {
624 List<Channel> removeList = new ArrayList<>();
626 removeListProcessChannel(removeList, CHANNEL_SHADE_POSITION, capabilities.supportsPrimary());
628 removeListProcessChannel(removeList, CHANNEL_SHADE_SECONDARY_POSITION,
629 capabilities.supportsSecondary() || capabilities.supportsSecondaryOverlapped());
631 removeListProcessChannel(removeList, CHANNEL_SHADE_VANE,
632 capabilities.supportsTiltAnywhere() || capabilities.supportsTiltOnClosed());
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());
640 updateThing(editThing().withoutChannels(removeList).build());