]> git.basschouten.com Git - openhab-addons.git/blob
bab514ffd8252caae0f9672f6cc73d7452f7f640
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.LocaleValidator;
34 import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
35 import org.openhab.binding.mielecloud.internal.webservice.ConnectionStatusListener;
36 import org.openhab.binding.mielecloud.internal.webservice.DeviceStateListener;
37 import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
38 import org.openhab.binding.mielecloud.internal.webservice.UnavailableMieleWebservice;
39 import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
40 import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
41 import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceInitializationException;
42 import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
43 import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.Thing;
47 import org.openhab.core.thing.ThingStatus;
48 import org.openhab.core.thing.ThingStatusDetail;
49 import org.openhab.core.thing.binding.BaseBridgeHandler;
50 import org.openhab.core.thing.binding.ThingHandlerService;
51 import org.openhab.core.types.Command;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 /**
56  * BridgeHandler implementation for the Miele cloud account.
57  *
58  * @author Roland Edelhoff - Initial contribution
59  * @author Björn Lange - Introduced CombiningLanguageProvider field and interactions, added LanguageProvider super
60  *         interface, switched from polling to SSE, added support for multiple bridges, removed e-mail validation
61  */
62 @NonNullByDefault
63 public class MieleBridgeHandler extends BaseBridgeHandler
64         implements OAuthTokenRefreshListener, LanguageProvider, ConnectionStatusListener, DeviceStateListener {
65     private static final int NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED = 6;
66
67     private final Supplier<MieleWebservice> webserviceFactory;
68
69     private final OAuthTokenRefresher tokenRefresher;
70     private final CombiningLanguageProvider languageProvider;
71     private final Logger logger = LoggerFactory.getLogger(this.getClass());
72
73     private @Nullable CompletableFuture<@Nullable Void> logoutFuture;
74     private @Nullable MieleWebservice webService;
75     private @Nullable ThingDiscoveryService discoveryService;
76
77     /**
78      * Creates a new {@link MieleBridgeHandler}.
79      *
80      * @param bridge The bridge to handle.
81      * @param webserviceFactory Factory for creating {@link MieleWebservice} instances.
82      * @param tokenRefresher Token refresher.
83      * @param languageProvider Language provider.
84      */
85     public MieleBridgeHandler(Bridge bridge, Function<ScheduledExecutorService, MieleWebservice> webserviceFactory,
86             OAuthTokenRefresher tokenRefresher, CombiningLanguageProvider languageProvider) {
87         super(bridge);
88         this.webserviceFactory = () -> webserviceFactory.apply(scheduler);
89         this.tokenRefresher = tokenRefresher;
90         this.languageProvider = languageProvider;
91     }
92
93     public void setDiscoveryService(@Nullable ThingDiscoveryService discoveryService) {
94         this.discoveryService = discoveryService;
95     }
96
97     /**
98      * Gets the current webservice instance for communication with the Miele service.
99      *
100      * This function may return an {@link UnavailableMieleWebservice} in case no webservice is available at the moment.
101      */
102     public MieleWebservice getWebservice() {
103         MieleWebservice webservice = webService;
104         if (webservice != null) {
105             return webservice;
106         } else {
107             return UnavailableMieleWebservice.INSTANCE;
108         }
109     }
110
111     private String getOAuthServiceHandle() {
112         return getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString();
113     }
114
115     @Override
116     public void initialize() {
117         // It is required to set a status in this method as stated in the Javadoc of ThingHandler.initialize
118         updateStatus(ThingStatus.UNKNOWN);
119
120         initializeWebservice();
121     }
122
123     public void initializeWebservice() {
124         try {
125             webService = webserviceFactory.get();
126         } catch (MieleWebserviceInitializationException e) {
127             logger.warn("Failed to initialize webservice.", e);
128             updateStatus(ThingStatus.OFFLINE);
129             return;
130         }
131
132         try {
133             tokenRefresher.setRefreshListener(this, getOAuthServiceHandle());
134         } catch (OAuthException e) {
135             logger.debug("Could not initialize Miele Cloud bridge.", e);
136             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
137                     I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
138             // When the authorization takes place a new initialization will be triggered. Therefore, we can leave the
139             // bridge in this state.
140             return;
141         }
142         languageProvider.setPrioritizedLanguageProvider(this);
143         tryInitializeWebservice();
144
145         MieleWebservice webservice = getWebservice();
146         webservice.addConnectionStatusListener(this);
147         webservice.addDeviceStateListener(this);
148         if (webservice.hasAccessToken()) {
149             webservice.connectSse();
150         }
151     }
152
153     @Override
154     public void handleRemoval() {
155         performLogout();
156         tokenRefresher.removeTokensFromStorage(getOAuthServiceHandle());
157         super.handleRemoval();
158     }
159
160     @Override
161     public void dispose() {
162         logger.debug("Disposing {}", this.getClass().getName());
163         disposeWebservice();
164     }
165
166     public void disposeWebservice() {
167         getWebservice().removeConnectionStatusListener(this);
168         getWebservice().removeDeviceStateListener(this);
169         getWebservice().disconnectSse();
170         languageProvider.unsetPrioritizedLanguageProvider();
171         tokenRefresher.unsetRefreshListener(getOAuthServiceHandle());
172
173         stopWebservice();
174     }
175
176     private void stopWebservice() {
177         final MieleWebservice webService = this.webService;
178         this.webService = null;
179         if (webService == null) {
180             return;
181         }
182
183         scheduler.submit(() -> {
184             CompletableFuture<@Nullable Void> logoutFuture = this.logoutFuture;
185             if (logoutFuture != null) {
186                 try {
187                     logoutFuture.get();
188                 } catch (InterruptedException e) {
189                     logger.warn("Interrupted while waiting for logout!");
190                 } catch (ExecutionException e) {
191                     logger.warn("Failed to wait for logout.", e);
192                 }
193             }
194
195             try {
196                 webService.close();
197             } catch (Exception e) {
198                 logger.warn("Failed to close webservice.", e);
199             }
200         });
201     }
202
203     @Override
204     public void onNewAccessToken(String accessToken) {
205         logger.debug("Setting new access token for webservice access.");
206         updateProperty(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN, accessToken);
207
208         // Without this the retry would fail causing the thing to go OFFLINE
209         getWebservice().setAccessToken(accessToken);
210
211         // If there was no access token during initialization then the SSE connection was not established.
212         getWebservice().connectSse();
213     }
214
215     @Override
216     public void handleCommand(ChannelUID channelUID, Command command) {
217     }
218
219     private void performLogout() {
220         logoutFuture = new CompletableFuture<>();
221         scheduler.execute(() -> {
222             try {
223                 getWebservice().logout();
224             } catch (Exception e) {
225                 logger.warn("Failed to logout from Miele cloud.", e);
226             }
227             Optional.ofNullable(logoutFuture).map(future -> future.complete(null));
228         });
229     }
230
231     private void tryInitializeWebservice() {
232         Optional<String> accessToken = tokenRefresher.getAccessTokenFromStorage(getOAuthServiceHandle());
233         if (!accessToken.isPresent()) {
234             logger.debug("No OAuth2 access token available. Retrying later.");
235             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
236                     I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED);
237             return;
238         }
239         getWebservice().setAccessToken(accessToken.get());
240         updateProperty(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN, accessToken.get());
241     }
242
243     @Override
244     public void onConnectionAlive() {
245         updateStatus(ThingStatus.ONLINE);
246     }
247
248     @Override
249     public void onConnectionError(ConnectionError connectionError, int failedReconnectionAttempts) {
250         if (connectionError == ConnectionError.AUTHORIZATION_FAILED) {
251             tryToRefreshAccessToken();
252             return;
253         }
254
255         if (failedReconnectionAttempts <= NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED
256                 && getThing().getStatus() != ThingStatus.UNKNOWN) {
257             return;
258         }
259
260         if (getThing().getStatus() == ThingStatus.UNKNOWN && connectionError == ConnectionError.REQUEST_INTERRUPTED
261                 && failedReconnectionAttempts <= NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED) {
262             return;
263         }
264
265         switch (connectionError) {
266             case AUTHORIZATION_FAILED:
267                 // Handled above.
268                 break;
269
270             case REQUEST_EXECUTION_FAILED:
271             case SERVICE_UNAVAILABLE:
272             case RESPONSE_MALFORMED:
273             case TIMEOUT:
274             case TOO_MANY_RERQUESTS:
275             case SSE_STREAM_ENDED:
276                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
277                 break;
278
279             case SERVER_ERROR:
280             case REQUEST_INTERRUPTED:
281             case OTHER_HTTP_ERROR:
282             default:
283                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
284                         I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
285                 break;
286         }
287     }
288
289     private void tryToRefreshAccessToken() {
290         try {
291             tokenRefresher.refreshToken(getOAuthServiceHandle());
292             getWebservice().connectSse();
293         } catch (OAuthException e) {
294             logger.debug("Failed to refresh OAuth token!", e);
295             getWebservice().disconnectSse();
296             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
297                     I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
298         }
299     }
300
301     @Override
302     public Optional<String> getLanguage() {
303         Object languageObject = thing.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE);
304         if (languageObject instanceof String) {
305             String language = (String) languageObject;
306             if (language.isEmpty() || !LocaleValidator.isValidLanguage(language)) {
307                 return Optional.empty();
308             } else {
309                 return Optional.of(language);
310             }
311         } else {
312             return Optional.empty();
313         }
314     }
315
316     @Override
317     public void onDeviceStateUpdated(DeviceState deviceState) {
318         ThingDiscoveryService discoveryService = this.discoveryService;
319         if (discoveryService != null) {
320             discoveryService.onDeviceStateUpdated(deviceState);
321         }
322
323         invokeOnThingHandlers(deviceState.getDeviceIdentifier(), handler -> handler.onDeviceStateUpdated(deviceState));
324     }
325
326     @Override
327     public void onProcessActionUpdated(ActionsState actionState) {
328         invokeOnThingHandlers(actionState.getDeviceIdentifier(),
329                 handler -> handler.onProcessActionUpdated(actionState));
330     }
331
332     @Override
333     public void onDeviceRemoved(String deviceIdentifier) {
334         ThingDiscoveryService discoveryService = this.discoveryService;
335         if (discoveryService != null) {
336             discoveryService.onDeviceRemoved(deviceIdentifier);
337         }
338
339         invokeOnThingHandlers(deviceIdentifier, handler -> handler.onDeviceRemoved());
340     }
341
342     private void invokeOnThingHandlers(String deviceIdentifier, Consumer<AbstractMieleThingHandler> action) {
343         getThing().getThings().stream().map(Thing::getHandler)
344                 .filter(handler -> handler instanceof AbstractMieleThingHandler)
345                 .map(handler -> (AbstractMieleThingHandler) handler)
346                 .filter(handler -> deviceIdentifier.equals(handler.getDeviceId())).forEach(action);
347     }
348
349     @Override
350     public Collection<Class<? extends ThingHandlerService>> getServices() {
351         return Collections.singleton(ThingDiscoveryService.class);
352     }
353 }