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