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.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.atomic.AtomicBoolean;
32 import java.util.function.Function;
33 import java.util.stream.Collectors;
35 import javax.measure.UnconvertibleException;
36 import javax.measure.Unit;
37 import javax.measure.quantity.Temperature;
39 import org.eclipse.jdt.annotation.NonNullByDefault;
40 import org.eclipse.jdt.annotation.Nullable;
41 import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
42 import org.openhab.binding.homeconnect.internal.client.HomeConnectEventSourceClient;
43 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
44 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
45 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
46 import org.openhab.binding.homeconnect.internal.client.listener.HomeConnectEventListener;
47 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
48 import org.openhab.binding.homeconnect.internal.client.model.Data;
49 import org.openhab.binding.homeconnect.internal.client.model.Event;
50 import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
51 import org.openhab.binding.homeconnect.internal.client.model.Option;
52 import org.openhab.binding.homeconnect.internal.client.model.Program;
53 import org.openhab.binding.homeconnect.internal.handler.cache.ExpiringStateMap;
54 import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
55 import org.openhab.core.auth.client.oauth2.OAuthException;
56 import org.openhab.core.library.types.DecimalType;
57 import org.openhab.core.library.types.HSBType;
58 import org.openhab.core.library.types.IncreaseDecreaseType;
59 import org.openhab.core.library.types.OnOffType;
60 import org.openhab.core.library.types.OpenClosedType;
61 import org.openhab.core.library.types.PercentType;
62 import org.openhab.core.library.types.QuantityType;
63 import org.openhab.core.library.types.StringType;
64 import org.openhab.core.library.unit.ImperialUnits;
65 import org.openhab.core.library.unit.SIUnits;
66 import org.openhab.core.thing.Bridge;
67 import org.openhab.core.thing.Channel;
68 import org.openhab.core.thing.ChannelUID;
69 import org.openhab.core.thing.Thing;
70 import org.openhab.core.thing.ThingStatusDetail;
71 import org.openhab.core.thing.ThingStatusInfo;
72 import org.openhab.core.thing.binding.BaseThingHandler;
73 import org.openhab.core.thing.binding.BridgeHandler;
74 import org.openhab.core.types.Command;
75 import org.openhab.core.types.RefreshType;
76 import org.openhab.core.types.StateOption;
77 import org.openhab.core.types.UnDefType;
78 import org.slf4j.Logger;
79 import org.slf4j.LoggerFactory;
82 * The {@link AbstractHomeConnectThingHandler} is responsible for handling commands, which are
83 * sent to one of the channels.
85 * @author Jonas Brüstel - Initial contribution
88 public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler implements HomeConnectEventListener {
90 private static final int CACHE_TTL_SEC = 2;
91 private static final int OFFLINE_MONITOR_1_DELAY_MIN = 30;
92 private static final int OFFLINE_MONITOR_2_DELAY_MIN = 4;
93 private static final int EVENT_LISTENER_CONNECT_RETRY_DELAY_MIN = 10;
95 private @Nullable String operationState;
96 private @Nullable ScheduledFuture<?> reinitializationFuture1;
97 private @Nullable ScheduledFuture<?> reinitializationFuture2;
98 private @Nullable ScheduledFuture<?> reinitializationFuture3;
99 private boolean ignoreEventSourceClosedEvent;
100 private @Nullable String programOptionsDelayedUpdate;
102 private final ConcurrentHashMap<String, EventHandler> eventHandlers;
103 private final ConcurrentHashMap<String, ChannelUpdateHandler> channelUpdateHandlers;
104 private final HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
105 private final ExpiringStateMap expiringStateMap;
106 private final AtomicBoolean accessible;
107 private final Logger logger = LoggerFactory.getLogger(AbstractHomeConnectThingHandler.class);
108 private final Map<String, List<AvailableProgramOption>> availableProgramOptionsCache;
110 public AbstractHomeConnectThingHandler(Thing thing,
111 HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
113 eventHandlers = new ConcurrentHashMap<>();
114 channelUpdateHandlers = new ConcurrentHashMap<>();
115 this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider;
116 expiringStateMap = new ExpiringStateMap(Duration.ofSeconds(CACHE_TTL_SEC));
117 accessible = new AtomicBoolean(false);
118 availableProgramOptionsCache = new ConcurrentHashMap<>();
120 configureEventHandlers(eventHandlers);
121 configureChannelUpdateHandlers(channelUpdateHandlers);
125 public void initialize() {
126 if (getBridgeHandler().isEmpty()) {
127 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
128 accessible.set(false);
129 } else if (isBridgeOffline()) {
130 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
131 accessible.set(false);
133 updateStatus(UNKNOWN);
134 scheduler.submit(() -> {
135 refreshThingStatus(); // set ONLINE / OFFLINE
136 updateSelectedProgramStateDescription();
138 registerEventListener();
139 scheduleOfflineMonitor1();
140 scheduleOfflineMonitor2();
146 public void dispose() {
147 stopRetryRegistering();
148 stopOfflineMonitor1();
149 stopOfflineMonitor2();
150 unregisterEventListener(true);
154 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
155 logger.debug("Bridge status changed to {} ({}). haId={}", bridgeStatusInfo, getThingLabel(), getThingHaId());
159 private void reinitialize() {
160 logger.debug("Reinitialize thing handler ({}). haId={}", getThingLabel(), getThingHaId());
161 stopRetryRegistering();
162 stopOfflineMonitor1();
163 stopOfflineMonitor2();
164 unregisterEventListener();
169 * Handles a command for a given channel.
171 * This method is only called, if the thing has been initialized (status ONLINE/OFFLINE/UNKNOWN).
174 * @param channelUID the {@link ChannelUID} of the channel to which the command was sent
175 * @param command the {@link Command}
176 * @param apiClient the {@link HomeConnectApiClient}
177 * @throws CommunicationException communication problem
178 * @throws AuthorizationException authorization problem
179 * @throws ApplianceOfflineException appliance offline
181 protected void handleCommand(ChannelUID channelUID, Command command, HomeConnectApiClient apiClient)
182 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
183 if (command instanceof RefreshType) {
184 updateChannel(channelUID);
185 } else if (command instanceof StringType && CHANNEL_BASIC_ACTIONS_STATE.equals(channelUID.getId())
186 && getBridgeHandler().isPresent()) {
187 updateState(channelUID, new StringType(""));
189 if (COMMAND_START.equalsIgnoreCase(command.toFullString())) {
190 HomeConnectBridgeHandler homeConnectBridgeHandler = getBridgeHandler().get();
191 // workaround for api bug
192 // if simulator, program options have to be passed along with the desired program
193 // if non simulator, some options throw a "SDK.Error.UnsupportedOption" error
194 if (homeConnectBridgeHandler.getConfiguration().isSimulator()) {
195 apiClient.startSelectedProgram(getThingHaId());
197 Program selectedProgram = apiClient.getSelectedProgram(getThingHaId());
198 if (selectedProgram != null) {
199 apiClient.startProgram(getThingHaId(), selectedProgram.getKey());
202 } else if (COMMAND_STOP.equalsIgnoreCase(command.toFullString())) {
203 apiClient.stopProgram(getThingHaId());
204 } else if (COMMAND_SELECTED.equalsIgnoreCase(command.toFullString())) {
205 apiClient.getSelectedProgram(getThingHaId());
207 logger.debug("Start custom program. command={} haId={}", command.toFullString(), getThingHaId());
208 apiClient.startCustomProgram(getThingHaId(), command.toFullString());
210 } else if (command instanceof StringType && CHANNEL_SELECTED_PROGRAM_STATE.equals(channelUID.getId())) {
211 apiClient.setSelectedProgram(getThingHaId(), command.toFullString());
216 public final void handleCommand(ChannelUID channelUID, Command command) {
217 var apiClient = getApiClient();
218 if ((isThingReadyToHandleCommand() || (this instanceof HomeConnectHoodHandler && isBridgeOnline()
219 && isThingAccessibleViaServerSentEvents())) && apiClient.isPresent()) {
220 logger.debug("Handle \"{}\" command ({}). haId={}", command, channelUID.getId(), getThingHaId());
222 handleCommand(channelUID, command, apiClient.get());
223 } catch (ApplianceOfflineException e) {
224 logger.debug("Could not handle command {}. Appliance offline. thing={}, haId={}, error={}",
225 command.toFullString(), getThingLabel(), getThingHaId(), e.getMessage());
226 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
227 resetChannelsOnOfflineEvent();
228 resetProgramStateChannels(true);
229 } catch (CommunicationException e) {
230 logger.debug("Could not handle command {}. API communication problem! error={}, haId={}",
231 command.toFullString(), e.getMessage(), getThingHaId());
232 } catch (AuthorizationException e) {
233 logger.debug("Could not handle command {}. Authorization problem! error={}, haId={}",
234 command.toFullString(), e.getMessage(), getThingHaId());
236 handleAuthenticationError(e);
242 public void onEvent(Event event) {
243 if (DISCONNECTED.equals(event.getType())) {
244 logger.debug("Received DISCONNECTED event. Set {} to OFFLINE. haId={}", getThing().getLabel(),
246 updateStatus(OFFLINE);
247 resetChannelsOnOfflineEvent();
248 resetProgramStateChannels(true);
249 } else if (isThingOnline() && CONNECTED.equals(event.getType())) {
250 logger.debug("Received CONNECTED event. Update power state channel. haId={}", getThingHaId());
251 getThingChannel(CHANNEL_POWER_STATE).ifPresent(c -> updateChannel(c.getUID()));
252 } else if (isThingOffline() && !KEEP_ALIVE.equals(event.getType())) {
253 updateStatus(ONLINE);
254 logger.debug("Set {} to ONLINE and update channels. haId={}", getThing().getLabel(), getThingHaId());
255 updateSelectedProgramStateDescription();
259 String key = event.getKey();
260 if (EVENT_OPERATION_STATE.equals(key)) {
261 operationState = event.getValue() == null ? null : event.getValue();
264 if (key != null && eventHandlers.containsKey(key)) {
265 EventHandler eventHandler = eventHandlers.get(key);
266 if (eventHandler != null) {
267 eventHandler.handle(event);
271 accessible.set(true);
275 public void onClosed() {
276 if (ignoreEventSourceClosedEvent) {
277 logger.debug("Ignoring event source close event. thing={}, haId={}", getThing().getLabel(), getThingHaId());
279 unregisterEventListener();
280 refreshThingStatus();
281 registerEventListener();
286 public void onRateLimitReached() {
287 unregisterEventListener();
290 scheduleRetryRegistering();
294 * Register event listener.
296 protected void registerEventListener() {
297 if (isBridgeOnline() && isThingAccessibleViaServerSentEvents()) {
298 getEventSourceClient().ifPresent(client -> {
300 ignoreEventSourceClosedEvent = false;
301 client.registerEventListener(getThingHaId(), this);
302 } catch (CommunicationException | AuthorizationException e) {
303 logger.warn("Could not open event source connection. thing={}, haId={}, error={}", getThingLabel(),
304 getThingHaId(), e.getMessage());
311 * Unregister event listener.
313 protected void unregisterEventListener() {
314 unregisterEventListener(false);
317 private void unregisterEventListener(boolean immediate) {
318 getEventSourceClient().ifPresent(client -> {
319 ignoreEventSourceClosedEvent = true;
320 client.unregisterEventListener(this, immediate, false);
325 * Get {@link HomeConnectApiClient}.
327 * @return client instance
329 protected Optional<HomeConnectApiClient> getApiClient() {
330 return getBridgeHandler().map(HomeConnectBridgeHandler::getApiClient);
334 * Get {@link HomeConnectEventSourceClient}.
336 * @return client instance if present
338 protected Optional<HomeConnectEventSourceClient> getEventSourceClient() {
339 return getBridgeHandler().map(HomeConnectBridgeHandler::getEventSourceClient);
343 * Update state description of selected program (Fetch programs via API).
345 protected void updateSelectedProgramStateDescription() {
346 if (isBridgeOffline() || isThingOffline()) {
350 Optional<HomeConnectApiClient> apiClient = getApiClient();
351 if (apiClient.isPresent()) {
353 List<StateOption> stateOptions = apiClient.get().getPrograms(getThingHaId()).stream()
354 .map(p -> new StateOption(p.getKey(), mapStringType(p.getKey()))).collect(Collectors.toList());
356 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(
357 channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions));
358 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
359 logger.debug("Could not fetch available programs. thing={}, haId={}, error={}", getThingLabel(),
360 getThingHaId(), e.getMessage());
361 removeSelectedProgramStateDescription();
364 removeSelectedProgramStateDescription();
369 * Remove state description of selected program.
371 protected void removeSelectedProgramStateDescription() {
372 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
373 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList()));
377 * Is thing ready to process commands. If bridge or thing itself is offline commands will be ignored.
379 * @return true if ready
381 protected boolean isThingReadyToHandleCommand() {
382 if (isBridgeOffline()) {
383 logger.debug("Bridge is OFFLINE. Ignore command. thing={}, haId={}", getThingLabel(), getThingHaId());
387 if (isThingOffline()) {
388 logger.debug("{} is OFFLINE. Ignore command. haId={}", getThing().getLabel(), getThingHaId());
396 * Checks if bridge is online and set.
398 * @return true if online
400 protected boolean isBridgeOnline() {
401 Bridge bridge = getBridge();
402 return bridge != null && ONLINE.equals(bridge.getStatus());
406 * Checks if bridge is offline or not set.
408 * @return true if offline
410 protected boolean isBridgeOffline() {
411 return !isBridgeOnline();
415 * Checks if thing is online.
417 * @return true if online
419 protected boolean isThingOnline() {
420 return ONLINE.equals(getThing().getStatus());
424 * Checks if thing is connected to the cloud and accessible via SSE.
426 * @return true if yes
428 public boolean isThingAccessibleViaServerSentEvents() {
429 return accessible.get();
433 * Checks if thing is offline.
435 * @return true if offline
437 protected boolean isThingOffline() {
438 return !isThingOnline();
442 * Get {@link HomeConnectBridgeHandler}.
444 * @return bridge handler
446 protected Optional<HomeConnectBridgeHandler> getBridgeHandler() {
447 Bridge bridge = getBridge();
448 if (bridge != null) {
449 BridgeHandler bridgeHandler = bridge.getHandler();
450 if (bridgeHandler instanceof HomeConnectBridgeHandler) {
451 return Optional.of((HomeConnectBridgeHandler) bridgeHandler);
454 return Optional.empty();
458 * Get thing channel by given channel id.
460 * @param channelId channel id
463 protected Optional<Channel> getThingChannel(String channelId) {
464 Channel channel = getThing().getChannel(channelId);
465 if (channel == null) {
466 return Optional.empty();
468 return Optional.of(channel);
473 * Configure channel update handlers. Classes which extend {@link AbstractHomeConnectThingHandler} must implement
474 * this class and add handlers.
476 * @param handlers channel update handlers
478 protected abstract void configureChannelUpdateHandlers(final Map<String, ChannelUpdateHandler> handlers);
481 * Configure event handlers. Classes which extend {@link AbstractHomeConnectThingHandler} must implement
482 * this class and add handlers.
484 * @param handlers Server-Sent-Event handlers
486 protected abstract void configureEventHandlers(final Map<String, EventHandler> handlers);
488 protected boolean isChannelLinkedToProgramOptionNotFullySupportedByApi() {
493 * Update all channels via API.
496 protected void updateChannels() {
497 if (isBridgeOffline()) {
498 logger.debug("Bridge handler not found or offline. Stopping update of channels. thing={}, haId={}",
499 getThingLabel(), getThingHaId());
500 } else if (isThingOffline()) {
501 logger.debug("{} offline. Stopping update of channels. haId={}", getThing().getLabel(), getThingHaId());
503 List<Channel> channels = getThing().getChannels();
504 for (Channel channel : channels) {
505 updateChannel(channel.getUID());
511 * Update Channel values via API.
513 * @param channelUID channel UID
515 protected void updateChannel(ChannelUID channelUID) {
516 if (!getApiClient().isPresent()) {
517 logger.error("Cannot update channel. No instance of api client found! thing={}, haId={}", getThingLabel(),
522 if (!isThingReadyToHandleCommand()) {
526 if ((isLinked(channelUID) || CHANNEL_OPERATION_STATE.equals(channelUID.getId())) // always update operation
528 && channelUpdateHandlers.containsKey(channelUID.getId())) {
530 ChannelUpdateHandler channelUpdateHandler = channelUpdateHandlers.get(channelUID.getId());
531 if (channelUpdateHandler != null) {
532 channelUpdateHandler.handle(channelUID, expiringStateMap);
534 } catch (ApplianceOfflineException e) {
536 "API communication problem while trying to update! Appliance offline. thing={}, haId={}, error={}",
537 getThingLabel(), getThingHaId(), e.getMessage());
538 updateStatus(OFFLINE);
539 resetChannelsOnOfflineEvent();
540 resetProgramStateChannels(true);
541 } catch (CommunicationException e) {
542 logger.debug("API communication problem while trying to update! thing={}, haId={}, error={}",
543 getThingLabel(), getThingHaId(), e.getMessage());
544 } catch (AuthorizationException e) {
545 logger.debug("Authentication problem while trying to update! thing={}, haId={}", getThingLabel(),
547 handleAuthenticationError(e);
553 * Reset program related channels.
555 * @param offline true if the device is considered as OFFLINE
557 protected void resetProgramStateChannels(boolean offline) {
558 logger.debug("Resetting active program channel states. thing={}, haId={}", getThingLabel(), getThingHaId());
562 * Reset all channels on OFFLINE event.
564 protected void resetChannelsOnOfflineEvent() {
565 logger.debug("Resetting channel states due to OFFLINE event. thing={}, haId={}", getThingLabel(),
567 getThingChannel(CHANNEL_POWER_STATE).ifPresent(channel -> updateState(channel.getUID(), OnOffType.OFF));
568 getThingChannel(CHANNEL_OPERATION_STATE).ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
569 getThingChannel(CHANNEL_DOOR_STATE).ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
570 getThingChannel(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE)
571 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
572 getThingChannel(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE)
573 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
574 getThingChannel(CHANNEL_REMOTE_START_ALLOWANCE_STATE)
575 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
576 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
577 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
581 * Map Home Connect key and value names to label.
582 * e.g. Dishcare.Dishwasher.Program.Eco50 --> Eco50 or BSH.Common.EnumType.OperationState.DelayedStart --> Delayed
586 * @return human readable label
588 protected String mapStringType(String type) {
589 int index = type.lastIndexOf(".");
590 if (index > 0 && type.length() > index) {
591 String sub = type.substring(index + 1);
592 StringBuilder sb = new StringBuilder();
593 for (String word : sub.split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])")) {
597 return sb.toString().trim();
603 * Map Home Connect stage value to label.
604 * e.g. Cooking.Hood.EnumType.IntensiveStage.IntensiveStage1 --> 1
607 * @return human readable label
609 protected String mapStageStringType(String stage) {
612 case STAGE_INTENSIVE_STAGE_OFF:
615 case STAGE_FAN_STAGE_01:
616 case STAGE_INTENSIVE_STAGE_1:
619 case STAGE_FAN_STAGE_02:
620 case STAGE_INTENSIVE_STAGE_2:
623 case STAGE_FAN_STAGE_03:
626 case STAGE_FAN_STAGE_04:
629 case STAGE_FAN_STAGE_05:
633 stage = mapStringType(stage);
640 * Map unit string (returned by home connect api) to Unit
642 * @param unit String eg. "°C"
645 protected Unit<Temperature> mapTemperature(@Nullable String unit) {
648 } else if (unit.endsWith("C")) {
656 * Map hex representation of color to HSB type.
658 * @param colorCode color code e.g. #001122
661 protected HSBType mapColor(String colorCode) {
662 HSBType color = HSBType.WHITE;
664 if (colorCode.length() == 7) {
665 int r = Integer.valueOf(colorCode.substring(1, 3), 16);
666 int g = Integer.valueOf(colorCode.substring(3, 5), 16);
667 int b = Integer.valueOf(colorCode.substring(5, 7), 16);
668 color = HSBType.fromRGB(r, g, b);
674 * Map HSB color type to hex representation.
676 * @param color HSB color
677 * @return color code e.g. #001122
679 protected String mapColor(HSBType color) {
680 String redValue = String.format("%02X", (int) (color.getRed().floatValue() * 2.55));
681 String greenValue = String.format("%02X", (int) (color.getGreen().floatValue() * 2.55));
682 String blueValue = String.format("%02X", (int) (color.getBlue().floatValue() * 2.55));
683 return "#" + redValue + greenValue + blueValue;
687 * Check bridge status and refresh connection status of thing accordingly.
689 protected void refreshThingStatus() {
690 Optional<HomeConnectApiClient> apiClient = getApiClient();
692 apiClient.ifPresent(client -> {
694 HomeAppliance homeAppliance = client.getHomeAppliance(getThingHaId());
695 if (!homeAppliance.isConnected()) {
696 updateStatus(OFFLINE);
698 updateStatus(ONLINE);
700 accessible.set(true);
701 } catch (CommunicationException e) {
703 "Update status to OFFLINE. Home Connect service is not reachable or a problem occurred! thing={}, haId={}, error={}.",
704 getThingLabel(), getThingHaId(), e.getMessage());
705 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
706 "Home Connect service is not reachable or a problem occurred! (" + e.getMessage() + ").");
707 accessible.set(false);
708 } catch (AuthorizationException e) {
710 "Update status to OFFLINE. Home Connect service is not reachable or a problem occurred! thing={}, haId={}, error={}",
711 getThingLabel(), getThingHaId(), e.getMessage());
712 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
713 "Home Connect service is not reachable or a problem occurred! (" + e.getMessage() + ").");
714 accessible.set(false);
715 handleAuthenticationError(e);
718 if (apiClient.isEmpty()) {
719 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
720 accessible.set(false);
725 * Get home appliance id of Thing.
727 * @return home appliance id
729 public String getThingHaId() {
730 return getThing().getConfiguration().get(HA_ID).toString();
734 * Returns the human readable label for this thing.
736 * @return the human readable label
738 protected @Nullable String getThingLabel() {
739 return getThing().getLabel();
743 * Handle authentication exception.
745 protected void handleAuthenticationError(AuthorizationException exception) {
746 if (isBridgeOnline()) {
748 "Thing handler threw authentication exception --> clear credential storage thing={}, haId={} error={}",
749 getThingLabel(), getThingHaId(), exception.getMessage());
751 getBridgeHandler().ifPresent(homeConnectBridgeHandler -> {
753 homeConnectBridgeHandler.getOAuthClientService().remove();
754 homeConnectBridgeHandler.reinitialize();
755 } catch (OAuthException e) {
756 // client is already closed --> we can ignore it
763 * Get operation state of device.
765 * @return operation state string
767 protected @Nullable String getOperationState() {
768 return operationState;
771 protected EventHandler defaultElapsedProgramTimeEventHandler() {
772 return event -> getThingChannel(CHANNEL_ELAPSED_PROGRAM_TIME)
773 .ifPresent(channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND)));
776 protected EventHandler defaultPowerStateEventHandler() {
778 getThingChannel(CHANNEL_POWER_STATE).ifPresent(
779 channel -> updateState(channel.getUID(), OnOffType.from(STATE_POWER_ON.equals(event.getValue()))));
781 if (STATE_POWER_ON.equals(event.getValue())) {
782 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID()));
784 resetProgramStateChannels(true);
785 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
786 .ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
791 protected EventHandler defaultDoorStateEventHandler() {
792 return event -> getThingChannel(CHANNEL_DOOR_STATE).ifPresent(channel -> updateState(channel.getUID(),
793 STATE_DOOR_OPEN.equals(event.getValue()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED));
796 protected EventHandler defaultOperationStateEventHandler() {
798 String value = event.getValue();
799 getThingChannel(CHANNEL_OPERATION_STATE).ifPresent(channel -> updateState(channel.getUID(),
800 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
802 if (STATE_OPERATION_FINISHED.equals(value)) {
803 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
804 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(100, PERCENT)));
805 getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
806 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, SECOND)));
807 } else if (STATE_OPERATION_RUN.equals(value)) {
808 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
809 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, PERCENT)));
810 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID()));
811 } else if (STATE_OPERATION_READY.equals(value)) {
812 resetProgramStateChannels(false);
817 protected EventHandler defaultActiveProgramEventHandler() {
819 String value = event.getValue();
820 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(channel -> updateState(channel.getUID(),
821 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
823 resetProgramStateChannels(false);
828 protected EventHandler updateProgramOptionsAndActiveProgramStateEventHandler() {
830 String value = event.getValue();
831 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(channel -> updateState(channel.getUID(),
832 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
834 resetProgramStateChannels(false);
837 Optional<HomeConnectApiClient> apiClient = getApiClient();
838 if (apiClient.isPresent() && isChannelLinkedToProgramOptionNotFullySupportedByApi()
839 && apiClient.get().isRemoteControlActive(getThingHaId())) {
840 // update channels linked to program options
841 Program program = apiClient.get().getSelectedProgram(getThingHaId());
842 if (program != null) {
843 processProgramOptions(program.getOptions());
846 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
847 logger.debug("Could not update program options. {}", e.getMessage());
853 protected EventHandler defaultEventPresentStateEventHandler(String channelId) {
854 return event -> getThingChannel(channelId).ifPresent(channel -> updateState(channel.getUID(),
855 OnOffType.from(!STATE_EVENT_PRESENT_STATE_OFF.equals(event.getValue()))));
858 protected EventHandler defaultBooleanEventHandler(String channelId) {
859 return event -> getThingChannel(channelId)
860 .ifPresent(channel -> updateState(channel.getUID(), OnOffType.from(event.getValueAsBoolean())));
863 protected EventHandler defaultRemainingProgramTimeEventHandler() {
864 return event -> getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
865 .ifPresent(channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND)));
868 protected EventHandler defaultSelectedProgramStateEventHandler() {
869 return event -> getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
870 .ifPresent(channel -> updateState(channel.getUID(),
871 event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue())));
874 protected EventHandler defaultAmbientLightColorStateEventHandler() {
875 return event -> getThingChannel(CHANNEL_AMBIENT_LIGHT_COLOR_STATE)
876 .ifPresent(channel -> updateState(channel.getUID(),
877 event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue())));
880 protected EventHandler defaultAmbientLightCustomColorStateEventHandler() {
881 return event -> getThingChannel(CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE).ifPresent(channel -> {
882 String value = event.getValue();
884 updateState(channel.getUID(), mapColor(value));
886 updateState(channel.getUID(), UnDefType.UNDEF);
891 protected EventHandler updateRemoteControlActiveAndProgramOptionsStateEventHandler() {
893 defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE).handle(event);
896 if (Boolean.parseBoolean(event.getValue())) {
897 // update available program options if update was previously delayed and remote control is enabled
898 String programKey = programOptionsDelayedUpdate;
899 if (programKey != null) {
900 logger.debug("Delayed update of options for program {}", programKey);
901 updateProgramOptionsStateDescriptions(programKey);
902 programOptionsDelayedUpdate = null;
905 if (isChannelLinkedToProgramOptionNotFullySupportedByApi()) {
906 Optional<HomeConnectApiClient> apiClient = getApiClient();
907 if (apiClient.isPresent()) {
908 Program program = apiClient.get().getSelectedProgram(getThingHaId());
909 if (program != null) {
910 processProgramOptions(program.getOptions());
915 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
916 logger.debug("Could not update program options. {}", e.getMessage());
921 protected EventHandler updateProgramOptionsAndSelectedProgramStateEventHandler() {
923 defaultSelectedProgramStateEventHandler().handle(event);
926 Optional<HomeConnectApiClient> apiClient = getApiClient();
927 String programKey = event.getValue();
929 if (apiClient.isPresent() && programKey != null) {
930 Boolean remoteControl = (availableProgramOptionsCache.get(programKey) == null
931 || isChannelLinkedToProgramOptionNotFullySupportedByApi())
932 ? apiClient.get().isRemoteControlActive(getThingHaId())
935 // Delay the update of available program options if options are not yet cached and remote control is
937 if (availableProgramOptionsCache.get(programKey) == null && !remoteControl) {
938 logger.debug("Delay update of options for program {}", programKey);
939 programOptionsDelayedUpdate = programKey;
941 updateProgramOptionsStateDescriptions(programKey);
944 if (isChannelLinkedToProgramOptionNotFullySupportedByApi() && remoteControl) {
945 // update channels linked to program options
946 Program program = apiClient.get().getSelectedProgram(getThingHaId());
947 if (program != null) {
948 processProgramOptions(program.getOptions());
952 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
953 logger.debug("Could not update program options. {}", e.getMessage());
958 protected EventHandler defaultPercentQuantityTypeEventHandler(String channelId) {
959 return event -> getThingChannel(channelId).ifPresent(
960 channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), PERCENT)));
963 protected EventHandler defaultPercentHandler(String channelId) {
964 return event -> getThingChannel(channelId)
965 .ifPresent(channel -> updateState(channel.getUID(), new PercentType(event.getValueAsInt())));
968 protected ChannelUpdateHandler defaultDoorStateChannelUpdateHandler() {
969 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
970 Optional<HomeConnectApiClient> apiClient = getApiClient();
971 if (apiClient.isPresent()) {
972 Data data = apiClient.get().getDoorState(getThingHaId());
973 if (data.getValue() != null) {
974 return STATE_DOOR_OPEN.equals(data.getValue()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
976 return UnDefType.UNDEF;
979 return UnDefType.UNDEF;
984 protected ChannelUpdateHandler defaultPowerStateChannelUpdateHandler() {
985 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
986 Optional<HomeConnectApiClient> apiClient = getApiClient();
987 if (apiClient.isPresent()) {
988 Data data = apiClient.get().getPowerState(getThingHaId());
989 if (data.getValue() != null) {
990 return OnOffType.from(STATE_POWER_ON.equals(data.getValue()));
992 return UnDefType.UNDEF;
995 return UnDefType.UNDEF;
1000 protected ChannelUpdateHandler defaultAmbientLightChannelUpdateHandler() {
1001 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1002 Optional<HomeConnectApiClient> apiClient = getApiClient();
1003 if (apiClient.isPresent()) {
1004 Data data = apiClient.get().getAmbientLightState(getThingHaId());
1005 if (data.getValue() != null) {
1006 boolean enabled = data.getValueAsBoolean();
1009 Data brightnessData = apiClient.get().getAmbientLightBrightnessState(getThingHaId());
1010 getThingChannel(CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE)
1011 .ifPresent(channel -> updateState(channel.getUID(),
1012 new PercentType(brightnessData.getValueAsInt())));
1015 Data colorData = apiClient.get().getAmbientLightColorState(getThingHaId());
1016 getThingChannel(CHANNEL_AMBIENT_LIGHT_COLOR_STATE).ifPresent(
1017 channel -> updateState(channel.getUID(), new StringType(colorData.getValue())));
1020 Data customColorData = apiClient.get().getAmbientLightCustomColorState(getThingHaId());
1021 getThingChannel(CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE).ifPresent(channel -> {
1022 String value = customColorData.getValue();
1023 if (value != null) {
1024 updateState(channel.getUID(), mapColor(value));
1026 updateState(channel.getUID(), UnDefType.UNDEF);
1031 return OnOffType.from(enabled);
1033 return UnDefType.UNDEF;
1036 return UnDefType.UNDEF;
1041 protected ChannelUpdateHandler defaultNoOpUpdateHandler() {
1042 return (channelUID, cache) -> updateState(channelUID, UnDefType.UNDEF);
1045 protected ChannelUpdateHandler defaultOperationStateChannelUpdateHandler() {
1046 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1047 Optional<HomeConnectApiClient> apiClient = getApiClient();
1048 if (apiClient.isPresent()) {
1049 Data data = apiClient.get().getOperationState(getThingHaId());
1051 String value = data.getValue();
1052 if (value != null) {
1053 operationState = data.getValue();
1054 return new StringType(mapStringType(value));
1056 operationState = null;
1057 return UnDefType.UNDEF;
1060 return UnDefType.UNDEF;
1065 protected ChannelUpdateHandler defaultRemoteControlActiveStateChannelUpdateHandler() {
1066 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1067 Optional<HomeConnectApiClient> apiClient = getApiClient();
1068 if (apiClient.isPresent()) {
1069 return OnOffType.from(apiClient.get().isRemoteControlActive(getThingHaId()));
1071 return OnOffType.OFF;
1075 protected ChannelUpdateHandler defaultLocalControlActiveStateChannelUpdateHandler() {
1076 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1077 Optional<HomeConnectApiClient> apiClient = getApiClient();
1078 if (apiClient.isPresent()) {
1079 return OnOffType.from(apiClient.get().isLocalControlActive(getThingHaId()));
1081 return OnOffType.OFF;
1085 protected ChannelUpdateHandler defaultRemoteStartAllowanceChannelUpdateHandler() {
1086 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1087 Optional<HomeConnectApiClient> apiClient = getApiClient();
1088 if (apiClient.isPresent()) {
1089 return OnOffType.from(apiClient.get().isRemoteControlStartAllowed(getThingHaId()));
1091 return OnOffType.OFF;
1095 protected ChannelUpdateHandler defaultSelectedProgramStateUpdateHandler() {
1096 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1097 Optional<HomeConnectApiClient> apiClient = getApiClient();
1098 if (apiClient.isPresent()) {
1099 Program program = apiClient.get().getSelectedProgram(getThingHaId());
1100 if (program != null) {
1101 processProgramOptions(program.getOptions());
1102 return new StringType(program.getKey());
1104 return UnDefType.UNDEF;
1107 return UnDefType.UNDEF;
1111 protected ChannelUpdateHandler updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() {
1112 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1113 Optional<HomeConnectApiClient> apiClient = getApiClient();
1114 if (apiClient.isPresent()) {
1115 Program program = apiClient.get().getSelectedProgram(getThingHaId());
1117 if (program != null) {
1118 updateProgramOptionsStateDescriptions(program.getKey());
1119 processProgramOptions(program.getOptions());
1121 return new StringType(program.getKey());
1123 return UnDefType.UNDEF;
1126 return UnDefType.UNDEF;
1130 protected ChannelUpdateHandler getAndUpdateSelectedProgramStateUpdateHandler() {
1131 return (channelUID, cache) -> {
1132 Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
1133 if (channel.isPresent()) {
1134 defaultSelectedProgramStateUpdateHandler().handle(channel.get().getUID(), cache);
1139 protected ChannelUpdateHandler getAndUpdateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() {
1140 return (channelUID, cache) -> {
1141 Optional<Channel> channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE);
1142 if (channel.isPresent()) {
1143 updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler()
1144 .handle(channel.get().getUID(), cache);
1149 protected ChannelUpdateHandler defaultActiveProgramStateUpdateHandler() {
1150 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1151 Optional<HomeConnectApiClient> apiClient = getApiClient();
1152 if (apiClient.isPresent()) {
1153 Program program = apiClient.get().getActiveProgram(getThingHaId());
1155 if (program != null) {
1156 processProgramOptions(program.getOptions());
1157 return new StringType(mapStringType(program.getKey()));
1159 resetProgramStateChannels(false);
1160 return UnDefType.UNDEF;
1163 return UnDefType.UNDEF;
1167 protected void handleTemperatureCommand(final ChannelUID channelUID, final Command command,
1168 final HomeConnectApiClient apiClient)
1169 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1170 if (command instanceof QuantityType) {
1171 QuantityType<?> quantity = (QuantityType<?>) command;
1177 if (quantity.getUnit().equals(SIUnits.CELSIUS) || quantity.getUnit().equals(ImperialUnits.FAHRENHEIT)) {
1178 unit = quantity.getUnit().toString();
1179 value = String.valueOf(quantity.intValue());
1181 logger.debug("Converting target temperature from {}{} to °C value. thing={}, haId={}",
1182 quantity.intValue(), quantity.getUnit().toString(), getThingLabel(), getThingHaId());
1184 var celsius = quantity.toUnit(SIUnits.CELSIUS);
1185 if (celsius == null) {
1186 logger.warn("Converting temperature to celsius failed! quantity={}", quantity);
1189 value = String.valueOf(celsius.intValue());
1191 logger.debug("Converted value {}{}", value, unit);
1194 if (value != null) {
1195 logger.debug("Set temperature to {} {}. thing={}, haId={}", value, unit, getThingLabel(),
1197 switch (channelUID.getId()) {
1198 case CHANNEL_REFRIGERATOR_SETPOINT_TEMPERATURE:
1199 apiClient.setFridgeSetpointTemperature(getThingHaId(), value, unit);
1200 case CHANNEL_FREEZER_SETPOINT_TEMPERATURE:
1201 apiClient.setFreezerSetpointTemperature(getThingHaId(), value, unit);
1203 case CHANNEL_SETPOINT_TEMPERATURE:
1204 apiClient.setProgramOptions(getThingHaId(), OPTION_SETPOINT_TEMPERATURE, value, unit, true,
1208 logger.debug("Unknown channel! Cannot set temperature. channelUID={}", channelUID);
1211 } catch (UnconvertibleException e) {
1212 logger.warn("Could not set temperature! haId={}, error={}", getThingHaId(), e.getMessage());
1217 protected void handleLightCommands(final ChannelUID channelUID, final Command command,
1218 final HomeConnectApiClient apiClient)
1219 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1220 switch (channelUID.getId()) {
1221 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1222 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1223 // turn light on if turned off
1224 turnLightOn(channelUID, apiClient);
1226 int newBrightness = BRIGHTNESS_MIN;
1227 if (command instanceof OnOffType) {
1228 newBrightness = command == OnOffType.ON ? BRIGHTNESS_MAX : BRIGHTNESS_MIN;
1229 } else if (command instanceof IncreaseDecreaseType) {
1230 int currentBrightness = getCurrentBrightness(channelUID, apiClient);
1231 if (command.equals(IncreaseDecreaseType.INCREASE)) {
1232 newBrightness = currentBrightness + BRIGHTNESS_DIM_STEP;
1234 newBrightness = currentBrightness - BRIGHTNESS_DIM_STEP;
1236 } else if (command instanceof PercentType) {
1237 newBrightness = (int) Math.floor(((PercentType) command).doubleValue());
1238 } else if (command instanceof DecimalType) {
1239 newBrightness = ((DecimalType) command).intValue();
1242 // check in in range
1243 newBrightness = Math.min(Math.max(newBrightness, BRIGHTNESS_MIN), BRIGHTNESS_MAX);
1245 setLightBrightness(channelUID, apiClient, newBrightness);
1247 case CHANNEL_FUNCTIONAL_LIGHT_STATE:
1248 if (command instanceof OnOffType) {
1249 apiClient.setFunctionalLightState(getThingHaId(), OnOffType.ON.equals(command));
1252 case CHANNEL_AMBIENT_LIGHT_STATE:
1253 if (command instanceof OnOffType) {
1254 apiClient.setAmbientLightState(getThingHaId(), OnOffType.ON.equals(command));
1257 case CHANNEL_AMBIENT_LIGHT_COLOR_STATE:
1258 if (command instanceof StringType) {
1259 turnLightOn(channelUID, apiClient);
1260 apiClient.setAmbientLightColorState(getThingHaId(), command.toFullString());
1263 case CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE:
1264 turnLightOn(channelUID, apiClient);
1266 // make sure 'custom color' is set as color
1267 Data ambientLightColorState = apiClient.getAmbientLightColorState(getThingHaId());
1268 if (!STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR.equals(ambientLightColorState.getValue())) {
1269 apiClient.setAmbientLightColorState(getThingHaId(), STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR);
1272 if (command instanceof HSBType) {
1273 apiClient.setAmbientLightCustomColorState(getThingHaId(), mapColor((HSBType) command));
1274 } else if (command instanceof StringType) {
1275 apiClient.setAmbientLightCustomColorState(getThingHaId(), command.toFullString());
1281 protected void handlePowerCommand(final ChannelUID channelUID, final Command command,
1282 final HomeConnectApiClient apiClient, String stateNotOn)
1283 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1284 if (command instanceof OnOffType && CHANNEL_POWER_STATE.equals(channelUID.getId())) {
1285 apiClient.setPowerState(getThingHaId(), OnOffType.ON.equals(command) ? STATE_POWER_ON : stateNotOn);
1289 private int getCurrentBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient)
1290 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1291 String id = channelUID.getId();
1292 if (CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE.equals(id)) {
1293 return apiClient.getFunctionalLightBrightnessState(getThingHaId()).getValueAsInt();
1295 return apiClient.getAmbientLightBrightnessState(getThingHaId()).getValueAsInt();
1299 private void setLightBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient, int value)
1300 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1301 switch (channelUID.getId()) {
1302 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1303 apiClient.setFunctionalLightBrightnessState(getThingHaId(), value);
1305 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1306 apiClient.setAmbientLightBrightnessState(getThingHaId(), value);
1311 private void turnLightOn(final ChannelUID channelUID, final HomeConnectApiClient apiClient)
1312 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1313 switch (channelUID.getId()) {
1314 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1315 Data functionalLightState = apiClient.getFunctionalLightState(getThingHaId());
1316 if (!functionalLightState.getValueAsBoolean()) {
1317 apiClient.setFunctionalLightState(getThingHaId(), true);
1320 case CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE:
1321 case CHANNEL_AMBIENT_LIGHT_COLOR_STATE:
1322 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1323 Data ambientLightState = apiClient.getAmbientLightState(getThingHaId());
1324 if (!ambientLightState.getValueAsBoolean()) {
1325 apiClient.setAmbientLightState(getThingHaId(), true);
1331 protected void processProgramOptions(List<Option> options) {
1332 options.forEach(option -> {
1333 String key = option.getKey();
1336 case OPTION_WASHER_TEMPERATURE:
1337 getThingChannel(CHANNEL_WASHER_TEMPERATURE)
1338 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1340 case OPTION_WASHER_SPIN_SPEED:
1341 getThingChannel(CHANNEL_WASHER_SPIN_SPEED)
1342 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1344 case OPTION_WASHER_IDOS_1_DOSING_LEVEL:
1345 getThingChannel(CHANNEL_WASHER_IDOS1_LEVEL)
1346 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1348 case OPTION_WASHER_IDOS_2_DOSING_LEVEL:
1349 getThingChannel(CHANNEL_WASHER_IDOS2_LEVEL)
1350 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1352 case OPTION_DRYER_DRYING_TARGET:
1353 getThingChannel(CHANNEL_DRYER_DRYING_TARGET)
1354 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1356 case OPTION_HOOD_INTENSIVE_LEVEL:
1357 String hoodIntensiveLevelValue = option.getValue();
1358 if (hoodIntensiveLevelValue != null) {
1359 getThingChannel(CHANNEL_HOOD_INTENSIVE_LEVEL)
1360 .ifPresent(channel -> updateState(channel.getUID(),
1361 new StringType(mapStageStringType(hoodIntensiveLevelValue))));
1364 case OPTION_HOOD_VENTING_LEVEL:
1365 String hoodVentingLevel = option.getValue();
1366 if (hoodVentingLevel != null) {
1367 getThingChannel(CHANNEL_HOOD_VENTING_LEVEL)
1368 .ifPresent(channel -> updateState(channel.getUID(),
1369 new StringType(mapStageStringType(hoodVentingLevel))));
1372 case OPTION_SETPOINT_TEMPERATURE:
1373 getThingChannel(CHANNEL_SETPOINT_TEMPERATURE).ifPresent(channel -> updateState(channel.getUID(),
1374 new QuantityType<>(option.getValueAsInt(), mapTemperature(option.getUnit()))));
1376 case OPTION_DURATION:
1377 getThingChannel(CHANNEL_DURATION).ifPresent(channel -> updateState(channel.getUID(),
1378 new QuantityType<>(option.getValueAsInt(), SECOND)));
1380 case OPTION_FINISH_IN_RELATIVE:
1381 case OPTION_REMAINING_PROGRAM_TIME:
1382 getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
1383 .ifPresent(channel -> updateState(channel.getUID(),
1384 new QuantityType<>(option.getValueAsInt(), SECOND)));
1386 case OPTION_ELAPSED_PROGRAM_TIME:
1387 getThingChannel(CHANNEL_ELAPSED_PROGRAM_TIME).ifPresent(channel -> updateState(channel.getUID(),
1388 new QuantityType<>(option.getValueAsInt(), SECOND)));
1390 case OPTION_PROGRAM_PROGRESS:
1391 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
1392 .ifPresent(channel -> updateState(channel.getUID(),
1393 new QuantityType<>(option.getValueAsInt(), PERCENT)));
1395 case OPTION_WASHER_IDOS_1_ACTIVE:
1396 getThingChannel(CHANNEL_WASHER_IDOS1).ifPresent(
1397 channel -> updateState(channel.getUID(), OnOffType.from(option.getValueAsBoolean())));
1399 case OPTION_WASHER_IDOS_2_ACTIVE:
1400 getThingChannel(CHANNEL_WASHER_IDOS2).ifPresent(
1401 channel -> updateState(channel.getUID(), OnOffType.from(option.getValueAsBoolean())));
1403 case OPTION_WASHER_VARIO_PERFECT:
1404 getThingChannel(CHANNEL_WASHER_VARIO_PERFECT)
1405 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1407 case OPTION_WASHER_LESS_IRONING:
1408 getThingChannel(CHANNEL_WASHER_LESS_IRONING).ifPresent(
1409 channel -> updateState(channel.getUID(), OnOffType.from(option.getValueAsBoolean())));
1411 case OPTION_WASHER_PRE_WASH:
1412 getThingChannel(CHANNEL_WASHER_PRE_WASH).ifPresent(
1413 channel -> updateState(channel.getUID(), OnOffType.from(option.getValueAsBoolean())));
1415 case OPTION_WASHER_RINSE_PLUS:
1416 getThingChannel(CHANNEL_WASHER_RINSE_PLUS)
1417 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1419 case OPTION_WASHER_SOAK:
1420 getThingChannel(CHANNEL_WASHER_SOAK).ifPresent(
1421 channel -> updateState(channel.getUID(), OnOffType.from(option.getValueAsBoolean())));
1423 case OPTION_WASHER_ENERGY_FORECAST:
1424 getThingChannel(CHANNEL_PROGRAM_ENERGY).ifPresent(channel -> updateState(channel.getUID(),
1425 new QuantityType<>(option.getValueAsInt(), PERCENT)));
1427 case OPTION_WASHER_WATER_FORECAST:
1428 getThingChannel(CHANNEL_PROGRAM_WATER).ifPresent(channel -> updateState(channel.getUID(),
1429 new QuantityType<>(option.getValueAsInt(), PERCENT)));
1436 protected String convertWasherTemperature(String value) {
1437 if (value.startsWith("LaundryCare.Washer.EnumType.Temperature.GC")) {
1438 return value.replace("LaundryCare.Washer.EnumType.Temperature.GC", "") + "°C";
1441 if (value.startsWith("LaundryCare.Washer.EnumType.Temperature.Ul")) {
1442 return mapStringType(value.replace("LaundryCare.Washer.EnumType.Temperature.Ul", ""));
1445 return mapStringType(value);
1448 protected String convertWasherSpinSpeed(String value) {
1449 if (value.startsWith("LaundryCare.Washer.EnumType.SpinSpeed.RPM")) {
1450 return value.replace("LaundryCare.Washer.EnumType.SpinSpeed.RPM", "") + " RPM";
1453 if (value.startsWith("LaundryCare.Washer.EnumType.SpinSpeed.Ul")) {
1454 return value.replace("LaundryCare.Washer.EnumType.SpinSpeed.Ul", "");
1457 return mapStringType(value);
1460 protected void updateProgramOptionsStateDescriptions(String programKey)
1461 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1462 Optional<HomeConnectApiClient> apiClient = getApiClient();
1463 if (apiClient.isPresent()) {
1464 List<AvailableProgramOption> availableProgramOptions;
1465 if (availableProgramOptionsCache.containsKey(programKey)) {
1466 logger.debug("Returning cached options for '{}'.", programKey);
1467 availableProgramOptions = availableProgramOptionsCache.get(programKey);
1468 availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
1469 : Collections.emptyList();
1471 availableProgramOptions = apiClient.get().getProgramOptions(getThingHaId(), programKey);
1472 availableProgramOptionsCache.put(programKey, availableProgramOptions);
1475 Optional<Channel> channelSpinSpeed = getThingChannel(CHANNEL_WASHER_SPIN_SPEED);
1476 Optional<Channel> channelTemperature = getThingChannel(CHANNEL_WASHER_TEMPERATURE);
1477 Optional<Channel> channelDryingTarget = getThingChannel(CHANNEL_DRYER_DRYING_TARGET);
1479 if (availableProgramOptions.isEmpty()) {
1480 channelSpinSpeed.ifPresent(
1481 channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList()));
1482 channelTemperature.ifPresent(
1483 channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList()));
1484 channelDryingTarget.ifPresent(
1485 channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList()));
1488 availableProgramOptions.forEach(option -> {
1489 switch (option.getKey()) {
1490 case OPTION_WASHER_SPIN_SPEED: {
1492 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1493 createStateOptions(option, this::convertWasherSpinSpeed)));
1496 case OPTION_WASHER_TEMPERATURE: {
1498 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1499 createStateOptions(option, this::convertWasherTemperature)));
1502 case OPTION_DRYER_DRYING_TARGET: {
1503 channelDryingTarget.ifPresent(channel -> dynamicStateDescriptionProvider
1504 .setStateOptions(channel.getUID(), createStateOptions(option, this::mapStringType)));
1512 protected HomeConnectDynamicStateDescriptionProvider getDynamicStateDescriptionProvider() {
1513 return dynamicStateDescriptionProvider;
1516 private List<StateOption> createStateOptions(AvailableProgramOption option,
1517 Function<String, String> stateConverter) {
1518 return option.getAllowedValues().stream().map(av -> new StateOption(av, stateConverter.apply(av)))
1519 .collect(Collectors.toList());
1522 private synchronized void scheduleOfflineMonitor1() {
1523 this.reinitializationFuture1 = scheduler.schedule(() -> {
1524 if (isBridgeOnline() && isThingOffline()) {
1525 logger.debug("Offline monitor 1: Check if thing is ONLINE. thing={}, haId={}", getThingLabel(),
1527 refreshThingStatus();
1528 if (isThingOnline()) {
1529 logger.debug("Offline monitor 1: Thing status changed to ONLINE. thing={}, haId={}",
1530 getThingLabel(), getThingHaId());
1533 scheduleOfflineMonitor1();
1536 scheduleOfflineMonitor1();
1538 }, AbstractHomeConnectThingHandler.OFFLINE_MONITOR_1_DELAY_MIN, TimeUnit.MINUTES);
1541 private synchronized void stopOfflineMonitor1() {
1542 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture1;
1543 if (reinitializationFuture != null) {
1544 reinitializationFuture.cancel(false);
1545 this.reinitializationFuture1 = null;
1549 private synchronized void scheduleOfflineMonitor2() {
1550 this.reinitializationFuture2 = scheduler.schedule(() -> {
1551 if (isBridgeOnline() && !accessible.get()) {
1552 logger.debug("Offline monitor 2: Check if thing is ONLINE. thing={}, haId={}", getThingLabel(),
1554 refreshThingStatus();
1555 if (isThingOnline()) {
1556 logger.debug("Offline monitor 2: Thing status changed to ONLINE. thing={}, haId={}",
1557 getThingLabel(), getThingHaId());
1560 scheduleOfflineMonitor2();
1563 scheduleOfflineMonitor2();
1565 }, AbstractHomeConnectThingHandler.OFFLINE_MONITOR_2_DELAY_MIN, TimeUnit.MINUTES);
1568 private synchronized void stopOfflineMonitor2() {
1569 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture2;
1570 if (reinitializationFuture != null) {
1571 reinitializationFuture.cancel(false);
1572 this.reinitializationFuture2 = null;
1576 private synchronized void scheduleRetryRegistering() {
1577 this.reinitializationFuture3 = scheduler.schedule(() -> {
1578 logger.debug("Try to register event listener again. haId={}", getThingHaId());
1579 unregisterEventListener();
1580 registerEventListener();
1581 }, AbstractHomeConnectThingHandler.EVENT_LISTENER_CONNECT_RETRY_DELAY_MIN, TimeUnit.MINUTES);
1584 private synchronized void stopRetryRegistering() {
1585 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture3;
1586 if (reinitializationFuture != null) {
1587 reinitializationFuture.cancel(true);
1588 this.reinitializationFuture3 = null;