]> git.basschouten.com Git - openhab-addons.git/blob
e0d7fe5c1a6d341d6fda0759c7864c3122d17f5e
[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 org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.eclipse.jetty.client.HttpClient;
34 import org.eclipse.jetty.client.api.ContentResponse;
35 import org.eclipse.jetty.client.api.Request;
36 import org.eclipse.jetty.client.util.InputStreamContentProvider;
37 import org.eclipse.jetty.http.HttpHeader;
38 import org.eclipse.jetty.http.HttpMethod;
39 import org.eclipse.jetty.http.HttpStatus;
40 import org.eclipse.jetty.http.HttpStatus.Code;
41 import org.openhab.binding.netatmo.internal.api.ApiError;
42 import org.openhab.binding.netatmo.internal.api.AuthenticationApi;
43 import org.openhab.binding.netatmo.internal.api.NetatmoException;
44 import org.openhab.binding.netatmo.internal.api.RestManager;
45 import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope;
46 import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration;
47 import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration.Credentials;
48 import org.openhab.binding.netatmo.internal.config.BindingConfiguration;
49 import org.openhab.binding.netatmo.internal.deserialization.NADeserializer;
50 import org.openhab.binding.netatmo.internal.discovery.NetatmoDiscoveryService;
51 import org.openhab.binding.netatmo.internal.webhook.NetatmoServlet;
52 import org.openhab.core.thing.Bridge;
53 import org.openhab.core.thing.ChannelUID;
54 import org.openhab.core.thing.Thing;
55 import org.openhab.core.thing.ThingStatus;
56 import org.openhab.core.thing.ThingStatusDetail;
57 import org.openhab.core.thing.binding.BaseBridgeHandler;
58 import org.openhab.core.thing.binding.ThingHandlerService;
59 import org.openhab.core.types.Command;
60 import org.osgi.service.http.HttpService;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64 /**
65  * {@link ApiBridgeHandler} is the handler for a Netatmo API and connects it to the framework.
66  *
67  * @author GaĆ«l L'hopital - Initial contribution
68  *
69  */
70 @NonNullByDefault
71 public class ApiBridgeHandler extends BaseBridgeHandler {
72     private static final int TIMEOUT_S = 20;
73
74     private final Logger logger = LoggerFactory.getLogger(ApiBridgeHandler.class);
75     private final BindingConfiguration bindingConf;
76     private final HttpService httpService;
77     private final AuthenticationApi connectApi;
78     private final HttpClient httpClient;
79     private final NADeserializer deserializer;
80
81     private Optional<ScheduledFuture<?>> connectJob = Optional.empty();
82     private Optional<NetatmoServlet> servlet = Optional.empty();
83     private @NonNullByDefault({}) ApiHandlerConfiguration thingConf;
84
85     private Map<Class<? extends RestManager>, RestManager> managers = new HashMap<>();
86
87     public ApiBridgeHandler(Bridge bridge, HttpClient httpClient, HttpService httpService, NADeserializer deserializer,
88             BindingConfiguration configuration) {
89         super(bridge);
90         this.bindingConf = configuration;
91         this.httpService = httpService;
92         this.connectApi = new AuthenticationApi(this, scheduler);
93         this.httpClient = httpClient;
94         this.deserializer = deserializer;
95     }
96
97     @Override
98     public void initialize() {
99         logger.debug("Initializing Netatmo API bridge handler.");
100         thingConf = getConfigAs(ApiHandlerConfiguration.class);
101         updateStatus(ThingStatus.UNKNOWN);
102         scheduler.execute(() -> {
103             openConnection();
104             String webHookUrl = thingConf.webHookUrl;
105             if (webHookUrl != null && !webHookUrl.isBlank()) {
106                 servlet = Optional.of(new NetatmoServlet(httpService, this, webHookUrl));
107             }
108         });
109     }
110
111     private void openConnection() {
112         try {
113             Credentials credentials = thingConf.getCredentials();
114             logger.debug("Connecting to Netatmo API.");
115             try {
116                 connectApi.authenticate(credentials, bindingConf.features);
117                 updateStatus(ThingStatus.ONLINE);
118                 getThing().getThings().stream().filter(Thing::isEnabled).map(Thing::getHandler).filter(Objects::nonNull)
119                         .map(CommonInterface.class::cast).forEach(CommonInterface::expireData);
120             } catch (NetatmoException e) {
121                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
122                 prepareReconnection();
123             }
124         } catch (NetatmoException e) {
125             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
126         }
127     }
128
129     private void prepareReconnection() {
130         connectApi.disconnect();
131         freeConnectJob();
132         connectJob = Optional
133                 .of(scheduler.schedule(() -> openConnection(), thingConf.reconnectInterval, TimeUnit.SECONDS));
134     }
135
136     private void freeConnectJob() {
137         connectJob.ifPresent(j -> j.cancel(true));
138         connectJob = Optional.empty();
139     }
140
141     @Override
142     public void dispose() {
143         logger.debug("Shutting down Netatmo API bridge handler.");
144         servlet.ifPresent(servlet -> servlet.dispose());
145         servlet = Optional.empty();
146         connectApi.dispose();
147         freeConnectJob();
148         super.dispose();
149     }
150
151     @Override
152     public void handleCommand(ChannelUID channelUID, Command command) {
153         logger.debug("Netatmo Bridge is read-only and does not handle commands");
154     }
155
156     @Override
157     public Collection<Class<? extends ThingHandlerService>> getServices() {
158         return Set.of(NetatmoDiscoveryService.class);
159     }
160
161     @SuppressWarnings("unchecked")
162     public <T extends RestManager> @Nullable T getRestManager(Class<T> clazz) {
163         if (!managers.containsKey(clazz)) {
164             try {
165                 Constructor<T> constructor = clazz.getConstructor(getClass());
166                 T instance = constructor.newInstance(this);
167                 Set<Scope> expected = instance.getRequiredScopes();
168                 if (connectApi.matchesScopes(expected)) {
169                     managers.put(clazz, instance);
170                 } else {
171                     logger.info("Unable to instantiate {}, expected scope {} is not active", clazz, expected);
172                 }
173             } catch (SecurityException | ReflectiveOperationException e) {
174                 logger.warn("Error invoking RestManager constructor for class {} : {}", clazz, e.getMessage());
175             }
176         }
177         return (T) managers.get(clazz);
178     }
179
180     public synchronized <T> T executeUri(URI uri, HttpMethod method, Class<T> clazz, @Nullable String payload,
181             @Nullable String contentType, int retryCount) throws NetatmoException {
182         try {
183             logger.trace("executeUri {}  {} ", method.toString(), uri);
184
185             Request request = httpClient.newRequest(uri).method(method).timeout(TIMEOUT_S, TimeUnit.SECONDS);
186
187             String auth = connectApi.getAuthorization();
188             if (auth != null) {
189                 request.header(HttpHeader.AUTHORIZATION, auth);
190             }
191
192             if (payload != null && contentType != null
193                     && (HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method))) {
194                 InputStream stream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));
195                 try (InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(stream)) {
196                     request.content(inputStreamContentProvider, contentType);
197                 }
198             }
199
200             ContentResponse response = request.send();
201
202             Code statusCode = HttpStatus.getCode(response.getStatus());
203             String responseBody = new String(response.getContent(), StandardCharsets.UTF_8);
204             logger.trace("executeUri returned : code {} body {}", statusCode, responseBody);
205
206             if (statusCode != Code.OK) {
207                 ApiError error = deserializer.deserialize(ApiError.class, responseBody);
208                 throw new NetatmoException(error);
209             }
210             return deserializer.deserialize(clazz, responseBody);
211         } catch (InterruptedException e) {
212             Thread.currentThread().interrupt();
213             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
214             throw new NetatmoException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
215         } catch (TimeoutException | ExecutionException e) {
216             if (retryCount > 0) {
217                 logger.debug("Request timedout, retry counter : {}", retryCount);
218                 return executeUri(uri, method, clazz, payload, contentType, retryCount - 1);
219             }
220             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/request-time-out");
221             prepareReconnection();
222             throw new NetatmoException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
223         }
224     }
225
226     public BindingConfiguration getConfiguration() {
227         return bindingConf;
228     }
229
230     public Optional<NetatmoServlet> getServlet() {
231         return servlet;
232     }
233
234     public NADeserializer getDeserializer() {
235         return deserializer;
236     }
237
238     public boolean isConnected() {
239         return connectApi.isConnected();
240     }
241 }