2 * Copyright (c) 2010-2022 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.netatmo.internal.handler;
15 import java.io.ByteArrayInputStream;
16 import java.io.InputStream;
17 import java.lang.reflect.Constructor;
19 import java.nio.charset.StandardCharsets;
20 import java.util.Collection;
21 import java.util.HashMap;
23 import java.util.Objects;
24 import java.util.Optional;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.TimeoutException;
31 import javax.ws.rs.core.UriBuilder;
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.config.ApiHandlerConfiguration;
50 import org.openhab.binding.netatmo.internal.config.BindingConfiguration;
51 import org.openhab.binding.netatmo.internal.config.ConfigurationLevel;
52 import org.openhab.binding.netatmo.internal.deserialization.NADeserializer;
53 import org.openhab.binding.netatmo.internal.discovery.NetatmoDiscoveryService;
54 import org.openhab.binding.netatmo.internal.servlet.GrantServlet;
55 import org.openhab.binding.netatmo.internal.servlet.WebhookServlet;
56 import org.openhab.core.config.core.Configuration;
57 import org.openhab.core.thing.Bridge;
58 import org.openhab.core.thing.ChannelUID;
59 import org.openhab.core.thing.Thing;
60 import org.openhab.core.thing.ThingStatus;
61 import org.openhab.core.thing.ThingStatusDetail;
62 import org.openhab.core.thing.binding.BaseBridgeHandler;
63 import org.openhab.core.thing.binding.ThingHandlerService;
64 import org.openhab.core.types.Command;
65 import org.osgi.service.http.HttpService;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
70 * {@link ApiBridgeHandler} is the handler for a Netatmo API and connects it to the framework.
72 * @author Gaƫl L'hopital - Initial contribution
76 public class ApiBridgeHandler extends BaseBridgeHandler {
77 private static final int TIMEOUT_S = 20;
79 private final Logger logger = LoggerFactory.getLogger(ApiBridgeHandler.class);
80 private final BindingConfiguration bindingConf;
81 private final AuthenticationApi connectApi;
82 private final HttpClient httpClient;
83 private final NADeserializer deserializer;
84 private final HttpService httpService;
86 private Optional<ScheduledFuture<?>> connectJob = Optional.empty();
87 private Map<Class<? extends RestManager>, RestManager> managers = new HashMap<>();
88 private @Nullable WebhookServlet webHookServlet;
89 private @Nullable GrantServlet grantServlet;
91 public ApiBridgeHandler(Bridge bridge, HttpClient httpClient, NADeserializer deserializer,
92 BindingConfiguration configuration, HttpService httpService) {
94 this.bindingConf = configuration;
95 this.connectApi = new AuthenticationApi(this, scheduler);
96 this.httpClient = httpClient;
97 this.deserializer = deserializer;
98 this.httpService = httpService;
102 public void initialize() {
103 logger.debug("Initializing Netatmo API bridge handler.");
104 updateStatus(ThingStatus.UNKNOWN);
105 GrantServlet servlet = new GrantServlet(this, httpService);
106 servlet.startListening();
107 this.grantServlet = servlet;
108 scheduler.execute(() -> openConnection(null, null));
111 public void openConnection(@Nullable String code, @Nullable String redirectUri) {
112 ApiHandlerConfiguration configuration = getConfiguration();
113 ConfigurationLevel level = configuration.check();
115 case EMPTY_CLIENT_ID:
116 case EMPTY_CLIENT_SECRET:
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, level.message);
119 case REFRESH_TOKEN_NEEDED:
120 if (code == null || redirectUri == null) {
121 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, level.message);
123 } // else we can proceed to get the token refresh
126 logger.debug("Connecting to Netatmo API.");
128 String refreshToken = connectApi.authorize(configuration, code, redirectUri);
130 if (configuration.refreshToken.isBlank()) {
131 Configuration thingConfig = editConfiguration();
132 thingConfig.put(ApiHandlerConfiguration.REFRESH_TOKEN, refreshToken);
133 updateConfiguration(thingConfig);
134 configuration = getConfiguration();
137 if (!configuration.webHookUrl.isBlank()) {
138 SecurityApi securityApi = getRestManager(SecurityApi.class);
139 if (securityApi != null) {
140 WebhookServlet servlet = new WebhookServlet(this, httpService, deserializer, securityApi,
141 configuration.webHookUrl);
142 servlet.startListening();
143 this.webHookServlet = servlet;
147 updateStatus(ThingStatus.ONLINE);
149 getThing().getThings().stream().filter(Thing::isEnabled).map(Thing::getHandler)
150 .filter(Objects::nonNull).map(CommonInterface.class::cast)
151 .forEach(CommonInterface::expireData);
153 } catch (NetatmoException e) {
154 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
155 prepareReconnection(code, redirectUri);
161 public ApiHandlerConfiguration getConfiguration() {
162 return getConfigAs(ApiHandlerConfiguration.class);
165 private void prepareReconnection(@Nullable String code, @Nullable String redirectUri) {
166 connectApi.disconnect();
168 connectJob = Optional.of(scheduler.schedule(() -> openConnection(code, redirectUri),
169 getConfiguration().reconnectInterval, TimeUnit.SECONDS));
172 private void freeConnectJob() {
173 connectJob.ifPresent(j -> j.cancel(true));
174 connectJob = Optional.empty();
178 public void dispose() {
179 logger.debug("Shutting down Netatmo API bridge handler.");
180 WebhookServlet localWebHook = this.webHookServlet;
181 if (localWebHook != null) {
182 localWebHook.dispose();
184 GrantServlet localGrant = this.grantServlet;
185 if (localGrant != null) {
186 localGrant.dispose();
188 connectApi.dispose();
194 public void handleCommand(ChannelUID channelUID, Command command) {
195 logger.debug("Netatmo Bridge is read-only and does not handle commands");
198 @SuppressWarnings("unchecked")
199 public <T extends RestManager> @Nullable T getRestManager(Class<T> clazz) {
200 if (!managers.containsKey(clazz)) {
202 Constructor<T> constructor = clazz.getConstructor(getClass());
203 T instance = constructor.newInstance(this);
204 Set<Scope> expected = instance.getRequiredScopes();
205 if (connectApi.matchesScopes(expected)) {
206 managers.put(clazz, instance);
208 logger.info("Unable to instantiate {}, expected scope {} is not active", clazz, expected);
210 } catch (SecurityException | ReflectiveOperationException e) {
211 logger.warn("Error invoking RestManager constructor for class {} : {}", clazz, e.getMessage());
214 return (T) managers.get(clazz);
217 public synchronized <T> T executeUri(URI uri, HttpMethod method, Class<T> clazz, @Nullable String payload,
218 @Nullable String contentType, int retryCount) throws NetatmoException {
220 logger.trace("executeUri {} {} ", method.toString(), uri);
222 Request request = httpClient.newRequest(uri).method(method).timeout(TIMEOUT_S, TimeUnit.SECONDS);
224 String auth = connectApi.getAuthorization();
226 request.header(HttpHeader.AUTHORIZATION, auth);
229 if (payload != null && contentType != null
230 && (HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method))) {
231 InputStream stream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));
232 try (InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(stream)) {
233 request.content(inputStreamContentProvider, contentType);
237 ContentResponse response = request.send();
239 Code statusCode = HttpStatus.getCode(response.getStatus());
240 String responseBody = new String(response.getContent(), StandardCharsets.UTF_8);
241 logger.trace("executeUri returned : code {} body {}", statusCode, responseBody);
243 if (statusCode != Code.OK) {
244 ApiError error = deserializer.deserialize(ApiError.class, responseBody);
245 throw new NetatmoException(error);
247 return deserializer.deserialize(clazz, responseBody);
248 } catch (InterruptedException e) {
249 Thread.currentThread().interrupt();
250 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
251 throw new NetatmoException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
252 } catch (TimeoutException | ExecutionException e) {
253 if (retryCount > 0) {
254 logger.debug("Request timedout, retry counter : {}", retryCount);
255 return executeUri(uri, method, clazz, payload, contentType, retryCount - 1);
257 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/request-time-out");
258 prepareReconnection(null, null);
259 throw new NetatmoException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
263 public boolean getReadFriends() {
264 return bindingConf.readFriends;
267 public boolean isConnected() {
268 return connectApi.isConnected();
271 public String getId() {
272 return (String) getThing().getConfiguration().get(ApiHandlerConfiguration.CLIENT_ID);
275 public UriBuilder formatAuthorizationUrl() {
276 return AuthenticationApi.getAuthorizationBuilder(getId());
280 public Collection<Class<? extends ThingHandlerService>> getServices() {
281 return Set.of(NetatmoDiscoveryService.class);
284 public Optional<WebhookServlet> getWebHookServlet() {
285 return Optional.ofNullable(webHookServlet);