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