]> git.basschouten.com Git - openhab-addons.git/blob
db483b3b1fdc4f9ad105031feeb66ade9b031ff2
[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.homeconnect.internal.handler;
14
15 import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
16
17 import java.io.IOException;
18 import java.time.ZonedDateTime;
19 import java.time.format.DateTimeFormatter;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Map.Entry;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.stream.Collectors;
29
30 import javax.ws.rs.client.ClientBuilder;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.eclipse.jetty.client.HttpClient;
35 import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
36 import org.openhab.binding.homeconnect.internal.client.HomeConnectEventSourceClient;
37 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
38 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
39 import org.openhab.binding.homeconnect.internal.client.model.ApiRequest;
40 import org.openhab.binding.homeconnect.internal.client.model.Event;
41 import org.openhab.binding.homeconnect.internal.configuration.ApiBridgeConfiguration;
42 import org.openhab.binding.homeconnect.internal.discovery.HomeConnectDiscoveryService;
43 import org.openhab.binding.homeconnect.internal.servlet.HomeConnectServlet;
44 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
45 import org.openhab.core.auth.client.oauth2.OAuthClientService;
46 import org.openhab.core.auth.client.oauth2.OAuthException;
47 import org.openhab.core.auth.client.oauth2.OAuthFactory;
48 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
49 import org.openhab.core.config.core.Configuration;
50 import org.openhab.core.thing.Bridge;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.ThingStatus;
53 import org.openhab.core.thing.ThingStatusDetail;
54 import org.openhab.core.thing.binding.BaseBridgeHandler;
55 import org.openhab.core.thing.binding.ThingHandlerCallback;
56 import org.openhab.core.thing.binding.ThingHandlerService;
57 import org.openhab.core.types.Command;
58 import org.osgi.service.jaxrs.client.SseEventSourceFactory;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 /**
63  * The {@link HomeConnectBridgeHandler} is responsible for handling commands, which are
64  * sent to one of the channels.
65  *
66  * @author Jonas BrĂ¼stel - Initial contribution
67  */
68 @NonNullByDefault
69 public class HomeConnectBridgeHandler extends BaseBridgeHandler {
70
71     private static final int REINITIALIZATION_DELAY_SEC = 120;
72     private static final String CLIENT_SECRET = "clientSecret";
73     private static final String CLIENT_ID = "clientId";
74
75     private final HttpClient httpClient;
76     private final ClientBuilder clientBuilder;
77     private final SseEventSourceFactory eventSourceFactory;
78     private final OAuthFactory oAuthFactory;
79     private final HomeConnectServlet homeConnectServlet;
80     private final Logger logger = LoggerFactory.getLogger(HomeConnectBridgeHandler.class);
81
82     private @Nullable ScheduledFuture<?> reinitializationFuture;
83     private @Nullable List<ApiRequest> apiRequestHistory;
84     private @Nullable List<Event> eventHistory;
85
86     private @NonNullByDefault({}) OAuthClientService oAuthClientService;
87     private @Nullable String oAuthServiceHandleId;
88     private @NonNullByDefault({}) HomeConnectApiClient apiClient;
89     private @NonNullByDefault({}) HomeConnectEventSourceClient eventSourceClient;
90
91     public HomeConnectBridgeHandler(Bridge bridge, HttpClient httpClient, ClientBuilder clientBuilder,
92             SseEventSourceFactory eventSourceFactory, OAuthFactory oAuthFactory,
93             HomeConnectServlet homeConnectServlet) {
94         super(bridge);
95
96         this.httpClient = httpClient;
97         this.clientBuilder = clientBuilder;
98         this.eventSourceFactory = eventSourceFactory;
99         this.oAuthFactory = oAuthFactory;
100         this.homeConnectServlet = homeConnectServlet;
101     }
102
103     @Override
104     public void handleCommand(ChannelUID channelUID, Command command) {
105         // not used for bridge
106     }
107
108     @Override
109     public void initialize() {
110         // let the bridge configuration servlet know about this handler
111         homeConnectServlet.addBridgeHandler(this);
112
113         // create oAuth service
114         ApiBridgeConfiguration config = getConfiguration();
115         String tokenUrl = (config.isSimulator() ? API_SIMULATOR_BASE_URL : API_BASE_URL) + OAUTH_TOKEN_PATH;
116         String authorizeUrl = (config.isSimulator() ? API_SIMULATOR_BASE_URL : API_BASE_URL) + OAUTH_AUTHORIZE_PATH;
117         String oAuthServiceHandleId = thing.getUID().getAsString() + (config.isSimulator() ? "simulator" : "");
118
119         oAuthClientService = oAuthFactory.createOAuthClientService(oAuthServiceHandleId, tokenUrl, authorizeUrl,
120                 config.getClientId(), config.getClientSecret(), OAUTH_SCOPE, true);
121         this.oAuthServiceHandleId = oAuthServiceHandleId;
122         logger.debug(
123                 "Initialize oAuth client service. tokenUrl={}, authorizeUrl={}, oAuthServiceHandleId={}, scope={}, oAuthClientService={}",
124                 tokenUrl, authorizeUrl, oAuthServiceHandleId, OAUTH_SCOPE, oAuthClientService);
125
126         // create api client
127         apiClient = new HomeConnectApiClient(httpClient, oAuthClientService, config.isSimulator(), apiRequestHistory,
128                 config);
129         eventSourceClient = new HomeConnectEventSourceClient(clientBuilder, eventSourceFactory, oAuthClientService,
130                 config.isSimulator(), scheduler, eventHistory);
131
132         updateStatus(ThingStatus.UNKNOWN);
133
134         scheduler.submit(() -> {
135             try {
136                 @Nullable
137                 AccessTokenResponse accessTokenResponse = oAuthClientService.getAccessTokenResponse();
138
139                 if (accessTokenResponse == null) {
140                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
141                             "Please authenticate your account at http(s)://[YOUROPENHAB]:[YOURPORT]/homeconnect (e.g. http://192.168.178.100:8080/homeconnect).");
142                 } else {
143                     apiClient.getHomeAppliances();
144                     updateStatus(ThingStatus.ONLINE);
145                 }
146             } catch (OAuthException | IOException | OAuthResponseException | CommunicationException
147                     | AuthorizationException e) {
148                 ZonedDateTime nextReinitializeDateTime = ZonedDateTime.now().plusSeconds(REINITIALIZATION_DELAY_SEC);
149
150                 String offlineMessage = String.format(
151                         "Home Connect service is not reachable or a problem occurred! Retrying at %s (%s). bridge=%s",
152                         nextReinitializeDateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME), e.getMessage(),
153                         getThing().getLabel());
154                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, offlineMessage);
155
156                 scheduleReinitialize();
157             }
158         });
159     }
160
161     @Override
162     public void dispose() {
163         stopReinitializer();
164         cleanup(true);
165     }
166
167     public void reinitialize() {
168         logger.debug("Reinitialize bridge {}", getThing().getLabel());
169         stopReinitializer();
170         cleanup(false);
171         initialize();
172     }
173
174     @Override
175     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
176         if (isModifyingCurrentConfig(configurationParameters)) {
177             List<String> parameters = configurationParameters.entrySet().stream().map((entry) -> {
178                 if (CLIENT_ID.equals(entry.getKey()) || CLIENT_SECRET.equals(entry.getKey())) {
179                     return entry.getKey() + ": ***";
180                 }
181                 return entry.getKey() + ": " + entry.getValue();
182             }).collect(Collectors.toList());
183
184             logger.debug("Update bridge configuration. bridge={}, parameters={}", getThing().getLabel(), parameters);
185
186             validateConfigurationParameters(configurationParameters);
187             Configuration configuration = editConfiguration();
188             for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
189                 configuration.put(configurationParameter.getKey(), configurationParameter.getValue());
190             }
191
192             // invalidate oAuth credentials
193             try {
194                 logger.debug("Clear oAuth credential store. bridge={}", getThing().getLabel());
195                 var oAuthClientService = this.oAuthClientService;
196                 if (oAuthClientService != null) {
197                     oAuthClientService.remove();
198                 }
199             } catch (OAuthException e) {
200                 logger.error("Could not clear oAuth credentials. bridge={}", getThing().getLabel(), e);
201             }
202
203             if (isInitialized()) {
204                 // persist new configuration and reinitialize handler
205                 dispose();
206                 updateConfiguration(configuration);
207                 initialize();
208             } else {
209                 // persist new configuration and notify Thing Manager
210                 updateConfiguration(configuration);
211                 @Nullable
212                 ThingHandlerCallback callback = getCallback();
213                 if (callback != null) {
214                     callback.configurationUpdated(this.getThing());
215                 } else {
216                     logger.warn(
217                             "Handler {} tried updating its configuration although the handler was already disposed.",
218                             this.getClass().getSimpleName());
219                 }
220             }
221         }
222     }
223
224     @Override
225     public void handleRemoval() {
226         String handleId = this.oAuthServiceHandleId;
227         if (handleId != null) {
228             oAuthFactory.deleteServiceAndAccessToken(handleId);
229         }
230         super.handleRemoval();
231     }
232
233     @Override
234     public Collection<Class<? extends ThingHandlerService>> getServices() {
235         return Collections.singleton(HomeConnectDiscoveryService.class);
236     }
237
238     /**
239      * Get {@link HomeConnectApiClient}.
240      *
241      * @return api client instance
242      */
243     public HomeConnectApiClient getApiClient() {
244         return apiClient;
245     }
246
247     /**
248      * Get {@link HomeConnectEventSourceClient}.
249      *
250      * @return event source client instance
251      */
252     public HomeConnectEventSourceClient getEventSourceClient() {
253         return eventSourceClient;
254     }
255
256     /**
257      * Get children of bridge (disabled things are ignored)
258      *
259      * @return list of child handlers
260      */
261     public List<AbstractHomeConnectThingHandler> getThingHandler() {
262         return getThing().getThings().stream()
263                 .filter(thing -> thing.isEnabled() && thing.getHandler() instanceof AbstractHomeConnectThingHandler)
264                 .map(thing -> (AbstractHomeConnectThingHandler) thing.getHandler()).collect(Collectors.toList());
265     }
266
267     /**
268      * Get {@link ApiBridgeConfiguration}.
269      *
270      * @return bridge configuration (clientId, clientSecret, etc.)
271      */
272     public ApiBridgeConfiguration getConfiguration() {
273         return getConfigAs(ApiBridgeConfiguration.class);
274     }
275
276     /**
277      * Get {@link OAuthClientService} instance.
278      *
279      * @return oAuth client service instance
280      */
281     public OAuthClientService getOAuthClientService() {
282         return oAuthClientService;
283     }
284
285     private void cleanup(boolean immediate) {
286         ArrayList<ApiRequest> apiRequestHistory = new ArrayList<>();
287         apiRequestHistory.addAll(apiClient.getLatestApiRequests());
288         this.apiRequestHistory = apiRequestHistory;
289         apiClient.getLatestApiRequests().clear();
290
291         ArrayList<Event> eventHistory = new ArrayList<>();
292         eventHistory.addAll(eventSourceClient.getLatestEvents());
293         this.eventHistory = eventHistory;
294         eventSourceClient.getLatestEvents().clear();
295         eventSourceClient.dispose(immediate);
296
297         String handleId = this.oAuthServiceHandleId;
298         if (handleId != null) {
299             oAuthFactory.ungetOAuthService(handleId);
300         }
301         homeConnectServlet.removeBridgeHandler(this);
302     }
303
304     private synchronized void scheduleReinitialize() {
305         @Nullable
306         ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture;
307         if (reinitializationFuture != null && !reinitializationFuture.isDone()) {
308             logger.debug("Reinitialization is already scheduled. Starting in {} seconds. bridge={}",
309                     reinitializationFuture.getDelay(TimeUnit.SECONDS), getThing().getLabel());
310         } else {
311             this.reinitializationFuture = scheduler.schedule(() -> {
312                 cleanup(false);
313                 initialize();
314             }, HomeConnectBridgeHandler.REINITIALIZATION_DELAY_SEC, TimeUnit.SECONDS);
315         }
316     }
317
318     private synchronized void stopReinitializer() {
319         @Nullable
320         ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture;
321         if (reinitializationFuture != null) {
322             reinitializationFuture.cancel(true);
323             this.reinitializationFuture = null;
324         }
325     }
326 }