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.StateOption;
79 import org.openhab.core.types.UnDefType;
80 import org.slf4j.Logger;
81 import org.slf4j.LoggerFactory;
84 * The {@link AbstractHomeConnectThingHandler} is responsible for handling commands, which are
85 * sent to one of the channels.
87 * @author Jonas Brüstel - Initial contribution
88 * @author Laurent Garnier - programs cache moved and enhanced to allow adding unsupported programs
91 public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler implements HomeConnectEventListener {
93 private static final int CACHE_TTL_SEC = 2;
94 private static final int OFFLINE_MONITOR_1_DELAY_MIN = 30;
95 private static final int OFFLINE_MONITOR_2_DELAY_MIN = 4;
96 private static final int EVENT_LISTENER_CONNECT_RETRY_DELAY_MIN = 10;
98 private @Nullable String operationState;
99 private @Nullable ScheduledFuture<?> reinitializationFuture1;
100 private @Nullable ScheduledFuture<?> reinitializationFuture2;
101 private @Nullable ScheduledFuture<?> reinitializationFuture3;
102 private boolean ignoreEventSourceClosedEvent;
103 private @Nullable String programOptionsDelayedUpdate;
105 private final ConcurrentHashMap<String, EventHandler> eventHandlers;
106 private final ConcurrentHashMap<String, ChannelUpdateHandler> channelUpdateHandlers;
107 private final HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
108 private final ExpiringStateMap expiringStateMap;
109 private final AtomicBoolean accessible;
110 private final Logger logger = LoggerFactory.getLogger(AbstractHomeConnectThingHandler.class);
111 private final List<AvailableProgram> programsCache;
112 private final Map<String, List<AvailableProgramOption>> availableProgramOptionsCache;
113 private final Map<String, List<AvailableProgramOption>> unsupportedProgramOptions;
115 public AbstractHomeConnectThingHandler(Thing thing,
116 HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
118 eventHandlers = new ConcurrentHashMap<>();
119 channelUpdateHandlers = new ConcurrentHashMap<>();
120 this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider;
121 expiringStateMap = new ExpiringStateMap(Duration.ofSeconds(CACHE_TTL_SEC));
122 accessible = new AtomicBoolean(false);
123 programsCache = new CopyOnWriteArrayList<>();
124 availableProgramOptionsCache = new ConcurrentHashMap<>();
125 unsupportedProgramOptions = new ConcurrentHashMap<>();
127 configureEventHandlers(eventHandlers);
128 configureChannelUpdateHandlers(channelUpdateHandlers);
129 configureUnsupportedProgramOptions(unsupportedProgramOptions);
133 public void initialize() {
134 if (getBridgeHandler().isEmpty()) {
135 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
136 accessible.set(false);
137 } else if (isBridgeOffline()) {
138 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
139 accessible.set(false);
141 updateStatus(UNKNOWN);
142 scheduler.submit(() -> {
143 refreshThingStatus(); // set ONLINE / OFFLINE
144 updateSelectedProgramStateDescription();
146 registerEventListener();
147 scheduleOfflineMonitor1();
148 scheduleOfflineMonitor2();
154 public void dispose() {
155 stopRetryRegistering();
156 stopOfflineMonitor1();
157 stopOfflineMonitor2();
158 unregisterEventListener(true);
162 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
163 logger.debug("Bridge status changed to {} ({}). haId={}", bridgeStatusInfo, getThingLabel(), getThingHaId());
167 private void reinitialize() {
168 logger.debug("Reinitialize thing handler ({}). haId={}", getThingLabel(), getThingHaId());
169 stopRetryRegistering();
170 stopOfflineMonitor1();
171 stopOfflineMonitor2();
172 unregisterEventListener();
177 * Handles a command for a given channel.
179 * This method is only called, if the thing has been initialized (status ONLINE/OFFLINE/UNKNOWN).
182 * @param channelUID the {@link ChannelUID} of the channel to which the command was sent
183 * @param command the {@link Command}
184 * @param apiClient the {@link HomeConnectApiClient}
185 * @throws CommunicationException communication problem
186 * @throws AuthorizationException authorization problem
187 * @throws ApplianceOfflineException appliance offline
189 protected void handleCommand(ChannelUID channelUID, Command command, HomeConnectApiClient apiClient)
190 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
191 if (command instanceof RefreshType) {
192 updateChannel(channelUID);
193 } else if (command instanceof StringType && CHANNEL_BASIC_ACTIONS_STATE.equals(channelUID.getId())
194 && getBridgeHandler().isPresent()) {
195 updateState(channelUID, new StringType(""));
197 if (COMMAND_START.equalsIgnoreCase(command.toFullString())) {
198 HomeConnectBridgeHandler homeConnectBridgeHandler = getBridgeHandler().get();
199 // workaround for api bug
200 // if simulator, program options have to be passed along with the desired program
201 // if non simulator, some options throw a "SDK.Error.UnsupportedOption" error
202 if (homeConnectBridgeHandler.getConfiguration().isSimulator()) {
203 apiClient.startSelectedProgram(getThingHaId());
205 Program selectedProgram = apiClient.getSelectedProgram(getThingHaId());
206 if (selectedProgram != null) {
207 apiClient.startProgram(getThingHaId(), selectedProgram.getKey());
210 } else if (COMMAND_STOP.equalsIgnoreCase(command.toFullString())) {
211 apiClient.stopProgram(getThingHaId());
212 } else if (COMMAND_SELECTED.equalsIgnoreCase(command.toFullString())) {
213 apiClient.getSelectedProgram(getThingHaId());
215 logger.debug("Start custom program. command={} haId={}", command.toFullString(), getThingHaId());
216 apiClient.startCustomProgram(getThingHaId(), command.toFullString());
218 } else if (command instanceof StringType && CHANNEL_SELECTED_PROGRAM_STATE.equals(channelUID.getId())
219 && isProgramSupported(command.toFullString())) {
220 apiClient.setSelectedProgram(getThingHaId(), command.toFullString());
225 public final void handleCommand(ChannelUID channelUID, Command command) {
226 var apiClient = getApiClient();
227 if ((isThingReadyToHandleCommand() || (this instanceof HomeConnectHoodHandler && isBridgeOnline()
228 && isThingAccessibleViaServerSentEvents())) && apiClient.isPresent()) {
229 logger.debug("Handle \"{}\" command ({}). haId={}", command, channelUID.getId(), getThingHaId());
231 handleCommand(channelUID, command, apiClient.get());
232 } catch (ApplianceOfflineException e) {
233 logger.debug("Could not handle command {}. Appliance offline. thing={}, haId={}, error={}",
234 command.toFullString(), getThingLabel(), getThingHaId(), e.getMessage());
235 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
236 resetChannelsOnOfflineEvent();
237 resetProgramStateChannels(true);
238 } catch (CommunicationException e) {
239 logger.debug("Could not handle command {}. API communication problem! error={}, haId={}",
240 command.toFullString(), e.getMessage(), getThingHaId());
241 } catch (AuthorizationException e) {
242 logger.debug("Could not handle command {}. Authorization problem! error={}, haId={}",
243 command.toFullString(), e.getMessage(), getThingHaId());
245 handleAuthenticationError(e);
251 public void onEvent(Event event) {
252 if (DISCONNECTED.equals(event.getType())) {
253 logger.debug("Received DISCONNECTED event. Set {} to OFFLINE. haId={}", getThing().getLabel(),
255 updateStatus(OFFLINE);
256 resetChannelsOnOfflineEvent();
257 resetProgramStateChannels(true);
258 } else if (isThingOnline() && CONNECTED.equals(event.getType())) {
259 logger.debug("Received CONNECTED event. Update power state channel. haId={}", getThingHaId());
260 getThingChannel(CHANNEL_POWER_STATE).ifPresent(c -> updateChannel(c.getUID()));
261 } else if (isThingOffline() && !KEEP_ALIVE.equals(event.getType())) {
262 updateStatus(ONLINE);
263 logger.debug("Set {} to ONLINE and update channels. haId={}", getThing().getLabel(), getThingHaId());
264 updateSelectedProgramStateDescription();
268 String key = event.getKey();
269 if (EVENT_OPERATION_STATE.equals(key)) {
270 operationState = event.getValue() == null ? null : event.getValue();
273 if (key != null && eventHandlers.containsKey(key)) {
274 EventHandler eventHandler = eventHandlers.get(key);
275 if (eventHandler != null) {
276 eventHandler.handle(event);
280 accessible.set(true);
284 public void onClosed() {
285 if (ignoreEventSourceClosedEvent) {
286 logger.debug("Ignoring event source close event. thing={}, haId={}", getThing().getLabel(), getThingHaId());
288 unregisterEventListener();
289 refreshThingStatus();
290 registerEventListener();
295 public void onRateLimitReached() {
296 unregisterEventListener();
299 scheduleRetryRegistering();
303 * Register event listener.
305 protected void registerEventListener() {
306 if (isBridgeOnline() && isThingAccessibleViaServerSentEvents()) {
307 getEventSourceClient().ifPresent(client -> {
309 ignoreEventSourceClosedEvent = false;
310 client.registerEventListener(getThingHaId(), this);
311 } catch (CommunicationException | AuthorizationException e) {
312 logger.warn("Could not open event source connection. thing={}, haId={}, error={}", getThingLabel(),
313 getThingHaId(), e.getMessage());
320 * Unregister event listener.
322 protected void unregisterEventListener() {
323 unregisterEventListener(false);
326 private void unregisterEventListener(boolean immediate) {
327 getEventSourceClient().ifPresent(client -> {
328 ignoreEventSourceClosedEvent = true;
329 client.unregisterEventListener(this, immediate, false);
334 * Get {@link HomeConnectApiClient}.
336 * @return client instance
338 protected Optional<HomeConnectApiClient> getApiClient() {
339 return getBridgeHandler().map(HomeConnectBridgeHandler::getApiClient);
343 * Get {@link HomeConnectEventSourceClient}.
345 * @return client instance if present
347 protected Optional<HomeConnectEventSourceClient> getEventSourceClient() {
348 return getBridgeHandler().map(HomeConnectBridgeHandler::getEventSourceClient);
352 * Update state description of selected program (Fetch programs via API).
354 protected void updateSelectedProgramStateDescription() {
355 if (isBridgeOffline() || isThingOffline()) {
360 List<StateOption> stateOptions = getPrograms().stream()
361 .map(p -> new StateOption(p.getKey(), mapStringType(p.getKey()))).collect(Collectors.toList());
363 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(
364 channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions));
365 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
366 logger.debug("Could not fetch available programs. thing={}, haId={}, error={}", getThingLabel(),
367 getThingHaId(), e.getMessage());
368 removeSelectedProgramStateDescription();
373 * Remove state description of selected program.
375 protected void removeSelectedProgramStateDescription() {
376 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
377 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList()));
381 * Is thing ready to process commands. If bridge or thing itself is offline commands will be ignored.
383 * @return true if ready
385 protected boolean isThingReadyToHandleCommand() {
386 if (isBridgeOffline()) {
387 logger.debug("Bridge is OFFLINE. Ignore command. thing={}, haId={}", getThingLabel(), getThingHaId());
391 if (isThingOffline()) {
392 logger.debug("{} is OFFLINE. Ignore command. haId={}", getThing().getLabel(), getThingHaId());
400 * Checks if bridge is online and set.
402 * @return true if online
404 protected boolean isBridgeOnline() {
405 Bridge bridge = getBridge();
406 return bridge != null && ONLINE.equals(bridge.getStatus());
410 * Checks if bridge is offline or not set.
412 * @return true if offline
414 protected boolean isBridgeOffline() {
415 return !isBridgeOnline();
419 * Checks if thing is online.
421 * @return true if online
423 protected boolean isThingOnline() {
424 return ONLINE.equals(getThing().getStatus());
428 * Checks if thing is connected to the cloud and accessible via SSE.
430 * @return true if yes
432 public boolean isThingAccessibleViaServerSentEvents() {
433 return accessible.get();
437 * Checks if thing is offline.
439 * @return true if offline
441 protected boolean isThingOffline() {
442 return !isThingOnline();
446 * Get {@link HomeConnectBridgeHandler}.
448 * @return bridge handler
450 protected Optional<HomeConnectBridgeHandler> getBridgeHandler() {
451 Bridge bridge = getBridge();
452 if (bridge != null) {
453 BridgeHandler bridgeHandler = bridge.getHandler();
454 if (bridgeHandler instanceof HomeConnectBridgeHandler) {
455 return Optional.of((HomeConnectBridgeHandler) bridgeHandler);
458 return Optional.empty();
462 * Get thing channel by given channel id.
464 * @param channelId channel id
467 protected Optional<Channel> getThingChannel(String channelId) {
468 Channel channel = getThing().getChannel(channelId);
469 if (channel == null) {
470 return Optional.empty();
472 return Optional.of(channel);
477 * Configure channel update handlers. Classes which extend {@link AbstractHomeConnectThingHandler} must implement
478 * this class and add handlers.
480 * @param handlers channel update handlers
482 protected abstract void configureChannelUpdateHandlers(final Map<String, ChannelUpdateHandler> handlers);
485 * Configure event handlers. Classes which extend {@link AbstractHomeConnectThingHandler} must implement
486 * this class and add handlers.
488 * @param handlers Server-Sent-Event handlers
490 protected abstract void configureEventHandlers(final Map<String, EventHandler> handlers);
492 protected void configureUnsupportedProgramOptions(final Map<String, List<AvailableProgramOption>> programOptions) {
495 protected boolean isChannelLinkedToProgramOptionNotFullySupportedByApi() {
500 * Update all channels via API.
503 protected void updateChannels() {
504 if (isBridgeOffline()) {
505 logger.debug("Bridge handler not found or offline. Stopping update of channels. thing={}, haId={}",
506 getThingLabel(), getThingHaId());
507 } else if (isThingOffline()) {
508 logger.debug("{} offline. Stopping update of channels. haId={}", getThing().getLabel(), getThingHaId());
510 List<Channel> channels = getThing().getChannels();
511 for (Channel channel : channels) {
512 updateChannel(channel.getUID());
518 * Update Channel values via API.
520 * @param channelUID channel UID
522 protected void updateChannel(ChannelUID channelUID) {
523 if (!getApiClient().isPresent()) {
524 logger.error("Cannot update channel. No instance of api client found! thing={}, haId={}", getThingLabel(),
529 if (!isThingReadyToHandleCommand()) {
533 if ((isLinked(channelUID) || CHANNEL_OPERATION_STATE.equals(channelUID.getId())) // always update operation
535 && channelUpdateHandlers.containsKey(channelUID.getId())) {
537 ChannelUpdateHandler channelUpdateHandler = channelUpdateHandlers.get(channelUID.getId());
538 if (channelUpdateHandler != null) {
539 channelUpdateHandler.handle(channelUID, expiringStateMap);
541 } catch (ApplianceOfflineException e) {
543 "API communication problem while trying to update! Appliance offline. thing={}, haId={}, error={}",
544 getThingLabel(), getThingHaId(), e.getMessage());
545 updateStatus(OFFLINE);
546 resetChannelsOnOfflineEvent();
547 resetProgramStateChannels(true);
548 } catch (CommunicationException e) {
549 logger.debug("API communication problem while trying to update! thing={}, haId={}, error={}",
550 getThingLabel(), getThingHaId(), e.getMessage());
551 } catch (AuthorizationException e) {
552 logger.debug("Authentication problem while trying to update! thing={}, haId={}", getThingLabel(),
554 handleAuthenticationError(e);
560 * Reset program related channels.
562 * @param offline true if the device is considered as OFFLINE
564 protected void resetProgramStateChannels(boolean offline) {
565 logger.debug("Resetting active program channel states. thing={}, haId={}", getThingLabel(), getThingHaId());
569 * Reset all channels on OFFLINE event.
571 protected void resetChannelsOnOfflineEvent() {
572 logger.debug("Resetting channel states due to OFFLINE event. thing={}, haId={}", getThingLabel(),
574 getThingChannel(CHANNEL_POWER_STATE).ifPresent(channel -> updateState(channel.getUID(), OnOffType.OFF));
575 getThingChannel(CHANNEL_OPERATION_STATE).ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
576 getThingChannel(CHANNEL_DOOR_STATE).ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
577 getThingChannel(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE)
578 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
579 getThingChannel(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE)
580 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
581 getThingChannel(CHANNEL_REMOTE_START_ALLOWANCE_STATE)
582 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
583 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
584 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
588 * Map Home Connect key and value names to label.
589 * e.g. Dishcare.Dishwasher.Program.Eco50 --> Eco50 or BSH.Common.EnumType.OperationState.DelayedStart --> Delayed
593 * @return human readable label
595 protected String mapStringType(String type) {
596 int index = type.lastIndexOf(".");
597 if (index > 0 && type.length() > index) {
598 String sub = type.substring(index + 1);
599 StringBuilder sb = new StringBuilder();
600 for (String word : sub.split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])")) {
604 return sb.toString().trim();
610 * Map Home Connect stage value to label.
611 * e.g. Cooking.Hood.EnumType.IntensiveStage.IntensiveStage1 --> 1
614 * @return human readable label
616 protected String mapStageStringType(String stage) {
619 case STAGE_INTENSIVE_STAGE_OFF:
622 case STAGE_FAN_STAGE_01:
623 case STAGE_INTENSIVE_STAGE_1:
626 case STAGE_FAN_STAGE_02:
627 case STAGE_INTENSIVE_STAGE_2:
630 case STAGE_FAN_STAGE_03:
633 case STAGE_FAN_STAGE_04:
636 case STAGE_FAN_STAGE_05:
640 stage = mapStringType(stage);
647 * Map unit string (returned by home connect api) to Unit
649 * @param unit String eg. "°C"
652 protected Unit<Temperature> mapTemperature(@Nullable String unit) {
655 } else if (unit.endsWith("C")) {
663 * Map hex representation of color to HSB type.
665 * @param colorCode color code e.g. #001122
668 protected HSBType mapColor(String colorCode) {
669 HSBType color = HSBType.WHITE;
671 if (colorCode.length() == 7) {
672 int r = Integer.valueOf(colorCode.substring(1, 3), 16);
673 int g = Integer.valueOf(colorCode.substring(3, 5), 16);
674 int b = Integer.valueOf(colorCode.substring(5, 7), 16);
675 color = HSBType.fromRGB(r, g, b);
681 * Map HSB color type to hex representation.
683 * @param color HSB color
684 * @return color code e.g. #001122
686 protected String mapColor(HSBType color) {
687 String redValue = String.format("%02X", (int) (color.getRed().floatValue() * 2.55));
688 String greenValue = String.format("%02X", (int) (color.getGreen().floatValue() * 2.55));
689 String blueValue = String.format("%02X", (int) (color.getBlue().floatValue() * 2.55));
690 return "#" + redValue + greenValue + blueValue;
694 * Check bridge status and refresh connection status of thing accordingly.
696 protected void refreshThingStatus() {
697 Optional<HomeConnectApiClient> apiClient = getApiClient();
699 apiClient.ifPresent(client -> {
701 HomeAppliance homeAppliance = client.getHomeAppliance(getThingHaId());
702 if (!homeAppliance.isConnected()) {
703 updateStatus(OFFLINE);
705 updateStatus(ONLINE);
707 accessible.set(true);
708 } catch (CommunicationException e) {
710 "Update status to OFFLINE. Home Connect service is not reachable or a problem occurred! thing={}, haId={}, error={}.",
711 getThingLabel(), getThingHaId(), e.getMessage());
712 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
713 "Home Connect service is not reachable or a problem occurred! (" + e.getMessage() + ").");
714 accessible.set(false);
715 } catch (AuthorizationException e) {
717 "Update status to OFFLINE. Home Connect service is not reachable or a problem occurred! thing={}, haId={}, error={}",
718 getThingLabel(), getThingHaId(), e.getMessage());
719 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
720 "Home Connect service is not reachable or a problem occurred! (" + e.getMessage() + ").");
721 accessible.set(false);
722 handleAuthenticationError(e);
725 if (apiClient.isEmpty()) {
726 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
727 accessible.set(false);
732 * Get home appliance id of Thing.
734 * @return home appliance id
736 public String getThingHaId() {
737 return getThing().getConfiguration().get(HA_ID).toString();
741 * Returns the human readable label for this thing.
743 * @return the human readable label
745 protected @Nullable String getThingLabel() {
746 return getThing().getLabel();
750 * Handle authentication exception.
752 protected void handleAuthenticationError(AuthorizationException exception) {
753 if (isBridgeOnline()) {
755 "Thing handler threw authentication exception --> clear credential storage thing={}, haId={} error={}",
756 getThingLabel(), getThingHaId(), exception.getMessage());
758 getBridgeHandler().ifPresent(homeConnectBridgeHandler -> {
760 homeConnectBridgeHandler.getOAuthClientService().remove();
761 homeConnectBridgeHandler.reinitialize();
762 } catch (OAuthException e) {
763 // client is already closed --> we can ignore it
770 * Get operation state of device.
772 * @return operation state string
774 protected @Nullable String getOperationState() {
775 return operationState;
778 protected EventHandler defaultElapsedProgramTimeEventHandler() {
779 return event -> getThingChannel(CHANNEL_ELAPSED_PROGRAM_TIME)
780 .ifPresent(channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND)));
783 protected EventHandler defaultPowerStateEventHandler() {
785 getThingChannel(CHANNEL_POWER_STATE).ifPresent(
786 channel -> updateState(channel.getUID(), OnOffType.from(STATE_POWER_ON.equals(event.getValue()))));
788 if (STATE_POWER_ON.equals(event.getValue())) {
789 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID()));
791 resetProgramStateChannels(true);
792 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
793 .ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
798 protected EventHandler defaultDoorStateEventHandler() {
799 return event -> getThingChannel(CHANNEL_DOOR_STATE).ifPresent(channel -> updateState(channel.getUID(),
800 STATE_DOOR_OPEN.equals(event.getValue()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED));
803 protected EventHandler defaultOperationStateEventHandler() {
805 String value = event.getValue();
806 getThingChannel(CHANNEL_OPERATION_STATE).ifPresent(channel -> updateState(channel.getUID(),
807 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
809 if (STATE_OPERATION_FINISHED.equals(value)) {
810 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
811 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(100, PERCENT)));
812 getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
813 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, SECOND)));
814 } else if (STATE_OPERATION_RUN.equals(value)) {
815 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
816 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, PERCENT)));
817 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID()));
818 } else if (STATE_OPERATION_READY.equals(value)) {
819 resetProgramStateChannels(false);
824 protected EventHandler defaultActiveProgramEventHandler() {
826 String value = event.getValue();
827 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(channel -> updateState(channel.getUID(),
828 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
830 resetProgramStateChannels(false);
835 protected EventHandler updateProgramOptionsAndActiveProgramStateEventHandler() {
837 String value = event.getValue();
838 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(channel -> updateState(channel.getUID(),
839 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
841 resetProgramStateChannels(false);
844 Optional<HomeConnectApiClient> apiClient = getApiClient();
845 if (apiClient.isPresent() && isChannelLinkedToProgramOptionNotFullySupportedByApi()
846 && apiClient.get().isRemoteControlActive(getThingHaId())) {
847 // update channels linked to program options
848 Program program = apiClient.get().getSelectedProgram(getThingHaId());
849 if (program != null) {
850 processProgramOptions(program.getOptions());
853 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
854 logger.debug("Could not update program options. {}", e.getMessage());
860 protected EventHandler defaultEventPresentStateEventHandler(String channelId) {
861 return event -> getThingChannel(channelId).ifPresent(channel -> updateState(channel.getUID(),
862 OnOffType.from(!STATE_EVENT_PRESENT_STATE_OFF.equals(event.getValue()))));
865 protected EventHandler defaultBooleanEventHandler(String channelId) {
866 return event -> getThingChannel(channelId)
867 .ifPresent(channel -> updateState(channel.getUID(), OnOffType.from(event.getValueAsBoolean())));
870 protected EventHandler defaultRemainingProgramTimeEventHandler() {
871 return event -> getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
872 .ifPresent(channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND)));
875 protected EventHandler defaultSelectedProgramStateEventHandler() {
876 return event -> getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
877 .ifPresent(channel -> updateState(channel.getUID(),
878 event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue())));
881 protected EventHandler defaultAmbientLightColorStateEventHandler() {
882 return event -> getThingChannel(CHANNEL_AMBIENT_LIGHT_COLOR_STATE)
883 .ifPresent(channel -> updateState(channel.getUID(),
884 event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue())));
887 protected EventHandler defaultAmbientLightCustomColorStateEventHandler() {
888 return event -> getThingChannel(CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE).ifPresent(channel -> {
889 String value = event.getValue();
891 updateState(channel.getUID(), mapColor(value));
893 updateState(channel.getUID(), UnDefType.UNDEF);
898 protected EventHandler updateRemoteControlActiveAndProgramOptionsStateEventHandler() {
900 defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE).handle(event);
903 if (Boolean.parseBoolean(event.getValue())) {
904 // update available program options if update was previously delayed and remote control is enabled
905 String programKey = programOptionsDelayedUpdate;
906 if (programKey != null) {
907 logger.debug("Delayed update of options for program {}", programKey);
908 updateProgramOptionsStateDescriptions(programKey);
909 programOptionsDelayedUpdate = null;
912 if (isChannelLinkedToProgramOptionNotFullySupportedByApi()) {
913 Optional<HomeConnectApiClient> apiClient = getApiClient();
914 if (apiClient.isPresent()) {
915 Program program = apiClient.get().getSelectedProgram(getThingHaId());
916 if (program != null) {
917 processProgramOptions(program.getOptions());
922 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
923 logger.debug("Could not update program options. {}", e.getMessage());
928 protected EventHandler updateProgramOptionsAndSelectedProgramStateEventHandler() {
930 defaultSelectedProgramStateEventHandler().handle(event);
933 Optional<HomeConnectApiClient> apiClient = getApiClient();
934 String programKey = event.getValue();
936 if (apiClient.isPresent() && programKey != null) {
937 Boolean remoteControl = (availableProgramOptionsCache.get(programKey) == null
938 || isChannelLinkedToProgramOptionNotFullySupportedByApi())
939 ? apiClient.get().isRemoteControlActive(getThingHaId())
942 // Delay the update of available program options if options are not yet cached and remote control is
944 if (availableProgramOptionsCache.get(programKey) == null && !remoteControl) {
945 logger.debug("Delay update of options for program {}", programKey);
946 programOptionsDelayedUpdate = programKey;
948 updateProgramOptionsStateDescriptions(programKey);
951 if (isChannelLinkedToProgramOptionNotFullySupportedByApi() && remoteControl) {
952 // update channels linked to program options
953 Program program = apiClient.get().getSelectedProgram(getThingHaId());
954 if (program != null) {
955 processProgramOptions(program.getOptions());
959 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
960 logger.debug("Could not update program options. {}", e.getMessage());
965 protected EventHandler defaultPercentQuantityTypeEventHandler(String channelId) {
966 return event -> getThingChannel(channelId).ifPresent(
967 channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), PERCENT)));
970 protected EventHandler defaultPercentHandler(String channelId) {
971 return event -> getThingChannel(channelId)
972 .ifPresent(channel -> updateState(channel.getUID(), new PercentType(event.getValueAsInt())));
975 protected ChannelUpdateHandler defaultDoorStateChannelUpdateHandler() {
976 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
977 Optional<HomeConnectApiClient> apiClient = getApiClient();
978 if (apiClient.isPresent()) {
979 Data data = apiClient.get().getDoorState(getThingHaId());
980 if (data.getValue() != null) {
981 return STATE_DOOR_OPEN.equals(data.getValue()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
983 return UnDefType.UNDEF;
986 return UnDefType.UNDEF;
991 protected ChannelUpdateHandler defaultPowerStateChannelUpdateHandler() {
992 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
993 Optional<HomeConnectApiClient> apiClient = getApiClient();
994 if (apiClient.isPresent()) {
995 Data data = apiClient.get().getPowerState(getThingHaId());
996 if (data.getValue() != null) {
997 return OnOffType.from(STATE_POWER_ON.equals(data.getValue()));
999 return UnDefType.UNDEF;
1002 return UnDefType.UNDEF;
1007 protected ChannelUpdateHandler defaultAmbientLightChannelUpdateHandler() {
1008 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1009 Optional<HomeConnectApiClient> apiClient = getApiClient();
1010 if (apiClient.isPresent()) {
1011 Data data = apiClient.get().getAmbientLightState(getThingHaId());
1012 if (data.getValue() != null) {
1013 boolean enabled = data.getValueAsBoolean();
1016 Data brightnessData = apiClient.get().getAmbientLightBrightnessState(getThingHaId());
1017 getThingChannel(CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE)
1018 .ifPresent(channel -> updateState(channel.getUID(),
1019 new PercentType(brightnessData.getValueAsInt())));
1022 Data colorData = apiClient.get().getAmbientLightColorState(getThingHaId());
1023 getThingChannel(CHANNEL_AMBIENT_LIGHT_COLOR_STATE).ifPresent(
1024 channel -> updateState(channel.getUID(), new StringType(colorData.getValue())));
1027 Data customColorData = apiClient.get().getAmbientLightCustomColorState(getThingHaId());
1028 getThingChannel(CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE).ifPresent(channel -> {
1029 String value = customColorData.getValue();
1030 if (value != null) {
1031 updateState(channel.getUID(), mapColor(value));
1033 updateState(channel.getUID(), UnDefType.UNDEF);
1037 return OnOffType.from(enabled);
1039 return UnDefType.UNDEF;
1042 return UnDefType.UNDEF;
1047 protected ChannelUpdateHandler defaultNoOpUpdateHandler() {
1048 return (channelUID, cache) -> updateState(channelUID, UnDefType.UNDEF);
1051 protected ChannelUpdateHandler defaultOperationStateChannelUpdateHandler() {
1052 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1053 Optional<HomeConnectApiClient> apiClient = getApiClient();
1054 if (apiClient.isPresent()) {
1055 Data data = apiClient.get().getOperationState(getThingHaId());
1057 String value = data.getValue();
1058 if (value != null) {
1059 operationState = data.getValue();
1060 return new StringType(mapStringType(value));
1062 operationState = null;
1063 return UnDefType.UNDEF;
1066 return UnDefType.UNDEF;
1071 protected ChannelUpdateHandler defaultRemoteControlActiveStateChannelUpdateHandler() {
1072 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1073 Optional<HomeConnectApiClient> apiClient = getApiClient();
1074 if (apiClient.isPresent()) {
1075 return OnOffType.from(apiClient.get().isRemoteControlActive(getThingHaId()));
1077 return OnOffType.OFF;
1081 protected ChannelUpdateHandler defaultLocalControlActiveStateChannelUpdateHandler() {
1082 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1083 Optional<HomeConnectApiClient> apiClient = getApiClient();
1084 if (apiClient.isPresent()) {
1085 return OnOffType.from(apiClient.get().isLocalControlActive(getThingHaId()));
1087 return OnOffType.OFF;
1091 protected ChannelUpdateHandler defaultRemoteStartAllowanceChannelUpdateHandler() {
1092 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1093 Optional<HomeConnectApiClient> apiClient = getApiClient();
1094 if (apiClient.isPresent()) {
1095 return OnOffType.from(apiClient.get().isRemoteControlStartAllowed(getThingHaId()));
1097 return OnOffType.OFF;
1101 protected ChannelUpdateHandler defaultSelectedProgramStateUpdateHandler() {
1102 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1103 Optional<HomeConnectApiClient> apiClient = getApiClient();
1104 if (apiClient.isPresent()) {
1105 Program program = apiClient.get().getSelectedProgram(getThingHaId());
1106 if (program != null) {
1107 processProgramOptions(program.getOptions());
1108 return new StringType(program.getKey());
1110 return UnDefType.UNDEF;
1113 return UnDefType.UNDEF;
1117 protected ChannelUpdateHandler updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() {
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());
1123 if (program != null) {
1124 updateProgramOptionsStateDescriptions(program.getKey());
1125 processProgramOptions(program.getOptions());
1127 return new StringType(program.getKey());
1129 return UnDefType.UNDEF;
1132 return UnDefType.UNDEF;
1136 protected ChannelUpdateHandler getAndUpdateSelectedProgramStateUpdateHandler() {
1137 return (channelUID, cache) -> {
1138 Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
1139 if (channel.isPresent()) {
1140 defaultSelectedProgramStateUpdateHandler().handle(channel.get().getUID(), cache);
1145 protected ChannelUpdateHandler getAndUpdateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() {
1146 return (channelUID, cache) -> {
1147 Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
1148 if (channel.isPresent()) {
1149 updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler()
1150 .handle(channel.get().getUID(), cache);
1155 protected ChannelUpdateHandler defaultActiveProgramStateUpdateHandler() {
1156 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1157 Optional<HomeConnectApiClient> apiClient = getApiClient();
1158 if (apiClient.isPresent()) {
1159 Program program = apiClient.get().getActiveProgram(getThingHaId());
1161 if (program != null) {
1162 processProgramOptions(program.getOptions());
1163 return new StringType(mapStringType(program.getKey()));
1165 resetProgramStateChannels(false);
1166 return UnDefType.UNDEF;
1169 return UnDefType.UNDEF;
1173 protected void handleTemperatureCommand(final ChannelUID channelUID, final Command command,
1174 final HomeConnectApiClient apiClient)
1175 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1176 if (command instanceof QuantityType) {
1177 QuantityType<?> quantity = (QuantityType<?>) command;
1183 if (quantity.getUnit().equals(SIUnits.CELSIUS) || quantity.getUnit().equals(ImperialUnits.FAHRENHEIT)) {
1184 unit = quantity.getUnit().toString();
1185 value = String.valueOf(quantity.intValue());
1187 logger.debug("Converting target temperature from {}{} to °C value. thing={}, haId={}",
1188 quantity.intValue(), quantity.getUnit().toString(), getThingLabel(), getThingHaId());
1190 var celsius = quantity.toUnit(SIUnits.CELSIUS);
1191 if (celsius == null) {
1192 logger.warn("Converting temperature to celsius failed! quantity={}", quantity);
1195 value = String.valueOf(celsius.intValue());
1197 logger.debug("Converted value {}{}", value, unit);
1200 if (value != null) {
1201 logger.debug("Set temperature to {} {}. thing={}, haId={}", value, unit, getThingLabel(),
1203 switch (channelUID.getId()) {
1204 case CHANNEL_REFRIGERATOR_SETPOINT_TEMPERATURE:
1205 apiClient.setFridgeSetpointTemperature(getThingHaId(), value, unit);
1206 case CHANNEL_FREEZER_SETPOINT_TEMPERATURE:
1207 apiClient.setFreezerSetpointTemperature(getThingHaId(), value, unit);
1209 case CHANNEL_SETPOINT_TEMPERATURE:
1210 apiClient.setProgramOptions(getThingHaId(), OPTION_SETPOINT_TEMPERATURE, value, unit, true,
1214 logger.debug("Unknown channel! Cannot set temperature. channelUID={}", channelUID);
1217 } catch (UnconvertibleException e) {
1218 logger.warn("Could not set temperature! haId={}, error={}", getThingHaId(), e.getMessage());
1223 protected void handleLightCommands(final ChannelUID channelUID, final Command command,
1224 final HomeConnectApiClient apiClient)
1225 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1226 switch (channelUID.getId()) {
1227 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1228 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1229 // turn light on if turned off
1230 turnLightOn(channelUID, apiClient);
1232 int newBrightness = BRIGHTNESS_MIN;
1233 if (command instanceof OnOffType) {
1234 newBrightness = command == OnOffType.ON ? BRIGHTNESS_MAX : BRIGHTNESS_MIN;
1235 } else if (command instanceof IncreaseDecreaseType) {
1236 int currentBrightness = getCurrentBrightness(channelUID, apiClient);
1237 if (command.equals(IncreaseDecreaseType.INCREASE)) {
1238 newBrightness = currentBrightness + BRIGHTNESS_DIM_STEP;
1240 newBrightness = currentBrightness - BRIGHTNESS_DIM_STEP;
1242 } else if (command instanceof PercentType) {
1243 newBrightness = (int) Math.floor(((PercentType) command).doubleValue());
1244 } else if (command instanceof DecimalType) {
1245 newBrightness = ((DecimalType) command).intValue();
1248 // check in in range
1249 newBrightness = Math.min(Math.max(newBrightness, BRIGHTNESS_MIN), BRIGHTNESS_MAX);
1251 setLightBrightness(channelUID, apiClient, newBrightness);
1253 case CHANNEL_FUNCTIONAL_LIGHT_STATE:
1254 if (command instanceof OnOffType) {
1255 apiClient.setFunctionalLightState(getThingHaId(), OnOffType.ON.equals(command));
1258 case CHANNEL_AMBIENT_LIGHT_STATE:
1259 if (command instanceof OnOffType) {
1260 apiClient.setAmbientLightState(getThingHaId(), OnOffType.ON.equals(command));
1263 case CHANNEL_AMBIENT_LIGHT_COLOR_STATE:
1264 if (command instanceof StringType) {
1265 turnLightOn(channelUID, apiClient);
1266 apiClient.setAmbientLightColorState(getThingHaId(), command.toFullString());
1269 case CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE:
1270 turnLightOn(channelUID, apiClient);
1272 // make sure 'custom color' is set as color
1273 Data ambientLightColorState = apiClient.getAmbientLightColorState(getThingHaId());
1274 if (!STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR.equals(ambientLightColorState.getValue())) {
1275 apiClient.setAmbientLightColorState(getThingHaId(), STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR);
1278 if (command instanceof HSBType) {
1279 apiClient.setAmbientLightCustomColorState(getThingHaId(), mapColor((HSBType) command));
1280 } else if (command instanceof StringType) {
1281 apiClient.setAmbientLightCustomColorState(getThingHaId(), command.toFullString());
1287 protected void handlePowerCommand(final ChannelUID channelUID, final Command command,
1288 final HomeConnectApiClient apiClient, String stateNotOn)
1289 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1290 if (command instanceof OnOffType && CHANNEL_POWER_STATE.equals(channelUID.getId())) {
1291 apiClient.setPowerState(getThingHaId(), OnOffType.ON.equals(command) ? STATE_POWER_ON : stateNotOn);
1295 private int getCurrentBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient)
1296 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1297 String id = channelUID.getId();
1298 if (CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE.equals(id)) {
1299 return apiClient.getFunctionalLightBrightnessState(getThingHaId()).getValueAsInt();
1301 return apiClient.getAmbientLightBrightnessState(getThingHaId()).getValueAsInt();
1305 private void setLightBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient, int value)
1306 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1307 switch (channelUID.getId()) {
1308 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1309 apiClient.setFunctionalLightBrightnessState(getThingHaId(), value);
1311 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1312 apiClient.setAmbientLightBrightnessState(getThingHaId(), value);
1317 private void turnLightOn(final ChannelUID channelUID, final HomeConnectApiClient apiClient)
1318 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1319 switch (channelUID.getId()) {
1320 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1321 Data functionalLightState = apiClient.getFunctionalLightState(getThingHaId());
1322 if (!functionalLightState.getValueAsBoolean()) {
1323 apiClient.setFunctionalLightState(getThingHaId(), true);
1326 case CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE:
1327 case CHANNEL_AMBIENT_LIGHT_COLOR_STATE:
1328 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1329 Data ambientLightState = apiClient.getAmbientLightState(getThingHaId());
1330 if (!ambientLightState.getValueAsBoolean()) {
1331 apiClient.setAmbientLightState(getThingHaId(), true);
1337 protected void processProgramOptions(List<Option> options) {
1338 options.forEach(option -> {
1339 String key = option.getKey();
1342 case OPTION_WASHER_TEMPERATURE:
1343 getThingChannel(CHANNEL_WASHER_TEMPERATURE)
1344 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1346 case OPTION_WASHER_SPIN_SPEED:
1347 getThingChannel(CHANNEL_WASHER_SPIN_SPEED)
1348 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1350 case OPTION_WASHER_IDOS_1_DOSING_LEVEL:
1351 getThingChannel(CHANNEL_WASHER_IDOS1_LEVEL)
1352 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1354 case OPTION_WASHER_IDOS_2_DOSING_LEVEL:
1355 getThingChannel(CHANNEL_WASHER_IDOS2_LEVEL)
1356 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1358 case OPTION_DRYER_DRYING_TARGET:
1359 getThingChannel(CHANNEL_DRYER_DRYING_TARGET)
1360 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1362 case OPTION_HOOD_INTENSIVE_LEVEL:
1363 String hoodIntensiveLevelValue = option.getValue();
1364 if (hoodIntensiveLevelValue != null) {
1365 getThingChannel(CHANNEL_HOOD_INTENSIVE_LEVEL)
1366 .ifPresent(channel -> updateState(channel.getUID(),
1367 new StringType(mapStageStringType(hoodIntensiveLevelValue))));
1370 case OPTION_HOOD_VENTING_LEVEL:
1371 String hoodVentingLevel = option.getValue();
1372 if (hoodVentingLevel != null) {
1373 getThingChannel(CHANNEL_HOOD_VENTING_LEVEL)
1374 .ifPresent(channel -> updateState(channel.getUID(),
1375 new StringType(mapStageStringType(hoodVentingLevel))));
1378 case OPTION_SETPOINT_TEMPERATURE:
1379 getThingChannel(CHANNEL_SETPOINT_TEMPERATURE).ifPresent(channel -> updateState(channel.getUID(),
1380 new QuantityType<>(option.getValueAsInt(), mapTemperature(option.getUnit()))));
1382 case OPTION_DURATION:
1383 getThingChannel(CHANNEL_DURATION).ifPresent(channel -> updateState(channel.getUID(),
1384 new QuantityType<>(option.getValueAsInt(), SECOND)));
1386 case OPTION_FINISH_IN_RELATIVE:
1387 case OPTION_REMAINING_PROGRAM_TIME:
1388 getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
1389 .ifPresent(channel -> updateState(channel.getUID(),
1390 new QuantityType<>(option.getValueAsInt(), SECOND)));
1392 case OPTION_ELAPSED_PROGRAM_TIME:
1393 getThingChannel(CHANNEL_ELAPSED_PROGRAM_TIME).ifPresent(channel -> updateState(channel.getUID(),
1394 new QuantityType<>(option.getValueAsInt(), SECOND)));
1396 case OPTION_PROGRAM_PROGRESS:
1397 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
1398 .ifPresent(channel -> updateState(channel.getUID(),
1399 new QuantityType<>(option.getValueAsInt(), PERCENT)));
1401 case OPTION_WASHER_IDOS_1_ACTIVE:
1402 getThingChannel(CHANNEL_WASHER_IDOS1).ifPresent(
1403 channel -> updateState(channel.getUID(), OnOffType.from(option.getValueAsBoolean())));
1405 case OPTION_WASHER_IDOS_2_ACTIVE:
1406 getThingChannel(CHANNEL_WASHER_IDOS2).ifPresent(
1407 channel -> updateState(channel.getUID(), OnOffType.from(option.getValueAsBoolean())));
1409 case OPTION_WASHER_VARIO_PERFECT:
1410 getThingChannel(CHANNEL_WASHER_VARIO_PERFECT)
1411 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1413 case OPTION_WASHER_LESS_IRONING:
1414 getThingChannel(CHANNEL_WASHER_LESS_IRONING).ifPresent(
1415 channel -> updateState(channel.getUID(), OnOffType.from(option.getValueAsBoolean())));
1417 case OPTION_WASHER_PRE_WASH:
1418 getThingChannel(CHANNEL_WASHER_PRE_WASH).ifPresent(
1419 channel -> updateState(channel.getUID(), OnOffType.from(option.getValueAsBoolean())));
1421 case OPTION_WASHER_RINSE_PLUS:
1422 getThingChannel(CHANNEL_WASHER_RINSE_PLUS)
1423 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1425 case OPTION_WASHER_SOAK:
1426 getThingChannel(CHANNEL_WASHER_SOAK).ifPresent(
1427 channel -> updateState(channel.getUID(), OnOffType.from(option.getValueAsBoolean())));
1429 case OPTION_WASHER_ENERGY_FORECAST:
1430 getThingChannel(CHANNEL_PROGRAM_ENERGY).ifPresent(channel -> updateState(channel.getUID(),
1431 new QuantityType<>(option.getValueAsInt(), PERCENT)));
1433 case OPTION_WASHER_WATER_FORECAST:
1434 getThingChannel(CHANNEL_PROGRAM_WATER).ifPresent(channel -> updateState(channel.getUID(),
1435 new QuantityType<>(option.getValueAsInt(), PERCENT)));
1442 protected String convertWasherTemperature(String value) {
1443 if (value.startsWith(TEMPERATURE_PREFIX + "GC")) {
1444 return value.replace(TEMPERATURE_PREFIX + "GC", "") + "°C";
1447 if (value.startsWith(TEMPERATURE_PREFIX + "Ul")) {
1448 return mapStringType(value.replace(TEMPERATURE_PREFIX + "Ul", ""));
1451 return mapStringType(value);
1454 protected String convertWasherSpinSpeed(String value) {
1455 if (value.startsWith(SPIN_SPEED_PREFIX + "RPM")) {
1456 return value.replace(SPIN_SPEED_PREFIX + "RPM", "") + " RPM";
1459 if (value.startsWith(SPIN_SPEED_PREFIX + "Ul")) {
1460 return value.replace(SPIN_SPEED_PREFIX + "Ul", "");
1463 return mapStringType(value);
1466 protected void updateProgramOptionsStateDescriptions(String programKey)
1467 throws AuthorizationException, ApplianceOfflineException {
1468 Optional<HomeConnectApiClient> apiClient = getApiClient();
1469 if (apiClient.isPresent()) {
1470 boolean cacheToSet = false;
1471 List<AvailableProgramOption> availableProgramOptions;
1472 if (availableProgramOptionsCache.containsKey(programKey)) {
1473 logger.debug("Returning cached options for program '{}'.", programKey);
1474 availableProgramOptions = availableProgramOptionsCache.get(programKey);
1475 availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
1476 : Collections.emptyList();
1478 // Depending on the current program operation state, the APi request could trigger a
1479 // CommunicationException exception due to returned status code 409
1481 availableProgramOptions = apiClient.get().getProgramOptions(getThingHaId(), programKey);
1482 if (availableProgramOptions == null) {
1483 // Program is unsupported, to avoid calling again the API for this program, save in cache either
1484 // the predefined options provided by the binding if they exist, or an empty list of options
1485 if (unsupportedProgramOptions.containsKey(programKey)) {
1486 availableProgramOptions = unsupportedProgramOptions.get(programKey);
1487 availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
1489 logger.debug("Saving predefined options in cache for unsupported program '{}'.",
1492 availableProgramOptions = emptyList();
1493 logger.debug("Saving empty options in cache for unsupported program '{}'.", programKey);
1495 availableProgramOptionsCache.put(programKey, availableProgramOptions);
1497 // Add the unsupported program in programs cache and refresh the dynamic state description
1498 if (addUnsupportedProgramInCache(programKey)) {
1499 updateSelectedProgramStateDescription();
1502 // If no options are returned by the API, using predefined options if available
1503 if (availableProgramOptions.isEmpty() && unsupportedProgramOptions.containsKey(programKey)) {
1504 availableProgramOptions = unsupportedProgramOptions.get(programKey);
1505 availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
1510 } catch (CommunicationException e) {
1511 availableProgramOptions = emptyList();
1515 Optional<Channel> channelSpinSpeed = getThingChannel(CHANNEL_WASHER_SPIN_SPEED);
1516 Optional<Channel> channelTemperature = getThingChannel(CHANNEL_WASHER_TEMPERATURE);
1517 Optional<Channel> channelDryingTarget = getThingChannel(CHANNEL_DRYER_DRYING_TARGET);
1519 Optional<AvailableProgramOption> optionsSpinSpeed = availableProgramOptions.stream()
1520 .filter(option -> OPTION_WASHER_SPIN_SPEED.equals(option.getKey())).findFirst();
1521 Optional<AvailableProgramOption> optionsTemperature = availableProgramOptions.stream()
1522 .filter(option -> OPTION_WASHER_TEMPERATURE.equals(option.getKey())).findFirst();
1523 Optional<AvailableProgramOption> optionsDryingTarget = availableProgramOptions.stream()
1524 .filter(option -> OPTION_DRYER_DRYING_TARGET.equals(option.getKey())).findFirst();
1526 // Save options in cache only if we got options for all expected channels
1527 if (cacheToSet && (!channelSpinSpeed.isPresent() || optionsSpinSpeed.isPresent())
1528 && (!channelTemperature.isPresent() || optionsTemperature.isPresent())
1529 && (!channelDryingTarget.isPresent() || optionsDryingTarget.isPresent())) {
1530 logger.debug("Saving options in cache for program '{}'.", programKey);
1531 availableProgramOptionsCache.put(programKey, availableProgramOptions);
1534 channelSpinSpeed.ifPresent(channel -> optionsSpinSpeed.ifPresentOrElse(
1535 option -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1536 createStateOptions(option, this::convertWasherSpinSpeed)),
1537 () -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList())));
1538 channelTemperature.ifPresent(channel -> optionsTemperature.ifPresentOrElse(
1539 option -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1540 createStateOptions(option, this::convertWasherTemperature)),
1541 () -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList())));
1542 channelDryingTarget.ifPresent(channel -> optionsDryingTarget.ifPresentOrElse(
1543 option -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1544 createStateOptions(option, this::mapStringType)),
1545 () -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList())));
1549 protected HomeConnectDynamicStateDescriptionProvider getDynamicStateDescriptionProvider() {
1550 return dynamicStateDescriptionProvider;
1553 private List<StateOption> createStateOptions(AvailableProgramOption option,
1554 Function<String, String> stateConverter) {
1555 return option.getAllowedValues().stream().map(av -> new StateOption(av, stateConverter.apply(av)))
1556 .collect(Collectors.toList());
1559 private synchronized void scheduleOfflineMonitor1() {
1560 this.reinitializationFuture1 = scheduler.schedule(() -> {
1561 if (isBridgeOnline() && isThingOffline()) {
1562 logger.debug("Offline monitor 1: Check if thing is ONLINE. thing={}, haId={}", getThingLabel(),
1564 refreshThingStatus();
1565 if (isThingOnline()) {
1566 logger.debug("Offline monitor 1: Thing status changed to ONLINE. thing={}, haId={}",
1567 getThingLabel(), getThingHaId());
1570 scheduleOfflineMonitor1();
1573 scheduleOfflineMonitor1();
1575 }, AbstractHomeConnectThingHandler.OFFLINE_MONITOR_1_DELAY_MIN, TimeUnit.MINUTES);
1578 private synchronized void stopOfflineMonitor1() {
1579 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture1;
1580 if (reinitializationFuture != null) {
1581 reinitializationFuture.cancel(false);
1582 this.reinitializationFuture1 = null;
1586 private synchronized void scheduleOfflineMonitor2() {
1587 this.reinitializationFuture2 = scheduler.schedule(() -> {
1588 if (isBridgeOnline() && !accessible.get()) {
1589 logger.debug("Offline monitor 2: Check if thing is ONLINE. thing={}, haId={}", getThingLabel(),
1591 refreshThingStatus();
1592 if (isThingOnline()) {
1593 logger.debug("Offline monitor 2: Thing status changed to ONLINE. thing={}, haId={}",
1594 getThingLabel(), getThingHaId());
1597 scheduleOfflineMonitor2();
1600 scheduleOfflineMonitor2();
1602 }, AbstractHomeConnectThingHandler.OFFLINE_MONITOR_2_DELAY_MIN, TimeUnit.MINUTES);
1605 private synchronized void stopOfflineMonitor2() {
1606 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture2;
1607 if (reinitializationFuture != null) {
1608 reinitializationFuture.cancel(false);
1609 this.reinitializationFuture2 = null;
1613 private synchronized void scheduleRetryRegistering() {
1614 this.reinitializationFuture3 = scheduler.schedule(() -> {
1615 logger.debug("Try to register event listener again. haId={}", getThingHaId());
1616 unregisterEventListener();
1617 registerEventListener();
1618 }, AbstractHomeConnectThingHandler.EVENT_LISTENER_CONNECT_RETRY_DELAY_MIN, TimeUnit.MINUTES);
1621 private synchronized void stopRetryRegistering() {
1622 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture3;
1623 if (reinitializationFuture != null) {
1624 reinitializationFuture.cancel(true);
1625 this.reinitializationFuture3 = null;
1629 protected List<AvailableProgram> getPrograms()
1630 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1631 if (!programsCache.isEmpty()) {
1632 logger.debug("Returning cached programs for '{}'.", getThingHaId());
1633 return programsCache;
1635 Optional<HomeConnectApiClient> apiClient = getApiClient();
1636 if (apiClient.isPresent()) {
1637 programsCache.addAll(apiClient.get().getPrograms(getThingHaId()));
1638 return programsCache;
1640 throw new CommunicationException("API not initialized");
1646 * Add an entry in the programs cache and mark it as unsupported
1648 * @param programKey program id
1649 * @return true if an entry was added in the cache
1651 private boolean addUnsupportedProgramInCache(String programKey) {
1652 Optional<AvailableProgram> prog = programsCache.stream().filter(program -> programKey.equals(program.getKey()))
1654 if (!prog.isPresent()) {
1655 programsCache.add(new AvailableProgram(programKey, false));
1656 logger.debug("{} added in programs cache as an unsupported program", programKey);
1663 * Check if a program is marked as supported in the programs cache
1665 * @param programKey program id
1666 * @return true if the program is in the cache and marked as supported
1668 protected boolean isProgramSupported(String programKey) {
1669 Optional<AvailableProgram> prog = programsCache.stream().filter(program -> programKey.equals(program.getKey()))
1671 return prog.isPresent() && prog.get().isSupported();