]> git.basschouten.com Git - openhab-addons.git/blob
03d93ee28c3b84dde6feb4a9622cdd3ba63cb5cb
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mielecloud.internal.handler;
14
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;
24
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;
55
56 /**
57  * BridgeHandler implementation for the Miele cloud account.
58  *
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
62  */
63 @NonNullByDefault
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;
67
68     private final Supplier<MieleWebservice> webserviceFactory;
69
70     private final OAuthTokenRefresher tokenRefresher;
71     private final CombiningLanguageProvider languageProvider;
72     private final Logger logger = LoggerFactory.getLogger(this.getClass());
73
74     private @Nullable CompletableFuture<@Nullable Void> logoutFuture;
75     private @Nullable MieleWebservice webService;
76     private @Nullable ThingDiscoveryService discoveryService;
77
78     /**
79      * Creates a new {@link MieleBridgeHandler}.
80      *
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.
85      */
86     public MieleBridgeHandler(Bridge bridge, Function<ScheduledExecutorService, MieleWebservice> webserviceFactory,
87             OAuthTokenRefresher tokenRefresher, CombiningLanguageProvider languageProvider) {
88         super(bridge);
89         this.webserviceFactory = () -> webserviceFactory.apply(scheduler);
90         this.tokenRefresher = tokenRefresher;
91         this.languageProvider = languageProvider;
92     }
93
94     public void setDiscoveryService(@Nullable ThingDiscoveryService discoveryService) {
95         this.discoveryService = discoveryService;
96     }
97
98     /**
99      * Gets the current webservice instance for communication with the Miele service.
100      *
101      * This function may return an {@link UnavailableMieleWebservice} in case no webservice is available at the moment.
102      */
103     public MieleWebservice getWebservice() {
104         MieleWebservice webservice = webService;
105         if (webservice != null) {
106             return webservice;
107         } else {
108             return UnavailableMieleWebservice.INSTANCE;
109         }
110     }
111
112     private String getOAuthServiceHandle() {
113         return getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString();
114     }
115
116     @Override
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);
120
121         initializeWebservice();
122     }
123
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.
130             return;
131         }
132
133         try {
134             webService = webserviceFactory.get();
135         } catch (MieleWebserviceInitializationException e) {
136             logger.warn("Failed to initialize webservice.", e);
137             updateStatus(ThingStatus.OFFLINE);
138             return;
139         }
140
141         try {
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.
149             return;
150         }
151         languageProvider.setPrioritizedLanguageProvider(this);
152         tryInitializeWebservice();
153
154         MieleWebservice webservice = getWebservice();
155         webservice.addConnectionStatusListener(this);
156         webservice.addDeviceStateListener(this);
157         if (webservice.hasAccessToken()) {
158             webservice.connectSse();
159         }
160     }
161
162     @Override
163     public void handleRemoval() {
164         performLogout();
165         tokenRefresher.removeTokensFromStorage(getOAuthServiceHandle());
166         super.handleRemoval();
167     }
168
169     @Override
170     public void dispose() {
171         logger.debug("Disposing {}", this.getClass().getName());
172         disposeWebservice();
173     }
174
175     public void disposeWebservice() {
176         getWebservice().removeConnectionStatusListener(this);
177         getWebservice().removeDeviceStateListener(this);
178         getWebservice().disconnectSse();
179         languageProvider.unsetPrioritizedLanguageProvider();
180         tokenRefresher.unsetRefreshListener(getOAuthServiceHandle());
181
182         stopWebservice();
183     }
184
185     private void stopWebservice() {
186         final MieleWebservice webService = this.webService;
187         this.webService = null;
188         if (webService == null) {
189             return;
190         }
191
192         scheduler.submit(() -> {
193             CompletableFuture<@Nullable Void> logoutFuture = this.logoutFuture;
194             if (logoutFuture != null) {
195                 try {
196                     logoutFuture.get();
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);
201                 }
202             }
203
204             try {
205                 webService.close();
206             } catch (Exception e) {
207                 logger.warn("Failed to close webservice.", e);
208             }
209         });
210     }
211
212     @Override
213     public void onNewAccessToken(String accessToken) {
214         logger.debug("Setting new access token for webservice access.");
215         updateProperty(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN, accessToken);
216
217         // Without this the retry would fail causing the thing to go OFFLINE
218         getWebservice().setAccessToken(accessToken);
219
220         // If there was no access token during initialization then the SSE connection was not established.
221         getWebservice().connectSse();
222     }
223
224     @Override
225     public void handleCommand(ChannelUID channelUID, Command command) {
226     }
227
228     private void performLogout() {
229         logoutFuture = new CompletableFuture<>();
230         scheduler.execute(() -> {
231             try {
232                 getWebservice().logout();
233             } catch (Exception e) {
234                 logger.warn("Failed to logout from Miele cloud.", e);
235             }
236             Optional.ofNullable(logoutFuture).map(future -> future.complete(null));
237         });
238     }
239
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);
246             return;
247         }
248         getWebservice().setAccessToken(accessToken.get());
249         updateProperty(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN, accessToken.get());
250     }
251
252     @Override
253     public void onConnectionAlive() {
254         updateStatus(ThingStatus.ONLINE);
255     }
256
257     @Override
258     public void onConnectionError(ConnectionError connectionError, int failedReconnectionAttempts) {
259         if (connectionError == ConnectionError.AUTHORIZATION_FAILED) {
260             tryToRefreshAccessToken();
261             return;
262         }
263
264         if (failedReconnectionAttempts <= NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED
265                 && getThing().getStatus() != ThingStatus.UNKNOWN) {
266             return;
267         }
268
269         if (getThing().getStatus() == ThingStatus.UNKNOWN && connectionError == ConnectionError.REQUEST_INTERRUPTED
270                 && failedReconnectionAttempts <= NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED) {
271             return;
272         }
273
274         switch (connectionError) {
275             case AUTHORIZATION_FAILED:
276                 // Handled above.
277                 break;
278
279             case REQUEST_EXECUTION_FAILED:
280             case SERVICE_UNAVAILABLE:
281             case RESPONSE_MALFORMED:
282             case TIMEOUT:
283             case TOO_MANY_RERQUESTS:
284             case SSE_STREAM_ENDED:
285                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
286                 break;
287
288             case SERVER_ERROR:
289             case REQUEST_INTERRUPTED:
290             case OTHER_HTTP_ERROR:
291             default:
292                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
293                         I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
294                 break;
295         }
296     }
297
298     private void tryToRefreshAccessToken() {
299         try {
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);
307         }
308     }
309
310     @Override
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();
317             } else {
318                 return Optional.of(language);
319             }
320         } else {
321             return Optional.empty();
322         }
323     }
324
325     @Override
326     public void onDeviceStateUpdated(DeviceState deviceState) {
327         ThingDiscoveryService discoveryService = this.discoveryService;
328         if (discoveryService != null) {
329             discoveryService.onDeviceStateUpdated(deviceState);
330         }
331
332         invokeOnThingHandlers(deviceState.getDeviceIdentifier(), handler -> handler.onDeviceStateUpdated(deviceState));
333     }
334
335     @Override
336     public void onProcessActionUpdated(ActionsState actionState) {
337         invokeOnThingHandlers(actionState.getDeviceIdentifier(),
338                 handler -> handler.onProcessActionUpdated(actionState));
339     }
340
341     @Override
342     public void onDeviceRemoved(String deviceIdentifier) {
343         ThingDiscoveryService discoveryService = this.discoveryService;
344         if (discoveryService != null) {
345             discoveryService.onDeviceRemoved(deviceIdentifier);
346         }
347
348         invokeOnThingHandlers(deviceIdentifier, handler -> handler.onDeviceRemoved());
349     }
350
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);
356     }
357
358     @Override
359     public Collection<Class<? extends ThingHandlerService>> getServices() {
360         return Collections.singleton(ThingDiscoveryService.class);
361     }
362 }