2 * Copyright (c) 2010-2023 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 org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
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.List;
24 import java.util.Map.Entry;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.stream.Collectors;
30 import javax.ws.rs.client.ClientBuilder;
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;
63 * The {@link HomeConnectBridgeHandler} is responsible for handling commands, which are
64 * sent to one of the channels.
66 * @author Jonas BrĂ¼stel - Initial contribution
69 public class HomeConnectBridgeHandler extends BaseBridgeHandler {
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";
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);
82 private @Nullable ScheduledFuture<?> reinitializationFuture;
83 private @Nullable List<ApiRequest> apiRequestHistory;
84 private @Nullable List<Event> eventHistory;
86 private @NonNullByDefault({}) OAuthClientService oAuthClientService;
87 private @Nullable String oAuthServiceHandleId;
88 private @NonNullByDefault({}) HomeConnectApiClient apiClient;
89 private @NonNullByDefault({}) HomeConnectEventSourceClient eventSourceClient;
91 public HomeConnectBridgeHandler(Bridge bridge, HttpClient httpClient, ClientBuilder clientBuilder,
92 SseEventSourceFactory eventSourceFactory, OAuthFactory oAuthFactory,
93 HomeConnectServlet homeConnectServlet) {
96 this.httpClient = httpClient;
97 this.clientBuilder = clientBuilder;
98 this.eventSourceFactory = eventSourceFactory;
99 this.oAuthFactory = oAuthFactory;
100 this.homeConnectServlet = homeConnectServlet;
104 public void handleCommand(ChannelUID channelUID, Command command) {
105 // not used for bridge
109 public void initialize() {
110 // let the bridge configuration servlet know about this handler
111 homeConnectServlet.addBridgeHandler(this);
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" : "");
119 oAuthClientService = oAuthFactory.createOAuthClientService(oAuthServiceHandleId, tokenUrl, authorizeUrl,
120 config.getClientId(), config.getClientSecret(), OAUTH_SCOPE, true);
121 this.oAuthServiceHandleId = oAuthServiceHandleId;
123 "Initialize oAuth client service. tokenUrl={}, authorizeUrl={}, oAuthServiceHandleId={}, scope={}, oAuthClientService={}",
124 tokenUrl, authorizeUrl, oAuthServiceHandleId, OAUTH_SCOPE, oAuthClientService);
127 apiClient = new HomeConnectApiClient(httpClient, oAuthClientService, config.isSimulator(), apiRequestHistory,
129 eventSourceClient = new HomeConnectEventSourceClient(clientBuilder, eventSourceFactory, oAuthClientService,
130 config.isSimulator(), scheduler, eventHistory);
132 updateStatus(ThingStatus.UNKNOWN);
134 scheduler.submit(() -> {
137 AccessTokenResponse accessTokenResponse = oAuthClientService.getAccessTokenResponse();
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).");
143 apiClient.getHomeAppliances();
144 updateStatus(ThingStatus.ONLINE);
146 } catch (OAuthException | IOException | OAuthResponseException | CommunicationException
147 | AuthorizationException e) {
148 ZonedDateTime nextReinitializeDateTime = ZonedDateTime.now().plusSeconds(REINITIALIZATION_DELAY_SEC);
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);
156 scheduleReinitialize();
162 public void dispose() {
167 public void reinitialize() {
168 logger.debug("Reinitialize bridge {}", getThing().getLabel());
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() + ": ***";
181 return entry.getKey() + ": " + entry.getValue();
182 }).collect(Collectors.toList());
184 logger.debug("Update bridge configuration. bridge={}, parameters={}", getThing().getLabel(), parameters);
186 validateConfigurationParameters(configurationParameters);
187 Configuration configuration = editConfiguration();
188 for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
189 configuration.put(configurationParameter.getKey(), configurationParameter.getValue());
192 // invalidate oAuth credentials
194 logger.debug("Clear oAuth credential store. bridge={}", getThing().getLabel());
195 var oAuthClientService = this.oAuthClientService;
196 if (oAuthClientService != null) {
197 oAuthClientService.remove();
199 } catch (OAuthException e) {
200 logger.error("Could not clear oAuth credentials. bridge={}", getThing().getLabel(), e);
203 if (isInitialized()) {
204 // persist new configuration and reinitialize handler
206 updateConfiguration(configuration);
209 // persist new configuration and notify Thing Manager
210 updateConfiguration(configuration);
212 ThingHandlerCallback callback = getCallback();
213 if (callback != null) {
214 callback.configurationUpdated(this.getThing());
217 "Handler {} tried updating its configuration although the handler was already disposed.",
218 this.getClass().getSimpleName());
225 public void handleRemoval() {
226 String handleId = this.oAuthServiceHandleId;
227 if (handleId != null) {
228 oAuthFactory.deleteServiceAndAccessToken(handleId);
230 super.handleRemoval();
234 public Collection<Class<? extends ThingHandlerService>> getServices() {
235 return Set.of(HomeConnectDiscoveryService.class);
239 * Get {@link HomeConnectApiClient}.
241 * @return api client instance
243 public HomeConnectApiClient getApiClient() {
248 * Get {@link HomeConnectEventSourceClient}.
250 * @return event source client instance
252 public HomeConnectEventSourceClient getEventSourceClient() {
253 return eventSourceClient;
257 * Get children of bridge (disabled things are ignored)
259 * @return list of child handlers
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());
268 * Get {@link ApiBridgeConfiguration}.
270 * @return bridge configuration (clientId, clientSecret, etc.)
272 public ApiBridgeConfiguration getConfiguration() {
273 return getConfigAs(ApiBridgeConfiguration.class);
277 * Get {@link OAuthClientService} instance.
279 * @return oAuth client service instance
281 public OAuthClientService getOAuthClientService() {
282 return oAuthClientService;
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();
291 ArrayList<Event> eventHistory = new ArrayList<>();
292 eventHistory.addAll(eventSourceClient.getLatestEvents());
293 this.eventHistory = eventHistory;
294 eventSourceClient.getLatestEvents().clear();
295 eventSourceClient.dispose(immediate);
297 String handleId = this.oAuthServiceHandleId;
298 if (handleId != null) {
299 oAuthFactory.ungetOAuthService(handleId);
301 homeConnectServlet.removeBridgeHandler(this);
304 private synchronized void scheduleReinitialize() {
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());
311 this.reinitializationFuture = scheduler.schedule(() -> {
314 }, HomeConnectBridgeHandler.REINITIALIZATION_DELAY_SEC, TimeUnit.SECONDS);
318 private synchronized void stopReinitializer() {
320 ScheduledFuture<?> reinitializationFuture = this.reinitializationFuture;
321 if (reinitializationFuture != null) {
322 reinitializationFuture.cancel(true);
323 this.reinitializationFuture = null;