]> git.basschouten.com Git - openhab-addons.git/blob
e6e370a273768a0e77712d44cd3a2c1f74f3717d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.homeconnect.internal.handler;
14
15 import static java.util.Collections.emptyList;
16 import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
17 import static org.openhab.binding.homeconnect.internal.client.model.EventType.*;
18 import static org.openhab.core.library.unit.ImperialUnits.FAHRENHEIT;
19 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
20 import static org.openhab.core.library.unit.Units.*;
21 import static org.openhab.core.thing.ThingStatus.*;
22
23 import java.time.Duration;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Optional;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.atomic.AtomicBoolean;
32 import java.util.function.Function;
33 import java.util.stream.Collectors;
34
35 import javax.measure.UnconvertibleException;
36 import javax.measure.Unit;
37 import javax.measure.quantity.Temperature;
38
39 import org.eclipse.jdt.annotation.NonNullByDefault;
40 import org.eclipse.jdt.annotation.Nullable;
41 import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
42 import org.openhab.binding.homeconnect.internal.client.HomeConnectEventSourceClient;
43 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
44 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
45 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
46 import org.openhab.binding.homeconnect.internal.client.listener.HomeConnectEventListener;
47 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
48 import org.openhab.binding.homeconnect.internal.client.model.Data;
49 import org.openhab.binding.homeconnect.internal.client.model.Event;
50 import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
51 import org.openhab.binding.homeconnect.internal.client.model.Option;
52 import org.openhab.binding.homeconnect.internal.client.model.Program;
53 import org.openhab.binding.homeconnect.internal.handler.cache.ExpiringStateMap;
54 import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
55 import org.openhab.core.auth.client.oauth2.OAuthException;
56 import org.openhab.core.library.types.DecimalType;
57 import org.openhab.core.library.types.HSBType;
58 import org.openhab.core.library.types.IncreaseDecreaseType;
59 import org.openhab.core.library.types.OnOffType;
60 import org.openhab.core.library.types.OpenClosedType;
61 import org.openhab.core.library.types.PercentType;
62 import org.openhab.core.library.types.QuantityType;
63 import org.openhab.core.library.types.StringType;
64 import org.openhab.core.library.unit.ImperialUnits;
65 import org.openhab.core.library.unit.SIUnits;
66 import org.openhab.core.thing.Bridge;
67 import org.openhab.core.thing.Channel;
68 import org.openhab.core.thing.ChannelUID;
69 import org.openhab.core.thing.Thing;
70 import org.openhab.core.thing.ThingStatusDetail;
71 import org.openhab.core.thing.ThingStatusInfo;
72 import org.openhab.core.thing.binding.BaseThingHandler;
73 import org.openhab.core.thing.binding.BridgeHandler;
74 import org.openhab.core.types.Command;
75 import org.openhab.core.types.RefreshType;
76 import org.openhab.core.types.StateOption;
77 import org.openhab.core.types.UnDefType;
78 import org.slf4j.Logger;
79 import org.slf4j.LoggerFactory;
80
81 /**
82  * The {@link AbstractHomeConnectThingHandler} is responsible for handling commands, which are
83  * sent to one of the channels.
84  *
85  * @author Jonas Brüstel - Initial contribution
86  */
87 @NonNullByDefault
88 public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler implements HomeConnectEventListener {
89
90     private static final int CACHE_TTL_SEC = 2;
91     private static final int OFFLINE_MONITOR_1_DELAY_MIN = 30;
92     private static final int OFFLINE_MONITOR_2_DELAY_MIN = 4;
93     private static final int EVENT_LISTENER_CONNECT_RETRY_DELAY_MIN = 10;
94
95     private @Nullable String operationState;
96     private @Nullable ScheduledFuture<?> reinitializationFuture1;
97     private @Nullable ScheduledFuture<?> reinitializationFuture2;
98     private @Nullable ScheduledFuture<?> reinitializationFuture3;
99     private boolean ignoreEventSourceClosedEvent;
100     private @Nullable String programOptionsDelayedUpdate;
101
102     private final ConcurrentHashMap<String, EventHandler> eventHandlers;
103     private final ConcurrentHashMap<String, ChannelUpdateHandler> channelUpdateHandlers;
104     private final HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
105     private final ExpiringStateMap expiringStateMap;
106     private final AtomicBoolean accessible;
107     private final Logger logger = LoggerFactory.getLogger(AbstractHomeConnectThingHandler.class);
108     private final Map<String, List<AvailableProgramOption>> availableProgramOptionsCache;
109
110     public AbstractHomeConnectThingHandler(Thing thing,
111             HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
112         super(thing);
113         eventHandlers = new ConcurrentHashMap<>();
114         channelUpdateHandlers = new ConcurrentHashMap<>();
115         this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider;
116         expiringStateMap = new ExpiringStateMap(Duration.ofSeconds(CACHE_TTL_SEC));
117         accessible = new AtomicBoolean(false);
118         availableProgramOptionsCache = new ConcurrentHashMap<>();
119
120         configureEventHandlers(eventHandlers);
121         configureChannelUpdateHandlers(channelUpdateHandlers);
122     }
123
124     @Override
125     public void initialize() {
126         if (getBridgeHandler().isEmpty()) {
127             updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
128             accessible.set(false);
129         } else if (isBridgeOffline()) {
130             updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
131             accessible.set(false);
132         } else {
133             updateStatus(UNKNOWN);
134             scheduler.submit(() -> {
135                 refreshThingStatus(); // set ONLINE / OFFLINE
136                 updateSelectedProgramStateDescription();
137                 updateChannels();
138                 registerEventListener();
139                 scheduleOfflineMonitor1();
140                 scheduleOfflineMonitor2();
141             });
142         }
143     }
144
145     @Override
146     public void dispose() {
147         stopRetryRegistering();
148         stopOfflineMonitor1();
149         stopOfflineMonitor2();
150         unregisterEventListener(true);
151     }
152
153     @Override
154     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
155         logger.debug("Bridge status changed to {} ({}). haId={}", bridgeStatusInfo, getThingLabel(), getThingHaId());
156         reinitialize();
157     }
158
159     private void reinitialize() {
160         logger.debug("Reinitialize thing handler ({}). haId={}", getThingLabel(), getThingHaId());
161         stopRetryRegistering();
162         stopOfflineMonitor1();
163         stopOfflineMonitor2();
164         unregisterEventListener();
165         initialize();
166     }
167
168     /**
169      * Handles a command for a given channel.
170      * <p>
171      * This method is only called, if the thing has been initialized (status ONLINE/OFFLINE/UNKNOWN).
172      * <p>
173      *
174      * @param channelUID the {@link ChannelUID} of the channel to which the command was sent
175      * @param command the {@link Command}
176      * @param apiClient the {@link HomeConnectApiClient}
177      * @throws CommunicationException communication problem
178      * @throws AuthorizationException authorization problem
179      * @throws ApplianceOfflineException appliance offline
180      */
181     protected void handleCommand(ChannelUID channelUID, Command command, HomeConnectApiClient apiClient)
182             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
183         if (command instanceof RefreshType) {
184             updateChannel(channelUID);
185         } else if (command instanceof StringType && CHANNEL_BASIC_ACTIONS_STATE.equals(channelUID.getId())
186                 && getBridgeHandler().isPresent()) {
187             updateState(channelUID, new StringType(""));
188
189             if (COMMAND_START.equalsIgnoreCase(command.toFullString())) {
190                 HomeConnectBridgeHandler homeConnectBridgeHandler = getBridgeHandler().get();
191                 // workaround for api bug
192                 // if simulator, program options have to be passed along with the desired program
193                 // if non simulator, some options throw a "SDK.Error.UnsupportedOption" error
194                 if (homeConnectBridgeHandler.getConfiguration().isSimulator()) {
195                     apiClient.startSelectedProgram(getThingHaId());
196                 } else {
197                     Program selectedProgram = apiClient.getSelectedProgram(getThingHaId());
198                     if (selectedProgram != null) {
199                         apiClient.startProgram(getThingHaId(), selectedProgram.getKey());
200                     }
201                 }
202             } else if (COMMAND_STOP.equalsIgnoreCase(command.toFullString())) {
203                 apiClient.stopProgram(getThingHaId());
204             } else if (COMMAND_SELECTED.equalsIgnoreCase(command.toFullString())) {
205                 apiClient.getSelectedProgram(getThingHaId());
206             } else {
207                 logger.debug("Start custom program. command={} haId={}", command.toFullString(), getThingHaId());
208                 apiClient.startCustomProgram(getThingHaId(), command.toFullString());
209             }
210         } else if (command instanceof StringType && CHANNEL_SELECTED_PROGRAM_STATE.equals(channelUID.getId())) {
211             apiClient.setSelectedProgram(getThingHaId(), command.toFullString());
212         }
213     }
214
215     @Override
216     public final void handleCommand(ChannelUID channelUID, Command command) {
217         var apiClient = getApiClient();
218         if ((isThingReadyToHandleCommand() || (this instanceof HomeConnectHoodHandler && isBridgeOnline()
219                 && isThingAccessibleViaServerSentEvents())) && apiClient.isPresent()) {
220             logger.debug("Handle \"{}\" command ({}). haId={}", command, channelUID.getId(), getThingHaId());
221             try {
222                 handleCommand(channelUID, command, apiClient.get());
223             } catch (ApplianceOfflineException e) {
224                 logger.debug("Could not handle command {}. Appliance offline. thing={}, haId={}, error={}",
225                         command.toFullString(), getThingLabel(), getThingHaId(), e.getMessage());
226                 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
227                 resetChannelsOnOfflineEvent();
228                 resetProgramStateChannels(true);
229             } catch (CommunicationException e) {
230                 logger.debug("Could not handle command {}. API communication problem! error={}, haId={}",
231                         command.toFullString(), e.getMessage(), getThingHaId());
232             } catch (AuthorizationException e) {
233                 logger.debug("Could not handle command {}. Authorization problem! error={}, haId={}",
234                         command.toFullString(), e.getMessage(), getThingHaId());
235
236                 handleAuthenticationError(e);
237             }
238         }
239     }
240
241     @Override
242     public void onEvent(Event event) {
243         if (DISCONNECTED.equals(event.getType())) {
244             logger.debug("Received DISCONNECTED event. Set {} to OFFLINE. haId={}", getThing().getLabel(),
245                     getThingHaId());
246             updateStatus(OFFLINE);
247             resetChannelsOnOfflineEvent();
248             resetProgramStateChannels(true);
249         } else if (isThingOnline() && CONNECTED.equals(event.getType())) {
250             logger.debug("Received CONNECTED event. Update power state channel. haId={}", getThingHaId());
251             getThingChannel(CHANNEL_POWER_STATE).ifPresent(c -> updateChannel(c.getUID()));
252         } else if (isThingOffline() && !KEEP_ALIVE.equals(event.getType())) {
253             updateStatus(ONLINE);
254             logger.debug("Set {} to ONLINE and update channels. haId={}", getThing().getLabel(), getThingHaId());
255             updateSelectedProgramStateDescription();
256             updateChannels();
257         }
258
259         String key = event.getKey();
260         if (EVENT_OPERATION_STATE.equals(key)) {
261             operationState = event.getValue() == null ? null : event.getValue();
262         }
263
264         if (key != null && eventHandlers.containsKey(key)) {
265             EventHandler eventHandler = eventHandlers.get(key);
266             if (eventHandler != null) {
267                 eventHandler.handle(event);
268             }
269         }
270
271         accessible.set(true);
272     }
273
274     @Override
275     public void onClosed() {
276         if (ignoreEventSourceClosedEvent) {
277             logger.debug("Ignoring event source close event. thing={}, haId={}", getThing().getLabel(), getThingHaId());
278         } else {
279             unregisterEventListener();
280             refreshThingStatus();
281             registerEventListener();
282         }
283     }
284
285     @Override
286     public void onRateLimitReached() {
287         unregisterEventListener();
288
289         // retry registering
290         scheduleRetryRegistering();
291     }
292
293     /**
294      * Register event listener.
295      */
296     protected void registerEventListener() {
297         if (isBridgeOnline() && isThingAccessibleViaServerSentEvents()) {
298             getEventSourceClient().ifPresent(client -> {
299                 try {
300                     ignoreEventSourceClosedEvent = false;
301                     client.registerEventListener(getThingHaId(), this);
302                 } catch (CommunicationException | AuthorizationException e) {
303                     logger.warn("Could not open event source connection. thing={}, haId={}, error={}", getThingLabel(),
304                             getThingHaId(), e.getMessage());
305                 }
306             });
307         }
308     }
309
310     /**
311      * Unregister event listener.
312      */
313     protected void unregisterEventListener() {
314         unregisterEventListener(false);
315     }
316
317     private void unregisterEventListener(boolean immediate) {
318         getEventSourceClient().ifPresent(client -> {
319             ignoreEventSourceClosedEvent = true;
320             client.unregisterEventListener(this, immediate, false);
321         });
322     }
323
324     /**
325      * Get {@link HomeConnectApiClient}.
326      *
327      * @return client instance
328      */
329     protected Optional<HomeConnectApiClient> getApiClient() {
330         return getBridgeHandler().map(HomeConnectBridgeHandler::getApiClient);
331     }
332
333     /**
334      * Get {@link HomeConnectEventSourceClient}.
335      *
336      * @return client instance if present
337      */
338     protected Optional<HomeConnectEventSourceClient> getEventSourceClient() {
339         return getBridgeHandler().map(HomeConnectBridgeHandler::getEventSourceClient);
340     }
341
342     /**
343      * Update state description of selected program (Fetch programs via API).
344      */
345     protected void updateSelectedProgramStateDescription() {
346         if (isBridgeOffline() || isThingOffline()) {
347             return;
348         }
349
350         Optional<HomeConnectApiClient> apiClient = getApiClient();
351         if (apiClient.isPresent()) {
352             try {
353                 List<StateOption> stateOptions = apiClient.get().getPrograms(getThingHaId()).stream()
354                         .map(p -> new StateOption(p.getKey(), mapStringType(p.getKey()))).collect(Collectors.toList());
355
356                 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(
357                         channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions));
358             } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
359                 logger.debug("Could not fetch available programs. thing={}, haId={}, error={}", getThingLabel(),
360                         getThingHaId(), e.getMessage());
361                 removeSelectedProgramStateDescription();
362             }
363         } else {
364             removeSelectedProgramStateDescription();
365         }
366     }
367
368     /**
369      * Remove state description of selected program.
370      */
371     protected void removeSelectedProgramStateDescription() {
372         getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
373                 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList()));
374     }
375
376     /**
377      * Is thing ready to process commands. If bridge or thing itself is offline commands will be ignored.
378      *
379      * @return true if ready
380      */
381     protected boolean isThingReadyToHandleCommand() {
382         if (isBridgeOffline()) {
383             logger.debug("Bridge is OFFLINE. Ignore command. thing={}, haId={}", getThingLabel(), getThingHaId());
384             return false;
385         }
386
387         if (isThingOffline()) {
388             logger.debug("{} is OFFLINE. Ignore command. haId={}", getThing().getLabel(), getThingHaId());
389             return false;
390         }
391
392         return true;
393     }
394
395     /**
396      * Checks if bridge is online and set.
397      *
398      * @return true if online
399      */
400     protected boolean isBridgeOnline() {
401         Bridge bridge = getBridge();
402         return bridge != null && ONLINE.equals(bridge.getStatus());
403     }
404
405     /**
406      * Checks if bridge is offline or not set.
407      *
408      * @return true if offline
409      */
410     protected boolean isBridgeOffline() {
411         return !isBridgeOnline();
412     }
413
414     /**
415      * Checks if thing is online.
416      *
417      * @return true if online
418      */
419     protected boolean isThingOnline() {
420         return ONLINE.equals(getThing().getStatus());
421     }
422
423     /**
424      * Checks if thing is connected to the cloud and accessible via SSE.
425      *
426      * @return true if yes
427      */
428     public boolean isThingAccessibleViaServerSentEvents() {
429         return accessible.get();
430     }
431
432     /**
433      * Checks if thing is offline.
434      *
435      * @return true if offline
436      */
437     protected boolean isThingOffline() {
438         return !isThingOnline();
439     }
440
441     /**
442      * Get {@link HomeConnectBridgeHandler}.
443      *
444      * @return bridge handler
445      */
446     protected Optional<HomeConnectBridgeHandler> getBridgeHandler() {
447         Bridge bridge = getBridge();
448         if (bridge != null) {
449             BridgeHandler bridgeHandler = bridge.getHandler();
450             if (bridgeHandler instanceof HomeConnectBridgeHandler) {
451                 return Optional.of((HomeConnectBridgeHandler) bridgeHandler);
452             }
453         }
454         return Optional.empty();
455     }
456
457     /**
458      * Get thing channel by given channel id.
459      *
460      * @param channelId channel id
461      * @return channel
462      */
463     protected Optional<Channel> getThingChannel(String channelId) {
464         Channel channel = getThing().getChannel(channelId);
465         if (channel == null) {
466             return Optional.empty();
467         } else {
468             return Optional.of(channel);
469         }
470     }
471
472     /**
473      * Configure channel update handlers. Classes which extend {@link AbstractHomeConnectThingHandler} must implement
474      * this class and add handlers.
475      *
476      * @param handlers channel update handlers
477      */
478     protected abstract void configureChannelUpdateHandlers(final Map<String, ChannelUpdateHandler> handlers);
479
480     /**
481      * Configure event handlers. Classes which extend {@link AbstractHomeConnectThingHandler} must implement
482      * this class and add handlers.
483      *
484      * @param handlers Server-Sent-Event handlers
485      */
486     protected abstract void configureEventHandlers(final Map<String, EventHandler> handlers);
487
488     /**
489      * Update all channels via API.
490      *
491      */
492     protected void updateChannels() {
493         if (isBridgeOffline()) {
494             logger.debug("Bridge handler not found or offline. Stopping update of channels. thing={}, haId={}",
495                     getThingLabel(), getThingHaId());
496         } else if (isThingOffline()) {
497             logger.debug("{} offline. Stopping update of channels. haId={}", getThing().getLabel(), getThingHaId());
498         } else {
499             List<Channel> channels = getThing().getChannels();
500             for (Channel channel : channels) {
501                 updateChannel(channel.getUID());
502             }
503         }
504     }
505
506     /**
507      * Update Channel values via API.
508      *
509      * @param channelUID channel UID
510      */
511     protected void updateChannel(ChannelUID channelUID) {
512         if (!getApiClient().isPresent()) {
513             logger.error("Cannot update channel. No instance of api client found! thing={}, haId={}", getThingLabel(),
514                     getThingHaId());
515             return;
516         }
517
518         if (!isThingReadyToHandleCommand()) {
519             return;
520         }
521
522         if ((isLinked(channelUID) || CHANNEL_OPERATION_STATE.equals(channelUID.getId())) // always update operation
523                 // state channel
524                 && channelUpdateHandlers.containsKey(channelUID.getId())) {
525             try {
526                 ChannelUpdateHandler channelUpdateHandler = channelUpdateHandlers.get(channelUID.getId());
527                 if (channelUpdateHandler != null) {
528                     channelUpdateHandler.handle(channelUID, expiringStateMap);
529                 }
530             } catch (ApplianceOfflineException e) {
531                 logger.debug(
532                         "API communication problem while trying to update! Appliance offline. thing={}, haId={}, error={}",
533                         getThingLabel(), getThingHaId(), e.getMessage());
534                 updateStatus(OFFLINE);
535                 resetChannelsOnOfflineEvent();
536                 resetProgramStateChannels(true);
537             } catch (CommunicationException e) {
538                 logger.debug("API communication problem while trying to update! thing={}, haId={}, error={}",
539                         getThingLabel(), getThingHaId(), e.getMessage());
540             } catch (AuthorizationException e) {
541                 logger.debug("Authentication problem while trying to update! thing={}, haId={}", getThingLabel(),
542                         getThingHaId(), e);
543                 handleAuthenticationError(e);
544             }
545         }
546     }
547
548     /**
549      * Reset program related channels.
550      *
551      * @param offline true if the device is considered as OFFLINE
552      */
553     protected void resetProgramStateChannels(boolean offline) {
554         logger.debug("Resetting active program channel states. thing={}, haId={}", getThingLabel(), getThingHaId());
555     }
556
557     /**
558      * Reset all channels on OFFLINE event.
559      */
560     protected void resetChannelsOnOfflineEvent() {
561         logger.debug("Resetting channel states due to OFFLINE event. thing={}, haId={}", getThingLabel(),
562                 getThingHaId());
563         getThingChannel(CHANNEL_POWER_STATE).ifPresent(channel -> updateState(channel.getUID(), OnOffType.OFF));
564         getThingChannel(CHANNEL_OPERATION_STATE).ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
565         getThingChannel(CHANNEL_DOOR_STATE).ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
566         getThingChannel(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE)
567                 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
568         getThingChannel(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE)
569                 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
570         getThingChannel(CHANNEL_REMOTE_START_ALLOWANCE_STATE)
571                 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
572         getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
573                 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
574     }
575
576     /**
577      * Map Home Connect key and value names to label.
578      * e.g. Dishcare.Dishwasher.Program.Eco50 --> Eco50 or BSH.Common.EnumType.OperationState.DelayedStart --> Delayed
579      * Start
580      *
581      * @param type type
582      * @return human readable label
583      */
584     protected String mapStringType(String type) {
585         int index = type.lastIndexOf(".");
586         if (index > 0 && type.length() > index) {
587             String sub = type.substring(index + 1);
588             StringBuilder sb = new StringBuilder();
589             for (String word : sub.split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])")) {
590                 sb.append(" ");
591                 sb.append(word);
592             }
593             return sb.toString().trim();
594         }
595         return type;
596     }
597
598     /**
599      * Map Home Connect stage value to label.
600      * e.g. Cooking.Hood.EnumType.IntensiveStage.IntensiveStage1 --> 1
601      *
602      * @param stage stage
603      * @return human readable label
604      */
605     protected String mapStageStringType(String stage) {
606         switch (stage) {
607             case STAGE_FAN_OFF:
608             case STAGE_INTENSIVE_STAGE_OFF:
609                 stage = "Off";
610                 break;
611             case STAGE_FAN_STAGE_01:
612             case STAGE_INTENSIVE_STAGE_1:
613                 stage = "1";
614                 break;
615             case STAGE_FAN_STAGE_02:
616             case STAGE_INTENSIVE_STAGE_2:
617                 stage = "2";
618                 break;
619             case STAGE_FAN_STAGE_03:
620                 stage = "3";
621                 break;
622             case STAGE_FAN_STAGE_04:
623                 stage = "4";
624                 break;
625             case STAGE_FAN_STAGE_05:
626                 stage = "5";
627                 break;
628             default:
629                 stage = mapStringType(stage);
630         }
631
632         return stage;
633     }
634
635     /**
636      * Map unit string (returned by home connect api) to Unit
637      *
638      * @param unit String eg. "°C"
639      * @return Unit
640      */
641     protected Unit<Temperature> mapTemperature(@Nullable String unit) {
642         if (unit == null) {
643             return CELSIUS;
644         } else if (unit.endsWith("C")) {
645             return CELSIUS;
646         } else {
647             return FAHRENHEIT;
648         }
649     }
650
651     /**
652      * Map hex representation of color to HSB type.
653      *
654      * @param colorCode color code e.g. #001122
655      * @return HSB type
656      */
657     protected HSBType mapColor(String colorCode) {
658         HSBType color = HSBType.WHITE;
659
660         if (colorCode.length() == 7) {
661             int r = Integer.valueOf(colorCode.substring(1, 3), 16);
662             int g = Integer.valueOf(colorCode.substring(3, 5), 16);
663             int b = Integer.valueOf(colorCode.substring(5, 7), 16);
664             color = HSBType.fromRGB(r, g, b);
665         }
666         return color;
667     }
668
669     /**
670      * Map HSB color type to hex representation.
671      *
672      * @param color HSB color
673      * @return color code e.g. #001122
674      */
675     protected String mapColor(HSBType color) {
676         String redValue = String.format("%02X", (int) (color.getRed().floatValue() * 2.55));
677         String greenValue = String.format("%02X", (int) (color.getGreen().floatValue() * 2.55));
678         String blueValue = String.format("%02X", (int) (color.getBlue().floatValue() * 2.55));
679         return "#" + redValue + greenValue + blueValue;
680     }
681
682     /**
683      * Check bridge status and refresh connection status of thing accordingly.
684      */
685     protected void refreshThingStatus() {
686         Optional<HomeConnectApiClient> apiClient = getApiClient();
687
688         apiClient.ifPresent(client -> {
689             try {
690                 HomeAppliance homeAppliance = client.getHomeAppliance(getThingHaId());
691                 if (!homeAppliance.isConnected()) {
692                     updateStatus(OFFLINE);
693                 } else {
694                     updateStatus(ONLINE);
695                 }
696                 accessible.set(true);
697             } catch (CommunicationException e) {
698                 logger.debug(
699                         "Update status to OFFLINE. Home Connect service is not reachable or a problem occurred!  thing={}, haId={}, error={}.",
700                         getThingLabel(), getThingHaId(), e.getMessage());
701                 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
702                         "Home Connect service is not reachable or a problem occurred! (" + e.getMessage() + ").");
703                 accessible.set(false);
704             } catch (AuthorizationException e) {
705                 logger.debug(
706                         "Update status to OFFLINE. Home Connect service is not reachable or a problem occurred!  thing={}, haId={}, error={}",
707                         getThingLabel(), getThingHaId(), e.getMessage());
708                 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
709                         "Home Connect service is not reachable or a problem occurred! (" + e.getMessage() + ").");
710                 accessible.set(false);
711                 handleAuthenticationError(e);
712             }
713         });
714         if (apiClient.isEmpty()) {
715             updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
716             accessible.set(false);
717         }
718     }
719
720     /**
721      * Get home appliance id of Thing.
722      *
723      * @return home appliance id
724      */
725     public String getThingHaId() {
726         return getThing().getConfiguration().get(HA_ID).toString();
727     }
728
729     /**
730      * Returns the human readable label for this thing.
731      *
732      * @return the human readable label
733      */
734     protected @Nullable String getThingLabel() {
735         return getThing().getLabel();
736     }
737
738     /**
739      * Handle authentication exception.
740      */
741     protected void handleAuthenticationError(AuthorizationException exception) {
742         if (isBridgeOnline()) {
743             logger.debug(
744                     "Thing handler threw authentication exception --> clear credential storage thing={}, haId={} error={}",
745                     getThingLabel(), getThingHaId(), exception.getMessage());
746
747             getBridgeHandler().ifPresent(homeConnectBridgeHandler -> {
748                 try {
749                     homeConnectBridgeHandler.getOAuthClientService().remove();
750                     homeConnectBridgeHandler.reinitialize();
751                 } catch (OAuthException e) {
752                     // client is already closed --> we can ignore it
753                 }
754             });
755         }
756     }
757
758     /**
759      * Get operation state of device.
760      *
761      * @return operation state string
762      */
763     protected @Nullable String getOperationState() {
764         return operationState;
765     }
766
767     protected EventHandler defaultElapsedProgramTimeEventHandler() {
768         return event -> getThingChannel(CHANNEL_ELAPSED_PROGRAM_TIME)
769                 .ifPresent(channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND)));
770     }
771
772     protected EventHandler defaultPowerStateEventHandler() {
773         return event -> {
774             getThingChannel(CHANNEL_POWER_STATE).ifPresent(
775                     channel -> updateState(channel.getUID(), OnOffType.from(STATE_POWER_ON.equals(event.getValue()))));
776
777             if (STATE_POWER_ON.equals(event.getValue())) {
778                 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID()));
779             } else {
780                 resetProgramStateChannels(true);
781                 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
782                         .ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
783             }
784         };
785     }
786
787     protected EventHandler defaultDoorStateEventHandler() {
788         return event -> getThingChannel(CHANNEL_DOOR_STATE).ifPresent(channel -> updateState(channel.getUID(),
789                 STATE_DOOR_OPEN.equals(event.getValue()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED));
790     }
791
792     protected EventHandler defaultOperationStateEventHandler() {
793         return event -> {
794             String value = event.getValue();
795             getThingChannel(CHANNEL_OPERATION_STATE).ifPresent(channel -> updateState(channel.getUID(),
796                     value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
797
798             if (STATE_OPERATION_FINISHED.equals(event.getValue())) {
799                 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
800                         .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(100, PERCENT)));
801                 getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
802                         .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, SECOND)));
803             } else if (STATE_OPERATION_RUN.equals(event.getValue())) {
804                 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
805                         .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, PERCENT)));
806                 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID()));
807             } else if (STATE_OPERATION_READY.equals(event.getValue())) {
808                 resetProgramStateChannels(false);
809             }
810         };
811     }
812
813     protected EventHandler defaultActiveProgramEventHandler() {
814         return event -> {
815             String value = event.getValue();
816             getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(channel -> updateState(channel.getUID(),
817                     value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
818             if (event.getValue() == null) {
819                 resetProgramStateChannels(false);
820             }
821         };
822     }
823
824     protected EventHandler defaultEventPresentStateEventHandler(String channelId) {
825         return event -> getThingChannel(channelId).ifPresent(channel -> updateState(channel.getUID(),
826                 OnOffType.from(!STATE_EVENT_PRESENT_STATE_OFF.equals(event.getValue()))));
827     }
828
829     protected EventHandler defaultBooleanEventHandler(String channelId) {
830         return event -> getThingChannel(channelId)
831                 .ifPresent(channel -> updateState(channel.getUID(), OnOffType.from(event.getValueAsBoolean())));
832     }
833
834     protected EventHandler defaultRemainingProgramTimeEventHandler() {
835         return event -> getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
836                 .ifPresent(channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND)));
837     }
838
839     protected EventHandler defaultSelectedProgramStateEventHandler() {
840         return event -> getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
841                 .ifPresent(channel -> updateState(channel.getUID(),
842                         event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue())));
843     }
844
845     protected EventHandler defaultAmbientLightColorStateEventHandler() {
846         return event -> getThingChannel(CHANNEL_AMBIENT_LIGHT_COLOR_STATE)
847                 .ifPresent(channel -> updateState(channel.getUID(),
848                         event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue())));
849     }
850
851     protected EventHandler defaultAmbientLightCustomColorStateEventHandler() {
852         return event -> getThingChannel(CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE).ifPresent(channel -> {
853             String value = event.getValue();
854             if (value != null) {
855                 updateState(channel.getUID(), mapColor(value));
856             } else {
857                 updateState(channel.getUID(), UnDefType.UNDEF);
858             }
859         });
860     }
861
862     protected EventHandler updateRemoteControlActiveAndProgramOptionsStateEventHandler() {
863         return event -> {
864             defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE).handle(event);
865
866             // update available program options if update was previously delayed and remote control is enabled
867             try {
868                 String programKey = programOptionsDelayedUpdate;
869                 if (programKey != null && Boolean.parseBoolean(event.getValue())) {
870                     logger.debug("Delayed update of options for program {}", programKey);
871                     updateProgramOptionsStateDescriptions(programKey, null);
872                     programOptionsDelayedUpdate = null;
873                 }
874             } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
875                 logger.debug("Could not update program options. {}", e.getMessage());
876             }
877         };
878     }
879
880     protected EventHandler updateProgramOptionsAndSelectedProgramStateEventHandler() {
881         return event -> {
882             defaultSelectedProgramStateEventHandler().handle(event);
883
884             // update available program options
885             try {
886                 Optional<HomeConnectApiClient> apiClient = getApiClient();
887                 String programKey = event.getValue();
888                 if (apiClient.isPresent() && programKey != null) {
889                     // Delay the update if options are not yet cached and remote control is disabled
890                     if (availableProgramOptionsCache.get(programKey) == null
891                             && !apiClient.get().isRemoteControlActive(getThingHaId())) {
892                         logger.debug("Delay update of options for program {}", programKey);
893                         programOptionsDelayedUpdate = programKey;
894                     } else {
895                         updateProgramOptionsStateDescriptions(programKey, null);
896                     }
897                 }
898             } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
899                 logger.debug("Could not update program options. {}", e.getMessage());
900             }
901         };
902     }
903
904     protected EventHandler defaultPercentQuantityTypeEventHandler(String channelId) {
905         return event -> getThingChannel(channelId).ifPresent(
906                 channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), PERCENT)));
907     }
908
909     protected EventHandler defaultPercentHandler(String channelId) {
910         return event -> getThingChannel(channelId)
911                 .ifPresent(channel -> updateState(channel.getUID(), new PercentType(event.getValueAsInt())));
912     }
913
914     protected ChannelUpdateHandler defaultDoorStateChannelUpdateHandler() {
915         return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
916             Optional<HomeConnectApiClient> apiClient = getApiClient();
917             if (apiClient.isPresent()) {
918                 Data data = apiClient.get().getDoorState(getThingHaId());
919                 if (data.getValue() != null) {
920                     return STATE_DOOR_OPEN.equals(data.getValue()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
921                 } else {
922                     return UnDefType.UNDEF;
923                 }
924             } else {
925                 return UnDefType.UNDEF;
926             }
927         }));
928     }
929
930     protected ChannelUpdateHandler defaultPowerStateChannelUpdateHandler() {
931         return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
932             Optional<HomeConnectApiClient> apiClient = getApiClient();
933             if (apiClient.isPresent()) {
934                 Data data = apiClient.get().getPowerState(getThingHaId());
935                 if (data.getValue() != null) {
936                     return OnOffType.from(STATE_POWER_ON.equals(data.getValue()));
937                 } else {
938                     return UnDefType.UNDEF;
939                 }
940             } else {
941                 return UnDefType.UNDEF;
942             }
943         }));
944     }
945
946     protected ChannelUpdateHandler defaultAmbientLightChannelUpdateHandler() {
947         return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
948             Optional<HomeConnectApiClient> apiClient = getApiClient();
949             if (apiClient.isPresent()) {
950                 Data data = apiClient.get().getAmbientLightState(getThingHaId());
951                 if (data.getValue() != null) {
952                     boolean enabled = data.getValueAsBoolean();
953                     if (enabled) {
954                         // brightness
955                         Data brightnessData = apiClient.get().getAmbientLightBrightnessState(getThingHaId());
956                         getThingChannel(CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE)
957                                 .ifPresent(channel -> updateState(channel.getUID(),
958                                         new PercentType(brightnessData.getValueAsInt())));
959
960                         // color
961                         Data colorData = apiClient.get().getAmbientLightColorState(getThingHaId());
962                         getThingChannel(CHANNEL_AMBIENT_LIGHT_COLOR_STATE).ifPresent(
963                                 channel -> updateState(channel.getUID(), new StringType(colorData.getValue())));
964
965                         // custom color
966                         Data customColorData = apiClient.get().getAmbientLightCustomColorState(getThingHaId());
967                         getThingChannel(CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE).ifPresent(channel -> {
968                             String value = customColorData.getValue();
969                             if (value != null) {
970                                 updateState(channel.getUID(), mapColor(value));
971                             } else {
972                                 updateState(channel.getUID(), UnDefType.UNDEF);
973                             }
974                         });
975
976                     }
977                     return OnOffType.from(enabled);
978                 } else {
979                     return UnDefType.UNDEF;
980                 }
981             } else {
982                 return UnDefType.UNDEF;
983             }
984         }));
985     }
986
987     protected ChannelUpdateHandler defaultNoOpUpdateHandler() {
988         return (channelUID, cache) -> updateState(channelUID, UnDefType.UNDEF);
989     }
990
991     protected ChannelUpdateHandler defaultOperationStateChannelUpdateHandler() {
992         return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
993             Optional<HomeConnectApiClient> apiClient = getApiClient();
994             if (apiClient.isPresent()) {
995                 Data data = apiClient.get().getOperationState(getThingHaId());
996
997                 String value = data.getValue();
998                 if (value != null) {
999                     operationState = data.getValue();
1000                     return new StringType(mapStringType(value));
1001                 } else {
1002                     operationState = null;
1003                     return UnDefType.UNDEF;
1004                 }
1005             } else {
1006                 return UnDefType.UNDEF;
1007             }
1008         }));
1009     }
1010
1011     protected ChannelUpdateHandler defaultRemoteControlActiveStateChannelUpdateHandler() {
1012         return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1013             Optional<HomeConnectApiClient> apiClient = getApiClient();
1014             if (apiClient.isPresent()) {
1015                 return OnOffType.from(apiClient.get().isRemoteControlActive(getThingHaId()));
1016             }
1017             return OnOffType.OFF;
1018         }));
1019     }
1020
1021     protected ChannelUpdateHandler defaultLocalControlActiveStateChannelUpdateHandler() {
1022         return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1023             Optional<HomeConnectApiClient> apiClient = getApiClient();
1024             if (apiClient.isPresent()) {
1025                 return OnOffType.from(apiClient.get().isLocalControlActive(getThingHaId()));
1026             }
1027             return OnOffType.OFF;
1028         }));
1029     }
1030
1031     protected ChannelUpdateHandler defaultRemoteStartAllowanceChannelUpdateHandler() {
1032         return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1033             Optional<HomeConnectApiClient> apiClient = getApiClient();
1034             if (apiClient.isPresent()) {
1035                 return OnOffType.from(apiClient.get().isRemoteControlStartAllowed(getThingHaId()));
1036             }
1037             return OnOffType.OFF;
1038         }));
1039     }
1040
1041     protected ChannelUpdateHandler defaultSelectedProgramStateUpdateHandler() {
1042         return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1043             Optional<HomeConnectApiClient> apiClient = getApiClient();
1044             if (apiClient.isPresent()) {
1045                 Program program = apiClient.get().getSelectedProgram(getThingHaId());
1046                 if (program != null) {
1047                     processProgramOptions(program.getOptions());
1048                     return new StringType(program.getKey());
1049                 } else {
1050                     return UnDefType.UNDEF;
1051                 }
1052             }
1053             return UnDefType.UNDEF;
1054         }));
1055     }
1056
1057     protected ChannelUpdateHandler updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() {
1058         return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1059             Optional<HomeConnectApiClient> apiClient = getApiClient();
1060             if (apiClient.isPresent()) {
1061                 Program program = apiClient.get().getSelectedProgram(getThingHaId());
1062
1063                 if (program != null) {
1064                     updateProgramOptionsStateDescriptions(program.getKey(), program.getOptions());
1065                     processProgramOptions(program.getOptions());
1066
1067                     return new StringType(program.getKey());
1068                 } else {
1069                     return UnDefType.UNDEF;
1070                 }
1071             }
1072             return UnDefType.UNDEF;
1073         }));
1074     }
1075
1076     protected ChannelUpdateHandler getAndUpdateSelectedProgramStateUpdateHandler() {
1077         return (channelUID, cache) -> {
1078             Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
1079             if (channel.isPresent()) {
1080                 defaultSelectedProgramStateUpdateHandler().handle(channel.get().getUID(), cache);
1081             }
1082         };
1083     }
1084
1085     protected ChannelUpdateHandler getAndUpdateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() {
1086         return (channelUID, cache) -> {
1087             Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
1088             if (channel.isPresent()) {
1089                 updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler()
1090                         .handle(channel.get().getUID(), cache);
1091             }
1092         };
1093     }
1094
1095     protected ChannelUpdateHandler defaultActiveProgramStateUpdateHandler() {
1096         return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1097             Optional<HomeConnectApiClient> apiClient = getApiClient();
1098             if (apiClient.isPresent()) {
1099                 Program program = apiClient.get().getActiveProgram(getThingHaId());
1100
1101                 if (program != null) {
1102                     processProgramOptions(program.getOptions());
1103                     return new StringType(mapStringType(program.getKey()));
1104                 } else {
1105                     resetProgramStateChannels(false);
1106                     return UnDefType.UNDEF;
1107                 }
1108             }
1109             return UnDefType.UNDEF;
1110         }));
1111     }
1112
1113     protected void handleTemperatureCommand(final ChannelUID channelUID, final Command command,
1114             final HomeConnectApiClient apiClient)
1115             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1116         if (command instanceof QuantityType) {
1117             QuantityType<?> quantity = (QuantityType<?>) command;
1118
1119             String value;
1120             String unit;
1121
1122             try {
1123                 if (quantity.getUnit().equals(SIUnits.CELSIUS) || quantity.getUnit().equals(ImperialUnits.FAHRENHEIT)) {
1124                     unit = quantity.getUnit().toString();
1125                     value = String.valueOf(quantity.intValue());
1126                 } else {
1127                     logger.debug("Converting target temperature from {}{} to °C value. thing={}, haId={}",
1128                             quantity.intValue(), quantity.getUnit().toString(), getThingLabel(), getThingHaId());
1129                     unit = "°C";
1130                     var celsius = quantity.toUnit(SIUnits.CELSIUS);
1131                     if (celsius == null) {
1132                         logger.warn("Converting temperature to celsius failed! quantity={}", quantity);
1133                         value = null;
1134                     } else {
1135                         value = String.valueOf(celsius.intValue());
1136                     }
1137                     logger.debug("Converted value {}{}", value, unit);
1138                 }
1139
1140                 if (value != null) {
1141                     logger.debug("Set temperature to {} {}. thing={}, haId={}", value, unit, getThingLabel(),
1142                             getThingHaId());
1143                     switch (channelUID.getId()) {
1144                         case CHANNEL_REFRIGERATOR_SETPOINT_TEMPERATURE:
1145                             apiClient.setFridgeSetpointTemperature(getThingHaId(), value, unit);
1146                         case CHANNEL_FREEZER_SETPOINT_TEMPERATURE:
1147                             apiClient.setFreezerSetpointTemperature(getThingHaId(), value, unit);
1148                             break;
1149                         case CHANNEL_SETPOINT_TEMPERATURE:
1150                             apiClient.setProgramOptions(getThingHaId(), OPTION_SETPOINT_TEMPERATURE, value, unit, true,
1151                                     false);
1152                             break;
1153                         default:
1154                             logger.debug("Unknown channel! Cannot set temperature. channelUID={}", channelUID);
1155                     }
1156                 }
1157             } catch (UnconvertibleException e) {
1158                 logger.warn("Could not set temperature! haId={}, error={}", getThingHaId(), e.getMessage());
1159             }
1160         }
1161     }
1162
1163     protected void handleLightCommands(final ChannelUID channelUID, final Command command,
1164             final HomeConnectApiClient apiClient)
1165             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1166         switch (channelUID.getId()) {
1167             case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1168             case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1169                 // turn light on if turned off
1170                 turnLightOn(channelUID, apiClient);
1171
1172                 int newBrightness = BRIGHTNESS_MIN;
1173                 if (command instanceof OnOffType) {
1174                     newBrightness = command == OnOffType.ON ? BRIGHTNESS_MAX : BRIGHTNESS_MIN;
1175                 } else if (command instanceof IncreaseDecreaseType) {
1176                     int currentBrightness = getCurrentBrightness(channelUID, apiClient);
1177                     if (command.equals(IncreaseDecreaseType.INCREASE)) {
1178                         newBrightness = currentBrightness + BRIGHTNESS_DIM_STEP;
1179                     } else {
1180                         newBrightness = currentBrightness - BRIGHTNESS_DIM_STEP;
1181                     }
1182                 } else if (command instanceof PercentType) {
1183                     newBrightness = (int) Math.floor(((PercentType) command).doubleValue());
1184                 } else if (command instanceof DecimalType) {
1185                     newBrightness = ((DecimalType) command).intValue();
1186                 }
1187
1188                 // check in in range
1189                 newBrightness = Math.min(Math.max(newBrightness, BRIGHTNESS_MIN), BRIGHTNESS_MAX);
1190
1191                 setLightBrightness(channelUID, apiClient, newBrightness);
1192                 break;
1193             case CHANNEL_FUNCTIONAL_LIGHT_STATE:
1194                 if (command instanceof OnOffType) {
1195                     apiClient.setFunctionalLightState(getThingHaId(), OnOffType.ON.equals(command));
1196                 }
1197                 break;
1198             case CHANNEL_AMBIENT_LIGHT_STATE:
1199                 if (command instanceof OnOffType) {
1200                     apiClient.setAmbientLightState(getThingHaId(), OnOffType.ON.equals(command));
1201                 }
1202                 break;
1203             case CHANNEL_AMBIENT_LIGHT_COLOR_STATE:
1204                 if (command instanceof StringType) {
1205                     turnLightOn(channelUID, apiClient);
1206                     apiClient.setAmbientLightColorState(getThingHaId(), command.toFullString());
1207                 }
1208                 break;
1209             case CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE:
1210                 turnLightOn(channelUID, apiClient);
1211
1212                 // make sure 'custom color' is set as color
1213                 Data ambientLightColorState = apiClient.getAmbientLightColorState(getThingHaId());
1214                 if (!STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR.equals(ambientLightColorState.getValue())) {
1215                     apiClient.setAmbientLightColorState(getThingHaId(), STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR);
1216                 }
1217
1218                 if (command instanceof HSBType) {
1219                     apiClient.setAmbientLightCustomColorState(getThingHaId(), mapColor((HSBType) command));
1220                 } else if (command instanceof StringType) {
1221                     apiClient.setAmbientLightCustomColorState(getThingHaId(), command.toFullString());
1222                 }
1223                 break;
1224         }
1225     }
1226
1227     protected void handlePowerCommand(final ChannelUID channelUID, final Command command,
1228             final HomeConnectApiClient apiClient, String stateNotOn)
1229             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1230         if (command instanceof OnOffType && CHANNEL_POWER_STATE.equals(channelUID.getId())) {
1231             apiClient.setPowerState(getThingHaId(), OnOffType.ON.equals(command) ? STATE_POWER_ON : stateNotOn);
1232         }
1233     }
1234
1235     private int getCurrentBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient)
1236             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1237         String id = channelUID.getId();
1238         if (CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE.equals(id)) {
1239             return apiClient.getFunctionalLightBrightnessState(getThingHaId()).getValueAsInt();
1240         } else {
1241             return apiClient.getAmbientLightBrightnessState(getThingHaId()).getValueAsInt();
1242         }
1243     }
1244
1245     private void setLightBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient, int value)
1246             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1247         switch (channelUID.getId()) {
1248             case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1249                 apiClient.setFunctionalLightBrightnessState(getThingHaId(), value);
1250                 break;
1251             case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1252                 apiClient.setAmbientLightBrightnessState(getThingHaId(), value);
1253                 break;
1254         }
1255     }
1256
1257     private void turnLightOn(final ChannelUID channelUID, final HomeConnectApiClient apiClient)
1258             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1259         switch (channelUID.getId()) {
1260             case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1261                 Data functionalLightState = apiClient.getFunctionalLightState(getThingHaId());
1262                 if (!functionalLightState.getValueAsBoolean()) {
1263                     apiClient.setFunctionalLightState(getThingHaId(), true);
1264                 }
1265                 break;
1266             case CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE:
1267             case CHANNEL_AMBIENT_LIGHT_COLOR_STATE:
1268             case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1269                 Data ambientLightState = apiClient.getAmbientLightState(getThingHaId());
1270                 if (!ambientLightState.getValueAsBoolean()) {
1271                     apiClient.setAmbientLightState(getThingHaId(), true);
1272                 }
1273                 break;
1274         }
1275     }
1276
1277     protected void processProgramOptions(List<Option> options) {
1278         options.forEach(option -> {
1279             String key = option.getKey();
1280             if (key != null) {
1281                 switch (key) {
1282                     case OPTION_WASHER_TEMPERATURE:
1283                         getThingChannel(CHANNEL_WASHER_TEMPERATURE)
1284                                 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1285                         break;
1286                     case OPTION_WASHER_SPIN_SPEED:
1287                         getThingChannel(CHANNEL_WASHER_SPIN_SPEED)
1288                                 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1289                         break;
1290                     case OPTION_WASHER_IDOS_1_DOSING_LEVEL:
1291                         getThingChannel(CHANNEL_WASHER_IDOS1)
1292                                 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1293                         break;
1294                     case OPTION_WASHER_IDOS_2_DOSING_LEVEL:
1295                         getThingChannel(CHANNEL_WASHER_IDOS2)
1296                                 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1297                         break;
1298                     case OPTION_DRYER_DRYING_TARGET:
1299                         getThingChannel(CHANNEL_DRYER_DRYING_TARGET)
1300                                 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1301                         break;
1302                     case OPTION_HOOD_INTENSIVE_LEVEL:
1303                         String hoodIntensiveLevelValue = option.getValue();
1304                         if (hoodIntensiveLevelValue != null) {
1305                             getThingChannel(CHANNEL_HOOD_INTENSIVE_LEVEL)
1306                                     .ifPresent(channel -> updateState(channel.getUID(),
1307                                             new StringType(mapStageStringType(hoodIntensiveLevelValue))));
1308                         }
1309                         break;
1310                     case OPTION_HOOD_VENTING_LEVEL:
1311                         String hoodVentingLevel = option.getValue();
1312                         if (hoodVentingLevel != null) {
1313                             getThingChannel(CHANNEL_HOOD_VENTING_LEVEL)
1314                                     .ifPresent(channel -> updateState(channel.getUID(),
1315                                             new StringType(mapStageStringType(hoodVentingLevel))));
1316                         }
1317                         break;
1318                     case OPTION_SETPOINT_TEMPERATURE:
1319                         getThingChannel(CHANNEL_SETPOINT_TEMPERATURE).ifPresent(channel -> updateState(channel.getUID(),
1320                                 new QuantityType<>(option.getValueAsInt(), mapTemperature(option.getUnit()))));
1321                         break;
1322                     case OPTION_DURATION:
1323                         getThingChannel(CHANNEL_DURATION).ifPresent(channel -> updateState(channel.getUID(),
1324                                 new QuantityType<>(option.getValueAsInt(), SECOND)));
1325                         break;
1326                     case OPTION_FINISH_IN_RELATIVE:
1327                     case OPTION_REMAINING_PROGRAM_TIME:
1328                         getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
1329                                 .ifPresent(channel -> updateState(channel.getUID(),
1330                                         new QuantityType<>(option.getValueAsInt(), SECOND)));
1331                         break;
1332                     case OPTION_ELAPSED_PROGRAM_TIME:
1333                         getThingChannel(CHANNEL_ELAPSED_PROGRAM_TIME).ifPresent(channel -> updateState(channel.getUID(),
1334                                 new QuantityType<>(option.getValueAsInt(), SECOND)));
1335                         break;
1336                     case OPTION_PROGRAM_PROGRESS:
1337                         getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
1338                                 .ifPresent(channel -> updateState(channel.getUID(),
1339                                         new QuantityType<>(option.getValueAsInt(), PERCENT)));
1340                         break;
1341                 }
1342             }
1343         });
1344     }
1345
1346     protected String convertWasherTemperature(String value) {
1347         if (value.startsWith("LaundryCare.Washer.EnumType.Temperature.GC")) {
1348             return value.replace("LaundryCare.Washer.EnumType.Temperature.GC", "") + "°C";
1349         }
1350
1351         if (value.startsWith("LaundryCare.Washer.EnumType.Temperature.Ul")) {
1352             return mapStringType(value.replace("LaundryCare.Washer.EnumType.Temperature.Ul", ""));
1353         }
1354
1355         return mapStringType(value);
1356     }
1357
1358     protected String convertWasherSpinSpeed(String value) {
1359         if (value.startsWith("LaundryCare.Washer.EnumType.SpinSpeed.RPM")) {
1360             return value.replace("LaundryCare.Washer.EnumType.SpinSpeed.RPM", "") + " RPM";
1361         }
1362
1363         if (value.startsWith("LaundryCare.Washer.EnumType.SpinSpeed.Ul")) {
1364             return value.replace("LaundryCare.Washer.EnumType.SpinSpeed.Ul", "");
1365         }
1366
1367         return mapStringType(value);
1368     }
1369
1370     protected void updateProgramOptionsStateDescriptions(String programKey, @Nullable List<Option> optionsValues)
1371             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1372         Optional<HomeConnectApiClient> apiClient = getApiClient();
1373         if (apiClient.isPresent()) {
1374             List<AvailableProgramOption> availableProgramOptions;
1375             if (availableProgramOptionsCache.containsKey(programKey)) {
1376                 logger.debug("Returning cached options for '{}'.", programKey);
1377                 availableProgramOptions = availableProgramOptionsCache.get(programKey);
1378                 availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
1379                         : Collections.emptyList();
1380             } else {
1381                 availableProgramOptions = apiClient.get().getProgramOptions(getThingHaId(), programKey);
1382                 availableProgramOptionsCache.put(programKey, availableProgramOptions);
1383             }
1384
1385             Optional<Channel> channelSpinSpeed = getThingChannel(CHANNEL_WASHER_SPIN_SPEED);
1386             Optional<Channel> channelTemperature = getThingChannel(CHANNEL_WASHER_TEMPERATURE);
1387             Optional<Channel> channelDryingTarget = getThingChannel(CHANNEL_DRYER_DRYING_TARGET);
1388
1389             if (availableProgramOptions.isEmpty()) {
1390                 List<Option> options;
1391                 if (optionsValues != null) {
1392                     options = optionsValues;
1393                 } else if (channelSpinSpeed.isPresent() || channelTemperature.isPresent()
1394                         || channelDryingTarget.isPresent()) {
1395                     Program program = apiClient.get().getSelectedProgram(getThingHaId());
1396                     options = program != null ? program.getOptions() : emptyList();
1397                 } else {
1398                     options = emptyList();
1399                 }
1400
1401                 channelSpinSpeed.ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1402                         options.stream()
1403                                 .filter(option -> option.getKey() != null && option.getValue() != null
1404                                         && OPTION_WASHER_SPIN_SPEED.equals(option.getKey()))
1405                                 .map(option -> option.getValue())
1406                                 .map(value -> new StateOption(value == null ? "" : value,
1407                                         convertWasherSpinSpeed(value == null ? "" : value)))
1408                                 .collect(Collectors.toList())));
1409                 channelTemperature
1410                         .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1411                                 options.stream()
1412                                         .filter(option -> option.getKey() != null && option.getValue() != null
1413                                                 && OPTION_WASHER_TEMPERATURE.equals(option.getKey()))
1414                                         .map(option -> option.getValue())
1415                                         .map(value -> new StateOption(value == null ? "" : value,
1416                                                 convertWasherTemperature(value == null ? "" : value)))
1417                                         .collect(Collectors.toList())));
1418                 channelDryingTarget
1419                         .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1420                                 options.stream()
1421                                         .filter(option -> option.getKey() != null && option.getValue() != null
1422                                                 && OPTION_DRYER_DRYING_TARGET.equals(option.getKey()))
1423                                         .map(option -> option.getValue())
1424                                         .map(value -> new StateOption(value == null ? "" : value,
1425                                                 mapStringType(value == null ? "" : value)))
1426                                         .collect(Collectors.toList())));
1427             }
1428
1429             availableProgramOptions.forEach(option -> {
1430                 switch (option.getKey()) {
1431                     case OPTION_WASHER_SPIN_SPEED: {
1432                         channelSpinSpeed
1433                                 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1434                                         createStateOptions(option, this::convertWasherSpinSpeed)));
1435                         break;
1436                     }
1437                     case OPTION_WASHER_TEMPERATURE: {
1438                         channelTemperature
1439                                 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1440                                         createStateOptions(option, this::convertWasherTemperature)));
1441                         break;
1442                     }
1443                     case OPTION_DRYER_DRYING_TARGET: {
1444                         channelDryingTarget.ifPresent(channel -> dynamicStateDescriptionProvider
1445                                 .setStateOptions(channel.getUID(), createStateOptions(option, this::mapStringType)));
1446                         break;
1447                     }
1448                 }
1449             });
1450         }
1451     }
1452
1453     protected HomeConnectDynamicStateDescriptionProvider getDynamicStateDescriptionProvider() {
1454         return dynamicStateDescriptionProvider;
1455     }
1456
1457     private List<StateOption> createStateOptions(AvailableProgramOption option,
1458             Function<String, String> stateConverter) {
1459         return option.getAllowedValues().stream().map(av -> new StateOption(av, stateConverter.apply(av)))
1460                 .collect(Collectors.toList());
1461     }
1462
1463     private synchronized void scheduleOfflineMonitor1() {
1464         this.reinitializationFuture1 = scheduler.schedule(() -> {
1465             if (isBridgeOnline() && isThingOffline()) {
1466                 logger.debug("Offline monitor 1: Check if thing is ONLINE. thing={}, haId={}", getThingLabel(),
1467                         getThingHaId());
1468                 refreshThingStatus();
1469                 if (isThingOnline()) {
1470                     logger.debug("Offline monitor 1: Thing status changed to ONLINE. thing={}, haId={}",
1471                             getThingLabel(), getThingHaId());
1472                     reinitialize();
1473                 } else {
1474                     scheduleOfflineMonitor1();
1475                 }
1476             } else {
1477                 scheduleOfflineMonitor1();
1478             }
1479         }, AbstractHomeConnectThingHandler.OFFLINE_MONITOR_1_DELAY_MIN, TimeUnit.MINUTES);
1480     }
1481
1482     private synchronized void stopOfflineMonitor1() {
1483         ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture1;
1484         if (reinitializationFuture != null) {
1485             reinitializationFuture.cancel(false);
1486             this.reinitializationFuture1 = null;
1487         }
1488     }
1489
1490     private synchronized void scheduleOfflineMonitor2() {
1491         this.reinitializationFuture2 = scheduler.schedule(() -> {
1492             if (isBridgeOnline() && !accessible.get()) {
1493                 logger.debug("Offline monitor 2: Check if thing is ONLINE. thing={}, haId={}", getThingLabel(),
1494                         getThingHaId());
1495                 refreshThingStatus();
1496                 if (isThingOnline()) {
1497                     logger.debug("Offline monitor 2: Thing status changed to ONLINE. thing={}, haId={}",
1498                             getThingLabel(), getThingHaId());
1499                     reinitialize();
1500                 } else {
1501                     scheduleOfflineMonitor2();
1502                 }
1503             } else {
1504                 scheduleOfflineMonitor2();
1505             }
1506         }, AbstractHomeConnectThingHandler.OFFLINE_MONITOR_2_DELAY_MIN, TimeUnit.MINUTES);
1507     }
1508
1509     private synchronized void stopOfflineMonitor2() {
1510         ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture2;
1511         if (reinitializationFuture != null) {
1512             reinitializationFuture.cancel(false);
1513             this.reinitializationFuture2 = null;
1514         }
1515     }
1516
1517     private synchronized void scheduleRetryRegistering() {
1518         this.reinitializationFuture3 = scheduler.schedule(() -> {
1519             logger.debug("Try to register event listener again. haId={}", getThingHaId());
1520             unregisterEventListener();
1521             registerEventListener();
1522         }, AbstractHomeConnectThingHandler.EVENT_LISTENER_CONNECT_RETRY_DELAY_MIN, TimeUnit.MINUTES);
1523     }
1524
1525     private synchronized void stopRetryRegistering() {
1526         ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture3;
1527         if (reinitializationFuture != null) {
1528             reinitializationFuture.cancel(true);
1529             this.reinitializationFuture3 = null;
1530         }
1531     }
1532 }