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