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.List;
26 import java.util.Optional;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.atomic.AtomicBoolean;
31 import java.util.function.Function;
32 import java.util.stream.Collectors;
34 import javax.measure.UnconvertibleException;
35 import javax.measure.Unit;
36 import javax.measure.quantity.Temperature;
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
41 import org.openhab.binding.homeconnect.internal.client.HomeConnectEventSourceClient;
42 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
43 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
44 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
45 import org.openhab.binding.homeconnect.internal.client.listener.HomeConnectEventListener;
46 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
47 import org.openhab.binding.homeconnect.internal.client.model.Data;
48 import org.openhab.binding.homeconnect.internal.client.model.Event;
49 import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
50 import org.openhab.binding.homeconnect.internal.client.model.Option;
51 import org.openhab.binding.homeconnect.internal.client.model.Program;
52 import org.openhab.binding.homeconnect.internal.handler.cache.ExpiringStateMap;
53 import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
54 import org.openhab.core.auth.client.oauth2.OAuthException;
55 import org.openhab.core.library.types.DecimalType;
56 import org.openhab.core.library.types.HSBType;
57 import org.openhab.core.library.types.IncreaseDecreaseType;
58 import org.openhab.core.library.types.OnOffType;
59 import org.openhab.core.library.types.OpenClosedType;
60 import org.openhab.core.library.types.PercentType;
61 import org.openhab.core.library.types.QuantityType;
62 import org.openhab.core.library.types.StringType;
63 import org.openhab.core.library.unit.ImperialUnits;
64 import org.openhab.core.library.unit.SIUnits;
65 import org.openhab.core.thing.Bridge;
66 import org.openhab.core.thing.Channel;
67 import org.openhab.core.thing.ChannelUID;
68 import org.openhab.core.thing.Thing;
69 import org.openhab.core.thing.ThingStatusDetail;
70 import org.openhab.core.thing.ThingStatusInfo;
71 import org.openhab.core.thing.binding.BaseThingHandler;
72 import org.openhab.core.thing.binding.BridgeHandler;
73 import org.openhab.core.types.Command;
74 import org.openhab.core.types.RefreshType;
75 import org.openhab.core.types.StateOption;
76 import org.openhab.core.types.UnDefType;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
81 * The {@link AbstractHomeConnectThingHandler} is responsible for handling commands, which are
82 * sent to one of the channels.
84 * @author Jonas Brüstel - Initial contribution
87 public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler implements HomeConnectEventListener {
89 private static final int CACHE_TTL_SEC = 2;
90 private static final int OFFLINE_MONITOR_1_DELAY_MIN = 30;
91 private static final int OFFLINE_MONITOR_2_DELAY_MIN = 4;
92 private static final int EVENT_LISTENER_CONNECT_RETRY_DELAY_MIN = 10;
94 private @Nullable String operationState;
95 private @Nullable ScheduledFuture<?> reinitializationFuture1;
96 private @Nullable ScheduledFuture<?> reinitializationFuture2;
97 private @Nullable ScheduledFuture<?> reinitializationFuture3;
98 private boolean ignoreEventSourceClosedEvent;
100 private final ConcurrentHashMap<String, EventHandler> eventHandlers;
101 private final ConcurrentHashMap<String, ChannelUpdateHandler> channelUpdateHandlers;
102 private final HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
103 private final ExpiringStateMap expiringStateMap;
104 private final AtomicBoolean accessible;
105 private final Logger logger = LoggerFactory.getLogger(AbstractHomeConnectThingHandler.class);
107 public AbstractHomeConnectThingHandler(Thing thing,
108 HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
110 eventHandlers = new ConcurrentHashMap<>();
111 channelUpdateHandlers = new ConcurrentHashMap<>();
112 this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider;
113 expiringStateMap = new ExpiringStateMap(Duration.ofSeconds(CACHE_TTL_SEC));
114 accessible = new AtomicBoolean(false);
116 configureEventHandlers(eventHandlers);
117 configureChannelUpdateHandlers(channelUpdateHandlers);
121 public void initialize() {
122 if (getBridgeHandler().isEmpty()) {
123 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
124 accessible.set(false);
125 } else if (isBridgeOffline()) {
126 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
127 accessible.set(false);
129 updateStatus(UNKNOWN);
130 scheduler.submit(() -> {
131 refreshThingStatus(); // set ONLINE / OFFLINE
132 updateSelectedProgramStateDescription();
134 registerEventListener();
135 scheduleOfflineMonitor1();
136 scheduleOfflineMonitor2();
142 public void dispose() {
143 stopRetryRegistering();
144 stopOfflineMonitor1();
145 stopOfflineMonitor2();
146 unregisterEventListener(true);
150 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
151 logger.debug("Bridge status changed to {} ({}). haId={}", bridgeStatusInfo, getThingLabel(), getThingHaId());
155 private void reinitialize() {
156 logger.debug("Reinitialize thing handler ({}). haId={}", getThingLabel(), getThingHaId());
157 stopRetryRegistering();
158 stopOfflineMonitor1();
159 stopOfflineMonitor2();
160 unregisterEventListener();
165 * Handles a command for a given channel.
167 * This method is only called, if the thing has been initialized (status ONLINE/OFFLINE/UNKNOWN).
170 * @param channelUID the {@link ChannelUID} of the channel to which the command was sent
171 * @param command the {@link Command}
172 * @param apiClient the {@link HomeConnectApiClient}
173 * @throws CommunicationException communication problem
174 * @throws AuthorizationException authorization problem
175 * @throws ApplianceOfflineException appliance offline
177 protected void handleCommand(ChannelUID channelUID, Command command, HomeConnectApiClient apiClient)
178 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
179 if (command instanceof RefreshType) {
180 updateChannel(channelUID);
181 } else if (command instanceof StringType && CHANNEL_BASIC_ACTIONS_STATE.equals(channelUID.getId())
182 && getBridgeHandler().isPresent()) {
183 updateState(channelUID, new StringType(""));
185 if (COMMAND_START.equalsIgnoreCase(command.toFullString())) {
186 HomeConnectBridgeHandler homeConnectBridgeHandler = getBridgeHandler().get();
187 // workaround for api bug
188 // if simulator, program options have to be passed along with the desired program
189 // if non simulator, some options throw a "SDK.Error.UnsupportedOption" error
190 if (homeConnectBridgeHandler.getConfiguration().isSimulator()) {
191 apiClient.startSelectedProgram(getThingHaId());
193 Program selectedProgram = apiClient.getSelectedProgram(getThingHaId());
194 if (selectedProgram != null) {
195 apiClient.startProgram(getThingHaId(), selectedProgram.getKey());
198 } else if (COMMAND_STOP.equalsIgnoreCase(command.toFullString())) {
199 apiClient.stopProgram(getThingHaId());
200 } else if (COMMAND_SELECTED.equalsIgnoreCase(command.toFullString())) {
201 apiClient.getSelectedProgram(getThingHaId());
203 logger.debug("Start custom program. command={} haId={}", command.toFullString(), getThingHaId());
204 apiClient.startCustomProgram(getThingHaId(), command.toFullString());
206 } else if (command instanceof StringType && CHANNEL_SELECTED_PROGRAM_STATE.equals(channelUID.getId())) {
207 apiClient.setSelectedProgram(getThingHaId(), command.toFullString());
212 public final void handleCommand(ChannelUID channelUID, Command command) {
213 var apiClient = getApiClient();
214 if ((isThingReadyToHandleCommand() || (this instanceof HomeConnectHoodHandler && isBridgeOnline()
215 && isThingAccessibleViaServerSentEvents())) && apiClient.isPresent()) {
216 logger.debug("Handle \"{}\" command ({}). haId={}", command, channelUID.getId(), getThingHaId());
218 handleCommand(channelUID, command, apiClient.get());
219 } catch (ApplianceOfflineException e) {
220 logger.debug("Could not handle command {}. Appliance offline. thing={}, haId={}, error={}",
221 command.toFullString(), getThingLabel(), getThingHaId(), e.getMessage());
222 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
223 resetChannelsOnOfflineEvent();
224 resetProgramStateChannels(true);
225 } catch (CommunicationException e) {
226 logger.debug("Could not handle command {}. API communication problem! error={}, haId={}",
227 command.toFullString(), e.getMessage(), getThingHaId());
228 } catch (AuthorizationException e) {
229 logger.debug("Could not handle command {}. Authorization problem! error={}, haId={}",
230 command.toFullString(), e.getMessage(), getThingHaId());
232 handleAuthenticationError(e);
238 public void onEvent(Event event) {
239 if (DISCONNECTED.equals(event.getType())) {
240 logger.debug("Received DISCONNECTED event. Set {} to OFFLINE. haId={}", getThing().getLabel(),
242 updateStatus(OFFLINE);
243 resetChannelsOnOfflineEvent();
244 resetProgramStateChannels(true);
245 } else if (isThingOnline() && CONNECTED.equals(event.getType())) {
246 logger.debug("Received CONNECTED event. Update power state channel. haId={}", getThingHaId());
247 getThingChannel(CHANNEL_POWER_STATE).ifPresent(c -> updateChannel(c.getUID()));
248 } else if (isThingOffline() && !KEEP_ALIVE.equals(event.getType())) {
249 updateStatus(ONLINE);
250 logger.debug("Set {} to ONLINE and update channels. haId={}", getThing().getLabel(), getThingHaId());
251 updateSelectedProgramStateDescription();
255 String key = event.getKey();
256 if (EVENT_OPERATION_STATE.equals(key)) {
257 operationState = event.getValue() == null ? null : event.getValue();
260 if (key != null && eventHandlers.containsKey(key)) {
261 EventHandler eventHandler = eventHandlers.get(key);
262 if (eventHandler != null) {
263 eventHandler.handle(event);
267 accessible.set(true);
271 public void onClosed() {
272 if (ignoreEventSourceClosedEvent) {
273 logger.debug("Ignoring event source close event. thing={}, haId={}", getThing().getLabel(), getThingHaId());
275 unregisterEventListener();
276 refreshThingStatus();
277 registerEventListener();
282 public void onRateLimitReached() {
283 unregisterEventListener();
286 scheduleRetryRegistering();
290 * Register event listener.
292 protected void registerEventListener() {
293 if (isBridgeOnline() && isThingAccessibleViaServerSentEvents()) {
294 getEventSourceClient().ifPresent(client -> {
296 ignoreEventSourceClosedEvent = false;
297 client.registerEventListener(getThingHaId(), this);
298 } catch (CommunicationException | AuthorizationException e) {
299 logger.warn("Could not open event source connection. thing={}, haId={}, error={}", getThingLabel(),
300 getThingHaId(), e.getMessage());
307 * Unregister event listener.
309 protected void unregisterEventListener() {
310 unregisterEventListener(false);
313 private void unregisterEventListener(boolean immediate) {
314 getEventSourceClient().ifPresent(client -> {
315 ignoreEventSourceClosedEvent = true;
316 client.unregisterEventListener(this, immediate, false);
321 * Get {@link HomeConnectApiClient}.
323 * @return client instance
325 protected Optional<HomeConnectApiClient> getApiClient() {
326 return getBridgeHandler().map(HomeConnectBridgeHandler::getApiClient);
330 * Get {@link HomeConnectEventSourceClient}.
332 * @return client instance if present
334 protected Optional<HomeConnectEventSourceClient> getEventSourceClient() {
335 return getBridgeHandler().map(HomeConnectBridgeHandler::getEventSourceClient);
339 * Update state description of selected program (Fetch programs via API).
341 protected void updateSelectedProgramStateDescription() {
342 if (isBridgeOffline() || isThingOffline()) {
346 Optional<HomeConnectApiClient> apiClient = getApiClient();
347 if (apiClient.isPresent()) {
349 List<StateOption> stateOptions = apiClient.get().getPrograms(getThingHaId()).stream()
350 .map(p -> new StateOption(p.getKey(), mapStringType(p.getKey()))).collect(Collectors.toList());
352 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(
353 channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions));
354 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
355 logger.debug("Could not fetch available programs. thing={}, haId={}, error={}", getThingLabel(),
356 getThingHaId(), e.getMessage());
357 removeSelectedProgramStateDescription();
360 removeSelectedProgramStateDescription();
365 * Remove state description of selected program.
367 protected void removeSelectedProgramStateDescription() {
368 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
369 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), emptyList()));
373 * Is thing ready to process commands. If bridge or thing itself is offline commands will be ignored.
375 * @return true if ready
377 protected boolean isThingReadyToHandleCommand() {
378 if (isBridgeOffline()) {
379 logger.debug("Bridge is OFFLINE. Ignore command. thing={}, haId={}", getThingLabel(), getThingHaId());
383 if (isThingOffline()) {
384 logger.debug("{} is OFFLINE. Ignore command. haId={}", getThing().getLabel(), getThingHaId());
392 * Checks if bridge is online and set.
394 * @return true if online
396 protected boolean isBridgeOnline() {
397 Bridge bridge = getBridge();
398 return bridge != null && ONLINE.equals(bridge.getStatus());
402 * Checks if bridge is offline or not set.
404 * @return true if offline
406 protected boolean isBridgeOffline() {
407 return !isBridgeOnline();
411 * Checks if thing is online.
413 * @return true if online
415 protected boolean isThingOnline() {
416 return ONLINE.equals(getThing().getStatus());
420 * Checks if thing is connected to the cloud and accessible via SSE.
422 * @return true if yes
424 public boolean isThingAccessibleViaServerSentEvents() {
425 return accessible.get();
429 * Checks if thing is offline.
431 * @return true if offline
433 protected boolean isThingOffline() {
434 return !isThingOnline();
438 * Get {@link HomeConnectBridgeHandler}.
440 * @return bridge handler
442 protected Optional<HomeConnectBridgeHandler> getBridgeHandler() {
443 Bridge bridge = getBridge();
444 if (bridge != null) {
445 BridgeHandler bridgeHandler = bridge.getHandler();
446 if (bridgeHandler instanceof HomeConnectBridgeHandler) {
447 return Optional.of((HomeConnectBridgeHandler) bridgeHandler);
450 return Optional.empty();
454 * Get thing channel by given channel id.
456 * @param channelId channel id
459 protected Optional<Channel> getThingChannel(String channelId) {
460 Channel channel = getThing().getChannel(channelId);
461 if (channel == null) {
462 return Optional.empty();
464 return Optional.of(channel);
469 * Configure channel update handlers. Classes which extend {@link AbstractHomeConnectThingHandler} must implement
470 * this class and add handlers.
472 * @param handlers channel update handlers
474 protected abstract void configureChannelUpdateHandlers(final Map<String, ChannelUpdateHandler> handlers);
477 * Configure event handlers. Classes which extend {@link AbstractHomeConnectThingHandler} must implement
478 * this class and add handlers.
480 * @param handlers Server-Sent-Event handlers
482 protected abstract void configureEventHandlers(final Map<String, EventHandler> handlers);
485 * Update all channels via API.
488 protected void updateChannels() {
489 if (isBridgeOffline()) {
490 logger.debug("Bridge handler not found or offline. Stopping update of channels. thing={}, haId={}",
491 getThingLabel(), getThingHaId());
492 } else if (isThingOffline()) {
493 logger.debug("{} offline. Stopping update of channels. haId={}", getThing().getLabel(), getThingHaId());
495 List<Channel> channels = getThing().getChannels();
496 for (Channel channel : channels) {
497 updateChannel(channel.getUID());
503 * Update Channel values via API.
505 * @param channelUID channel UID
507 protected void updateChannel(ChannelUID channelUID) {
508 if (!getApiClient().isPresent()) {
509 logger.error("Cannot update channel. No instance of api client found! thing={}, haId={}", getThingLabel(),
514 if (!isThingReadyToHandleCommand()) {
518 if ((isLinked(channelUID) || CHANNEL_OPERATION_STATE.equals(channelUID.getId())) // always update operation
520 && channelUpdateHandlers.containsKey(channelUID.getId())) {
522 ChannelUpdateHandler channelUpdateHandler = channelUpdateHandlers.get(channelUID.getId());
523 if (channelUpdateHandler != null) {
524 channelUpdateHandler.handle(channelUID, expiringStateMap);
526 } catch (ApplianceOfflineException e) {
528 "API communication problem while trying to update! Appliance offline. thing={}, haId={}, error={}",
529 getThingLabel(), getThingHaId(), e.getMessage());
530 updateStatus(OFFLINE);
531 resetChannelsOnOfflineEvent();
532 resetProgramStateChannels(true);
533 } catch (CommunicationException e) {
534 logger.debug("API communication problem while trying to update! thing={}, haId={}, error={}",
535 getThingLabel(), getThingHaId(), e.getMessage());
536 } catch (AuthorizationException e) {
537 logger.debug("Authentication problem while trying to update! thing={}, haId={}", getThingLabel(),
539 handleAuthenticationError(e);
545 * Reset program related channels.
547 * @param offline true if the device is considered as OFFLINE
549 protected void resetProgramStateChannels(boolean offline) {
550 logger.debug("Resetting active program channel states. thing={}, haId={}", getThingLabel(), getThingHaId());
554 * Reset all channels on OFFLINE event.
556 protected void resetChannelsOnOfflineEvent() {
557 logger.debug("Resetting channel states due to OFFLINE event. thing={}, haId={}", getThingLabel(),
559 getThingChannel(CHANNEL_POWER_STATE).ifPresent(channel -> updateState(channel.getUID(), OnOffType.OFF));
560 getThingChannel(CHANNEL_OPERATION_STATE).ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
561 getThingChannel(CHANNEL_DOOR_STATE).ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
562 getThingChannel(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE)
563 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
564 getThingChannel(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE)
565 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
566 getThingChannel(CHANNEL_REMOTE_START_ALLOWANCE_STATE)
567 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
568 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
569 .ifPresent(channel -> updateState(channel.getUID(), UnDefType.UNDEF));
573 * Map Home Connect key and value names to label.
574 * e.g. Dishcare.Dishwasher.Program.Eco50 --> Eco50 or BSH.Common.EnumType.OperationState.DelayedStart --> Delayed
578 * @return human readable label
580 protected String mapStringType(String type) {
581 int index = type.lastIndexOf(".");
582 if (index > 0 && type.length() > index) {
583 String sub = type.substring(index + 1);
584 StringBuilder sb = new StringBuilder();
585 for (String word : sub.split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])")) {
589 return sb.toString().trim();
595 * Map Home Connect stage value to label.
596 * e.g. Cooking.Hood.EnumType.IntensiveStage.IntensiveStage1 --> 1
599 * @return human readable label
601 protected String mapStageStringType(String stage) {
604 case STAGE_INTENSIVE_STAGE_OFF:
607 case STAGE_FAN_STAGE_01:
608 case STAGE_INTENSIVE_STAGE_1:
611 case STAGE_FAN_STAGE_02:
612 case STAGE_INTENSIVE_STAGE_2:
615 case STAGE_FAN_STAGE_03:
618 case STAGE_FAN_STAGE_04:
621 case STAGE_FAN_STAGE_05:
625 stage = mapStringType(stage);
632 * Map unit string (returned by home connect api) to Unit
634 * @param unit String eg. "°C"
637 protected Unit<Temperature> mapTemperature(@Nullable String unit) {
640 } else if (unit.endsWith("C")) {
648 * Map hex representation of color to HSB type.
650 * @param colorCode color code e.g. #001122
653 protected HSBType mapColor(String colorCode) {
654 HSBType color = HSBType.WHITE;
656 if (colorCode.length() == 7) {
657 int r = Integer.valueOf(colorCode.substring(1, 3), 16);
658 int g = Integer.valueOf(colorCode.substring(3, 5), 16);
659 int b = Integer.valueOf(colorCode.substring(5, 7), 16);
660 color = HSBType.fromRGB(r, g, b);
666 * Map HSB color type to hex representation.
668 * @param color HSB color
669 * @return color code e.g. #001122
671 protected String mapColor(HSBType color) {
672 String redValue = String.format("%02X", (int) (color.getRed().floatValue() * 2.55));
673 String greenValue = String.format("%02X", (int) (color.getGreen().floatValue() * 2.55));
674 String blueValue = String.format("%02X", (int) (color.getBlue().floatValue() * 2.55));
675 return "#" + redValue + greenValue + blueValue;
679 * Check bridge status and refresh connection status of thing accordingly.
681 protected void refreshThingStatus() {
682 Optional<HomeConnectApiClient> apiClient = getApiClient();
684 apiClient.ifPresent(client -> {
686 HomeAppliance homeAppliance = client.getHomeAppliance(getThingHaId());
687 if (!homeAppliance.isConnected()) {
688 updateStatus(OFFLINE);
690 updateStatus(ONLINE);
692 accessible.set(true);
693 } catch (CommunicationException e) {
695 "Update status to OFFLINE. Home Connect service is not reachable or a problem occurred! thing={}, haId={}, error={}.",
696 getThingLabel(), getThingHaId(), e.getMessage());
697 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
698 "Home Connect service is not reachable or a problem occurred! (" + e.getMessage() + ").");
699 accessible.set(false);
700 } catch (AuthorizationException e) {
702 "Update status to OFFLINE. Home Connect service is not reachable or a problem occurred! thing={}, haId={}, error={}",
703 getThingLabel(), getThingHaId(), e.getMessage());
704 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
705 "Home Connect service is not reachable or a problem occurred! (" + e.getMessage() + ").");
706 accessible.set(false);
707 handleAuthenticationError(e);
710 if (apiClient.isEmpty()) {
711 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
712 accessible.set(false);
717 * Get home appliance id of Thing.
719 * @return home appliance id
721 public String getThingHaId() {
722 return getThing().getConfiguration().get(HA_ID).toString();
726 * Returns the human readable label for this thing.
728 * @return the human readable label
730 protected @Nullable String getThingLabel() {
731 return getThing().getLabel();
735 * Handle authentication exception.
737 protected void handleAuthenticationError(AuthorizationException exception) {
738 if (isBridgeOnline()) {
740 "Thing handler threw authentication exception --> clear credential storage thing={}, haId={} error={}",
741 getThingLabel(), getThingHaId(), exception.getMessage());
743 getBridgeHandler().ifPresent(homeConnectBridgeHandler -> {
745 homeConnectBridgeHandler.getOAuthClientService().remove();
746 homeConnectBridgeHandler.reinitialize();
747 } catch (OAuthException e) {
748 // client is already closed --> we can ignore it
755 * Get operation state of device.
757 * @return operation state string
759 protected @Nullable String getOperationState() {
760 return operationState;
763 protected EventHandler defaultElapsedProgramTimeEventHandler() {
764 return event -> getThingChannel(CHANNEL_ELAPSED_PROGRAM_TIME)
765 .ifPresent(channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND)));
768 protected EventHandler defaultPowerStateEventHandler() {
770 getThingChannel(CHANNEL_POWER_STATE).ifPresent(
771 channel -> updateState(channel.getUID(), OnOffType.from(STATE_POWER_ON.equals(event.getValue()))));
773 if (STATE_POWER_ON.equals(event.getValue())) {
774 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID()));
776 resetProgramStateChannels(true);
777 getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
778 .ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
783 protected EventHandler defaultDoorStateEventHandler() {
784 return event -> getThingChannel(CHANNEL_DOOR_STATE).ifPresent(channel -> updateState(channel.getUID(),
785 STATE_DOOR_OPEN.equals(event.getValue()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED));
788 protected EventHandler defaultOperationStateEventHandler() {
790 String value = event.getValue();
791 getThingChannel(CHANNEL_OPERATION_STATE).ifPresent(channel -> updateState(channel.getUID(),
792 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
794 if (STATE_OPERATION_FINISHED.equals(event.getValue())) {
795 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
796 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(100, PERCENT)));
797 getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
798 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, SECOND)));
799 } else if (STATE_OPERATION_RUN.equals(event.getValue())) {
800 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
801 .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, PERCENT)));
802 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID()));
803 } else if (STATE_OPERATION_READY.equals(event.getValue())) {
804 resetProgramStateChannels(false);
809 protected EventHandler defaultActiveProgramEventHandler() {
811 String value = event.getValue();
812 getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(channel -> updateState(channel.getUID(),
813 value == null ? UnDefType.UNDEF : new StringType(mapStringType(value))));
814 if (event.getValue() == null) {
815 resetProgramStateChannels(false);
820 protected EventHandler defaultEventPresentStateEventHandler(String channelId) {
821 return event -> getThingChannel(channelId).ifPresent(channel -> updateState(channel.getUID(),
822 OnOffType.from(!STATE_EVENT_PRESENT_STATE_OFF.equals(event.getValue()))));
825 protected EventHandler defaultBooleanEventHandler(String channelId) {
826 return event -> getThingChannel(channelId)
827 .ifPresent(channel -> updateState(channel.getUID(), OnOffType.from(event.getValueAsBoolean())));
830 protected EventHandler defaultRemainingProgramTimeEventHandler() {
831 return event -> getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
832 .ifPresent(channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), SECOND)));
835 protected EventHandler defaultSelectedProgramStateEventHandler() {
836 return event -> getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE)
837 .ifPresent(channel -> updateState(channel.getUID(),
838 event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue())));
841 protected EventHandler defaultAmbientLightColorStateEventHandler() {
842 return event -> getThingChannel(CHANNEL_AMBIENT_LIGHT_COLOR_STATE)
843 .ifPresent(channel -> updateState(channel.getUID(),
844 event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue())));
847 protected EventHandler defaultAmbientLightCustomColorStateEventHandler() {
848 return event -> getThingChannel(CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE).ifPresent(channel -> {
849 String value = event.getValue();
851 updateState(channel.getUID(), mapColor(value));
853 updateState(channel.getUID(), UnDefType.UNDEF);
858 protected EventHandler updateProgramOptionsAndSelectedProgramStateEventHandler() {
860 defaultSelectedProgramStateEventHandler().handle(event);
862 // update available program options
864 String programKey = event.getValue();
865 if (programKey != null) {
866 updateProgramOptionsStateDescriptions(programKey, null);
868 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
869 logger.debug("Could not update program options. {}", e.getMessage());
874 protected EventHandler defaultPercentQuantityTypeEventHandler(String channelId) {
875 return event -> getThingChannel(channelId).ifPresent(
876 channel -> updateState(channel.getUID(), new QuantityType<>(event.getValueAsInt(), PERCENT)));
879 protected EventHandler defaultPercentHandler(String channelId) {
880 return event -> getThingChannel(channelId)
881 .ifPresent(channel -> updateState(channel.getUID(), new PercentType(event.getValueAsInt())));
884 protected ChannelUpdateHandler defaultDoorStateChannelUpdateHandler() {
885 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
886 Optional<HomeConnectApiClient> apiClient = getApiClient();
887 if (apiClient.isPresent()) {
888 Data data = apiClient.get().getDoorState(getThingHaId());
889 if (data.getValue() != null) {
890 return STATE_DOOR_OPEN.equals(data.getValue()) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
892 return UnDefType.UNDEF;
895 return UnDefType.UNDEF;
900 protected ChannelUpdateHandler defaultPowerStateChannelUpdateHandler() {
901 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
902 Optional<HomeConnectApiClient> apiClient = getApiClient();
903 if (apiClient.isPresent()) {
904 Data data = apiClient.get().getPowerState(getThingHaId());
905 if (data.getValue() != null) {
906 return OnOffType.from(STATE_POWER_ON.equals(data.getValue()));
908 return UnDefType.UNDEF;
911 return UnDefType.UNDEF;
916 protected ChannelUpdateHandler defaultAmbientLightChannelUpdateHandler() {
917 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
918 Optional<HomeConnectApiClient> apiClient = getApiClient();
919 if (apiClient.isPresent()) {
920 Data data = apiClient.get().getAmbientLightState(getThingHaId());
921 if (data.getValue() != null) {
922 boolean enabled = data.getValueAsBoolean();
925 Data brightnessData = apiClient.get().getAmbientLightBrightnessState(getThingHaId());
926 getThingChannel(CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE)
927 .ifPresent(channel -> updateState(channel.getUID(),
928 new PercentType(brightnessData.getValueAsInt())));
931 Data colorData = apiClient.get().getAmbientLightColorState(getThingHaId());
932 getThingChannel(CHANNEL_AMBIENT_LIGHT_COLOR_STATE).ifPresent(
933 channel -> updateState(channel.getUID(), new StringType(colorData.getValue())));
936 Data customColorData = apiClient.get().getAmbientLightCustomColorState(getThingHaId());
937 getThingChannel(CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE).ifPresent(channel -> {
938 String value = customColorData.getValue();
940 updateState(channel.getUID(), mapColor(value));
942 updateState(channel.getUID(), UnDefType.UNDEF);
947 return OnOffType.from(enabled);
949 return UnDefType.UNDEF;
952 return UnDefType.UNDEF;
957 protected ChannelUpdateHandler defaultNoOpUpdateHandler() {
958 return (channelUID, cache) -> updateState(channelUID, UnDefType.UNDEF);
961 protected ChannelUpdateHandler defaultOperationStateChannelUpdateHandler() {
962 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
963 Optional<HomeConnectApiClient> apiClient = getApiClient();
964 if (apiClient.isPresent()) {
965 Data data = apiClient.get().getOperationState(getThingHaId());
967 String value = data.getValue();
969 operationState = data.getValue();
970 return new StringType(mapStringType(value));
972 operationState = null;
973 return UnDefType.UNDEF;
976 return UnDefType.UNDEF;
981 protected ChannelUpdateHandler defaultRemoteControlActiveStateChannelUpdateHandler() {
982 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
983 Optional<HomeConnectApiClient> apiClient = getApiClient();
984 if (apiClient.isPresent()) {
985 return OnOffType.from(apiClient.get().isRemoteControlActive(getThingHaId()));
987 return OnOffType.OFF;
991 protected ChannelUpdateHandler defaultLocalControlActiveStateChannelUpdateHandler() {
992 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
993 Optional<HomeConnectApiClient> apiClient = getApiClient();
994 if (apiClient.isPresent()) {
995 return OnOffType.from(apiClient.get().isLocalControlActive(getThingHaId()));
997 return OnOffType.OFF;
1001 protected ChannelUpdateHandler defaultRemoteStartAllowanceChannelUpdateHandler() {
1002 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1003 Optional<HomeConnectApiClient> apiClient = getApiClient();
1004 if (apiClient.isPresent()) {
1005 return OnOffType.from(apiClient.get().isRemoteControlStartAllowed(getThingHaId()));
1007 return OnOffType.OFF;
1011 protected ChannelUpdateHandler defaultSelectedProgramStateUpdateHandler() {
1012 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1013 Optional<HomeConnectApiClient> apiClient = getApiClient();
1014 if (apiClient.isPresent()) {
1015 Program program = apiClient.get().getSelectedProgram(getThingHaId());
1016 if (program != null) {
1017 processProgramOptions(program.getOptions());
1018 return new StringType(program.getKey());
1020 return UnDefType.UNDEF;
1023 return UnDefType.UNDEF;
1027 protected ChannelUpdateHandler updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() {
1028 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1029 Optional<HomeConnectApiClient> apiClient = getApiClient();
1030 if (apiClient.isPresent()) {
1031 Program program = apiClient.get().getSelectedProgram(getThingHaId());
1033 if (program != null) {
1034 updateProgramOptionsStateDescriptions(program.getKey(), program.getOptions());
1035 processProgramOptions(program.getOptions());
1037 return new StringType(program.getKey());
1039 return UnDefType.UNDEF;
1042 return UnDefType.UNDEF;
1046 protected ChannelUpdateHandler defaultActiveProgramStateUpdateHandler() {
1047 return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
1048 Optional<HomeConnectApiClient> apiClient = getApiClient();
1049 if (apiClient.isPresent()) {
1050 Program program = apiClient.get().getActiveProgram(getThingHaId());
1052 if (program != null) {
1053 processProgramOptions(program.getOptions());
1054 return new StringType(mapStringType(program.getKey()));
1056 resetProgramStateChannels(false);
1057 return UnDefType.UNDEF;
1060 return UnDefType.UNDEF;
1064 protected void handleTemperatureCommand(final ChannelUID channelUID, final Command command,
1065 final HomeConnectApiClient apiClient)
1066 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1067 if (command instanceof QuantityType) {
1068 QuantityType<?> quantity = (QuantityType<?>) command;
1074 if (quantity.getUnit().equals(SIUnits.CELSIUS) || quantity.getUnit().equals(ImperialUnits.FAHRENHEIT)) {
1075 unit = quantity.getUnit().toString();
1076 value = String.valueOf(quantity.intValue());
1078 logger.debug("Converting target temperature from {}{} to °C value. thing={}, haId={}",
1079 quantity.intValue(), quantity.getUnit().toString(), getThingLabel(), getThingHaId());
1081 var celsius = quantity.toUnit(SIUnits.CELSIUS);
1082 if (celsius == null) {
1083 logger.warn("Converting temperature to celsius failed! quantity={}", quantity);
1086 value = String.valueOf(celsius.intValue());
1088 logger.debug("Converted value {}{}", value, unit);
1091 if (value != null) {
1092 logger.debug("Set temperature to {} {}. thing={}, haId={}", value, unit, getThingLabel(),
1094 switch (channelUID.getId()) {
1095 case CHANNEL_REFRIGERATOR_SETPOINT_TEMPERATURE:
1096 apiClient.setFridgeSetpointTemperature(getThingHaId(), value, unit);
1097 case CHANNEL_FREEZER_SETPOINT_TEMPERATURE:
1098 apiClient.setFreezerSetpointTemperature(getThingHaId(), value, unit);
1100 case CHANNEL_SETPOINT_TEMPERATURE:
1101 apiClient.setProgramOptions(getThingHaId(), OPTION_SETPOINT_TEMPERATURE, value, unit, true,
1105 logger.debug("Unknown channel! Cannot set temperature. channelUID={}", channelUID);
1108 } catch (UnconvertibleException e) {
1109 logger.warn("Could not set temperature! haId={}, error={}", getThingHaId(), e.getMessage());
1114 protected void handleLightCommands(final ChannelUID channelUID, final Command command,
1115 final HomeConnectApiClient apiClient)
1116 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1117 switch (channelUID.getId()) {
1118 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1119 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1120 // turn light on if turned off
1121 turnLightOn(channelUID, apiClient);
1123 int newBrightness = BRIGHTNESS_MIN;
1124 if (command instanceof OnOffType) {
1125 newBrightness = command == OnOffType.ON ? BRIGHTNESS_MAX : BRIGHTNESS_MIN;
1126 } else if (command instanceof IncreaseDecreaseType) {
1127 int currentBrightness = getCurrentBrightness(channelUID, apiClient);
1128 if (command.equals(IncreaseDecreaseType.INCREASE)) {
1129 newBrightness = currentBrightness + BRIGHTNESS_DIM_STEP;
1131 newBrightness = currentBrightness - BRIGHTNESS_DIM_STEP;
1133 } else if (command instanceof PercentType) {
1134 newBrightness = (int) Math.floor(((PercentType) command).doubleValue());
1135 } else if (command instanceof DecimalType) {
1136 newBrightness = ((DecimalType) command).intValue();
1139 // check in in range
1140 newBrightness = Math.min(Math.max(newBrightness, BRIGHTNESS_MIN), BRIGHTNESS_MAX);
1142 setLightBrightness(channelUID, apiClient, newBrightness);
1144 case CHANNEL_FUNCTIONAL_LIGHT_STATE:
1145 if (command instanceof OnOffType) {
1146 apiClient.setFunctionalLightState(getThingHaId(), OnOffType.ON.equals(command));
1149 case CHANNEL_AMBIENT_LIGHT_STATE:
1150 if (command instanceof OnOffType) {
1151 apiClient.setAmbientLightState(getThingHaId(), OnOffType.ON.equals(command));
1154 case CHANNEL_AMBIENT_LIGHT_COLOR_STATE:
1155 if (command instanceof StringType) {
1156 turnLightOn(channelUID, apiClient);
1157 apiClient.setAmbientLightColorState(getThingHaId(), command.toFullString());
1160 case CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE:
1161 turnLightOn(channelUID, apiClient);
1163 // make sure 'custom color' is set as color
1164 Data ambientLightColorState = apiClient.getAmbientLightColorState(getThingHaId());
1165 if (!STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR.equals(ambientLightColorState.getValue())) {
1166 apiClient.setAmbientLightColorState(getThingHaId(), STATE_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR);
1169 if (command instanceof HSBType) {
1170 apiClient.setAmbientLightCustomColorState(getThingHaId(), mapColor((HSBType) command));
1171 } else if (command instanceof StringType) {
1172 apiClient.setAmbientLightCustomColorState(getThingHaId(), command.toFullString());
1178 protected void handlePowerCommand(final ChannelUID channelUID, final Command command,
1179 final HomeConnectApiClient apiClient, String stateNotOn)
1180 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1181 if (command instanceof OnOffType && CHANNEL_POWER_STATE.equals(channelUID.getId())) {
1182 apiClient.setPowerState(getThingHaId(), OnOffType.ON.equals(command) ? STATE_POWER_ON : stateNotOn);
1186 private int getCurrentBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient)
1187 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1188 String id = channelUID.getId();
1189 if (CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE.equals(id)) {
1190 return apiClient.getFunctionalLightBrightnessState(getThingHaId()).getValueAsInt();
1192 return apiClient.getAmbientLightBrightnessState(getThingHaId()).getValueAsInt();
1196 private void setLightBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient, int value)
1197 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1198 switch (channelUID.getId()) {
1199 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1200 apiClient.setFunctionalLightBrightnessState(getThingHaId(), value);
1202 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1203 apiClient.setAmbientLightBrightnessState(getThingHaId(), value);
1208 private void turnLightOn(final ChannelUID channelUID, final HomeConnectApiClient apiClient)
1209 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1210 switch (channelUID.getId()) {
1211 case CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE:
1212 Data functionalLightState = apiClient.getFunctionalLightState(getThingHaId());
1213 if (!functionalLightState.getValueAsBoolean()) {
1214 apiClient.setFunctionalLightState(getThingHaId(), true);
1217 case CHANNEL_AMBIENT_LIGHT_CUSTOM_COLOR_STATE:
1218 case CHANNEL_AMBIENT_LIGHT_COLOR_STATE:
1219 case CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE:
1220 Data ambientLightState = apiClient.getAmbientLightState(getThingHaId());
1221 if (!ambientLightState.getValueAsBoolean()) {
1222 apiClient.setAmbientLightState(getThingHaId(), true);
1228 protected void processProgramOptions(List<Option> options) {
1229 options.forEach(option -> {
1230 String key = option.getKey();
1233 case OPTION_WASHER_TEMPERATURE:
1234 getThingChannel(CHANNEL_WASHER_TEMPERATURE)
1235 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1237 case OPTION_WASHER_SPIN_SPEED:
1238 getThingChannel(CHANNEL_WASHER_SPIN_SPEED)
1239 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1241 case OPTION_WASHER_IDOS_1_DOSING_LEVEL:
1242 getThingChannel(CHANNEL_WASHER_IDOS1)
1243 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1245 case OPTION_WASHER_IDOS_2_DOSING_LEVEL:
1246 getThingChannel(CHANNEL_WASHER_IDOS2)
1247 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1249 case OPTION_DRYER_DRYING_TARGET:
1250 getThingChannel(CHANNEL_DRYER_DRYING_TARGET)
1251 .ifPresent(channel -> updateState(channel.getUID(), new StringType(option.getValue())));
1253 case OPTION_HOOD_INTENSIVE_LEVEL:
1254 String hoodIntensiveLevelValue = option.getValue();
1255 if (hoodIntensiveLevelValue != null) {
1256 getThingChannel(CHANNEL_HOOD_INTENSIVE_LEVEL)
1257 .ifPresent(channel -> updateState(channel.getUID(),
1258 new StringType(mapStageStringType(hoodIntensiveLevelValue))));
1261 case OPTION_HOOD_VENTING_LEVEL:
1262 String hoodVentingLevel = option.getValue();
1263 if (hoodVentingLevel != null) {
1264 getThingChannel(CHANNEL_HOOD_VENTING_LEVEL)
1265 .ifPresent(channel -> updateState(channel.getUID(),
1266 new StringType(mapStageStringType(hoodVentingLevel))));
1269 case OPTION_SETPOINT_TEMPERATURE:
1270 getThingChannel(CHANNEL_SETPOINT_TEMPERATURE).ifPresent(channel -> updateState(channel.getUID(),
1271 new QuantityType<>(option.getValueAsInt(), mapTemperature(option.getUnit()))));
1273 case OPTION_DURATION:
1274 getThingChannel(CHANNEL_DURATION).ifPresent(channel -> updateState(channel.getUID(),
1275 new QuantityType<>(option.getValueAsInt(), SECOND)));
1277 case OPTION_FINISH_IN_RELATIVE:
1278 case OPTION_REMAINING_PROGRAM_TIME:
1279 getThingChannel(CHANNEL_REMAINING_PROGRAM_TIME_STATE)
1280 .ifPresent(channel -> updateState(channel.getUID(),
1281 new QuantityType<>(option.getValueAsInt(), SECOND)));
1283 case OPTION_ELAPSED_PROGRAM_TIME:
1284 getThingChannel(CHANNEL_ELAPSED_PROGRAM_TIME).ifPresent(channel -> updateState(channel.getUID(),
1285 new QuantityType<>(option.getValueAsInt(), SECOND)));
1287 case OPTION_PROGRAM_PROGRESS:
1288 getThingChannel(CHANNEL_PROGRAM_PROGRESS_STATE)
1289 .ifPresent(channel -> updateState(channel.getUID(),
1290 new QuantityType<>(option.getValueAsInt(), PERCENT)));
1297 protected String convertWasherTemperature(String value) {
1298 if (value.startsWith("LaundryCare.Washer.EnumType.Temperature.GC")) {
1299 return value.replace("LaundryCare.Washer.EnumType.Temperature.GC", "") + "°C";
1302 if (value.startsWith("LaundryCare.Washer.EnumType.Temperature.Ul")) {
1303 return mapStringType(value.replace("LaundryCare.Washer.EnumType.Temperature.Ul", ""));
1306 return mapStringType(value);
1309 protected String convertWasherSpinSpeed(String value) {
1310 if (value.startsWith("LaundryCare.Washer.EnumType.SpinSpeed.RPM")) {
1311 return value.replace("LaundryCare.Washer.EnumType.SpinSpeed.RPM", "") + " RPM";
1314 if (value.startsWith("LaundryCare.Washer.EnumType.SpinSpeed.Ul")) {
1315 return value.replace("LaundryCare.Washer.EnumType.SpinSpeed.Ul", "");
1318 return mapStringType(value);
1321 protected void updateProgramOptionsStateDescriptions(String programKey, @Nullable List<Option> optionsValues)
1322 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
1323 Optional<HomeConnectApiClient> apiClient = getApiClient();
1324 if (apiClient.isPresent()) {
1325 List<AvailableProgramOption> availableProgramOptions = apiClient.get().getProgramOptions(getThingHaId(),
1328 Optional<Channel> channelSpinSpeed = getThingChannel(CHANNEL_WASHER_SPIN_SPEED);
1329 Optional<Channel> channelTemperature = getThingChannel(CHANNEL_WASHER_TEMPERATURE);
1330 Optional<Channel> channelDryingTarget = getThingChannel(CHANNEL_DRYER_DRYING_TARGET);
1332 if (availableProgramOptions.isEmpty()) {
1333 List<Option> options;
1334 if (optionsValues != null) {
1335 options = optionsValues;
1336 } else if (channelSpinSpeed.isPresent() || channelTemperature.isPresent()
1337 || channelDryingTarget.isPresent()) {
1338 Program program = apiClient.get().getSelectedProgram(getThingHaId());
1339 options = program != null ? program.getOptions() : emptyList();
1341 options = emptyList();
1344 channelSpinSpeed.ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1346 .filter(option -> option.getKey() != null && option.getValue() != null
1347 && OPTION_WASHER_SPIN_SPEED.equals(option.getKey()))
1348 .map(option -> option.getValue())
1349 .map(value -> new StateOption(value == null ? "" : value,
1350 convertWasherSpinSpeed(value == null ? "" : value)))
1351 .collect(Collectors.toList())));
1353 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1355 .filter(option -> option.getKey() != null && option.getValue() != null
1356 && OPTION_WASHER_TEMPERATURE.equals(option.getKey()))
1357 .map(option -> option.getValue())
1358 .map(value -> new StateOption(value == null ? "" : value,
1359 convertWasherTemperature(value == null ? "" : value)))
1360 .collect(Collectors.toList())));
1362 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1364 .filter(option -> option.getKey() != null && option.getValue() != null
1365 && OPTION_DRYER_DRYING_TARGET.equals(option.getKey()))
1366 .map(option -> option.getValue())
1367 .map(value -> new StateOption(value == null ? "" : value,
1368 mapStringType(value == null ? "" : value)))
1369 .collect(Collectors.toList())));
1372 availableProgramOptions.forEach(option -> {
1373 switch (option.getKey()) {
1374 case OPTION_WASHER_SPIN_SPEED: {
1376 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1377 createStateOptions(option, this::convertWasherSpinSpeed)));
1380 case OPTION_WASHER_TEMPERATURE: {
1382 .ifPresent(channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(),
1383 createStateOptions(option, this::convertWasherTemperature)));
1386 case OPTION_DRYER_DRYING_TARGET: {
1387 channelDryingTarget.ifPresent(channel -> dynamicStateDescriptionProvider
1388 .setStateOptions(channel.getUID(), createStateOptions(option, this::mapStringType)));
1396 protected HomeConnectDynamicStateDescriptionProvider getDynamicStateDescriptionProvider() {
1397 return dynamicStateDescriptionProvider;
1400 private List<StateOption> createStateOptions(AvailableProgramOption option,
1401 Function<String, String> stateConverter) {
1402 return option.getAllowedValues().stream().map(av -> new StateOption(av, stateConverter.apply(av)))
1403 .collect(Collectors.toList());
1406 private synchronized void scheduleOfflineMonitor1() {
1407 this.reinitializationFuture1 = scheduler.schedule(() -> {
1408 if (isBridgeOnline() && isThingOffline()) {
1409 logger.debug("Offline monitor 1: Check if thing is ONLINE. thing={}, haId={}", getThingLabel(),
1411 refreshThingStatus();
1412 if (isThingOnline()) {
1413 logger.debug("Offline monitor 1: Thing status changed to ONLINE. thing={}, haId={}",
1414 getThingLabel(), getThingHaId());
1417 scheduleOfflineMonitor1();
1420 scheduleOfflineMonitor1();
1422 }, AbstractHomeConnectThingHandler.OFFLINE_MONITOR_1_DELAY_MIN, TimeUnit.MINUTES);
1425 private synchronized void stopOfflineMonitor1() {
1426 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture1;
1427 if (reinitializationFuture != null) {
1428 reinitializationFuture.cancel(false);
1429 this.reinitializationFuture1 = null;
1433 private synchronized void scheduleOfflineMonitor2() {
1434 this.reinitializationFuture2 = scheduler.schedule(() -> {
1435 if (isBridgeOnline() && !accessible.get()) {
1436 logger.debug("Offline monitor 2: Check if thing is ONLINE. thing={}, haId={}", getThingLabel(),
1438 refreshThingStatus();
1439 if (isThingOnline()) {
1440 logger.debug("Offline monitor 2: Thing status changed to ONLINE. thing={}, haId={}",
1441 getThingLabel(), getThingHaId());
1444 scheduleOfflineMonitor2();
1447 scheduleOfflineMonitor2();
1449 }, AbstractHomeConnectThingHandler.OFFLINE_MONITOR_2_DELAY_MIN, TimeUnit.MINUTES);
1452 private synchronized void stopOfflineMonitor2() {
1453 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture2;
1454 if (reinitializationFuture != null) {
1455 reinitializationFuture.cancel(false);
1456 this.reinitializationFuture2 = null;
1460 private synchronized void scheduleRetryRegistering() {
1461 this.reinitializationFuture3 = scheduler.schedule(() -> {
1462 logger.debug("Try to register event listener again. haId={}", getThingHaId());
1463 unregisterEventListener();
1464 registerEventListener();
1465 }, AbstractHomeConnectThingHandler.EVENT_LISTENER_CONNECT_RETRY_DELAY_MIN, TimeUnit.MINUTES);
1468 private synchronized void stopRetryRegistering() {
1469 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture3;
1470 if (reinitializationFuture != null) {
1471 reinitializationFuture.cancel(true);
1472 this.reinitializationFuture3 = null;