]> git.basschouten.com Git - openhab-addons.git/blob
702c7820cfcf9c1e067cef476650af69ef8a9d39
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.netatmo.internal.handler;
14
15 import java.io.ByteArrayInputStream;
16 import java.io.InputStream;
17 import java.lang.reflect.Constructor;
18 import java.net.URI;
19 import java.nio.charset.StandardCharsets;
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.Objects;
24 import java.util.Optional;
25 import java.util.Set;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.TimeoutException;
30
31 import javax.ws.rs.core.UriBuilder;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.eclipse.jetty.client.HttpClient;
36 import org.eclipse.jetty.client.api.ContentResponse;
37 import org.eclipse.jetty.client.api.Request;
38 import org.eclipse.jetty.client.util.InputStreamContentProvider;
39 import org.eclipse.jetty.http.HttpHeader;
40 import org.eclipse.jetty.http.HttpMethod;
41 import org.eclipse.jetty.http.HttpStatus;
42 import org.eclipse.jetty.http.HttpStatus.Code;
43 import org.openhab.binding.netatmo.internal.api.ApiError;
44 import org.openhab.binding.netatmo.internal.api.AuthenticationApi;
45 import org.openhab.binding.netatmo.internal.api.NetatmoException;
46 import org.openhab.binding.netatmo.internal.api.RestManager;
47 import org.openhab.binding.netatmo.internal.api.SecurityApi;
48 import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope;
49 import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.ServiceError;
50 import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration;
51 import org.openhab.binding.netatmo.internal.config.BindingConfiguration;
52 import org.openhab.binding.netatmo.internal.config.ConfigurationLevel;
53 import org.openhab.binding.netatmo.internal.deserialization.NADeserializer;
54 import org.openhab.binding.netatmo.internal.discovery.NetatmoDiscoveryService;
55 import org.openhab.binding.netatmo.internal.servlet.GrantServlet;
56 import org.openhab.binding.netatmo.internal.servlet.WebhookServlet;
57 import org.openhab.core.config.core.Configuration;
58 import org.openhab.core.thing.Bridge;
59 import org.openhab.core.thing.ChannelUID;
60 import org.openhab.core.thing.Thing;
61 import org.openhab.core.thing.ThingStatus;
62 import org.openhab.core.thing.ThingStatusDetail;
63 import org.openhab.core.thing.binding.BaseBridgeHandler;
64 import org.openhab.core.thing.binding.ThingHandlerService;
65 import org.openhab.core.types.Command;
66 import org.osgi.service.http.HttpService;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
69
70 /**
71  * {@link ApiBridgeHandler} is the handler for a Netatmo API and connects it to the framework.
72  *
73  * @author GaĆ«l L'hopital - Initial contribution
74  *
75  */
76 @NonNullByDefault
77 public class ApiBridgeHandler extends BaseBridgeHandler {
78     private static final int TIMEOUT_S = 20;
79
80     private final Logger logger = LoggerFactory.getLogger(ApiBridgeHandler.class);
81     private final BindingConfiguration bindingConf;
82     private final AuthenticationApi connectApi;
83     private final HttpClient httpClient;
84     private final NADeserializer deserializer;
85     private final HttpService httpService;
86
87     private Optional<ScheduledFuture<?>> connectJob = Optional.empty();
88     private Map<Class<? extends RestManager>, RestManager> managers = new HashMap<>();
89     private @Nullable WebhookServlet webHookServlet;
90     private @Nullable GrantServlet grantServlet;
91
92     public ApiBridgeHandler(Bridge bridge, HttpClient httpClient, NADeserializer deserializer,
93             BindingConfiguration configuration, HttpService httpService) {
94         super(bridge);
95         this.bindingConf = configuration;
96         this.connectApi = new AuthenticationApi(this, scheduler);
97         this.httpClient = httpClient;
98         this.deserializer = deserializer;
99         this.httpService = httpService;
100     }
101
102     @Override
103     public void initialize() {
104         logger.debug("Initializing Netatmo API bridge handler.");
105         updateStatus(ThingStatus.UNKNOWN);
106         GrantServlet servlet = new GrantServlet(this, httpService);
107         servlet.startListening();
108         this.grantServlet = servlet;
109         scheduler.execute(() -> openConnection(null, null));
110     }
111
112     public void openConnection(@Nullable String code, @Nullable String redirectUri) {
113         ApiHandlerConfiguration configuration = getConfiguration();
114         ConfigurationLevel level = configuration.check();
115         switch (level) {
116             case EMPTY_CLIENT_ID:
117             case EMPTY_CLIENT_SECRET:
118                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, level.message);
119                 break;
120             case REFRESH_TOKEN_NEEDED:
121                 if (code == null || redirectUri == null) {
122                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, level.message);
123                     break;
124                 } // else we can proceed to get the token refresh
125             case COMPLETED:
126                 try {
127                     logger.debug("Connecting to Netatmo API.");
128
129                     String refreshToken = connectApi.authorize(configuration, code, redirectUri);
130
131                     if (configuration.refreshToken.isBlank()) {
132                         Configuration thingConfig = editConfiguration();
133                         thingConfig.put(ApiHandlerConfiguration.REFRESH_TOKEN, refreshToken);
134                         updateConfiguration(thingConfig);
135                         configuration = getConfiguration();
136                     }
137
138                     if (!configuration.webHookUrl.isBlank()) {
139                         SecurityApi securityApi = getRestManager(SecurityApi.class);
140                         if (securityApi != null) {
141                             WebhookServlet servlet = new WebhookServlet(this, httpService, deserializer, securityApi,
142                                     configuration.webHookUrl);
143                             servlet.startListening();
144                             this.webHookServlet = servlet;
145                         }
146                     }
147
148                     updateStatus(ThingStatus.ONLINE);
149
150                     getThing().getThings().stream().filter(Thing::isEnabled).map(Thing::getHandler)
151                             .filter(Objects::nonNull).map(CommonInterface.class::cast)
152                             .forEach(CommonInterface::expireData);
153
154                 } catch (NetatmoException e) {
155                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
156                     prepareReconnection(code, redirectUri);
157                 }
158                 break;
159         }
160     }
161
162     public ApiHandlerConfiguration getConfiguration() {
163         return getConfigAs(ApiHandlerConfiguration.class);
164     }
165
166     private void prepareReconnection(@Nullable String code, @Nullable String redirectUri) {
167         connectApi.disconnect();
168         freeConnectJob();
169         connectJob = Optional.of(scheduler.schedule(() -> openConnection(code, redirectUri),
170                 getConfiguration().reconnectInterval, TimeUnit.SECONDS));
171     }
172
173     private void freeConnectJob() {
174         connectJob.ifPresent(j -> j.cancel(true));
175         connectJob = Optional.empty();
176     }
177
178     @Override
179     public void dispose() {
180         logger.debug("Shutting down Netatmo API bridge handler.");
181         WebhookServlet localWebHook = this.webHookServlet;
182         if (localWebHook != null) {
183             localWebHook.dispose();
184         }
185         GrantServlet localGrant = this.grantServlet;
186         if (localGrant != null) {
187             localGrant.dispose();
188         }
189         connectApi.dispose();
190         freeConnectJob();
191         super.dispose();
192     }
193
194     @Override
195     public void handleCommand(ChannelUID channelUID, Command command) {
196         logger.debug("Netatmo Bridge is read-only and does not handle commands");
197     }
198
199     @SuppressWarnings("unchecked")
200     public <T extends RestManager> @Nullable T getRestManager(Class<T> clazz) {
201         if (!managers.containsKey(clazz)) {
202             try {
203                 Constructor<T> constructor = clazz.getConstructor(getClass());
204                 T instance = constructor.newInstance(this);
205                 Set<Scope> expected = instance.getRequiredScopes();
206                 if (connectApi.matchesScopes(expected)) {
207                     managers.put(clazz, instance);
208                 } else {
209                     logger.info("Unable to instantiate {}, expected scope {} is not active", clazz, expected);
210                 }
211             } catch (SecurityException | ReflectiveOperationException e) {
212                 logger.warn("Error invoking RestManager constructor for class {} : {}", clazz, e.getMessage());
213             }
214         }
215         return (T) managers.get(clazz);
216     }
217
218     public synchronized <T> T executeUri(URI uri, HttpMethod method, Class<T> clazz, @Nullable String payload,
219             @Nullable String contentType, int retryCount) throws NetatmoException {
220         try {
221             logger.trace("executeUri {}  {} ", method.toString(), uri);
222
223             Request request = httpClient.newRequest(uri).method(method).timeout(TIMEOUT_S, TimeUnit.SECONDS);
224
225             String auth = connectApi.getAuthorization();
226             if (auth != null) {
227                 request.header(HttpHeader.AUTHORIZATION, auth);
228             }
229
230             if (payload != null && contentType != null
231                     && (HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method))) {
232                 InputStream stream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));
233                 try (InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(stream)) {
234                     request.content(inputStreamContentProvider, contentType);
235                 }
236             }
237
238             ContentResponse response = request.send();
239
240             Code statusCode = HttpStatus.getCode(response.getStatus());
241             String responseBody = new String(response.getContent(), StandardCharsets.UTF_8);
242             logger.trace("executeUri returned : code {} body {}", statusCode, responseBody);
243
244             if (statusCode != Code.OK) {
245                 ApiError error = deserializer.deserialize(ApiError.class, responseBody);
246                 throw new NetatmoException(error);
247             }
248             return deserializer.deserialize(clazz, responseBody);
249         } catch (NetatmoException e) {
250             if (e.getStatusCode() == ServiceError.MAXIMUM_USAGE_REACHED) {
251                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
252                 prepareReconnection(null, null);
253             }
254             throw e;
255         } catch (InterruptedException e) {
256             Thread.currentThread().interrupt();
257             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
258             throw new NetatmoException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
259         } catch (TimeoutException | ExecutionException e) {
260             if (retryCount > 0) {
261                 logger.debug("Request timedout, retry counter : {}", retryCount);
262                 return executeUri(uri, method, clazz, payload, contentType, retryCount - 1);
263             }
264             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/request-time-out");
265             prepareReconnection(null, null);
266             throw new NetatmoException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
267         }
268     }
269
270     public boolean getReadFriends() {
271         return bindingConf.readFriends;
272     }
273
274     public boolean isConnected() {
275         return connectApi.isConnected();
276     }
277
278     public String getId() {
279         return (String) getThing().getConfiguration().get(ApiHandlerConfiguration.CLIENT_ID);
280     }
281
282     public UriBuilder formatAuthorizationUrl() {
283         return AuthenticationApi.getAuthorizationBuilder(getId());
284     }
285
286     @Override
287     public Collection<Class<? extends ThingHandlerService>> getServices() {
288         return Set.of(NetatmoDiscoveryService.class);
289     }
290
291     public Optional<WebhookServlet> getWebHookServlet() {
292         return Optional.ofNullable(webHookServlet);
293     }
294 }