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 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;
65 * {@link ApiBridgeHandler} is the handler for a Netatmo API and connects it to the framework.
67 * @author Gaƫl L'hopital - Initial contribution
71 public class ApiBridgeHandler extends BaseBridgeHandler {
72 private static final int TIMEOUT_S = 20;
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;
81 private Optional<ScheduledFuture<?>> connectJob = Optional.empty();
82 private Optional<NetatmoServlet> servlet = Optional.empty();
83 private @NonNullByDefault({}) ApiHandlerConfiguration thingConf;
85 private Map<Class<? extends RestManager>, RestManager> managers = new HashMap<>();
87 public ApiBridgeHandler(Bridge bridge, HttpClient httpClient, HttpService httpService, NADeserializer deserializer,
88 BindingConfiguration configuration) {
90 this.bindingConf = configuration;
91 this.httpService = httpService;
92 this.connectApi = new AuthenticationApi(this, scheduler);
93 this.httpClient = httpClient;
94 this.deserializer = deserializer;
98 public void initialize() {
99 logger.debug("Initializing Netatmo API bridge handler.");
100 thingConf = getConfigAs(ApiHandlerConfiguration.class);
101 updateStatus(ThingStatus.UNKNOWN);
102 scheduler.execute(() -> {
104 String webHookUrl = thingConf.webHookUrl;
105 if (webHookUrl != null && !webHookUrl.isBlank()) {
106 servlet = Optional.of(new NetatmoServlet(httpService, this, webHookUrl));
111 private void openConnection() {
113 Credentials credentials = thingConf.getCredentials();
114 logger.debug("Connecting to Netatmo API.");
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();
124 } catch (NetatmoException e) {
125 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
129 private void prepareReconnection() {
130 connectApi.disconnect();
132 connectJob = Optional
133 .of(scheduler.schedule(() -> openConnection(), thingConf.reconnectInterval, TimeUnit.SECONDS));
136 private void freeConnectJob() {
137 connectJob.ifPresent(j -> j.cancel(true));
138 connectJob = Optional.empty();
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();
152 public void handleCommand(ChannelUID channelUID, Command command) {
153 logger.debug("Netatmo Bridge is read-only and does not handle commands");
157 public Collection<Class<? extends ThingHandlerService>> getServices() {
158 return Set.of(NetatmoDiscoveryService.class);
161 @SuppressWarnings("unchecked")
162 public <T extends RestManager> @Nullable T getRestManager(Class<T> clazz) {
163 if (!managers.containsKey(clazz)) {
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);
171 logger.info("Unable to instantiate {}, expected scope {} is not active", clazz, expected);
173 } catch (SecurityException | ReflectiveOperationException e) {
174 logger.warn("Error invoking RestManager constructor for class {} : {}", clazz, e.getMessage());
177 return (T) managers.get(clazz);
180 public synchronized <T> T executeUri(URI uri, HttpMethod method, Class<T> clazz, @Nullable String payload,
181 @Nullable String contentType, int retryCount) throws NetatmoException {
183 logger.trace("executeUri {} {} ", method.toString(), uri);
185 Request request = httpClient.newRequest(uri).method(method).timeout(TIMEOUT_S, TimeUnit.SECONDS);
187 String auth = connectApi.getAuthorization();
189 request.header(HttpHeader.AUTHORIZATION, auth);
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);
200 ContentResponse response = request.send();
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);
206 if (statusCode != Code.OK) {
207 ApiError error = deserializer.deserialize(ApiError.class, responseBody);
208 throw new NetatmoException(error);
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);
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()));
226 public BindingConfiguration getConfiguration() {
230 public Optional<NetatmoServlet> getServlet() {
234 public NADeserializer getDeserializer() {
238 public boolean isConnected() {
239 return connectApi.isConnected();