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