2 * Copyright (c) 2010-2024 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.tado.internal.handler;
15 import static org.openhab.binding.tado.internal.api.TadoApiTypeUtils.terminationConditionTemplateToTerminationCondition;
17 import java.io.IOException;
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.StringJoiner;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.stream.Collectors;
25 import javax.measure.quantity.Temperature;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.tado.internal.CapabilitiesSupport;
30 import org.openhab.binding.tado.internal.TadoBindingConstants;
31 import org.openhab.binding.tado.internal.TadoBindingConstants.FanLevel;
32 import org.openhab.binding.tado.internal.TadoBindingConstants.HorizontalSwing;
33 import org.openhab.binding.tado.internal.TadoBindingConstants.OperationMode;
34 import org.openhab.binding.tado.internal.TadoBindingConstants.TemperatureUnit;
35 import org.openhab.binding.tado.internal.TadoBindingConstants.VerticalSwing;
36 import org.openhab.binding.tado.internal.TadoBindingConstants.ZoneType;
37 import org.openhab.binding.tado.internal.TadoHvacChange;
38 import org.openhab.binding.tado.internal.adapter.TadoZoneStateAdapter;
39 import org.openhab.binding.tado.internal.api.TadoApiTypeUtils;
40 import org.openhab.binding.tado.internal.config.TadoZoneConfig;
41 import org.openhab.binding.tado.swagger.codegen.api.ApiException;
42 import org.openhab.binding.tado.swagger.codegen.api.GsonBuilderFactory;
43 import org.openhab.binding.tado.swagger.codegen.api.model.ACFanLevel;
44 import org.openhab.binding.tado.swagger.codegen.api.model.ACHorizontalSwing;
45 import org.openhab.binding.tado.swagger.codegen.api.model.ACVerticalSwing;
46 import org.openhab.binding.tado.swagger.codegen.api.model.AcMode;
47 import org.openhab.binding.tado.swagger.codegen.api.model.AcModeCapabilities;
48 import org.openhab.binding.tado.swagger.codegen.api.model.CoolingZoneSetting;
49 import org.openhab.binding.tado.swagger.codegen.api.model.GenericZoneCapabilities;
50 import org.openhab.binding.tado.swagger.codegen.api.model.GenericZoneSetting;
51 import org.openhab.binding.tado.swagger.codegen.api.model.Overlay;
52 import org.openhab.binding.tado.swagger.codegen.api.model.OverlayTemplate;
53 import org.openhab.binding.tado.swagger.codegen.api.model.OverlayTerminationCondition;
54 import org.openhab.binding.tado.swagger.codegen.api.model.TadoSystemType;
55 import org.openhab.binding.tado.swagger.codegen.api.model.Zone;
56 import org.openhab.binding.tado.swagger.codegen.api.model.ZoneState;
57 import org.openhab.core.library.types.DecimalType;
58 import org.openhab.core.library.types.OnOffType;
59 import org.openhab.core.library.types.QuantityType;
60 import org.openhab.core.library.types.StringType;
61 import org.openhab.core.library.unit.ImperialUnits;
62 import org.openhab.core.library.unit.SIUnits;
63 import org.openhab.core.thing.Bridge;
64 import org.openhab.core.thing.Channel;
65 import org.openhab.core.thing.ChannelUID;
66 import org.openhab.core.thing.Thing;
67 import org.openhab.core.thing.ThingStatus;
68 import org.openhab.core.thing.ThingStatusDetail;
69 import org.openhab.core.thing.ThingStatusInfo;
70 import org.openhab.core.types.Command;
71 import org.openhab.core.types.RefreshType;
72 import org.openhab.core.types.StateOption;
73 import org.slf4j.Logger;
74 import org.slf4j.LoggerFactory;
76 import com.google.gson.Gson;
79 * The {@link TadoZoneHandler} is responsible for handling commands of zones and update their state.
81 * @author Dennis Frommknecht - Initial contribution
82 * @author Andrew Fiddian-Green - Added Low Battery Alarm, A/C Power and Open Window channels
86 public class TadoZoneHandler extends BaseHomeThingHandler {
87 private Logger logger = LoggerFactory.getLogger(TadoZoneHandler.class);
89 private final TadoStateDescriptionProvider stateDescriptionProvider;
90 private TadoZoneConfig configuration;
92 private @Nullable ScheduledFuture<?> refreshTimer;
93 private @Nullable ScheduledFuture<?> scheduledHvacChange;
94 private @Nullable GenericZoneCapabilities capabilities;
95 private @Nullable TadoHvacChange pendingHvacChange;
97 private boolean disposing = false;
98 private @Nullable Gson gson;
100 public TadoZoneHandler(Thing thing, TadoStateDescriptionProvider stateDescriptionProvider) {
102 this.stateDescriptionProvider = stateDescriptionProvider;
103 configuration = getConfigAs(TadoZoneConfig.class);
106 public long getZoneId() {
107 return configuration.id;
110 public int getFallbackTimerDuration() {
111 return configuration.fallbackTimerDuration;
114 public ZoneType getZoneType() {
115 String zoneTypeStr = thing.getProperties().get(TadoBindingConstants.PROPERTY_ZONE_TYPE);
116 if (zoneTypeStr == null) {
117 throw new IllegalStateException("Zone type not initialized");
119 return ZoneType.valueOf(zoneTypeStr);
122 public OverlayTerminationCondition getDefaultTerminationCondition() throws IOException, ApiException {
123 OverlayTemplate overlayTemplate = getApi().showZoneDefaultOverlay(getHomeId(), getZoneId());
124 logApiTransaction(overlayTemplate, false);
125 return terminationConditionTemplateToTerminationCondition(overlayTemplate.getTerminationCondition());
128 public ZoneState getZoneState() throws IOException, ApiException {
129 ZoneState zoneState = getApi().showZoneState(getHomeId(), getZoneId());
130 logApiTransaction(zoneState, false);
134 public GenericZoneCapabilities getZoneCapabilities() {
135 GenericZoneCapabilities capabilities = this.capabilities;
136 if (capabilities == null) {
137 throw new IllegalStateException("Zone capabilities not initialized");
142 public TemperatureUnit getTemperatureUnit() {
143 return getHomeHandler().getTemperatureUnit();
146 public Overlay setOverlay(Overlay overlay) throws IOException, ApiException {
148 logApiTransaction(overlay, true);
149 Overlay newOverlay = getApi().updateZoneOverlay(getHomeId(), getZoneId(), overlay);
150 logApiTransaction(newOverlay, false);
152 } catch (ApiException e) {
153 if (!logger.isTraceEnabled()) {
154 logger.warn("ApiException sending JSON content:\n{}", convertToJsonString(overlay));
160 public void removeOverlay() throws IOException, ApiException {
161 logger.debug("Removing overlay of home {} and zone {}", getHomeId(), getZoneId());
162 getApi().deleteZoneOverlay(getHomeId(), getZoneId());
166 public void handleCommand(ChannelUID channelUID, Command command) {
167 String id = channelUID.getId();
169 if (command == RefreshType.REFRESH) {
170 updateZoneState(false);
174 synchronized (this) {
175 TadoHvacChange pendingHvacChange = this.pendingHvacChange;
176 if (pendingHvacChange == null) {
177 throw new IllegalStateException("Zone pendingHvacChange not initialized");
181 case TadoBindingConstants.CHANNEL_ZONE_HVAC_MODE:
182 pendingHvacChange.withHvacMode(((StringType) command).toFullString());
183 scheduleHvacChange();
185 case TadoBindingConstants.CHANNEL_ZONE_TARGET_TEMPERATURE:
186 if (command instanceof QuantityType<?>) {
187 @SuppressWarnings("unchecked")
188 QuantityType<Temperature> state = (QuantityType<Temperature>) command;
189 QuantityType<Temperature> stateInTargetUnit = getTemperatureUnit() == TemperatureUnit.FAHRENHEIT
190 ? state.toUnit(ImperialUnits.FAHRENHEIT)
191 : state.toUnit(SIUnits.CELSIUS);
193 if (stateInTargetUnit != null) {
194 pendingHvacChange.withTemperature(stateInTargetUnit.floatValue());
195 scheduleHvacChange();
199 case TadoBindingConstants.CHANNEL_ZONE_SWING:
200 pendingHvacChange.withSwing(((OnOffType) command) == OnOffType.ON);
201 scheduleHvacChange();
203 case TadoBindingConstants.CHANNEL_ZONE_LIGHT:
204 pendingHvacChange.withLight(((OnOffType) command) == OnOffType.ON);
205 scheduleHvacChange();
207 case TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED:
208 pendingHvacChange.withFanSpeed(((StringType) command).toFullString());
209 scheduleHvacChange();
211 case TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL:
212 String fanLevelString = ((StringType) command).toFullString();
213 pendingHvacChange.withFanLevel(FanLevel.valueOf(fanLevelString.toUpperCase()));
214 scheduleHvacChange();
216 case TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING:
217 String horizontalSwingString = ((StringType) command).toFullString();
218 pendingHvacChange.withHorizontalSwing(HorizontalSwing.valueOf(horizontalSwingString.toUpperCase()));
219 scheduleHvacChange();
221 case TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING:
222 String verticalSwingString = ((StringType) command).toFullString();
223 pendingHvacChange.withVerticalSwing(VerticalSwing.valueOf(verticalSwingString.toUpperCase()));
224 scheduleHvacChange();
226 case TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE:
227 String operationMode = ((StringType) command).toFullString();
228 pendingHvacChange.withOperationMode(OperationMode.valueOf(operationMode));
229 scheduleHvacChange();
231 case TadoBindingConstants.CHANNEL_ZONE_TIMER_DURATION:
232 pendingHvacChange.activeForMinutes(((DecimalType) command).intValue());
233 scheduleHvacChange();
240 public void initialize() {
242 configuration = getConfigAs(TadoZoneConfig.class);
243 if (configuration.refreshInterval <= 0) {
244 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Refresh interval of zone "
245 + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
247 } else if (configuration.fallbackTimerDuration <= 0) {
248 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Fallback timer duration of zone "
249 + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
251 } else if (configuration.hvacChangeDebounce <= 0) {
252 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "HVAC change debounce of zone "
253 + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
257 Bridge bridge = getBridge();
258 if (bridge != null) {
259 bridgeStatusChanged(bridge.getStatusInfo());
264 public void dispose() {
266 cancelScheduledZoneStateUpdate();
270 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
271 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
273 Zone zoneDetails = getApi().showZoneDetails(getHomeId(), getZoneId());
274 logApiTransaction(zoneDetails, false);
276 GenericZoneCapabilities capabilities = getApi().showZoneCapabilities(getHomeId(), getZoneId());
277 logApiTransaction(capabilities, false);
279 if (zoneDetails == null || capabilities == null) {
280 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
281 "Can not access zone " + getZoneId() + " of home " + getHomeId());
285 updateProperty(TadoBindingConstants.PROPERTY_ZONE_NAME, zoneDetails.getName());
286 updateProperty(TadoBindingConstants.PROPERTY_ZONE_TYPE, zoneDetails.getType().name());
288 this.capabilities = capabilities;
290 CapabilitiesSupport capabilitiesSupport = new CapabilitiesSupport(capabilities,
291 getHomeHandler().getBatteryChecker().getZone(getZoneId()));
293 updateDynamicChannels(capabilitiesSupport);
294 } catch (IOException | ApiException e) {
295 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
296 "Could not connect to server due to " + e.getMessage());
297 cancelScheduledZoneStateUpdate();
301 scheduleZoneStateUpdate();
302 pendingHvacChange = new TadoHvacChange(getThing());
304 updateStatus(ThingStatus.ONLINE);
306 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
307 cancelScheduledZoneStateUpdate();
311 private void updateZoneState(boolean forceUpdate) {
312 if ((thing.getStatus() != ThingStatus.ONLINE) || disposing) {
316 getHomeHandler().updateHomeState();
318 // No update during HVAC change debounce
319 ScheduledFuture<?> scheduledHvacChange = this.scheduledHvacChange;
320 if (!forceUpdate && scheduledHvacChange != null && !scheduledHvacChange.isDone()) {
325 ZoneState zoneState = getZoneState();
327 logger.debug("Updating state of home {} and zone {}", getHomeId(), getZoneId());
329 TadoZoneStateAdapter state = new TadoZoneStateAdapter(zoneState, getTemperatureUnit());
330 updateState(TadoBindingConstants.CHANNEL_ZONE_CURRENT_TEMPERATURE, state.getInsideTemperature());
331 updateState(TadoBindingConstants.CHANNEL_ZONE_HUMIDITY, state.getHumidity());
333 updateState(TadoBindingConstants.CHANNEL_ZONE_HEATING_POWER, state.getHeatingPower());
334 updateState(TadoBindingConstants.CHANNEL_ZONE_AC_POWER, state.getAcPower());
336 updateState(TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE, state.getOperationMode());
338 updateState(TadoBindingConstants.CHANNEL_ZONE_HVAC_MODE, state.getMode());
339 updateState(TadoBindingConstants.CHANNEL_ZONE_TARGET_TEMPERATURE, state.getTargetTemperature());
340 updateState(TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED, state.getFanSpeed());
341 updateState(TadoBindingConstants.CHANNEL_ZONE_SWING, state.getSwing());
342 updateState(TadoBindingConstants.CHANNEL_ZONE_LIGHT, state.getLight());
343 updateState(TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL, state.getFanLevel());
344 updateState(TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING, state.getHorizontalSwing());
345 updateState(TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING, state.getVerticalSwing());
347 updateState(TadoBindingConstants.CHANNEL_ZONE_TIMER_DURATION, state.getRemainingTimerDuration());
349 updateState(TadoBindingConstants.CHANNEL_ZONE_OVERLAY_EXPIRY, state.getOverlayExpiration());
351 updateState(TadoBindingConstants.CHANNEL_ZONE_OPEN_WINDOW_DETECTED, state.getOpenWindowDetected());
353 updateDynamicStateDescriptions(zoneState);
355 onSuccessfulOperation();
356 } catch (IOException | ApiException e) {
357 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
358 "Could not connect to server due to " + e.getMessage());
361 updateState(TadoBindingConstants.CHANNEL_ZONE_BATTERY_LOW_ALARM,
362 getHomeHandler().getBatteryChecker().getBatteryLowAlarm(getZoneId()));
366 * Update the dynamic state descriptions for any channels which support an unknown sub- range of enumerator setting
367 * values, based on the list of capabilities reported by the respective zone.
369 * Note: currently this only applies to A/C devices that support fanLevel, horizontalSwing, or verticalSwing.
371 * @param zoneState the current zone Thing's state
373 private void updateDynamicStateDescriptions(ZoneState zoneState) {
374 GenericZoneSetting setting = zoneState.getSetting();
375 if (setting.getType() != TadoSystemType.AIR_CONDITIONING) {
379 AcMode acMode = ((CoolingZoneSetting) setting).getMode();
380 AcModeCapabilities acModeCapabilities = acMode == null ? new AcModeCapabilities()
381 : TadoApiTypeUtils.getModeCapabilities(acMode, capabilities);
383 // update the options list of supported fan levels
384 Channel channel = thing.getChannel(TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL);
385 if (channel != null) {
386 List<ACFanLevel> fanLevels = acModeCapabilities.getFanLevel();
387 if (fanLevels != null) {
388 stateDescriptionProvider.setStateOptions(channel.getUID(),
389 fanLevels.stream().map(u -> new StateOption(u.name(), u.name())).collect(Collectors.toList()));
393 // update the options list of supported horizontal swing settings
394 channel = thing.getChannel(TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING);
395 if (channel != null) {
396 List<ACHorizontalSwing> horizontalSwings = acModeCapabilities.getHorizontalSwing();
397 if (horizontalSwings != null) {
398 stateDescriptionProvider.setStateOptions(channel.getUID(), horizontalSwings.stream()
399 .map(u -> new StateOption(u.name(), u.name())).collect(Collectors.toList()));
403 // update the options list of supported vertical swing settings
404 channel = thing.getChannel(TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING);
405 if (channel != null) {
406 List<ACVerticalSwing> verticalSwings = acModeCapabilities.getVerticalSwing();
407 if (verticalSwings != null) {
408 stateDescriptionProvider.setStateOptions(channel.getUID(), verticalSwings.stream()
409 .map(u -> new StateOption(u.name(), u.name())).collect(Collectors.toList()));
414 private void scheduleZoneStateUpdate() {
415 ScheduledFuture<?> refreshTimer = this.refreshTimer;
416 if (refreshTimer == null || refreshTimer.isCancelled()) {
417 this.refreshTimer = scheduler.scheduleWithFixedDelay(new Runnable() {
420 updateZoneState(false);
422 }, 5, configuration.refreshInterval, TimeUnit.SECONDS);
426 private void cancelScheduledZoneStateUpdate() {
427 ScheduledFuture<?> refreshTimer = this.refreshTimer;
428 if (refreshTimer != null) {
429 refreshTimer.cancel(false);
433 private void scheduleHvacChange() {
434 ScheduledFuture<?> scheduledHvacChange = this.scheduledHvacChange;
435 if (scheduledHvacChange != null) {
436 scheduledHvacChange.cancel(false);
438 this.scheduledHvacChange = scheduler.schedule(() -> {
440 synchronized (this) {
441 TadoHvacChange pendingHvacChange = this.pendingHvacChange;
442 this.pendingHvacChange = new TadoHvacChange(getThing());
443 if (pendingHvacChange != null) {
444 pendingHvacChange.apply();
447 } catch (IOException e) {
448 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
449 } catch (ApiException e) {
450 logger.warn("Could not apply HVAC change on home {} and zone {}: {}", getHomeId(), getZoneId(),
453 updateZoneState(true);
455 }, configuration.hvacChangeDebounce, TimeUnit.SECONDS);
459 * Helper method to log an API transaction on the given object.
460 * If the logger level is 'debug', the transaction is simply logged.
461 * If the logger level is 'trace, the object's JSON serial contents are included.
463 * @param object the object to be logged.
464 * @param isCommand marks whether the transaction is a command to, or a response from, the server.
466 private void logApiTransaction(Object object, boolean isCommand) {
467 if (logger.isDebugEnabled() || logger.isTraceEnabled()) {
468 String logType = isCommand ? "command" : "response";
469 if (logger.isTraceEnabled()) {
470 logger.trace("Api {}: homeId:{}, zoneId:{}, objectId:{}, content:\n{}", logType, getHomeId(),
471 getZoneId(), object.getClass().getSimpleName(), convertToJsonString(object));
472 } else if (logger.isDebugEnabled()) {
473 logger.debug("Api {}: homeId:{}, zoneId:{}, objectId:{}", logType, getHomeId(), getZoneId(),
474 object.getClass().getSimpleName());
479 private synchronized String convertToJsonString(Object object) {
480 Gson gson = this.gson;
482 gson = this.gson = GsonBuilderFactory.defaultGsonBuilder().setPrettyPrinting().create();
484 return gson.toJson(object);
488 * If the given channel exists in the thing, but is NOT required in the thing, then add it to a list of channels to
489 * be removed. Or if the channel does NOT exist in the thing, but is required in the thing, then log a warning.
491 * @param removeList the list of channels to be removed from the thing.
492 * @param channelId the id of the channel to be (eventually) removed.
493 * @param channelRequired true if the thing requires this channel.
495 private void removeListProcessChannel(List<Channel> removeList, String channelId, boolean channelRequired) {
496 Channel channel = thing.getChannel(channelId);
497 if (!channelRequired && channel != null) {
498 removeList.add(channel);
499 } else if (channelRequired && channel == null) {
500 logger.warn("Thing {} does not have a '{}' channel => please reinitialize it", thing.getUID(), channelId);
505 * Remove previously statically created channels if the device does not support them.
507 * @param capabilitiesSupport a CapabilitiesSupport instance which summarizes the device's capabilities.
508 * @throws IllegalStateException if any of the channel builders failed.
510 private void updateDynamicChannels(CapabilitiesSupport capabilitiesSupport) {
511 List<Channel> removeList = new ArrayList<>();
513 removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_BATTERY_LOW_ALARM,
514 capabilitiesSupport.batteryLowAlarm());
515 removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_OPEN_WINDOW_DETECTED,
516 capabilitiesSupport.openWindow());
517 removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_LIGHT, capabilitiesSupport.light());
518 removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING,
519 capabilitiesSupport.horizontalSwing());
520 removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING,
521 capabilitiesSupport.verticalSwing());
522 removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_SWING, capabilitiesSupport.swing());
523 removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED,
524 capabilitiesSupport.fanSpeed());
525 removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL,
526 capabilitiesSupport.fanLevel());
527 removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_AC_POWER, capabilitiesSupport.acPower());
528 removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_HEATING_POWER,
529 capabilitiesSupport.heatingPower());
530 removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_HUMIDITY,
531 capabilitiesSupport.humidity());
532 removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_CURRENT_TEMPERATURE,
533 capabilitiesSupport.currentTemperature());
535 if (!removeList.isEmpty()) {
536 if (logger.isDebugEnabled()) {
537 StringJoiner joiner = new StringJoiner(", ");
538 removeList.forEach(c -> joiner.add(c.getUID().getId()));
539 logger.debug("Removing unsupported channels for {}: {}", thing.getUID(), joiner.toString());
541 updateThing(editThing().withoutChannels(removeList).build());