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