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.mielecloud.internal.handler;
15 import java.util.Collection;
16 import java.util.Collections;
17 import java.util.Optional;
18 import java.util.concurrent.CompletableFuture;
19 import java.util.concurrent.ExecutionException;
20 import java.util.concurrent.ScheduledExecutorService;
21 import java.util.function.Consumer;
22 import java.util.function.Function;
23 import java.util.function.Supplier;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
28 import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.I18NKeys;
29 import org.openhab.binding.mielecloud.internal.auth.OAuthException;
30 import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefreshListener;
31 import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
32 import org.openhab.binding.mielecloud.internal.discovery.ThingDiscoveryService;
33 import org.openhab.binding.mielecloud.internal.util.EmailValidator;
34 import org.openhab.binding.mielecloud.internal.util.LocaleValidator;
35 import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
36 import org.openhab.binding.mielecloud.internal.webservice.ConnectionStatusListener;
37 import org.openhab.binding.mielecloud.internal.webservice.DeviceStateListener;
38 import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
39 import org.openhab.binding.mielecloud.internal.webservice.UnavailableMieleWebservice;
40 import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
41 import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
42 import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceInitializationException;
43 import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
44 import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
45 import org.openhab.core.thing.Bridge;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.binding.BaseBridgeHandler;
51 import org.openhab.core.thing.binding.ThingHandlerService;
52 import org.openhab.core.types.Command;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 * BridgeHandler implementation for the Miele cloud account.
59 * @author Roland Edelhoff - Initial contribution
60 * @author Björn Lange - Introduced CombiningLanguageProvider field and interactions, added LanguageProvider super
61 * interface, switched from polling to SSE, added support for multiple bridges
64 public class MieleBridgeHandler extends BaseBridgeHandler
65 implements OAuthTokenRefreshListener, LanguageProvider, ConnectionStatusListener, DeviceStateListener {
66 private static final int NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED = 6;
68 private final Supplier<MieleWebservice> webserviceFactory;
70 private final OAuthTokenRefresher tokenRefresher;
71 private final CombiningLanguageProvider languageProvider;
72 private final Logger logger = LoggerFactory.getLogger(this.getClass());
74 private @Nullable CompletableFuture<@Nullable Void> logoutFuture;
75 private @Nullable MieleWebservice webService;
76 private @Nullable ThingDiscoveryService discoveryService;
79 * Creates a new {@link MieleBridgeHandler}.
81 * @param bridge The bridge to handle.
82 * @param webserviceFactory Factory for creating {@link MieleWebservice} instances.
83 * @param tokenRefresher Token refresher.
84 * @param languageProvider Language provider.
86 public MieleBridgeHandler(Bridge bridge, Function<ScheduledExecutorService, MieleWebservice> webserviceFactory,
87 OAuthTokenRefresher tokenRefresher, CombiningLanguageProvider languageProvider) {
89 this.webserviceFactory = () -> webserviceFactory.apply(scheduler);
90 this.tokenRefresher = tokenRefresher;
91 this.languageProvider = languageProvider;
94 public void setDiscoveryService(@Nullable ThingDiscoveryService discoveryService) {
95 this.discoveryService = discoveryService;
99 * Gets the current webservice instance for communication with the Miele service.
101 * This function may return an {@link UnavailableMieleWebservice} in case no webservice is available at the moment.
103 public MieleWebservice getWebservice() {
104 MieleWebservice webservice = webService;
105 if (webservice != null) {
108 return UnavailableMieleWebservice.INSTANCE;
112 private String getOAuthServiceHandle() {
113 return getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString();
117 public void initialize() {
118 // It is required to set a status in this method as stated in the Javadoc of ThingHandler.initialize
119 updateStatus(ThingStatus.UNKNOWN);
121 initializeWebservice();
124 public void initializeWebservice() {
125 if (!EmailValidator.isValid(getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString())) {
126 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
127 I18NKeys.BRIDGE_STATUS_DESCRIPTION_INVALID_EMAIL);
128 // When the e-mail configuration is changed a new initialization will be triggered. Therefore, we can leave
129 // the bridge in this state.
134 webService = webserviceFactory.get();
135 } catch (MieleWebserviceInitializationException e) {
136 logger.warn("Failed to initialize webservice.", e);
137 updateStatus(ThingStatus.OFFLINE);
142 tokenRefresher.setRefreshListener(this, getOAuthServiceHandle());
143 } catch (OAuthException e) {
144 logger.debug("Could not initialize Miele Cloud bridge.", e);
145 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
146 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
147 // When the authorization takes place a new initialization will be triggered. Therefore, we can leave the
148 // bridge in this state.
151 languageProvider.setPrioritizedLanguageProvider(this);
152 tryInitializeWebservice();
154 MieleWebservice webservice = getWebservice();
155 webservice.addConnectionStatusListener(this);
156 webservice.addDeviceStateListener(this);
157 if (webservice.hasAccessToken()) {
158 webservice.connectSse();
163 public void handleRemoval() {
165 tokenRefresher.removeTokensFromStorage(getOAuthServiceHandle());
166 super.handleRemoval();
170 public void dispose() {
171 logger.debug("Disposing {}", this.getClass().getName());
175 public void disposeWebservice() {
176 getWebservice().removeConnectionStatusListener(this);
177 getWebservice().removeDeviceStateListener(this);
178 getWebservice().disconnectSse();
179 languageProvider.unsetPrioritizedLanguageProvider();
180 tokenRefresher.unsetRefreshListener(getOAuthServiceHandle());
185 private void stopWebservice() {
186 final MieleWebservice webService = this.webService;
187 this.webService = null;
188 if (webService == null) {
192 scheduler.submit(() -> {
193 CompletableFuture<@Nullable Void> logoutFuture = this.logoutFuture;
194 if (logoutFuture != null) {
197 } catch (InterruptedException e) {
198 logger.warn("Interrupted while waiting for logout!");
199 } catch (ExecutionException e) {
200 logger.warn("Failed to wait for logout.", e);
206 } catch (Exception e) {
207 logger.warn("Failed to close webservice.", e);
213 public void onNewAccessToken(String accessToken) {
214 logger.debug("Setting new access token for webservice access.");
215 updateProperty(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN, accessToken);
217 // Without this the retry would fail causing the thing to go OFFLINE
218 getWebservice().setAccessToken(accessToken);
220 // If there was no access token during initialization then the SSE connection was not established.
221 getWebservice().connectSse();
225 public void handleCommand(ChannelUID channelUID, Command command) {
228 private void performLogout() {
229 logoutFuture = new CompletableFuture<>();
230 scheduler.execute(() -> {
232 getWebservice().logout();
233 } catch (Exception e) {
234 logger.warn("Failed to logout from Miele cloud.", e);
236 Optional.ofNullable(logoutFuture).map(future -> future.complete(null));
240 private void tryInitializeWebservice() {
241 Optional<String> accessToken = tokenRefresher.getAccessTokenFromStorage(getOAuthServiceHandle());
242 if (!accessToken.isPresent()) {
243 logger.debug("No OAuth2 access token available. Retrying later.");
244 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
245 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED);
248 getWebservice().setAccessToken(accessToken.get());
249 updateProperty(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN, accessToken.get());
253 public void onConnectionAlive() {
254 updateStatus(ThingStatus.ONLINE);
258 public void onConnectionError(ConnectionError connectionError, int failedReconnectionAttempts) {
259 if (connectionError == ConnectionError.AUTHORIZATION_FAILED) {
260 tryToRefreshAccessToken();
264 if (failedReconnectionAttempts <= NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED
265 && getThing().getStatus() != ThingStatus.UNKNOWN) {
269 if (getThing().getStatus() == ThingStatus.UNKNOWN && connectionError == ConnectionError.REQUEST_INTERRUPTED
270 && failedReconnectionAttempts <= NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED) {
274 switch (connectionError) {
275 case AUTHORIZATION_FAILED:
279 case REQUEST_EXECUTION_FAILED:
280 case SERVICE_UNAVAILABLE:
281 case RESPONSE_MALFORMED:
283 case TOO_MANY_RERQUESTS:
284 case SSE_STREAM_ENDED:
285 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
289 case REQUEST_INTERRUPTED:
290 case OTHER_HTTP_ERROR:
292 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
293 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
298 private void tryToRefreshAccessToken() {
300 tokenRefresher.refreshToken(getOAuthServiceHandle());
301 getWebservice().connectSse();
302 } catch (OAuthException e) {
303 logger.debug("Failed to refresh OAuth token!", e);
304 getWebservice().disconnectSse();
305 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
306 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
311 public Optional<String> getLanguage() {
312 Object languageObject = thing.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE);
313 if (languageObject instanceof String) {
314 String language = (String) languageObject;
315 if (language.isEmpty() || !LocaleValidator.isValidLanguage(language)) {
316 return Optional.empty();
318 return Optional.of(language);
321 return Optional.empty();
326 public void onDeviceStateUpdated(DeviceState deviceState) {
327 ThingDiscoveryService discoveryService = this.discoveryService;
328 if (discoveryService != null) {
329 discoveryService.onDeviceStateUpdated(deviceState);
332 invokeOnThingHandlers(deviceState.getDeviceIdentifier(), handler -> handler.onDeviceStateUpdated(deviceState));
336 public void onProcessActionUpdated(ActionsState actionState) {
337 invokeOnThingHandlers(actionState.getDeviceIdentifier(),
338 handler -> handler.onProcessActionUpdated(actionState));
342 public void onDeviceRemoved(String deviceIdentifier) {
343 ThingDiscoveryService discoveryService = this.discoveryService;
344 if (discoveryService != null) {
345 discoveryService.onDeviceRemoved(deviceIdentifier);
348 invokeOnThingHandlers(deviceIdentifier, handler -> handler.onDeviceRemoved());
351 private void invokeOnThingHandlers(String deviceIdentifier, Consumer<AbstractMieleThingHandler> action) {
352 getThing().getThings().stream().map(Thing::getHandler)
353 .filter(handler -> handler instanceof AbstractMieleThingHandler)
354 .map(handler -> (AbstractMieleThingHandler) handler)
355 .filter(handler -> deviceIdentifier.equals(handler.getDeviceId())).forEach(action);
359 public Collection<Class<? extends ThingHandlerService>> getServices() {
360 return Collections.singleton(ThingDiscoveryService.class);