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