2 * Copyright (c) 2010-2023 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.*;
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.Mass;
39 import javax.measure.quantity.Temperature;
41 import org.eclipse.jdt.annotation.NonNullByDefault;
42 import org.eclipse.jdt.annotation.Nullable;
43 import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
44 import org.openhab.binding.homeconnect.internal.client.HomeConnectEventSourceClient;
45 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
46 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
47 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
48 import org.openhab.binding.homeconnect.internal.client.listener.HomeConnectEventListener;
49 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgram;
50 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
51 import org.openhab.binding.homeconnect.internal.client.model.Data;
52 import org.openhab.binding.homeconnect.internal.client.model.Event;
53 import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
54 import org.openhab.binding.homeconnect.internal.client.model.Option;
55 import org.openhab.binding.homeconnect.internal.client.model.Program;
56 import org.openhab.binding.homeconnect.internal.handler.cache.ExpiringStateMap;
57 import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
58 import org.openhab.core.auth.client.oauth2.OAuthException;
59 import org.openhab.core.library.types.DecimalType;
60 import org.openhab.core.library.types.HSBType;
61 import org.openhab.core.library.types.IncreaseDecreaseType;
62 import org.openhab.core.library.types.OnOffType;
63 import org.openhab.core.library.types.OpenClosedType;
64 import org.openhab.core.library.types.PercentType;
65 import org.openhab.core.library.types.QuantityType;
66 import org.openhab.core.library.types.StringType;
67 import org.openhab.core.library.unit.ImperialUnits;
68 import org.openhab.core.library.unit.SIUnits;
69 import org.openhab.core.thing.Bridge;
70 import org.openhab.core.thing.Channel;
71 import org.openhab.core.thing.ChannelUID;
72 import org.openhab.core.thing.Thing;
73 import org.openhab.core.thing.ThingStatusDetail;
74 import org.openhab.core.thing.ThingStatusInfo;
75 import org.openhab.core.thing.binding.BaseThingHandler;
76 import org.openhab.core.thing.binding.BridgeHandler;
77 import org.openhab.core.types.Command;
78 import org.openhab.core.types.RefreshType;
79 import org.openhab.core.types.State;
80 import org.openhab.core.types.StateOption;
81 import org.openhab.core.types.UnDefType;
82 import org.slf4j.Logger;
83 import org.slf4j.LoggerFactory;
86 * The {@link AbstractHomeConnectThingHandler} is responsible for handling commands, which are
87 * sent to one of the channels.
89 * @author Jonas Brüstel - Initial contribution
90 * @author Laurent Garnier - programs cache moved and enhanced to allow adding unsupported programs
93 public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler implements HomeConnectEventListener {
95 private static final int CACHE_TTL_SEC = 2;
96 private static final int OFFLINE_MONITOR_1_DELAY_MIN = 30;
97 private static final int OFFLINE_MONITOR_2_DELAY_MIN = 4;
98 private static final int EVENT_LISTENER_CONNECT_RETRY_DELAY_MIN = 10;
100 private @Nullable String operationState;
101 private @Nullable ScheduledFuture<?> reinitializationFuture1;
102 private @Nullable ScheduledFuture<?> reinitializationFuture2;
103 private @Nullable ScheduledFuture<?> reinitializationFuture3;
104 private boolean ignoreEventSourceClosedEvent;
105 private @Nullable String programOptionsDelayedUpdate;
107 private final ConcurrentHashMap<String, EventHandler> eventHandlers;
108 private final ConcurrentHashMap<String, ChannelUpdateHandler> channelUpdateHandlers;
109 private final HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
110 private final ExpiringStateMap expiringStateMap;
111 private final AtomicBoolean accessible;
112 private final Logger logger = LoggerFactory.getLogger(AbstractHomeConnectThingHandler.class);
113 private final List<AvailableProgram> programsCache;
114 private final Map<String, List<AvailableProgramOption>> availableProgramOptionsCache;
115 private final Map<String, List<AvailableProgramOption>> unsupportedProgramOptions;
117 public AbstractHomeConnectThingHandler(Thing thing,
118 HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
120 eventHandlers = new ConcurrentHashMap<>();
121 channelUpdateHandlers = new ConcurrentHashMap<>();
122 this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider;
123 expiringStateMap = new ExpiringStateMap(Duration.ofSeconds(CACHE_TTL_SEC));
124 accessible = new AtomicBoolean(false);
125 programsCache = new CopyOnWriteArrayList<>();
126 availableProgramOptionsCache = new ConcurrentHashMap<>();
127 unsupportedProgramOptions = new ConcurrentHashMap<>();
129 configureEventHandlers(eventHandlers);
130 configureChannelUpdateHandlers(channelUpdateHandlers);
131 configureUnsupportedProgramOptions(unsupportedProgramOptions);
135 public void initialize() {
136 if (getBridgeHandler().isEmpty()) {
137 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
138 accessible.set(false);
139 } else if (isBridgeOffline()) {
140 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
141 accessible.set(false);
143 updateStatus(UNKNOWN);
144 scheduler.submit(() -> {
145 refreshThingStatus(); // set ONLINE / OFFLINE
146 updateSelectedProgramStateDescription();
148 registerEventListener();
149 scheduleOfflineMonitor1();
150 scheduleOfflineMonitor2();
156 public void dispose() {
157 stopRetryRegistering();
158 stopOfflineMonitor1();
159 stopOfflineMonitor2();
160 unregisterEventListener(true);
164 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
165 logger.debug("Bridge status changed to {} ({}). haId={}", bridgeStatusInfo, getThingLabel(), getThingHaId());
169 private void reinitialize() {
170 logger.debug("Reinitialize thing handler ({}). haId={}", getThingLabel(), getThingHaId());
171 stopRetryRegistering();
172 stopOfflineMonitor1();
173 stopOfflineMonitor2();
174 unregisterEventListener();
179 * Handles a command for a given channel.
181 * This method is only called, if the thing has been initialized (status ONLINE/OFFLINE/UNKNOWN).
184 * @param channelUID the {@link ChannelUID} of the channel to which the command was sent
185 * @param command the {@link Command}
186 * @param apiClient the {@link HomeConnectApiClient}
187 * @throws CommunicationException communication problem
188 * @throws AuthorizationException authorization problem
189 * @throws ApplianceOfflineException appliance offline
191 protected void handleCommand(ChannelUID channelUID, Command command, HomeConnectApiClient apiClient)
192 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
193 if (command instanceof RefreshType) {
194 updateChannel(channelUID);
195 } else if (command instanceof StringType && CHANNEL_BASIC_ACTIONS_STATE.equals(channelUID.getId())
196 && getBridgeHandler().isPresent()) {
197 updateState(channelUID, new StringType(""));
199 if (COMMAND_START.equalsIgnoreCase(command.toFullString())) {
200 HomeConnectBridgeHandler homeConnectBridgeHandler = getBridgeHandler().get();
201 // workaround for api bug
202 // if simulator, program options have to be passed along with the desired program
203 // if non simulator, some options throw a "SDK.Error.UnsupportedOption" error
204 if (homeConnectBridgeHandler.getConfiguration().isSimulator()) {
205 apiClient.startSelectedProgram(getThingHaId());
207 Program selectedProgram = apiClient.getSelectedProgram(getThingHaId());
208 if (selectedProgram != null) {
209 apiClient.startProgram(getThingHaId(), selectedProgram.getKey());
212 } else if (COMMAND_STOP.equalsIgnoreCase(command.toFullString())) {
213 apiClient.stopProgram(getThingHaId());
214 } else if (COMMAND_SELECTED.equalsIgnoreCase(command.toFullString())) {
215 apiClient.getSelectedProgram(getThingHaId());
217 logger.debug("Start custom program. command={} haId={}", command.toFullString(), getThingHaId());
218 apiClient.startCustomProgram(getThingHaId(), command.toFullString());
220 } else if (command instanceof StringType && CHANNEL_SELECTED_PROGRAM_STATE.equals(channelUID.getId())
221 && isProgramSupported(command.toFullString())) {
222 apiClient.setSelectedProgram(getThingHaId(), command.toFullString());
227 public final void handleCommand(ChannelUID channelUID, Command command) {
228 var apiClient = getApiClient();
229 if ((isThingReadyToHandleCommand() || (this instanceof HomeConnectHoodHandler && isBridgeOnline()
230 && isThingAccessibleViaServerSentEvents())) && apiClient.isPresent()) {
231 logger.debug("Handle \"{}\" command ({}). haId={}", command, channelUID.getId(), getThingHaId());
233 handleCommand(channelUID, command, apiClient.get());
234 } catch (ApplianceOfflineException e) {
235 logger.debug("Could not handle command {}. Appliance offline. thing={}, haId={}, error={}",
236 command.toFullString(), getThingLabel(), getThingHaId(), e.getMessage());
237 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
238 resetChannelsOnOfflineEvent();
239 resetProgramStateChannels(true);
240 } catch (CommunicationException e) {
241 logger.debug("Could not handle command {}. API communication problem! error={}, haId={}",
242 command.toFullString(), e.getMessage(), getThingHaId());
243 } catch (AuthorizationException e) {
244 logger.debug("Could not handle command {}. Authorization problem! error={}, haId={}",
245 command.toFullString(), e.getMessage(), getThingHaId());
247 handleAuthenticationError(e);
253 public void onEvent(Event event) {
254 if (DISCONNECTED.equals(event.getType())) {
255 logger.debug("Received DISCONNECTED event. Set {} to OFFLINE. haId={}", getThing().getLabel(),
257 updateStatus(OFFLINE);
258 resetChannelsOnOfflineEvent();
259 resetProgramStateChannels(true);
260 } else if (isThingOnline() && CONNECTED.equals(event.getType())) {
261 logger.debug("Received CONNECTED event. Update power state channel. haId={}", getThingHaId());
262 getThingChannel(CHANNEL_POWER_STATE).ifPresent(c -> updateChannel(c.getUID()));
263 } else if (isThingOffline() && !KEEP_ALIVE.equals(event.getType())) {
264 updateStatus(ONLINE);
265 logger.debug("Set {} to ONLINE and update channels. haId={}", getThing().getLabel(), getThingHaId());
266 updateSelectedProgramStateDescription();
270 String key = event.getKey();
271 if (EVENT_OPERATION_STATE.equals(key)) {
272 operationState = event.getValue() == null ? null : event.getValue();
275 if (key != null && eventHandlers.containsKey(key)) {
276 EventHandler eventHandler = eventHandlers.get(key);
277 if (eventHandler != null) {
278 eventHandler.handle(event);
282 accessible.set(true);
286 public void onClosed() {
287 if (ignoreEventSourceClosedEvent) {
288 logger.debug("Ignoring event source close event. thing={}, haId={}", getThing().getLabel(), getThingHaId());
290 unregisterEventListener();
291 refreshThingStatus();
292 registerEventListener();
297 public void onRateLimitReached() {
298 unregisterEventListener();
301 scheduleRetryRegistering();
305 * Register event listener.
307 protected void registerEventListener() {
308 if (isBridgeOnline() && isThingAccessibleViaServerSentEvents()) {
309 getEventSourceClient().ifPresent(client -> {
311 ignoreEventSourceClosedEvent = false;
312 client.registerEventListener(getThingHaId(), this);
313 } catch (CommunicationException | AuthorizationException e) {
314 logger.warn("Could not open event source connection. thing={}, haId={}, error={}", getThingLabel(),
315 getThingHaId(), e.getMessage());
322 * Unregister event listener.
324 protected void unregisterEventListener() {
325 unregisterEventListener(false);
328 private void unregisterEventListener(boolean immediate) {
329 getEventSourceClient().ifPresent(client -> {
330 ignoreEventSourceClosedEvent = true;
331 client.unregisterEventListener(this, immediate, false);
336 * Get {@link HomeConnectApiClient}.
338 * @return client instance
340 protected Optional<HomeConnectApiClient> getApiClient() {
341 return getBridgeHandler().map(HomeConnectBridgeHandler::getApiClient);
345 * Get {@link HomeConnectEventSourceClient}.
347 * @return client instance if present
349 protected Optional<HomeConnectEventSourceClient> getEventSourceClient() {
350 return getBridgeHandler().map(HomeConnectBridgeHandler::getEventSourceClient);
354 * Update state description of selected program (Fetch programs via API).
356 protected void updateSelectedProgramStateDescription() {
357 if (isBridgeOffline() || isThingOffline()) {
362 List<StateOption> stateOptions = getPrograms().stream()
363 .map(p -> new StateOption(p.getKey(), mapStringType(p.getKey()))).collect(Collectors.toList());
365 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(
366 channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions));
367 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
368 logger.debug("Could not fetch available programs. thing={}, haId={}, error={}", getThingLabel(),
369 getThingHaId(), e.getMessage());
370 removeSelectedProgramStateDescription();
375 * Remove state description of selected program.
377 protected void removeSelectedProgramStateDescription() {
378 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
379 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList()));
383 * Is thing ready to process commands. If bridge or thing itself is offline commands will be ignored.
385 * @return true if ready
387 protected boolean isThingReadyToHandleCommand() {
388 if (isBridgeOffline()) {
389 logger.debug("Bridge is OFFLINE. Ignore command. thing={}, haId={}", getThingLabel(), getThingHaId());
393 if (isThingOffline()) {
394 logger.debug("{} is OFFLINE. Ignore command. haId={}", getThing().getLabel(), getThingHaId());
402 * Checks if bridge is online and set.
404 * @return true if online
406 protected boolean isBridgeOnline() {
407 Bridge bridge = getBridge();
408 return bridge != null && ONLINE.equals(bridge.getStatus());
412 * Checks if bridge is offline or not set.
414 * @return true if offline
416 protected boolean isBridgeOffline() {
417 return !isBridgeOnline();
421 * Checks if thing is online.
423 * @return true if online
425 protected boolean isThingOnline() {
426 return ONLINE.equals(getThing().getStatus());
430 * Checks if thing is connected to the cloud and accessible via SSE.
432 * @return true if yes
434 public boolean isThingAccessibleViaServerSentEvents() {
435 return accessible.get();
439 * Checks if thing is offline.
441 * @return true if offline
443 protected boolean isThingOffline() {
444 return !isThingOnline();
448 * Get {@link HomeConnectBridgeHandler}.
450 * @return bridge handler
452 protected Optional<HomeConnectBridgeHandler> getBridgeHandler() {
453 Bridge bridge = getBridge();
454 if (bridge != null) {
455 BridgeHandler bridgeHandler = bridge.getHandler();
456 if (bridgeHandler instanceof HomeConnectBridgeHandler homeConnectBridgeHandler) {
457 return Optional.of(homeConnectBridgeHandler);
460 return Optional.empty();
464 * Get thing channel by given channel id.
466 * @param channelId channel id
469 protected Optional<Channel> getThingChannel(String channelId) {
470 Channel channel = getThing().getChannel(channelId);
471 if (channel == null) {
472 return Optional.empty();
474 return Optional.of(channel);
479 * Get thing linked channel by given channel id.
481 * @param channelId channel id
482 * @return channel if linked
484 protected Optional<Channel> getLinkedChannel(String channelId) {
485 Channel channel = getThing().getChannel(channelId);
486 if (channel == null || !isLinked(channelId)) {
487 return Optional.empty();
489 return Optional.of(channel);
494 * Configure channel update handlers. Classes which extend {@link AbstractHomeConnectThingHandler} must implement
495 * this class and add handlers.
497 * @param handlers channel update handlers
499 protected abstract void configureChannelUpdateHandlers(final Map<String, ChannelUpdateHandler> handlers);
502 * Configure event handlers. Classes which extend {@link AbstractHomeConnectThingHandler} must implement
503 * this class and add handlers.
505 * @param handlers Server-Sent-Event handlers
507 protected abstract void configureEventHandlers(final Map<String, EventHandler> handlers);
509 protected void configureUnsupportedProgramOptions(final Map<String, List<AvailableProgramOption>> programOptions) {
512 protected boolean isChannelLinkedToProgramOptionNotFullySupportedByApi() {
517 * Update all channels via API.
520 protected void updateChannels() {
521 if (isBridgeOffline()) {
522 logger.debug("Bridge handler not found or offline. Stopping update of channels. thing={}, haId={}",
523 getThingLabel(), getThingHaId());
524 } else if (isThingOffline()) {
525 logger.debug("{} offline. Stopping update of channels. haId={}", getThing().getLabel(), getThingHaId());
527 List<Channel> channels = getThing().getChannels();
528 for (Channel channel : channels) {
529 updateChannel(channel.getUID());
535 * Update Channel values via API.
537 * @param channelUID channel UID
539 protected void updateChannel(ChannelUID channelUID) {
540 if (getApiClient().isEmpty()) {
541 logger.error("Cannot update channel. No instance of api client found! thing={}, haId={}", getThingLabel(),
546 if (!isThingReadyToHandleCommand()) {
550 if ((isLinked(channelUID) || CHANNEL_OPERATION_STATE.equals(channelUID.getId())) // always update operation
552 && channelUpdateHandlers.containsKey(channelUID.getId())) {
554 ChannelUpdateHandler channelUpdateHandler = channelUpdateHandlers.get(channelUID.getId());
555 if (channelUpdateHandler != null) {
556 channelUpdateHandler.handle(channelUID, expiringStateMap);
558 } catch (ApplianceOfflineException e) {
560 "API communication problem while trying to update! Appliance offline. thing={}, haId={}, error={}",
561 getThingLabel(), getThingHaId(), e.getMessage());
562 updateStatus(OFFLINE);
563 resetChannelsOnOfflineEvent();
564 resetProgramStateChannels(true);
565 } catch (CommunicationException e) {
566 logger.debug("API communication problem while trying to update! thing={}, haId={}, error={}",
567 getThingLabel(), getThingHaId(), e.getMessage());
568 } catch (AuthorizationException e) {
569 logger.debug("Authentication problem while trying to update! thing={}, haId={}", getThingLabel(),
571 handleAuthenticationError(e);
577 * Reset program related channels.
579 * @param offline true if the device is considered as OFFLINE
581 protected void resetProgramStateChannels(boolean offline) {
582 logger.debug("Resetting active program channel states. thing={}, haId={}", getThingLabel(), getThingHaId());
586 * Reset all channels on OFFLINE event.
588 protected void resetChannelsOnOfflineEvent() {
589 logger.debug("Resetting channel states due to OFFLINE event. thing={}, haId={}", getThingLabel(),
591 getLinkedChannel(CHANNEL_POWER_STATE).ifPresent(channel -> updateState(channel.getUID(), OnOffType.OFF));
592 getLinkedChannel(CHANNEL_OPERATION_STATE).ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
593 getLinkedChannel(CHANNEL_DOOR_STATE).ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
594 getLinkedChannel(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE)
595 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
596 getLinkedChannel(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE)
597 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
598 getLinkedChannel(CHANNEL_REMOTE_START_ALLOWANCE_STATE)
599 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
600 getLinkedChannel(CHANNEL_SELECTED_PROGRAM_STATE)
601 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
605 * Map Home Connect key and value names to label.
606 * e.g. Dishcare.Dishwasher.Program.Eco50 --> Eco50 or BSH.Common.EnumType.OperationState.DelayedStart --> Delayed
610 * @return human readable label
612 protected String mapStringType(String type) {
613 int index = type.lastIndexOf(".");
614 if (index > 0 && type.length() > index) {
615 String sub = type.substring(index + 1);
616 StringBuilder sb = new StringBuilder();
617 for (String word : sub.split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])")) {
621 return sb.toString().trim();
627 * Map Home Connect stage value to label.
628 * e.g. Cooking.Hood.EnumType.IntensiveStage.IntensiveStage1 --> 1
631 * @return human readable label
633 protected String mapStageStringType(String stage) {
636 case STAGE_INTENSIVE_STAGE_OFF:
639 case STAGE_FAN_STAGE_01:
640 case STAGE_INTENSIVE_STAGE_1:
643 case STAGE_FAN_STAGE_02:
644 case STAGE_INTENSIVE_STAGE_2:
647 case STAGE_FAN_STAGE_03:
650 case STAGE_FAN_STAGE_04:
653 case STAGE_FAN_STAGE_05:
657 stage = mapStringType(stage);
664 * Map unit string (returned by home connect api) to Unit
666 * @param unit String eg. "°C"
669 protected Unit<Temperature> mapTemperature(@Nullable String unit) {
672 } else if (unit.endsWith("C")) {
680 * Map unit string (returned by home connect api) to Unit
682 * @param unit String eg. "gram"
685 protected Unit<Mass> mapMass(@Nullable String unit) {
686 if ("gram".equalsIgnoreCase(unit)) {
688 } else if ("kilogram".equalsIgnoreCase(unit)) {
696 * Map hex representation of color to HSB type.
698 * @param colorCode color code e.g. #001122
701 protected HSBType mapColor(String colorCode) {
702 HSBType color = HSBType.WHITE;
704 if (colorCode.length() == 7) {
705 int r = Integer.valueOf(colorCode.substring(1, 3), 16);
706 int g = Integer.valueOf(colorCode.substring(3, 5), 16);
707 int b = Integer.valueOf(colorCode.substring(5, 7), 16);
708 color = HSBType.fromRGB(r, g, b);
714 * Map HSB color type to hex representation.
716 * @param color HSB color
717 * @return color code e.g. #001122
719 protected String mapColor(HSBType color) {
720 String redValue = String.format("%02X", (int) (color.getRed().floatValue() * 2.55));
721 String greenValue = String.format("%02X", (int) (color.getGreen().floatValue() * 2.55));
722 String blueValue = String.format("%02X", (int) (color.getBlue().floatValue() * 2.55));
723 return "#" + redValue + greenValue + blueValue;
727 * Check bridge status and refresh connection status of thing accordingly.
729 protected void refreshThingStatus() {
730 Optional<HomeConnectApiClient> apiClient = getApiClient();
732 apiClient.ifPresent(client -> {
734 HomeAppliance homeAppliance = client.getHomeAppliance(getThingHaId());
735 if (!homeAppliance.isConnected()) {
736 updateStatus(OFFLINE);
738 updateStatus(ONLINE);
740 accessible.set(true);
741 } catch (CommunicationException e) {
743 "Update status to OFFLINE. Home Connect service is not reachable or a problem occurred! thing={}, haId={}, error={}.",
744 getThingLabel(), getThingHaId(), e.getMessage());
745 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
746 "Home Connect service is not reachable or a problem occurred! (" + e.getMessage() + ").");
747 accessible.set(false);
748 } catch (AuthorizationException e) {
750 "Update status to OFFLINE. Home Connect service is not reachable or a problem occurred! thing={}, haId={}, error={}",
751 getThingLabel(), getThingHaId(), e.getMessage());
752 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
753 "Home Connect service is not reachable or a problem occurred! (" + e.getMessage() + ").");
754 accessible.set(false);
755 handleAuthenticationError(e);
758 if (apiClient.isEmpty()) {
759 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
760 accessible.set(false);
765 * Get home appliance id of Thing.
767 * @return home appliance id
769 public String getThingHaId() {
770 return getThing().getConfiguration().get(HA_ID).toString();
774 * Returns the human readable label for this thing.
776 * @return the human readable label
778 protected @Nullable String getThingLabel() {
779 return getThing().getLabel();
783 * Handle authentication exception.
785 protected void handleAuthenticationError(AuthorizationException exception) {
786 if (isBridgeOnline()) {
788 "Thing handler threw authentication exception --> clear credential storage thing={}, haId={} error={}",
789 getThingLabel(), getThingHaId(), exception.getMessage());
791 getBridgeHandler().ifPresent(homeConnectBridgeHandler -> {
793 homeConnectBridgeHandler.getOAuthClientService().remove();
794 homeConnectBridgeHandler.reinitialize();
795 } catch (OAuthException e) {
796 // client is already closed --> we can ignore it
803 * Get operation state of device.
805 * @return operation state string
807 protected @Nullable String getOperationState() {
808 return operationState;
811 protected EventHandler defaultElapsedProgramTimeEventHandler() {
812 return event -> getLinkedChannel(CHANNEL_ELAPSED_PROGRAM_TIME)
813 .ifPresent(channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND)));
816 protected EventHandler defaultPowerStateEventHandler() {
818 getLinkedChannel(CHANNEL_POWER_STATE).ifPresent(
819 channel -> updateState(channel.getUID(), OnOffType.from(STATE_POWER_ON.equals(event.getValue()))));
821 if (STATE_POWER_ON.equals(event.getValue())) {
822 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID()));
824 resetProgramStateChannels(true);
825 getLinkedChannel(CHANNEL_SELECTED_PROGRAM_STATE)
826 .ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
831 protected EventHandler defaultDoorStateEventHandler() {
832 return event -> getLinkedChannel(CHANNEL_DOOR_STATE).ifPresent(channel -> updateState(channel.getUID(),
833 STATE_DOOR_OPEN.equals(event.getValue()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED));
836 protected EventHandler defaultOperationStateEventHandler() {
838 String value = event.getValue();
839 getLinkedChannel(CHANNEL_OPERATION_STATE).ifPresent(channel -> updateState(channel.getUID(),
840 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
842 if (STATE_OPERATION_FINISHED.equals(value)) {
843 getLinkedChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
844 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(100, PERCENT)));
845 getLinkedChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
846 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, SECOND)));
847 } else if (STATE_OPERATION_RUN.equals(value)) {
848 getLinkedChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
849 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, PERCENT)));
850 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID()));
851 } else if (STATE_OPERATION_READY.equals(value)) {
852 resetProgramStateChannels(false);
857 protected EventHandler defaultActiveProgramEventHandler() {
859 String value = event.getValue();
860 getLinkedChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(channel -> updateState(channel.getUID(),
861 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
863 resetProgramStateChannels(false);
868 protected EventHandler updateProgramOptionsAndActiveProgramStateEventHandler() {
870 String value = event.getValue();
871 getLinkedChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(channel -> updateState(channel.getUID(),
872 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
874 resetProgramStateChannels(false);
877 Optional<HomeConnectApiClient> apiClient = getApiClient();
878 if (apiClient.isPresent() && isChannelLinkedToProgramOptionNotFullySupportedByApi()
879 && apiClient.get().isRemoteControlActive(getThingHaId())) {
880 // update channels linked to program options
881 Program program = apiClient.get().getSelectedProgram(getThingHaId());
882 if (program != null) {
883 processProgramOptions(program.getOptions());
886 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
887 logger.debug("Could not update program options. {}", e.getMessage());
893 protected EventHandler defaultEventPresentStateEventHandler(String channelId) {
894 return event -> getLinkedChannel(channelId).ifPresent(channel -> updateState(channel.getUID(),
895 OnOffType.from(!STATE_EVENT_PRESENT_STATE_OFF.equals(event.getValue()))));
898 protected EventHandler defaultBooleanEventHandler(String channelId) {
899 return event -> getLinkedChannel(channelId)
900 .ifPresent(channel -> updateState(channel.getUID(), OnOffType.from(event.getValueAsBoolean())));
903 protected EventHandler defaultRemainingProgramTimeEventHandler() {
904 return event -> getLinkedChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
905 .ifPresent(channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND)));
908 protected EventHandler defaultSelectedProgramStateEventHandler() {
909 return event -> getLinkedChannel(CHANNEL_SELECTED_PROGRAM_STATE)
910 .ifPresent(channel -> updateState(channel.getUID(),
911 event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue())));
914 protected EventHandler defaultAmbientLightColorStateEventHandler() {
915 return event -> getLinkedChannel(CHANNEL_AMBIENT_LIGHT_COLOR_STATE)
916 .ifPresent(channel -> updateState(channel.getUID(),
917 event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue())));
920 protected EventHandler defaultAmbientLightCustomColorStateEventHandler() {
921 return event -> getLinkedChannel(CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE).ifPresent(channel -> {
922 String value = event.getValue();
924 updateState(channel.getUID(), mapColor(value));
926 updateState(channel.getUID(), UnDefType.UNDEF);
931 protected EventHandler updateRemoteControlActiveAndProgramOptionsStateEventHandler() {
933 defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE).handle(event);
936 if (Boolean.parseBoolean(event.getValue())) {
937 // update available program options if update was previously delayed and remote control is enabled
938 String programKey = programOptionsDelayedUpdate;
939 if (programKey != null) {
940 logger.debug("Delayed update of options for program {}", programKey);
941 updateProgramOptionsStateDescriptions(programKey);
942 programOptionsDelayedUpdate = null;
945 if (isChannelLinkedToProgramOptionNotFullySupportedByApi()) {
946 Optional<HomeConnectApiClient> apiClient = getApiClient();
947 if (apiClient.isPresent()) {
948 Program program = apiClient.get().getSelectedProgram(getThingHaId());
949 if (program != null) {
950 processProgramOptions(program.getOptions());
955 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
956 logger.debug("Could not update program options. {}", e.getMessage());
961 protected EventHandler updateProgramOptionsAndSelectedProgramStateEventHandler() {
963 defaultSelectedProgramStateEventHandler().handle(event);
966 Optional<HomeConnectApiClient> apiClient = getApiClient();
967 String programKey = event.getValue();
969 if (apiClient.isPresent() && programKey != null) {
970 Boolean remoteControl = (availableProgramOptionsCache.get(programKey) == null
971 || isChannelLinkedToProgramOptionNotFullySupportedByApi())
972 ? apiClient.get().isRemoteControlActive(getThingHaId())
975 // Delay the update of available program options if options are not yet cached and remote control is
977 if (availableProgramOptionsCache.get(programKey) == null && !remoteControl) {
978 logger.debug("Delay update of options for program {}", programKey);
979 programOptionsDelayedUpdate = programKey;
981 updateProgramOptionsStateDescriptions(programKey);
984 if (isChannelLinkedToProgramOptionNotFullySupportedByApi() && remoteControl) {
985 // update channels linked to program options
986 Program program = apiClient.get().getSelectedProgram(getThingHaId());
987 if (program != null) {
988 processProgramOptions(program.getOptions());
992 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
993 logger.debug("Could not update program options. {}", e.getMessage());
998 protected EventHandler defaultPercentQuantityTypeEventHandler(String channelId) {
999 return event -> getLinkedChannel(channelId).ifPresent(
1000 channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), PERCENT)));
1003 protected EventHandler defaultPercentHandler(String channelId) {
1004 return event -> getLinkedChannel(channelId)
1005 .ifPresent(channel -> updateState(channel.getUID(), new PercentType(event.getValueAsInt())));
1008 protected ChannelUpdateHandler defaultDoorStateChannelUpdateHandler() {
1009 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1010 Optional<HomeConnectApiClient> apiClient = getApiClient();
1011 if (apiClient.isPresent()) {
1012 Data data = apiClient.get().getDoorState(getThingHaId());
1013 if (data.getValue() != null) {
1014 return STATE_DOOR_OPEN.equals(data.getValue()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
1016 return UnDefType.UNDEF;
1019 return UnDefType.UNDEF;
1024 protected ChannelUpdateHandler defaultPowerStateChannelUpdateHandler() {
1025 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1026 Optional<HomeConnectApiClient> apiClient = getApiClient();
1027 if (apiClient.isPresent()) {
1028 Data data = apiClient.get().getPowerState(getThingHaId());
1029 if (data.getValue() != null) {
1030 return OnOffType.from(STATE_POWER_ON.equals(data.getValue()));
1032 return UnDefType.UNDEF;
1035 return UnDefType.UNDEF;
1040 protected ChannelUpdateHandler defaultAmbientLightChannelUpdateHandler() {
1041 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1042 Optional<HomeConnectApiClient> apiClient = getApiClient();
1043 if (apiClient.isPresent()) {
1044 Data data = apiClient.get().getAmbientLightState(getThingHaId());
1045 if (data.getValue() != null) {
1046 boolean enabled = data.getValueAsBoolean();
1049 Data brightnessData = apiClient.get().getAmbientLightBrightnessState(getThingHaId());
1050 getLinkedChannel(CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE)
1051 .ifPresent(channel -> updateState(channel.getUID(),
1052 new PercentType(brightnessData.getValueAsInt())));
1055 Data colorData = apiClient.get().getAmbientLightColorState(getThingHaId());
1056 getLinkedChannel(CHANNEL_AMBIENT_LIGHT_COLOR_STATE).ifPresent(
1057 channel -> updateState(channel.getUID(), new StringType(colorData.getValue())));
1060 Data customColorData = apiClient.get().getAmbientLightCustomColorState(getThingHaId());
1061 getLinkedChannel(CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE).ifPresent(channel -> {
1062 String value = customColorData.getValue();
1063 if (value != null) {
1064 updateState(channel.getUID(), mapColor(value));
1066 updateState(channel.getUID(), UnDefType.UNDEF);
1070 return OnOffType.from(enabled);
1072 return UnDefType.UNDEF;
1075 return UnDefType.UNDEF;
1080 protected ChannelUpdateHandler defaultNoOpUpdateHandler() {
1081 return (channelUID, cache) -> updateState(channelUID, UnDefType.UNDEF);
1084 protected ChannelUpdateHandler defaultOperationStateChannelUpdateHandler() {
1085 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1086 Optional<HomeConnectApiClient> apiClient = getApiClient();
1087 if (apiClient.isPresent()) {
1088 Data data = apiClient.get().getOperationState(getThingHaId());
1090 String value = data.getValue();
1091 if (value != null) {
1092 operationState = data.getValue();
1093 return new StringType(mapStringType(value));
1095 operationState = null;
1096 return UnDefType.UNDEF;
1099 return UnDefType.UNDEF;
1104 protected ChannelUpdateHandler defaultRemoteControlActiveStateChannelUpdateHandler() {
1105 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1106 Optional<HomeConnectApiClient> apiClient = getApiClient();
1107 if (apiClient.isPresent()) {
1108 return OnOffType.from(apiClient.get().isRemoteControlActive(getThingHaId()));
1110 return OnOffType.OFF;
1114 protected ChannelUpdateHandler defaultLocalControlActiveStateChannelUpdateHandler() {
1115 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1116 Optional<HomeConnectApiClient> apiClient = getApiClient();
1117 if (apiClient.isPresent()) {
1118 return OnOffType.from(apiClient.get().isLocalControlActive(getThingHaId()));
1120 return OnOffType.OFF;
1124 protected ChannelUpdateHandler defaultRemoteStartAllowanceChannelUpdateHandler() {
1125 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1126 Optional<HomeConnectApiClient> apiClient = getApiClient();
1127 if (apiClient.isPresent()) {
1128 return OnOffType.from(apiClient.get().isRemoteControlStartAllowed(getThingHaId()));
1130 return OnOffType.OFF;
1134 protected ChannelUpdateHandler defaultSelectedProgramStateUpdateHandler() {
1135 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1136 Optional<HomeConnectApiClient> apiClient = getApiClient();
1137 if (apiClient.isPresent()) {
1138 Program program = apiClient.get().getSelectedProgram(getThingHaId());
1139 if (program != null) {
1140 processProgramOptions(program.getOptions());
1141 return new StringType(program.getKey());
1143 return UnDefType.UNDEF;
1146 return UnDefType.UNDEF;
1150 protected ChannelUpdateHandler updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() {
1151 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1152 Optional<HomeConnectApiClient> apiClient = getApiClient();
1153 if (apiClient.isPresent()) {
1154 Program program = apiClient.get().getSelectedProgram(getThingHaId());
1156 if (program != null) {
1157 updateProgramOptionsStateDescriptions(program.getKey());
1158 processProgramOptions(program.getOptions());
1160 return new StringType(program.getKey());
1162 return UnDefType.UNDEF;
1165 return UnDefType.UNDEF;
1169 protected ChannelUpdateHandler getAndUpdateSelectedProgramStateUpdateHandler() {
1170 return (channelUID, cache) -> {
1171 Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
1172 if (channel.isPresent()) {
1173 defaultSelectedProgramStateUpdateHandler().handle(channel.get().getUID(), cache);
1178 protected ChannelUpdateHandler getAndUpdateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() {
1179 return (channelUID, cache) -> {
1180 Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
1181 if (channel.isPresent()) {
1182 updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler()
1183 .handle(channel.get().getUID(), cache);
1188 protected ChannelUpdateHandler defaultActiveProgramStateUpdateHandler() {
1189 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1190 Optional<HomeConnectApiClient> apiClient = getApiClient();
1191 if (apiClient.isPresent()) {
1192 Program program = apiClient.get().getActiveProgram(getThingHaId());
1194 if (program != null) {
1195 processProgramOptions(program.getOptions());
1196 return new StringType(mapStringType(program.getKey()));
1198 resetProgramStateChannels(false);
1199 return UnDefType.UNDEF;
1202 return UnDefType.UNDEF;
1206 protected void handleTemperatureCommand(final ChannelUID channelUID, final Command command,
1207 final HomeConnectApiClient apiClient)
1208 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1209 if (command instanceof QuantityType quantityCommand) {
1214 if (quantityCommand.getUnit().equals(SIUnits.CELSIUS)
1215 || quantityCommand.getUnit().equals(ImperialUnits.FAHRENHEIT)) {
1216 unit = quantityCommand.getUnit().toString();
1217 value = String.valueOf(quantityCommand.intValue());
1219 logger.debug("Converting target temperature from {}{} to °C value. thing={}, haId={}",
1220 quantityCommand.intValue(), quantityCommand.getUnit().toString(), getThingLabel(),
1223 var celsius = quantityCommand.toUnit(SIUnits.CELSIUS);
1224 if (celsius == null) {
1225 logger.warn("Converting temperature to celsius failed! quantity={}", quantityCommand);
1228 value = String.valueOf(celsius.intValue());
1230 logger.debug("Converted value {}{}", value, unit);
1233 if (value != null) {
1234 logger.debug("Set temperature to {} {}. thing={}, haId={}", value, unit, getThingLabel(),
1236 switch (channelUID.getId()) {
1237 case CHANNEL_REFRIGERATOR_SETPOINT_TEMPERATURE:
1238 apiClient.setFridgeSetpointTemperature(getThingHaId(), value, unit);
1239 case CHANNEL_FREEZER_SETPOINT_TEMPERATURE:
1240 apiClient.setFreezerSetpointTemperature(getThingHaId(), value, unit);
1242 case CHANNEL_SETPOINT_TEMPERATURE:
1243 apiClient.setProgramOptions(getThingHaId(), OPTION_SETPOINT_TEMPERATURE, value, unit, true,
1247 logger.debug("Unknown channel! Cannot set temperature. channelUID={}", channelUID);
1250 } catch (UnconvertibleException e) {
1251 logger.warn("Could not set temperature! haId={}, error={}", getThingHaId(), e.getMessage());
1256 protected void handleLightCommands(final ChannelUID channelUID, final Command command,
1257 final HomeConnectApiClient apiClient)
1258 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1259 switch (channelUID.getId()) {
1260 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1261 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1262 // turn light on if turned off
1263 turnLightOn(channelUID, apiClient);
1265 int newBrightness = BRIGHTNESS_MIN;
1266 if (command instanceof OnOffType) {
1267 newBrightness = command == OnOffType.ON ? BRIGHTNESS_MAX : BRIGHTNESS_MIN;
1268 } else if (command instanceof IncreaseDecreaseType) {
1269 int currentBrightness = getCurrentBrightness(channelUID, apiClient);
1270 if (command.equals(IncreaseDecreaseType.INCREASE)) {
1271 newBrightness = currentBrightness + BRIGHTNESS_DIM_STEP;
1273 newBrightness = currentBrightness - BRIGHTNESS_DIM_STEP;
1275 } else if (command instanceof PercentType percentCommand) {
1276 newBrightness = (int) Math.floor(percentCommand.doubleValue());
1277 } else if (command instanceof DecimalType decimalCommand) {
1278 newBrightness = decimalCommand.intValue();
1281 // check in in range
1282 newBrightness = Math.min(Math.max(newBrightness, BRIGHTNESS_MIN), BRIGHTNESS_MAX);
1284 setLightBrightness(channelUID, apiClient, newBrightness);
1286 case CHANNEL_FUNCTIONAL_LIGHT_STATE:
1287 if (command instanceof OnOffType) {
1288 apiClient.setFunctionalLightState(getThingHaId(), OnOffType.ON.equals(command));
1291 case CHANNEL_AMBIENT_LIGHT_STATE:
1292 if (command instanceof OnOffType) {
1293 apiClient.setAmbientLightState(getThingHaId(), OnOffType.ON.equals(command));
1296 case CHANNEL_AMBIENT_LIGHT_COLOR_STATE:
1297 if (command instanceof StringType) {
1298 turnLightOn(channelUID, apiClient);
1299 apiClient.setAmbientLightColorState(getThingHaId(), command.toFullString());
1302 case CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE:
1303 turnLightOn(channelUID, apiClient);
1305 // make sure 'custom color' is set as color
1306 Data ambientLightColorState = apiClient.getAmbientLightColorState(getThingHaId());
1307 if (!STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR.equals(ambientLightColorState.getValue())) {
1308 apiClient.setAmbientLightColorState(getThingHaId(), STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR);
1311 if (command instanceof HSBType hsbCommand) {
1312 apiClient.setAmbientLightCustomColorState(getThingHaId(), mapColor(hsbCommand));
1313 } else if (command instanceof StringType) {
1314 apiClient.setAmbientLightCustomColorState(getThingHaId(), command.toFullString());
1320 protected void handlePowerCommand(final ChannelUID channelUID, final Command command,
1321 final HomeConnectApiClient apiClient, String stateNotOn)
1322 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1323 if (command instanceof OnOffType && CHANNEL_POWER_STATE.equals(channelUID.getId())) {
1324 apiClient.setPowerState(getThingHaId(), OnOffType.ON.equals(command) ? STATE_POWER_ON : stateNotOn);
1328 private int getCurrentBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient)
1329 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1330 String id = channelUID.getId();
1331 if (CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE.equals(id)) {
1332 return apiClient.getFunctionalLightBrightnessState(getThingHaId()).getValueAsInt();
1334 return apiClient.getAmbientLightBrightnessState(getThingHaId()).getValueAsInt();
1338 private void setLightBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient, int value)
1339 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1340 switch (channelUID.getId()) {
1341 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1342 apiClient.setFunctionalLightBrightnessState(getThingHaId(), value);
1344 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1345 apiClient.setAmbientLightBrightnessState(getThingHaId(), value);
1350 private void turnLightOn(final ChannelUID channelUID, final HomeConnectApiClient apiClient)
1351 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1352 switch (channelUID.getId()) {
1353 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1354 Data functionalLightState = apiClient.getFunctionalLightState(getThingHaId());
1355 if (!functionalLightState.getValueAsBoolean()) {
1356 apiClient.setFunctionalLightState(getThingHaId(), true);
1359 case CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE:
1360 case CHANNEL_AMBIENT_LIGHT_COLOR_STATE:
1361 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1362 Data ambientLightState = apiClient.getAmbientLightState(getThingHaId());
1363 if (!ambientLightState.getValueAsBoolean()) {
1364 apiClient.setAmbientLightState(getThingHaId(), true);
1370 private Optional<Option> getOption(List<Option> options, String optionKey) {
1371 return options.stream().filter(option -> optionKey.equals(option.getKey())).findFirst();
1374 private void setStringChannelFromOption(String channelId, List<Option> options, String optionKey,
1375 @Nullable State defaultState) {
1376 setStringChannelFromOption(channelId, options, optionKey, value -> value, defaultState);
1379 private void setStringChannelFromOption(String channelId, List<Option> options, String optionKey,
1380 Function<String, String> mappingFunc, @Nullable State defaultState) {
1381 getLinkedChannel(channelId)
1382 .ifPresent(channel -> getOption(options, optionKey).map(option -> option.getValue()).ifPresentOrElse(
1383 value -> updateState(channel.getUID(), new StringType(mappingFunc.apply(value))), () -> {
1384 if (defaultState != null) {
1385 updateState(channel.getUID(), defaultState);
1390 private void setQuantityChannelFromOption(String channelId, List<Option> options, String optionKey,
1391 Function<@Nullable String, Unit<?>> unitMappingFunc, @Nullable State defaultState) {
1392 getLinkedChannel(channelId).ifPresent(channel -> getOption(options, optionKey) //
1394 option -> updateState(channel.getUID(),
1395 new QuantityType<>(option.getValueAsInt(), unitMappingFunc.apply(option.getUnit()))),
1397 if (defaultState != null) {
1398 updateState(channel.getUID(), defaultState);
1403 private void setOnOffChannelFromOption(String channelId, List<Option> options, String optionKey,
1404 @Nullable State defaultState) {
1405 getLinkedChannel(channelId)
1406 .ifPresent(channel -> getOption(options, optionKey).map(option -> option.getValueAsBoolean())
1407 .ifPresentOrElse(value -> updateState(channel.getUID(), OnOffType.from(value)), () -> {
1408 if (defaultState != null) {
1409 updateState(channel.getUID(), defaultState);
1414 protected void processProgramOptions(List<Option> options) {
1415 String operationState = getOperationState();
1417 Map.of(CHANNEL_WASHER_TEMPERATURE, OPTION_WASHER_TEMPERATURE, CHANNEL_WASHER_SPIN_SPEED,
1418 OPTION_WASHER_SPIN_SPEED, CHANNEL_WASHER_IDOS1_LEVEL, OPTION_WASHER_IDOS_1_DOSING_LEVEL,
1419 CHANNEL_WASHER_IDOS2_LEVEL, OPTION_WASHER_IDOS_2_DOSING_LEVEL, CHANNEL_DRYER_DRYING_TARGET,
1420 OPTION_DRYER_DRYING_TARGET)
1421 .forEach((channel, option) -> setStringChannelFromOption(channel, options, option, UnDefType.UNDEF));
1423 Map.of(CHANNEL_WASHER_IDOS1, OPTION_WASHER_IDOS_1_ACTIVE, CHANNEL_WASHER_IDOS2, OPTION_WASHER_IDOS_2_ACTIVE,
1424 CHANNEL_WASHER_LESS_IRONING, OPTION_WASHER_LESS_IRONING, CHANNEL_WASHER_PRE_WASH,
1425 OPTION_WASHER_PRE_WASH, CHANNEL_WASHER_SOAK, OPTION_WASHER_SOAK, CHANNEL_WASHER_RINSE_HOLD,
1426 OPTION_WASHER_RINSE_HOLD)
1427 .forEach((channel, option) -> setOnOffChannelFromOption(channel, options, option, OnOffType.OFF));
1429 setStringChannelFromOption(CHANNEL_HOOD_INTENSIVE_LEVEL, options, OPTION_HOOD_INTENSIVE_LEVEL,
1430 value -> mapStageStringType(value), UnDefType.UNDEF);
1431 setStringChannelFromOption(CHANNEL_HOOD_VENTING_LEVEL, options, OPTION_HOOD_VENTING_LEVEL,
1432 value -> mapStageStringType(value), UnDefType.UNDEF);
1433 setQuantityChannelFromOption(CHANNEL_SETPOINT_TEMPERATURE, options, OPTION_SETPOINT_TEMPERATURE,
1434 unit -> mapTemperature(unit), UnDefType.UNDEF);
1435 setQuantityChannelFromOption(CHANNEL_DURATION, options, OPTION_DURATION, unit -> SECOND, UnDefType.UNDEF);
1436 // The channel remaining_program_time_state depends on two program options: FinishInRelative and
1437 // RemainingProgramTime. When the start of the program is delayed, the two options are returned by the API with
1438 // different values. In this case, we consider the value of the option FinishInRelative.
1439 setQuantityChannelFromOption(CHANNEL_REMAINING_PROGRAM_TIME_STATE, options,
1440 getOption(options, OPTION_FINISH_IN_RELATIVE).isPresent() ? OPTION_FINISH_IN_RELATIVE
1441 : OPTION_REMAINING_PROGRAM_TIME,
1442 unit -> SECOND, UnDefType.UNDEF);
1443 setQuantityChannelFromOption(CHANNEL_ELAPSED_PROGRAM_TIME, options, OPTION_ELAPSED_PROGRAM_TIME, unit -> SECOND,
1444 new QuantityType<>(0, SECOND));
1445 setQuantityChannelFromOption(CHANNEL_PROGRAM_PROGRESS_STATE, options, OPTION_PROGRAM_PROGRESS, unit -> PERCENT,
1446 new QuantityType<>(0, PERCENT));
1447 // When the program is not in ready state, the vario perfect option is not always provided by the API returning
1448 // the options of the current active program. So in this case we avoid updating the channel (to the default
1449 // state) by passing a null value as parameter.to setStringChannelFromOption.
1450 setStringChannelFromOption(CHANNEL_WASHER_VARIO_PERFECT, options, OPTION_WASHER_VARIO_PERFECT,
1451 OPERATION_STATE_READY.equals(operationState)
1452 ? new StringType("LaundryCare.Common.EnumType.VarioPerfect.Off")
1454 setStringChannelFromOption(CHANNEL_WASHER_RINSE_PLUS, options, OPTION_WASHER_RINSE_PLUS,
1455 new StringType("LaundryCare.Washer.EnumType.RinsePlus.Off"));
1456 setQuantityChannelFromOption(CHANNEL_WASHER_LOAD_RECOMMENDATION, options, OPTION_WASHER_LOAD_RECOMMENDATION,
1457 unit -> mapMass(unit), UnDefType.UNDEF);
1458 setQuantityChannelFromOption(CHANNEL_PROGRAM_ENERGY, options, OPTION_WASHER_ENERGY_FORECAST, unit -> PERCENT,
1460 setQuantityChannelFromOption(CHANNEL_PROGRAM_WATER, options, OPTION_WASHER_WATER_FORECAST, unit -> PERCENT,
1464 protected String convertWasherTemperature(String value) {
1465 if (value.startsWith(TEMPERATURE_PREFIX + "GC")) {
1466 return value.replace(TEMPERATURE_PREFIX + "GC", "") + "°C";
1469 if (value.startsWith(TEMPERATURE_PREFIX + "Ul")) {
1470 return mapStringType(value.replace(TEMPERATURE_PREFIX + "Ul", ""));
1473 return mapStringType(value);
1476 protected String convertWasherSpinSpeed(String value) {
1477 if (value.startsWith(SPIN_SPEED_PREFIX + "RPM")) {
1478 return value.replace(SPIN_SPEED_PREFIX + "RPM", "") + " RPM";
1481 if (value.startsWith(SPIN_SPEED_PREFIX + "Ul")) {
1482 return value.replace(SPIN_SPEED_PREFIX + "Ul", "");
1485 return mapStringType(value);
1488 protected void updateProgramOptionsStateDescriptions(String programKey)
1489 throws AuthorizationException, ApplianceOfflineException {
1490 Optional<HomeConnectApiClient> apiClient = getApiClient();
1491 if (apiClient.isPresent()) {
1492 boolean cacheToSet = false;
1493 List<AvailableProgramOption> availableProgramOptions;
1494 if (availableProgramOptionsCache.containsKey(programKey)) {
1495 logger.debug("Returning cached options for program '{}'.", programKey);
1496 availableProgramOptions = availableProgramOptionsCache.get(programKey);
1497 availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
1498 : Collections.emptyList();
1500 // Depending on the current program operation state, the APi request could trigger a
1501 // CommunicationException exception due to returned status code 409
1503 availableProgramOptions = apiClient.get().getProgramOptions(getThingHaId(), programKey);
1504 if (availableProgramOptions == null) {
1505 // Program is unsupported, to avoid calling again the API for this program, save in cache either
1506 // the predefined options provided by the binding if they exist, or an empty list of options
1507 if (unsupportedProgramOptions.containsKey(programKey)) {
1508 availableProgramOptions = unsupportedProgramOptions.get(programKey);
1509 availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
1511 logger.debug("Saving predefined options in cache for unsupported program '{}'.",
1514 availableProgramOptions = emptyList();
1515 logger.debug("Saving empty options in cache for unsupported program '{}'.", programKey);
1517 availableProgramOptionsCache.put(programKey, availableProgramOptions);
1519 // Add the unsupported program in programs cache and refresh the dynamic state description
1520 if (addUnsupportedProgramInCache(programKey)) {
1521 updateSelectedProgramStateDescription();
1524 // If no options are returned by the API, using predefined options if available
1525 if (availableProgramOptions.isEmpty() && unsupportedProgramOptions.containsKey(programKey)) {
1526 availableProgramOptions = unsupportedProgramOptions.get(programKey);
1527 availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
1532 } catch (CommunicationException e) {
1533 availableProgramOptions = emptyList();
1537 Optional<Channel> channelSpinSpeed = getThingChannel(CHANNEL_WASHER_SPIN_SPEED);
1538 Optional<Channel> channelTemperature = getThingChannel(CHANNEL_WASHER_TEMPERATURE);
1539 Optional<Channel> channelDryingTarget = getThingChannel(CHANNEL_DRYER_DRYING_TARGET);
1541 Optional<AvailableProgramOption> optionsSpinSpeed = availableProgramOptions.stream()
1542 .filter(option -> OPTION_WASHER_SPIN_SPEED.equals(option.getKey())).findFirst();
1543 Optional<AvailableProgramOption> optionsTemperature = availableProgramOptions.stream()
1544 .filter(option -> OPTION_WASHER_TEMPERATURE.equals(option.getKey())).findFirst();
1545 Optional<AvailableProgramOption> optionsDryingTarget = availableProgramOptions.stream()
1546 .filter(option -> OPTION_DRYER_DRYING_TARGET.equals(option.getKey())).findFirst();
1548 // Save options in cache only if we got options for all expected channels
1549 if (cacheToSet && (channelSpinSpeed.isEmpty() || optionsSpinSpeed.isPresent())
1550 && (channelTemperature.isEmpty() || optionsTemperature.isPresent())
1551 && (channelDryingTarget.isEmpty() || optionsDryingTarget.isPresent())) {
1552 logger.debug("Saving options in cache for program '{}'.", programKey);
1553 availableProgramOptionsCache.put(programKey, availableProgramOptions);
1556 channelSpinSpeed.ifPresent(channel -> optionsSpinSpeed.ifPresentOrElse(
1557 option -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1558 createStateOptions(option, this::convertWasherSpinSpeed)),
1559 () -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList())));
1560 channelTemperature.ifPresent(channel -> optionsTemperature.ifPresentOrElse(
1561 option -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1562 createStateOptions(option, this::convertWasherTemperature)),
1563 () -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList())));
1564 channelDryingTarget.ifPresent(channel -> optionsDryingTarget.ifPresentOrElse(
1565 option -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1566 createStateOptions(option, this::mapStringType)),
1567 () -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList())));
1571 protected HomeConnectDynamicStateDescriptionProvider getDynamicStateDescriptionProvider() {
1572 return dynamicStateDescriptionProvider;
1575 private List<StateOption> createStateOptions(AvailableProgramOption option,
1576 Function<String, String> stateConverter) {
1577 return option.getAllowedValues().stream().map(av -> new StateOption(av, stateConverter.apply(av)))
1578 .collect(Collectors.toList());
1581 private synchronized void scheduleOfflineMonitor1() {
1582 this.reinitializationFuture1 = scheduler.schedule(() -> {
1583 if (isBridgeOnline() && isThingOffline()) {
1584 logger.debug("Offline monitor 1: Check if thing is ONLINE. thing={}, haId={}", getThingLabel(),
1586 refreshThingStatus();
1587 if (isThingOnline()) {
1588 logger.debug("Offline monitor 1: Thing status changed to ONLINE. thing={}, haId={}",
1589 getThingLabel(), getThingHaId());
1592 scheduleOfflineMonitor1();
1595 scheduleOfflineMonitor1();
1597 }, AbstractHomeConnectThingHandler.OFFLINE_MONITOR_1_DELAY_MIN, TimeUnit.MINUTES);
1600 private synchronized void stopOfflineMonitor1() {
1601 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture1;
1602 if (reinitializationFuture != null) {
1603 reinitializationFuture.cancel(false);
1604 this.reinitializationFuture1 = null;
1608 private synchronized void scheduleOfflineMonitor2() {
1609 this.reinitializationFuture2 = scheduler.schedule(() -> {
1610 if (isBridgeOnline() && !accessible.get()) {
1611 logger.debug("Offline monitor 2: Check if thing is ONLINE. thing={}, haId={}", getThingLabel(),
1613 refreshThingStatus();
1614 if (isThingOnline()) {
1615 logger.debug("Offline monitor 2: Thing status changed to ONLINE. thing={}, haId={}",
1616 getThingLabel(), getThingHaId());
1619 scheduleOfflineMonitor2();
1622 scheduleOfflineMonitor2();
1624 }, AbstractHomeConnectThingHandler.OFFLINE_MONITOR_2_DELAY_MIN, TimeUnit.MINUTES);
1627 private synchronized void stopOfflineMonitor2() {
1628 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture2;
1629 if (reinitializationFuture != null) {
1630 reinitializationFuture.cancel(false);
1631 this.reinitializationFuture2 = null;
1635 private synchronized void scheduleRetryRegistering() {
1636 this.reinitializationFuture3 = scheduler.schedule(() -> {
1637 logger.debug("Try to register event listener again. haId={}", getThingHaId());
1638 unregisterEventListener();
1639 registerEventListener();
1640 }, AbstractHomeConnectThingHandler.EVENT_LISTENER_CONNECT_RETRY_DELAY_MIN, TimeUnit.MINUTES);
1643 private synchronized void stopRetryRegistering() {
1644 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture3;
1645 if (reinitializationFuture != null) {
1646 reinitializationFuture.cancel(true);
1647 this.reinitializationFuture3 = null;
1651 protected List<AvailableProgram> getPrograms()
1652 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1653 if (!programsCache.isEmpty()) {
1654 logger.debug("Returning cached programs for '{}'.", getThingHaId());
1655 return programsCache;
1657 Optional<HomeConnectApiClient> apiClient = getApiClient();
1658 if (apiClient.isPresent()) {
1659 programsCache.addAll(apiClient.get().getPrograms(getThingHaId()));
1660 return programsCache;
1662 throw new CommunicationException("API not initialized");
1668 * Add an entry in the programs cache and mark it as unsupported
1670 * @param programKey program id
1671 * @return true if an entry was added in the cache
1673 private boolean addUnsupportedProgramInCache(String programKey) {
1674 Optional<AvailableProgram> prog = programsCache.stream().filter(program -> programKey.equals(program.getKey()))
1676 if (prog.isEmpty()) {
1677 programsCache.add(new AvailableProgram(programKey, false));
1678 logger.debug("{} added in programs cache as an unsupported program", programKey);
1685 * Check if a program is marked as supported in the programs cache
1687 * @param programKey program id
1688 * @return true if the program is in the cache and marked as supported
1690 protected boolean isProgramSupported(String programKey) {
1691 Optional<AvailableProgram> prog = programsCache.stream().filter(program -> programKey.equals(program.getKey()))
1693 return prog.isPresent() && prog.get().isSupported();