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