2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.homeconnect.internal.handler;
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.*;
23 import java.time.Duration;
24 import java.util.Collections;
25 import java.util.List;
27 import java.util.Optional;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.CopyOnWriteArrayList;
30 import java.util.concurrent.ScheduledFuture;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.atomic.AtomicBoolean;
33 import java.util.function.Function;
34 import java.util.stream.Collectors;
36 import javax.measure.UnconvertibleException;
37 import javax.measure.Unit;
38 import javax.measure.quantity.Temperature;
40 import org.eclipse.jdt.annotation.NonNullByDefault;
41 import org.eclipse.jdt.annotation.Nullable;
42 import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
43 import org.openhab.binding.homeconnect.internal.client.HomeConnectEventSourceClient;
44 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
45 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
46 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
47 import org.openhab.binding.homeconnect.internal.client.listener.HomeConnectEventListener;
48 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgram;
49 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
50 import org.openhab.binding.homeconnect.internal.client.model.Data;
51 import org.openhab.binding.homeconnect.internal.client.model.Event;
52 import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
53 import org.openhab.binding.homeconnect.internal.client.model.Option;
54 import org.openhab.binding.homeconnect.internal.client.model.Program;
55 import org.openhab.binding.homeconnect.internal.handler.cache.ExpiringStateMap;
56 import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
57 import org.openhab.core.auth.client.oauth2.OAuthException;
58 import org.openhab.core.library.types.DecimalType;
59 import org.openhab.core.library.types.HSBType;
60 import org.openhab.core.library.types.IncreaseDecreaseType;
61 import org.openhab.core.library.types.OnOffType;
62 import org.openhab.core.library.types.OpenClosedType;
63 import org.openhab.core.library.types.PercentType;
64 import org.openhab.core.library.types.QuantityType;
65 import org.openhab.core.library.types.StringType;
66 import org.openhab.core.library.unit.ImperialUnits;
67 import org.openhab.core.library.unit.SIUnits;
68 import org.openhab.core.thing.Bridge;
69 import org.openhab.core.thing.Channel;
70 import org.openhab.core.thing.ChannelUID;
71 import org.openhab.core.thing.Thing;
72 import org.openhab.core.thing.ThingStatusDetail;
73 import org.openhab.core.thing.ThingStatusInfo;
74 import org.openhab.core.thing.binding.BaseThingHandler;
75 import org.openhab.core.thing.binding.BridgeHandler;
76 import org.openhab.core.types.Command;
77 import org.openhab.core.types.RefreshType;
78 import org.openhab.core.types.State;
79 import org.openhab.core.types.StateOption;
80 import org.openhab.core.types.UnDefType;
81 import org.slf4j.Logger;
82 import org.slf4j.LoggerFactory;
85 * The {@link AbstractHomeConnectThingHandler} is responsible for handling commands, which are
86 * sent to one of the channels.
88 * @author Jonas Brüstel - Initial contribution
89 * @author Laurent Garnier - programs cache moved and enhanced to allow adding unsupported programs
92 public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler implements HomeConnectEventListener {
94 private static final int CACHE_TTL_SEC = 2;
95 private static final int OFFLINE_MONITOR_1_DELAY_MIN = 30;
96 private static final int OFFLINE_MONITOR_2_DELAY_MIN = 4;
97 private static final int EVENT_LISTENER_CONNECT_RETRY_DELAY_MIN = 10;
99 private @Nullable String operationState;
100 private @Nullable ScheduledFuture<?> reinitializationFuture1;
101 private @Nullable ScheduledFuture<?> reinitializationFuture2;
102 private @Nullable ScheduledFuture<?> reinitializationFuture3;
103 private boolean ignoreEventSourceClosedEvent;
104 private @Nullable String programOptionsDelayedUpdate;
106 private final ConcurrentHashMap<String, EventHandler> eventHandlers;
107 private final ConcurrentHashMap<String, ChannelUpdateHandler> channelUpdateHandlers;
108 private final HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
109 private final ExpiringStateMap expiringStateMap;
110 private final AtomicBoolean accessible;
111 private final Logger logger = LoggerFactory.getLogger(AbstractHomeConnectThingHandler.class);
112 private final List<AvailableProgram> programsCache;
113 private final Map<String, List<AvailableProgramOption>> availableProgramOptionsCache;
114 private final Map<String, List<AvailableProgramOption>> unsupportedProgramOptions;
116 public AbstractHomeConnectThingHandler(Thing thing,
117 HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
119 eventHandlers = new ConcurrentHashMap<>();
120 channelUpdateHandlers = new ConcurrentHashMap<>();
121 this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider;
122 expiringStateMap = new ExpiringStateMap(Duration.ofSeconds(CACHE_TTL_SEC));
123 accessible = new AtomicBoolean(false);
124 programsCache = new CopyOnWriteArrayList<>();
125 availableProgramOptionsCache = new ConcurrentHashMap<>();
126 unsupportedProgramOptions = new ConcurrentHashMap<>();
128 configureEventHandlers(eventHandlers);
129 configureChannelUpdateHandlers(channelUpdateHandlers);
130 configureUnsupportedProgramOptions(unsupportedProgramOptions);
134 public void initialize() {
135 if (getBridgeHandler().isEmpty()) {
136 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
137 accessible.set(false);
138 } else if (isBridgeOffline()) {
139 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
140 accessible.set(false);
142 updateStatus(UNKNOWN);
143 scheduler.submit(() -> {
144 refreshThingStatus(); // set ONLINE / OFFLINE
145 updateSelectedProgramStateDescription();
147 registerEventListener();
148 scheduleOfflineMonitor1();
149 scheduleOfflineMonitor2();
155 public void dispose() {
156 stopRetryRegistering();
157 stopOfflineMonitor1();
158 stopOfflineMonitor2();
159 unregisterEventListener(true);
163 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
164 logger.debug("Bridge status changed to {} ({}). haId={}", bridgeStatusInfo, getThingLabel(), getThingHaId());
168 private void reinitialize() {
169 logger.debug("Reinitialize thing handler ({}). haId={}", getThingLabel(), getThingHaId());
170 stopRetryRegistering();
171 stopOfflineMonitor1();
172 stopOfflineMonitor2();
173 unregisterEventListener();
178 * Handles a command for a given channel.
180 * This method is only called, if the thing has been initialized (status ONLINE/OFFLINE/UNKNOWN).
183 * @param channelUID the {@link ChannelUID} of the channel to which the command was sent
184 * @param command the {@link Command}
185 * @param apiClient the {@link HomeConnectApiClient}
186 * @throws CommunicationException communication problem
187 * @throws AuthorizationException authorization problem
188 * @throws ApplianceOfflineException appliance offline
190 protected void handleCommand(ChannelUID channelUID, Command command, HomeConnectApiClient apiClient)
191 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
192 if (command instanceof RefreshType) {
193 updateChannel(channelUID);
194 } else if (command instanceof StringType && CHANNEL_BASIC_ACTIONS_STATE.equals(channelUID.getId())
195 && getBridgeHandler().isPresent()) {
196 updateState(channelUID, new StringType(""));
198 if (COMMAND_START.equalsIgnoreCase(command.toFullString())) {
199 HomeConnectBridgeHandler homeConnectBridgeHandler = getBridgeHandler().get();
200 // workaround for api bug
201 // if simulator, program options have to be passed along with the desired program
202 // if non simulator, some options throw a "SDK.Error.UnsupportedOption" error
203 if (homeConnectBridgeHandler.getConfiguration().isSimulator()) {
204 apiClient.startSelectedProgram(getThingHaId());
206 Program selectedProgram = apiClient.getSelectedProgram(getThingHaId());
207 if (selectedProgram != null) {
208 apiClient.startProgram(getThingHaId(), selectedProgram.getKey());
211 } else if (COMMAND_STOP.equalsIgnoreCase(command.toFullString())) {
212 apiClient.stopProgram(getThingHaId());
213 } else if (COMMAND_SELECTED.equalsIgnoreCase(command.toFullString())) {
214 apiClient.getSelectedProgram(getThingHaId());
216 logger.debug("Start custom program. command={} haId={}", command.toFullString(), getThingHaId());
217 apiClient.startCustomProgram(getThingHaId(), command.toFullString());
219 } else if (command instanceof StringType && CHANNEL_SELECTED_PROGRAM_STATE.equals(channelUID.getId())
220 && isProgramSupported(command.toFullString())) {
221 apiClient.setSelectedProgram(getThingHaId(), command.toFullString());
226 public final void handleCommand(ChannelUID channelUID, Command command) {
227 var apiClient = getApiClient();
228 if ((isThingReadyToHandleCommand() || (this instanceof HomeConnectHoodHandler && isBridgeOnline()
229 && isThingAccessibleViaServerSentEvents())) && apiClient.isPresent()) {
230 logger.debug("Handle \"{}\" command ({}). haId={}", command, channelUID.getId(), getThingHaId());
232 handleCommand(channelUID, command, apiClient.get());
233 } catch (ApplianceOfflineException e) {
234 logger.debug("Could not handle command {}. Appliance offline. thing={}, haId={}, error={}",
235 command.toFullString(), getThingLabel(), getThingHaId(), e.getMessage());
236 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
237 resetChannelsOnOfflineEvent();
238 resetProgramStateChannels(true);
239 } catch (CommunicationException e) {
240 logger.debug("Could not handle command {}. API communication problem! error={}, haId={}",
241 command.toFullString(), e.getMessage(), getThingHaId());
242 } catch (AuthorizationException e) {
243 logger.debug("Could not handle command {}. Authorization problem! error={}, haId={}",
244 command.toFullString(), e.getMessage(), getThingHaId());
246 handleAuthenticationError(e);
252 public void onEvent(Event event) {
253 if (DISCONNECTED.equals(event.getType())) {
254 logger.debug("Received DISCONNECTED event. Set {} to OFFLINE. haId={}", getThing().getLabel(),
256 updateStatus(OFFLINE);
257 resetChannelsOnOfflineEvent();
258 resetProgramStateChannels(true);
259 } else if (isThingOnline() && CONNECTED.equals(event.getType())) {
260 logger.debug("Received CONNECTED event. Update power state channel. haId={}", getThingHaId());
261 getThingChannel(CHANNEL_POWER_STATE).ifPresent(c -> updateChannel(c.getUID()));
262 } else if (isThingOffline() && !KEEP_ALIVE.equals(event.getType())) {
263 updateStatus(ONLINE);
264 logger.debug("Set {} to ONLINE and update channels. haId={}", getThing().getLabel(), getThingHaId());
265 updateSelectedProgramStateDescription();
269 String key = event.getKey();
270 if (EVENT_OPERATION_STATE.equals(key)) {
271 operationState = event.getValue() == null ? null : event.getValue();
274 if (key != null && eventHandlers.containsKey(key)) {
275 EventHandler eventHandler = eventHandlers.get(key);
276 if (eventHandler != null) {
277 eventHandler.handle(event);
281 accessible.set(true);
285 public void onClosed() {
286 if (ignoreEventSourceClosedEvent) {
287 logger.debug("Ignoring event source close event. thing={}, haId={}", getThing().getLabel(), getThingHaId());
289 unregisterEventListener();
290 refreshThingStatus();
291 registerEventListener();
296 public void onRateLimitReached() {
297 unregisterEventListener();
300 scheduleRetryRegistering();
304 * Register event listener.
306 protected void registerEventListener() {
307 if (isBridgeOnline() && isThingAccessibleViaServerSentEvents()) {
308 getEventSourceClient().ifPresent(client -> {
310 ignoreEventSourceClosedEvent = false;
311 client.registerEventListener(getThingHaId(), this);
312 } catch (CommunicationException | AuthorizationException e) {
313 logger.warn("Could not open event source connection. thing={}, haId={}, error={}", getThingLabel(),
314 getThingHaId(), e.getMessage());
321 * Unregister event listener.
323 protected void unregisterEventListener() {
324 unregisterEventListener(false);
327 private void unregisterEventListener(boolean immediate) {
328 getEventSourceClient().ifPresent(client -> {
329 ignoreEventSourceClosedEvent = true;
330 client.unregisterEventListener(this, immediate, false);
335 * Get {@link HomeConnectApiClient}.
337 * @return client instance
339 protected Optional<HomeConnectApiClient> getApiClient() {
340 return getBridgeHandler().map(HomeConnectBridgeHandler::getApiClient);
344 * Get {@link HomeConnectEventSourceClient}.
346 * @return client instance if present
348 protected Optional<HomeConnectEventSourceClient> getEventSourceClient() {
349 return getBridgeHandler().map(HomeConnectBridgeHandler::getEventSourceClient);
353 * Update state description of selected program (Fetch programs via API).
355 protected void updateSelectedProgramStateDescription() {
356 if (isBridgeOffline() || isThingOffline()) {
361 List<StateOption> stateOptions = getPrograms().stream()
362 .map(p -> new StateOption(p.getKey(), mapStringType(p.getKey()))).collect(Collectors.toList());
364 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(
365 channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions));
366 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
367 logger.debug("Could not fetch available programs. thing={}, haId={}, error={}", getThingLabel(),
368 getThingHaId(), e.getMessage());
369 removeSelectedProgramStateDescription();
374 * Remove state description of selected program.
376 protected void removeSelectedProgramStateDescription() {
377 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
378 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList()));
382 * Is thing ready to process commands. If bridge or thing itself is offline commands will be ignored.
384 * @return true if ready
386 protected boolean isThingReadyToHandleCommand() {
387 if (isBridgeOffline()) {
388 logger.debug("Bridge is OFFLINE. Ignore command. thing={}, haId={}", getThingLabel(), getThingHaId());
392 if (isThingOffline()) {
393 logger.debug("{} is OFFLINE. Ignore command. haId={}", getThing().getLabel(), getThingHaId());
401 * Checks if bridge is online and set.
403 * @return true if online
405 protected boolean isBridgeOnline() {
406 Bridge bridge = getBridge();
407 return bridge != null && ONLINE.equals(bridge.getStatus());
411 * Checks if bridge is offline or not set.
413 * @return true if offline
415 protected boolean isBridgeOffline() {
416 return !isBridgeOnline();
420 * Checks if thing is online.
422 * @return true if online
424 protected boolean isThingOnline() {
425 return ONLINE.equals(getThing().getStatus());
429 * Checks if thing is connected to the cloud and accessible via SSE.
431 * @return true if yes
433 public boolean isThingAccessibleViaServerSentEvents() {
434 return accessible.get();
438 * Checks if thing is offline.
440 * @return true if offline
442 protected boolean isThingOffline() {
443 return !isThingOnline();
447 * Get {@link HomeConnectBridgeHandler}.
449 * @return bridge handler
451 protected Optional<HomeConnectBridgeHandler> getBridgeHandler() {
452 Bridge bridge = getBridge();
453 if (bridge != null) {
454 BridgeHandler bridgeHandler = bridge.getHandler();
455 if (bridgeHandler instanceof HomeConnectBridgeHandler) {
456 return Optional.of((HomeConnectBridgeHandler) bridgeHandler);
459 return Optional.empty();
463 * Get thing channel by given channel id.
465 * @param channelId channel id
468 protected Optional<Channel> getThingChannel(String channelId) {
469 Channel channel = getThing().getChannel(channelId);
470 if (channel == null) {
471 return Optional.empty();
473 return Optional.of(channel);
478 * Get thing linked channel by given channel id.
480 * @param channelId channel id
481 * @return channel if linked
483 protected Optional<Channel> getLinkedChannel(String channelId) {
484 Channel channel = getThing().getChannel(channelId);
485 if (channel == null || !isLinked(channelId)) {
486 return Optional.empty();
488 return Optional.of(channel);
493 * Configure channel update handlers. Classes which extend {@link AbstractHomeConnectThingHandler} must implement
494 * this class and add handlers.
496 * @param handlers channel update handlers
498 protected abstract void configureChannelUpdateHandlers(final Map<String, ChannelUpdateHandler> handlers);
501 * Configure event handlers. Classes which extend {@link AbstractHomeConnectThingHandler} must implement
502 * this class and add handlers.
504 * @param handlers Server-Sent-Event handlers
506 protected abstract void configureEventHandlers(final Map<String, EventHandler> handlers);
508 protected void configureUnsupportedProgramOptions(final Map<String, List<AvailableProgramOption>> programOptions) {
511 protected boolean isChannelLinkedToProgramOptionNotFullySupportedByApi() {
516 * Update all channels via API.
519 protected void updateChannels() {
520 if (isBridgeOffline()) {
521 logger.debug("Bridge handler not found or offline. Stopping update of channels. thing={}, haId={}",
522 getThingLabel(), getThingHaId());
523 } else if (isThingOffline()) {
524 logger.debug("{} offline. Stopping update of channels. haId={}", getThing().getLabel(), getThingHaId());
526 List<Channel> channels = getThing().getChannels();
527 for (Channel channel : channels) {
528 updateChannel(channel.getUID());
534 * Update Channel values via API.
536 * @param channelUID channel UID
538 protected void updateChannel(ChannelUID channelUID) {
539 if (!getApiClient().isPresent()) {
540 logger.error("Cannot update channel. No instance of api client found! thing={}, haId={}", getThingLabel(),
545 if (!isThingReadyToHandleCommand()) {
549 if ((isLinked(channelUID) || CHANNEL_OPERATION_STATE.equals(channelUID.getId())) // always update operation
551 && channelUpdateHandlers.containsKey(channelUID.getId())) {
553 ChannelUpdateHandler channelUpdateHandler = channelUpdateHandlers.get(channelUID.getId());
554 if (channelUpdateHandler != null) {
555 channelUpdateHandler.handle(channelUID, expiringStateMap);
557 } catch (ApplianceOfflineException e) {
559 "API communication problem while trying to update! Appliance offline. thing={}, haId={}, error={}",
560 getThingLabel(), getThingHaId(), e.getMessage());
561 updateStatus(OFFLINE);
562 resetChannelsOnOfflineEvent();
563 resetProgramStateChannels(true);
564 } catch (CommunicationException e) {
565 logger.debug("API communication problem while trying to update! thing={}, haId={}, error={}",
566 getThingLabel(), getThingHaId(), e.getMessage());
567 } catch (AuthorizationException e) {
568 logger.debug("Authentication problem while trying to update! thing={}, haId={}", getThingLabel(),
570 handleAuthenticationError(e);
576 * Reset program related channels.
578 * @param offline true if the device is considered as OFFLINE
580 protected void resetProgramStateChannels(boolean offline) {
581 logger.debug("Resetting active program channel states. thing={}, haId={}", getThingLabel(), getThingHaId());
585 * Reset all channels on OFFLINE event.
587 protected void resetChannelsOnOfflineEvent() {
588 logger.debug("Resetting channel states due to OFFLINE event. thing={}, haId={}", getThingLabel(),
590 getThingChannel(CHANNEL_POWER_STATE).ifPresent(channel -> updateState(channel.getUID(), OnOffType.OFF));
591 getThingChannel(CHANNEL_OPERATION_STATE).ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
592 getThingChannel(CHANNEL_DOOR_STATE).ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
593 getThingChannel(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE)
594 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
595 getThingChannel(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE)
596 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
597 getThingChannel(CHANNEL_REMOTE_START_ALLOWANCE_STATE)
598 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
599 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
600 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
604 * Map Home Connect key and value names to label.
605 * e.g. Dishcare.Dishwasher.Program.Eco50 --> Eco50 or BSH.Common.EnumType.OperationState.DelayedStart --> Delayed
609 * @return human readable label
611 protected String mapStringType(String type) {
612 int index = type.lastIndexOf(".");
613 if (index > 0 && type.length() > index) {
614 String sub = type.substring(index + 1);
615 StringBuilder sb = new StringBuilder();
616 for (String word : sub.split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])")) {
620 return sb.toString().trim();
626 * Map Home Connect stage value to label.
627 * e.g. Cooking.Hood.EnumType.IntensiveStage.IntensiveStage1 --> 1
630 * @return human readable label
632 protected String mapStageStringType(String stage) {
635 case STAGE_INTENSIVE_STAGE_OFF:
638 case STAGE_FAN_STAGE_01:
639 case STAGE_INTENSIVE_STAGE_1:
642 case STAGE_FAN_STAGE_02:
643 case STAGE_INTENSIVE_STAGE_2:
646 case STAGE_FAN_STAGE_03:
649 case STAGE_FAN_STAGE_04:
652 case STAGE_FAN_STAGE_05:
656 stage = mapStringType(stage);
663 * Map unit string (returned by home connect api) to Unit
665 * @param unit String eg. "°C"
668 protected Unit<Temperature> mapTemperature(@Nullable String unit) {
671 } else if (unit.endsWith("C")) {
679 * Map hex representation of color to HSB type.
681 * @param colorCode color code e.g. #001122
684 protected HSBType mapColor(String colorCode) {
685 HSBType color = HSBType.WHITE;
687 if (colorCode.length() == 7) {
688 int r = Integer.valueOf(colorCode.substring(1, 3), 16);
689 int g = Integer.valueOf(colorCode.substring(3, 5), 16);
690 int b = Integer.valueOf(colorCode.substring(5, 7), 16);
691 color = HSBType.fromRGB(r, g, b);
697 * Map HSB color type to hex representation.
699 * @param color HSB color
700 * @return color code e.g. #001122
702 protected String mapColor(HSBType color) {
703 String redValue = String.format("%02X", (int) (color.getRed().floatValue() * 2.55));
704 String greenValue = String.format("%02X", (int) (color.getGreen().floatValue() * 2.55));
705 String blueValue = String.format("%02X", (int) (color.getBlue().floatValue() * 2.55));
706 return "#" + redValue + greenValue + blueValue;
710 * Check bridge status and refresh connection status of thing accordingly.
712 protected void refreshThingStatus() {
713 Optional<HomeConnectApiClient> apiClient = getApiClient();
715 apiClient.ifPresent(client -> {
717 HomeAppliance homeAppliance = client.getHomeAppliance(getThingHaId());
718 if (!homeAppliance.isConnected()) {
719 updateStatus(OFFLINE);
721 updateStatus(ONLINE);
723 accessible.set(true);
724 } catch (CommunicationException e) {
726 "Update status to OFFLINE. Home Connect service is not reachable or a problem occurred! thing={}, haId={}, error={}.",
727 getThingLabel(), getThingHaId(), e.getMessage());
728 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
729 "Home Connect service is not reachable or a problem occurred! (" + e.getMessage() + ").");
730 accessible.set(false);
731 } catch (AuthorizationException e) {
733 "Update status to OFFLINE. Home Connect service is not reachable or a problem occurred! thing={}, haId={}, error={}",
734 getThingLabel(), getThingHaId(), e.getMessage());
735 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
736 "Home Connect service is not reachable or a problem occurred! (" + e.getMessage() + ").");
737 accessible.set(false);
738 handleAuthenticationError(e);
741 if (apiClient.isEmpty()) {
742 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
743 accessible.set(false);
748 * Get home appliance id of Thing.
750 * @return home appliance id
752 public String getThingHaId() {
753 return getThing().getConfiguration().get(HA_ID).toString();
757 * Returns the human readable label for this thing.
759 * @return the human readable label
761 protected @Nullable String getThingLabel() {
762 return getThing().getLabel();
766 * Handle authentication exception.
768 protected void handleAuthenticationError(AuthorizationException exception) {
769 if (isBridgeOnline()) {
771 "Thing handler threw authentication exception --> clear credential storage thing={}, haId={} error={}",
772 getThingLabel(), getThingHaId(), exception.getMessage());
774 getBridgeHandler().ifPresent(homeConnectBridgeHandler -> {
776 homeConnectBridgeHandler.getOAuthClientService().remove();
777 homeConnectBridgeHandler.reinitialize();
778 } catch (OAuthException e) {
779 // client is already closed --> we can ignore it
786 * Get operation state of device.
788 * @return operation state string
790 protected @Nullable String getOperationState() {
791 return operationState;
794 protected EventHandler defaultElapsedProgramTimeEventHandler() {
795 return event -> getThingChannel(CHANNEL_ELAPSED_PROGRAM_TIME)
796 .ifPresent(channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND)));
799 protected EventHandler defaultPowerStateEventHandler() {
801 getThingChannel(CHANNEL_POWER_STATE).ifPresent(
802 channel -> updateState(channel.getUID(), OnOffType.from(STATE_POWER_ON.equals(event.getValue()))));
804 if (STATE_POWER_ON.equals(event.getValue())) {
805 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID()));
807 resetProgramStateChannels(true);
808 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
809 .ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
814 protected EventHandler defaultDoorStateEventHandler() {
815 return event -> getThingChannel(CHANNEL_DOOR_STATE).ifPresent(channel -> updateState(channel.getUID(),
816 STATE_DOOR_OPEN.equals(event.getValue()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED));
819 protected EventHandler defaultOperationStateEventHandler() {
821 String value = event.getValue();
822 getThingChannel(CHANNEL_OPERATION_STATE).ifPresent(channel -> updateState(channel.getUID(),
823 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
825 if (STATE_OPERATION_FINISHED.equals(value)) {
826 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
827 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(100, PERCENT)));
828 getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
829 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, SECOND)));
830 } else if (STATE_OPERATION_RUN.equals(value)) {
831 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
832 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, PERCENT)));
833 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID()));
834 } else if (STATE_OPERATION_READY.equals(value)) {
835 resetProgramStateChannels(false);
840 protected EventHandler defaultActiveProgramEventHandler() {
842 String value = event.getValue();
843 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(channel -> updateState(channel.getUID(),
844 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
846 resetProgramStateChannels(false);
851 protected EventHandler updateProgramOptionsAndActiveProgramStateEventHandler() {
853 String value = event.getValue();
854 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(channel -> updateState(channel.getUID(),
855 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
857 resetProgramStateChannels(false);
860 Optional<HomeConnectApiClient> apiClient = getApiClient();
861 if (apiClient.isPresent() && isChannelLinkedToProgramOptionNotFullySupportedByApi()
862 && apiClient.get().isRemoteControlActive(getThingHaId())) {
863 // update channels linked to program options
864 Program program = apiClient.get().getSelectedProgram(getThingHaId());
865 if (program != null) {
866 processProgramOptions(program.getOptions());
869 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
870 logger.debug("Could not update program options. {}", e.getMessage());
876 protected EventHandler defaultEventPresentStateEventHandler(String channelId) {
877 return event -> getThingChannel(channelId).ifPresent(channel -> updateState(channel.getUID(),
878 OnOffType.from(!STATE_EVENT_PRESENT_STATE_OFF.equals(event.getValue()))));
881 protected EventHandler defaultBooleanEventHandler(String channelId) {
882 return event -> getThingChannel(channelId)
883 .ifPresent(channel -> updateState(channel.getUID(), OnOffType.from(event.getValueAsBoolean())));
886 protected EventHandler defaultRemainingProgramTimeEventHandler() {
887 return event -> getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
888 .ifPresent(channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND)));
891 protected EventHandler defaultSelectedProgramStateEventHandler() {
892 return event -> getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
893 .ifPresent(channel -> updateState(channel.getUID(),
894 event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue())));
897 protected EventHandler defaultAmbientLightColorStateEventHandler() {
898 return event -> getThingChannel(CHANNEL_AMBIENT_LIGHT_COLOR_STATE)
899 .ifPresent(channel -> updateState(channel.getUID(),
900 event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue())));
903 protected EventHandler defaultAmbientLightCustomColorStateEventHandler() {
904 return event -> getThingChannel(CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE).ifPresent(channel -> {
905 String value = event.getValue();
907 updateState(channel.getUID(), mapColor(value));
909 updateState(channel.getUID(), UnDefType.UNDEF);
914 protected EventHandler updateRemoteControlActiveAndProgramOptionsStateEventHandler() {
916 defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE).handle(event);
919 if (Boolean.parseBoolean(event.getValue())) {
920 // update available program options if update was previously delayed and remote control is enabled
921 String programKey = programOptionsDelayedUpdate;
922 if (programKey != null) {
923 logger.debug("Delayed update of options for program {}", programKey);
924 updateProgramOptionsStateDescriptions(programKey);
925 programOptionsDelayedUpdate = null;
928 if (isChannelLinkedToProgramOptionNotFullySupportedByApi()) {
929 Optional<HomeConnectApiClient> apiClient = getApiClient();
930 if (apiClient.isPresent()) {
931 Program program = apiClient.get().getSelectedProgram(getThingHaId());
932 if (program != null) {
933 processProgramOptions(program.getOptions());
938 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
939 logger.debug("Could not update program options. {}", e.getMessage());
944 protected EventHandler updateProgramOptionsAndSelectedProgramStateEventHandler() {
946 defaultSelectedProgramStateEventHandler().handle(event);
949 Optional<HomeConnectApiClient> apiClient = getApiClient();
950 String programKey = event.getValue();
952 if (apiClient.isPresent() && programKey != null) {
953 Boolean remoteControl = (availableProgramOptionsCache.get(programKey) == null
954 || isChannelLinkedToProgramOptionNotFullySupportedByApi())
955 ? apiClient.get().isRemoteControlActive(getThingHaId())
958 // Delay the update of available program options if options are not yet cached and remote control is
960 if (availableProgramOptionsCache.get(programKey) == null && !remoteControl) {
961 logger.debug("Delay update of options for program {}", programKey);
962 programOptionsDelayedUpdate = programKey;
964 updateProgramOptionsStateDescriptions(programKey);
967 if (isChannelLinkedToProgramOptionNotFullySupportedByApi() && remoteControl) {
968 // update channels linked to program options
969 Program program = apiClient.get().getSelectedProgram(getThingHaId());
970 if (program != null) {
971 processProgramOptions(program.getOptions());
975 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
976 logger.debug("Could not update program options. {}", e.getMessage());
981 protected EventHandler defaultPercentQuantityTypeEventHandler(String channelId) {
982 return event -> getThingChannel(channelId).ifPresent(
983 channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), PERCENT)));
986 protected EventHandler defaultPercentHandler(String channelId) {
987 return event -> getThingChannel(channelId)
988 .ifPresent(channel -> updateState(channel.getUID(), new PercentType(event.getValueAsInt())));
991 protected ChannelUpdateHandler defaultDoorStateChannelUpdateHandler() {
992 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
993 Optional<HomeConnectApiClient> apiClient = getApiClient();
994 if (apiClient.isPresent()) {
995 Data data = apiClient.get().getDoorState(getThingHaId());
996 if (data.getValue() != null) {
997 return STATE_DOOR_OPEN.equals(data.getValue()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
999 return UnDefType.UNDEF;
1002 return UnDefType.UNDEF;
1007 protected ChannelUpdateHandler defaultPowerStateChannelUpdateHandler() {
1008 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1009 Optional<HomeConnectApiClient> apiClient = getApiClient();
1010 if (apiClient.isPresent()) {
1011 Data data = apiClient.get().getPowerState(getThingHaId());
1012 if (data.getValue() != null) {
1013 return OnOffType.from(STATE_POWER_ON.equals(data.getValue()));
1015 return UnDefType.UNDEF;
1018 return UnDefType.UNDEF;
1023 protected ChannelUpdateHandler defaultAmbientLightChannelUpdateHandler() {
1024 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1025 Optional<HomeConnectApiClient> apiClient = getApiClient();
1026 if (apiClient.isPresent()) {
1027 Data data = apiClient.get().getAmbientLightState(getThingHaId());
1028 if (data.getValue() != null) {
1029 boolean enabled = data.getValueAsBoolean();
1032 Data brightnessData = apiClient.get().getAmbientLightBrightnessState(getThingHaId());
1033 getThingChannel(CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE)
1034 .ifPresent(channel -> updateState(channel.getUID(),
1035 new PercentType(brightnessData.getValueAsInt())));
1038 Data colorData = apiClient.get().getAmbientLightColorState(getThingHaId());
1039 getThingChannel(CHANNEL_AMBIENT_LIGHT_COLOR_STATE).ifPresent(
1040 channel -> updateState(channel.getUID(), new StringType(colorData.getValue())));
1043 Data customColorData = apiClient.get().getAmbientLightCustomColorState(getThingHaId());
1044 getThingChannel(CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE).ifPresent(channel -> {
1045 String value = customColorData.getValue();
1046 if (value != null) {
1047 updateState(channel.getUID(), mapColor(value));
1049 updateState(channel.getUID(), UnDefType.UNDEF);
1053 return OnOffType.from(enabled);
1055 return UnDefType.UNDEF;
1058 return UnDefType.UNDEF;
1063 protected ChannelUpdateHandler defaultNoOpUpdateHandler() {
1064 return (channelUID, cache) -> updateState(channelUID, UnDefType.UNDEF);
1067 protected ChannelUpdateHandler defaultOperationStateChannelUpdateHandler() {
1068 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1069 Optional<HomeConnectApiClient> apiClient = getApiClient();
1070 if (apiClient.isPresent()) {
1071 Data data = apiClient.get().getOperationState(getThingHaId());
1073 String value = data.getValue();
1074 if (value != null) {
1075 operationState = data.getValue();
1076 return new StringType(mapStringType(value));
1078 operationState = null;
1079 return UnDefType.UNDEF;
1082 return UnDefType.UNDEF;
1087 protected ChannelUpdateHandler defaultRemoteControlActiveStateChannelUpdateHandler() {
1088 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1089 Optional<HomeConnectApiClient> apiClient = getApiClient();
1090 if (apiClient.isPresent()) {
1091 return OnOffType.from(apiClient.get().isRemoteControlActive(getThingHaId()));
1093 return OnOffType.OFF;
1097 protected ChannelUpdateHandler defaultLocalControlActiveStateChannelUpdateHandler() {
1098 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1099 Optional<HomeConnectApiClient> apiClient = getApiClient();
1100 if (apiClient.isPresent()) {
1101 return OnOffType.from(apiClient.get().isLocalControlActive(getThingHaId()));
1103 return OnOffType.OFF;
1107 protected ChannelUpdateHandler defaultRemoteStartAllowanceChannelUpdateHandler() {
1108 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1109 Optional<HomeConnectApiClient> apiClient = getApiClient();
1110 if (apiClient.isPresent()) {
1111 return OnOffType.from(apiClient.get().isRemoteControlStartAllowed(getThingHaId()));
1113 return OnOffType.OFF;
1117 protected ChannelUpdateHandler defaultSelectedProgramStateUpdateHandler() {
1118 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1119 Optional<HomeConnectApiClient> apiClient = getApiClient();
1120 if (apiClient.isPresent()) {
1121 Program program = apiClient.get().getSelectedProgram(getThingHaId());
1122 if (program != null) {
1123 processProgramOptions(program.getOptions());
1124 return new StringType(program.getKey());
1126 return UnDefType.UNDEF;
1129 return UnDefType.UNDEF;
1133 protected ChannelUpdateHandler updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() {
1134 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1135 Optional<HomeConnectApiClient> apiClient = getApiClient();
1136 if (apiClient.isPresent()) {
1137 Program program = apiClient.get().getSelectedProgram(getThingHaId());
1139 if (program != null) {
1140 updateProgramOptionsStateDescriptions(program.getKey());
1141 processProgramOptions(program.getOptions());
1143 return new StringType(program.getKey());
1145 return UnDefType.UNDEF;
1148 return UnDefType.UNDEF;
1152 protected ChannelUpdateHandler getAndUpdateSelectedProgramStateUpdateHandler() {
1153 return (channelUID, cache) -> {
1154 Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
1155 if (channel.isPresent()) {
1156 defaultSelectedProgramStateUpdateHandler().handle(channel.get().getUID(), cache);
1161 protected ChannelUpdateHandler getAndUpdateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() {
1162 return (channelUID, cache) -> {
1163 Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
1164 if (channel.isPresent()) {
1165 updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler()
1166 .handle(channel.get().getUID(), cache);
1171 protected ChannelUpdateHandler defaultActiveProgramStateUpdateHandler() {
1172 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1173 Optional<HomeConnectApiClient> apiClient = getApiClient();
1174 if (apiClient.isPresent()) {
1175 Program program = apiClient.get().getActiveProgram(getThingHaId());
1177 if (program != null) {
1178 processProgramOptions(program.getOptions());
1179 return new StringType(mapStringType(program.getKey()));
1181 resetProgramStateChannels(false);
1182 return UnDefType.UNDEF;
1185 return UnDefType.UNDEF;
1189 protected void handleTemperatureCommand(final ChannelUID channelUID, final Command command,
1190 final HomeConnectApiClient apiClient)
1191 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1192 if (command instanceof QuantityType) {
1193 QuantityType<?> quantity = (QuantityType<?>) command;
1199 if (quantity.getUnit().equals(SIUnits.CELSIUS) || quantity.getUnit().equals(ImperialUnits.FAHRENHEIT)) {
1200 unit = quantity.getUnit().toString();
1201 value = String.valueOf(quantity.intValue());
1203 logger.debug("Converting target temperature from {}{} to °C value. thing={}, haId={}",
1204 quantity.intValue(), quantity.getUnit().toString(), getThingLabel(), getThingHaId());
1206 var celsius = quantity.toUnit(SIUnits.CELSIUS);
1207 if (celsius == null) {
1208 logger.warn("Converting temperature to celsius failed! quantity={}", quantity);
1211 value = String.valueOf(celsius.intValue());
1213 logger.debug("Converted value {}{}", value, unit);
1216 if (value != null) {
1217 logger.debug("Set temperature to {} {}. thing={}, haId={}", value, unit, getThingLabel(),
1219 switch (channelUID.getId()) {
1220 case CHANNEL_REFRIGERATOR_SETPOINT_TEMPERATURE:
1221 apiClient.setFridgeSetpointTemperature(getThingHaId(), value, unit);
1222 case CHANNEL_FREEZER_SETPOINT_TEMPERATURE:
1223 apiClient.setFreezerSetpointTemperature(getThingHaId(), value, unit);
1225 case CHANNEL_SETPOINT_TEMPERATURE:
1226 apiClient.setProgramOptions(getThingHaId(), OPTION_SETPOINT_TEMPERATURE, value, unit, true,
1230 logger.debug("Unknown channel! Cannot set temperature. channelUID={}", channelUID);
1233 } catch (UnconvertibleException e) {
1234 logger.warn("Could not set temperature! haId={}, error={}", getThingHaId(), e.getMessage());
1239 protected void handleLightCommands(final ChannelUID channelUID, final Command command,
1240 final HomeConnectApiClient apiClient)
1241 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1242 switch (channelUID.getId()) {
1243 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1244 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1245 // turn light on if turned off
1246 turnLightOn(channelUID, apiClient);
1248 int newBrightness = BRIGHTNESS_MIN;
1249 if (command instanceof OnOffType) {
1250 newBrightness = command == OnOffType.ON ? BRIGHTNESS_MAX : BRIGHTNESS_MIN;
1251 } else if (command instanceof IncreaseDecreaseType) {
1252 int currentBrightness = getCurrentBrightness(channelUID, apiClient);
1253 if (command.equals(IncreaseDecreaseType.INCREASE)) {
1254 newBrightness = currentBrightness + BRIGHTNESS_DIM_STEP;
1256 newBrightness = currentBrightness - BRIGHTNESS_DIM_STEP;
1258 } else if (command instanceof PercentType) {
1259 newBrightness = (int) Math.floor(((PercentType) command).doubleValue());
1260 } else if (command instanceof DecimalType) {
1261 newBrightness = ((DecimalType) command).intValue();
1264 // check in in range
1265 newBrightness = Math.min(Math.max(newBrightness, BRIGHTNESS_MIN), BRIGHTNESS_MAX);
1267 setLightBrightness(channelUID, apiClient, newBrightness);
1269 case CHANNEL_FUNCTIONAL_LIGHT_STATE:
1270 if (command instanceof OnOffType) {
1271 apiClient.setFunctionalLightState(getThingHaId(), OnOffType.ON.equals(command));
1274 case CHANNEL_AMBIENT_LIGHT_STATE:
1275 if (command instanceof OnOffType) {
1276 apiClient.setAmbientLightState(getThingHaId(), OnOffType.ON.equals(command));
1279 case CHANNEL_AMBIENT_LIGHT_COLOR_STATE:
1280 if (command instanceof StringType) {
1281 turnLightOn(channelUID, apiClient);
1282 apiClient.setAmbientLightColorState(getThingHaId(), command.toFullString());
1285 case CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE:
1286 turnLightOn(channelUID, apiClient);
1288 // make sure 'custom color' is set as color
1289 Data ambientLightColorState = apiClient.getAmbientLightColorState(getThingHaId());
1290 if (!STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR.equals(ambientLightColorState.getValue())) {
1291 apiClient.setAmbientLightColorState(getThingHaId(), STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR);
1294 if (command instanceof HSBType) {
1295 apiClient.setAmbientLightCustomColorState(getThingHaId(), mapColor((HSBType) command));
1296 } else if (command instanceof StringType) {
1297 apiClient.setAmbientLightCustomColorState(getThingHaId(), command.toFullString());
1303 protected void handlePowerCommand(final ChannelUID channelUID, final Command command,
1304 final HomeConnectApiClient apiClient, String stateNotOn)
1305 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1306 if (command instanceof OnOffType && CHANNEL_POWER_STATE.equals(channelUID.getId())) {
1307 apiClient.setPowerState(getThingHaId(), OnOffType.ON.equals(command) ? STATE_POWER_ON : stateNotOn);
1311 private int getCurrentBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient)
1312 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1313 String id = channelUID.getId();
1314 if (CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE.equals(id)) {
1315 return apiClient.getFunctionalLightBrightnessState(getThingHaId()).getValueAsInt();
1317 return apiClient.getAmbientLightBrightnessState(getThingHaId()).getValueAsInt();
1321 private void setLightBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient, int value)
1322 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1323 switch (channelUID.getId()) {
1324 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1325 apiClient.setFunctionalLightBrightnessState(getThingHaId(), value);
1327 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1328 apiClient.setAmbientLightBrightnessState(getThingHaId(), value);
1333 private void turnLightOn(final ChannelUID channelUID, final HomeConnectApiClient apiClient)
1334 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1335 switch (channelUID.getId()) {
1336 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1337 Data functionalLightState = apiClient.getFunctionalLightState(getThingHaId());
1338 if (!functionalLightState.getValueAsBoolean()) {
1339 apiClient.setFunctionalLightState(getThingHaId(), true);
1342 case CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE:
1343 case CHANNEL_AMBIENT_LIGHT_COLOR_STATE:
1344 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1345 Data ambientLightState = apiClient.getAmbientLightState(getThingHaId());
1346 if (!ambientLightState.getValueAsBoolean()) {
1347 apiClient.setAmbientLightState(getThingHaId(), true);
1353 private Optional<Option> getOption(List<Option> options, String optionKey) {
1354 return options.stream().filter(option -> optionKey.equals(option.getKey())).findFirst();
1357 private void setStringChannelFromOption(String channelId, List<Option> options, String optionKey,
1358 @Nullable State defaultState) {
1359 setStringChannelFromOption(channelId, options, optionKey, value -> value, defaultState);
1362 private void setStringChannelFromOption(String channelId, List<Option> options, String optionKey,
1363 Function<String, String> mappingFunc, @Nullable State defaultState) {
1364 getLinkedChannel(channelId)
1365 .ifPresent(channel -> getOption(options, optionKey).map(option -> option.getValue()).ifPresentOrElse(
1366 value -> updateState(channel.getUID(), new StringType(mappingFunc.apply(value))), () -> {
1367 if (defaultState != null) {
1368 updateState(channel.getUID(), defaultState);
1373 private void setQuantityChannelFromOption(String channelId, List<Option> options, String optionKey,
1374 Function<@Nullable String, Unit<?>> unitMappingFunc, @Nullable State defaultState) {
1375 getLinkedChannel(channelId).ifPresent(channel -> getOption(options, optionKey) //
1377 option -> updateState(channel.getUID(),
1378 new QuantityType<>(option.getValueAsInt(), unitMappingFunc.apply(option.getUnit()))),
1380 if (defaultState != null) {
1381 updateState(channel.getUID(), defaultState);
1386 private void setOnOffChannelFromOption(String channelId, List<Option> options, String optionKey,
1387 @Nullable State defaultState) {
1388 getLinkedChannel(channelId)
1389 .ifPresent(channel -> getOption(options, optionKey).map(option -> option.getValueAsBoolean())
1390 .ifPresentOrElse(value -> updateState(channel.getUID(), OnOffType.from(value)), () -> {
1391 if (defaultState != null) {
1392 updateState(channel.getUID(), defaultState);
1397 protected void processProgramOptions(List<Option> options) {
1398 String operationState = getOperationState();
1400 Map.of(CHANNEL_WASHER_TEMPERATURE, OPTION_WASHER_TEMPERATURE, CHANNEL_WASHER_SPIN_SPEED,
1401 OPTION_WASHER_SPIN_SPEED, CHANNEL_WASHER_IDOS1_LEVEL, OPTION_WASHER_IDOS_1_DOSING_LEVEL,
1402 CHANNEL_WASHER_IDOS2_LEVEL, OPTION_WASHER_IDOS_2_DOSING_LEVEL, CHANNEL_DRYER_DRYING_TARGET,
1403 OPTION_DRYER_DRYING_TARGET)
1404 .forEach((channel, option) -> setStringChannelFromOption(channel, options, option, UnDefType.UNDEF));
1406 Map.of(CHANNEL_WASHER_IDOS1, OPTION_WASHER_IDOS_1_ACTIVE, CHANNEL_WASHER_IDOS2, OPTION_WASHER_IDOS_2_ACTIVE,
1407 CHANNEL_WASHER_LESS_IRONING, OPTION_WASHER_LESS_IRONING, CHANNEL_WASHER_PRE_WASH,
1408 OPTION_WASHER_PRE_WASH, CHANNEL_WASHER_SOAK, OPTION_WASHER_SOAK)
1409 .forEach((channel, option) -> setOnOffChannelFromOption(channel, options, option, OnOffType.OFF));
1411 setStringChannelFromOption(CHANNEL_HOOD_INTENSIVE_LEVEL, options, OPTION_HOOD_INTENSIVE_LEVEL,
1412 value -> mapStageStringType(value), UnDefType.UNDEF);
1413 setStringChannelFromOption(CHANNEL_HOOD_VENTING_LEVEL, options, OPTION_HOOD_VENTING_LEVEL,
1414 value -> mapStageStringType(value), UnDefType.UNDEF);
1415 setQuantityChannelFromOption(CHANNEL_SETPOINT_TEMPERATURE, options, OPTION_SETPOINT_TEMPERATURE,
1416 unit -> mapTemperature(unit), UnDefType.UNDEF);
1417 setQuantityChannelFromOption(CHANNEL_DURATION, options, OPTION_DURATION, unit -> SECOND, UnDefType.UNDEF);
1418 // The channel remaining_program_time_state depends on two program options: FinishInRelative and
1419 // RemainingProgramTime. When the start of the program is delayed, the two options are returned by the API with
1420 // different values. In this case, we consider the value of the option FinishInRelative.
1421 setQuantityChannelFromOption(CHANNEL_REMAINING_PROGRAM_TIME_STATE, options,
1422 getOption(options, OPTION_FINISH_IN_RELATIVE).isPresent() ? OPTION_FINISH_IN_RELATIVE
1423 : OPTION_REMAINING_PROGRAM_TIME,
1424 unit -> SECOND, UnDefType.UNDEF);
1425 setQuantityChannelFromOption(CHANNEL_ELAPSED_PROGRAM_TIME, options, OPTION_ELAPSED_PROGRAM_TIME, unit -> SECOND,
1426 new QuantityType<>(0, SECOND));
1427 setQuantityChannelFromOption(CHANNEL_PROGRAM_PROGRESS_STATE, options, OPTION_PROGRAM_PROGRESS, unit -> PERCENT,
1428 new QuantityType<>(0, PERCENT));
1429 // When the program is not in ready state, the vario perfect option is not always provided by the API returning
1430 // the options of the current active program. So in this case we avoid updating the channel (to the default
1431 // state) by passing a null value as parameter.to setStringChannelFromOption.
1432 setStringChannelFromOption(CHANNEL_WASHER_VARIO_PERFECT, options, OPTION_WASHER_VARIO_PERFECT,
1433 OPERATION_STATE_READY.equals(operationState)
1434 ? new StringType("LaundryCare.Common.EnumType.VarioPerfect.Off")
1436 setStringChannelFromOption(CHANNEL_WASHER_RINSE_PLUS, options, OPTION_WASHER_RINSE_PLUS,
1437 new StringType("LaundryCare.Washer.EnumType.RinsePlus.Off"));
1438 setQuantityChannelFromOption(CHANNEL_PROGRAM_ENERGY, options, OPTION_WASHER_ENERGY_FORECAST, unit -> PERCENT,
1440 setQuantityChannelFromOption(CHANNEL_PROGRAM_WATER, options, OPTION_WASHER_WATER_FORECAST, unit -> PERCENT,
1444 protected String convertWasherTemperature(String value) {
1445 if (value.startsWith(TEMPERATURE_PREFIX + "GC")) {
1446 return value.replace(TEMPERATURE_PREFIX + "GC", "") + "°C";
1449 if (value.startsWith(TEMPERATURE_PREFIX + "Ul")) {
1450 return mapStringType(value.replace(TEMPERATURE_PREFIX + "Ul", ""));
1453 return mapStringType(value);
1456 protected String convertWasherSpinSpeed(String value) {
1457 if (value.startsWith(SPIN_SPEED_PREFIX + "RPM")) {
1458 return value.replace(SPIN_SPEED_PREFIX + "RPM", "") + " RPM";
1461 if (value.startsWith(SPIN_SPEED_PREFIX + "Ul")) {
1462 return value.replace(SPIN_SPEED_PREFIX + "Ul", "");
1465 return mapStringType(value);
1468 protected void updateProgramOptionsStateDescriptions(String programKey)
1469 throws AuthorizationException, ApplianceOfflineException {
1470 Optional<HomeConnectApiClient> apiClient = getApiClient();
1471 if (apiClient.isPresent()) {
1472 boolean cacheToSet = false;
1473 List<AvailableProgramOption> availableProgramOptions;
1474 if (availableProgramOptionsCache.containsKey(programKey)) {
1475 logger.debug("Returning cached options for program '{}'.", programKey);
1476 availableProgramOptions = availableProgramOptionsCache.get(programKey);
1477 availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
1478 : Collections.emptyList();
1480 // Depending on the current program operation state, the APi request could trigger a
1481 // CommunicationException exception due to returned status code 409
1483 availableProgramOptions = apiClient.get().getProgramOptions(getThingHaId(), programKey);
1484 if (availableProgramOptions == null) {
1485 // Program is unsupported, to avoid calling again the API for this program, save in cache either
1486 // the predefined options provided by the binding if they exist, or an empty list of options
1487 if (unsupportedProgramOptions.containsKey(programKey)) {
1488 availableProgramOptions = unsupportedProgramOptions.get(programKey);
1489 availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
1491 logger.debug("Saving predefined options in cache for unsupported program '{}'.",
1494 availableProgramOptions = emptyList();
1495 logger.debug("Saving empty options in cache for unsupported program '{}'.", programKey);
1497 availableProgramOptionsCache.put(programKey, availableProgramOptions);
1499 // Add the unsupported program in programs cache and refresh the dynamic state description
1500 if (addUnsupportedProgramInCache(programKey)) {
1501 updateSelectedProgramStateDescription();
1504 // If no options are returned by the API, using predefined options if available
1505 if (availableProgramOptions.isEmpty() && unsupportedProgramOptions.containsKey(programKey)) {
1506 availableProgramOptions = unsupportedProgramOptions.get(programKey);
1507 availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
1512 } catch (CommunicationException e) {
1513 availableProgramOptions = emptyList();
1517 Optional<Channel> channelSpinSpeed = getThingChannel(CHANNEL_WASHER_SPIN_SPEED);
1518 Optional<Channel> channelTemperature = getThingChannel(CHANNEL_WASHER_TEMPERATURE);
1519 Optional<Channel> channelDryingTarget = getThingChannel(CHANNEL_DRYER_DRYING_TARGET);
1521 Optional<AvailableProgramOption> optionsSpinSpeed = availableProgramOptions.stream()
1522 .filter(option -> OPTION_WASHER_SPIN_SPEED.equals(option.getKey())).findFirst();
1523 Optional<AvailableProgramOption> optionsTemperature = availableProgramOptions.stream()
1524 .filter(option -> OPTION_WASHER_TEMPERATURE.equals(option.getKey())).findFirst();
1525 Optional<AvailableProgramOption> optionsDryingTarget = availableProgramOptions.stream()
1526 .filter(option -> OPTION_DRYER_DRYING_TARGET.equals(option.getKey())).findFirst();
1528 // Save options in cache only if we got options for all expected channels
1529 if (cacheToSet && (!channelSpinSpeed.isPresent() || optionsSpinSpeed.isPresent())
1530 && (!channelTemperature.isPresent() || optionsTemperature.isPresent())
1531 && (!channelDryingTarget.isPresent() || optionsDryingTarget.isPresent())) {
1532 logger.debug("Saving options in cache for program '{}'.", programKey);
1533 availableProgramOptionsCache.put(programKey, availableProgramOptions);
1536 channelSpinSpeed.ifPresent(channel -> optionsSpinSpeed.ifPresentOrElse(
1537 option -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1538 createStateOptions(option, this::convertWasherSpinSpeed)),
1539 () -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList())));
1540 channelTemperature.ifPresent(channel -> optionsTemperature.ifPresentOrElse(
1541 option -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1542 createStateOptions(option, this::convertWasherTemperature)),
1543 () -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList())));
1544 channelDryingTarget.ifPresent(channel -> optionsDryingTarget.ifPresentOrElse(
1545 option -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1546 createStateOptions(option, this::mapStringType)),
1547 () -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList())));
1551 protected HomeConnectDynamicStateDescriptionProvider getDynamicStateDescriptionProvider() {
1552 return dynamicStateDescriptionProvider;
1555 private List<StateOption> createStateOptions(AvailableProgramOption option,
1556 Function<String, String> stateConverter) {
1557 return option.getAllowedValues().stream().map(av -> new StateOption(av, stateConverter.apply(av)))
1558 .collect(Collectors.toList());
1561 private synchronized void scheduleOfflineMonitor1() {
1562 this.reinitializationFuture1 = scheduler.schedule(() -> {
1563 if (isBridgeOnline() && isThingOffline()) {
1564 logger.debug("Offline monitor 1: Check if thing is ONLINE. thing={}, haId={}", getThingLabel(),
1566 refreshThingStatus();
1567 if (isThingOnline()) {
1568 logger.debug("Offline monitor 1: Thing status changed to ONLINE. thing={}, haId={}",
1569 getThingLabel(), getThingHaId());
1572 scheduleOfflineMonitor1();
1575 scheduleOfflineMonitor1();
1577 }, AbstractHomeConnectThingHandler.OFFLINE_MONITOR_1_DELAY_MIN, TimeUnit.MINUTES);
1580 private synchronized void stopOfflineMonitor1() {
1581 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture1;
1582 if (reinitializationFuture != null) {
1583 reinitializationFuture.cancel(false);
1584 this.reinitializationFuture1 = null;
1588 private synchronized void scheduleOfflineMonitor2() {
1589 this.reinitializationFuture2 = scheduler.schedule(() -> {
1590 if (isBridgeOnline() && !accessible.get()) {
1591 logger.debug("Offline monitor 2: Check if thing is ONLINE. thing={}, haId={}", getThingLabel(),
1593 refreshThingStatus();
1594 if (isThingOnline()) {
1595 logger.debug("Offline monitor 2: Thing status changed to ONLINE. thing={}, haId={}",
1596 getThingLabel(), getThingHaId());
1599 scheduleOfflineMonitor2();
1602 scheduleOfflineMonitor2();
1604 }, AbstractHomeConnectThingHandler.OFFLINE_MONITOR_2_DELAY_MIN, TimeUnit.MINUTES);
1607 private synchronized void stopOfflineMonitor2() {
1608 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture2;
1609 if (reinitializationFuture != null) {
1610 reinitializationFuture.cancel(false);
1611 this.reinitializationFuture2 = null;
1615 private synchronized void scheduleRetryRegistering() {
1616 this.reinitializationFuture3 = scheduler.schedule(() -> {
1617 logger.debug("Try to register event listener again. haId={}", getThingHaId());
1618 unregisterEventListener();
1619 registerEventListener();
1620 }, AbstractHomeConnectThingHandler.EVENT_LISTENER_CONNECT_RETRY_DELAY_MIN, TimeUnit.MINUTES);
1623 private synchronized void stopRetryRegistering() {
1624 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture3;
1625 if (reinitializationFuture != null) {
1626 reinitializationFuture.cancel(true);
1627 this.reinitializationFuture3 = null;
1631 protected List<AvailableProgram> getPrograms()
1632 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1633 if (!programsCache.isEmpty()) {
1634 logger.debug("Returning cached programs for '{}'.", getThingHaId());
1635 return programsCache;
1637 Optional<HomeConnectApiClient> apiClient = getApiClient();
1638 if (apiClient.isPresent()) {
1639 programsCache.addAll(apiClient.get().getPrograms(getThingHaId()));
1640 return programsCache;
1642 throw new CommunicationException("API not initialized");
1648 * Add an entry in the programs cache and mark it as unsupported
1650 * @param programKey program id
1651 * @return true if an entry was added in the cache
1653 private boolean addUnsupportedProgramInCache(String programKey) {
1654 Optional<AvailableProgram> prog = programsCache.stream().filter(program -> programKey.equals(program.getKey()))
1656 if (!prog.isPresent()) {
1657 programsCache.add(new AvailableProgram(programKey, false));
1658 logger.debug("{} added in programs cache as an unsupported program", programKey);
1665 * Check if a program is marked as supported in the programs cache
1667 * @param programKey program id
1668 * @return true if the program is in the cache and marked as supported
1670 protected boolean isProgramSupported(String programKey) {
1671 Optional<AvailableProgram> prog = programsCache.stream().filter(program -> programKey.equals(program.getKey()))
1673 return prog.isPresent() && prog.get().isSupported();