]> git.basschouten.com Git - openhab-addons.git/blob
813877c78cc62475c774618479eeadedea39afdc
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.nest.internal.handler;
14
15 import static java.util.concurrent.TimeUnit.SECONDS;
16 import static org.openhab.binding.nest.internal.NestBindingConstants.JSON_CONTENT_TYPE;
17
18 import java.io.ByteArrayInputStream;
19 import java.io.IOException;
20 import java.nio.charset.StandardCharsets;
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Properties;
24 import java.util.Set;
25 import java.util.concurrent.CopyOnWriteArrayList;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28
29 import javax.ws.rs.client.ClientBuilder;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.nest.internal.NestUtils;
34 import org.openhab.binding.nest.internal.config.NestBridgeConfiguration;
35 import org.openhab.binding.nest.internal.data.ErrorData;
36 import org.openhab.binding.nest.internal.data.NestIdentifiable;
37 import org.openhab.binding.nest.internal.data.TopLevelData;
38 import org.openhab.binding.nest.internal.exceptions.FailedResolvingNestUrlException;
39 import org.openhab.binding.nest.internal.exceptions.FailedSendingNestDataException;
40 import org.openhab.binding.nest.internal.exceptions.InvalidAccessTokenException;
41 import org.openhab.binding.nest.internal.listener.NestStreamingDataListener;
42 import org.openhab.binding.nest.internal.listener.NestThingDataListener;
43 import org.openhab.binding.nest.internal.rest.NestAuthorizer;
44 import org.openhab.binding.nest.internal.rest.NestStreamingRestClient;
45 import org.openhab.binding.nest.internal.rest.NestUpdateRequest;
46 import org.openhab.binding.nest.internal.update.NestCompositeUpdateHandler;
47 import org.openhab.core.config.core.Configuration;
48 import org.openhab.core.io.net.http.HttpUtil;
49 import org.openhab.core.thing.Bridge;
50 import org.openhab.core.thing.ChannelUID;
51 import org.openhab.core.thing.Thing;
52 import org.openhab.core.thing.ThingStatus;
53 import org.openhab.core.thing.ThingStatusDetail;
54 import org.openhab.core.thing.binding.BaseBridgeHandler;
55 import org.openhab.core.thing.binding.ThingHandler;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.osgi.service.jaxrs.client.SseEventSourceFactory;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 /**
63  * This bridge handler connects to Nest and handles all the API requests. It pulls down the
64  * updated data, polls the system and does all the co-ordination with the other handlers
65  * to get the data updated to the correct things.
66  *
67  * @author David Bennett - Initial contribution
68  * @author Martin van Wingerden - Use listeners not only for discovery but for all data processing
69  * @author Wouter Born - Improve exception and URL redirect handling
70  */
71 @NonNullByDefault
72 public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamingDataListener {
73
74     private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30);
75
76     private final Logger logger = LoggerFactory.getLogger(NestBridgeHandler.class);
77
78     private final ClientBuilder clientBuilder;
79     private final SseEventSourceFactory eventSourceFactory;
80     private final List<NestUpdateRequest> nestUpdateRequests = new CopyOnWriteArrayList<>();
81     private final NestCompositeUpdateHandler updateHandler = new NestCompositeUpdateHandler(
82             this::getPresentThingsNestIds);
83
84     private @NonNullByDefault({}) NestAuthorizer authorizer;
85     private @NonNullByDefault({}) NestBridgeConfiguration config;
86
87     private @Nullable ScheduledFuture<?> initializeJob;
88     private @Nullable ScheduledFuture<?> transmitJob;
89     private @Nullable NestRedirectUrlSupplier redirectUrlSupplier;
90     private @Nullable NestStreamingRestClient streamingRestClient;
91
92     /**
93      * Creates the bridge handler to connect to Nest.
94      *
95      * @param bridge The bridge to connect to Nest with.
96      */
97     public NestBridgeHandler(Bridge bridge, ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory) {
98         super(bridge);
99         this.clientBuilder = clientBuilder;
100         this.eventSourceFactory = eventSourceFactory;
101     }
102
103     /**
104      * Initialize the connection to Nest.
105      */
106     @Override
107     public void initialize() {
108         logger.debug("Initializing Nest bridge handler");
109
110         config = getConfigAs(NestBridgeConfiguration.class);
111         authorizer = new NestAuthorizer(config);
112         updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Starting poll query");
113
114         initializeJob = scheduler.schedule(() -> {
115             try {
116                 logger.debug("Product ID      {}", config.productId);
117                 logger.debug("Product Secret  {}", config.productSecret);
118                 logger.debug("Pincode         {}", config.pincode);
119                 logger.debug("Access Token    {}", getExistingOrNewAccessToken());
120                 redirectUrlSupplier = createRedirectUrlSupplier();
121                 restartStreamingUpdates();
122             } catch (InvalidAccessTokenException e) {
123                 logger.debug("Invalid access token", e);
124                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
125                         "Token is invalid and could not be refreshed: " + e.getMessage());
126             }
127         }, 0, TimeUnit.SECONDS);
128
129         logger.debug("Finished initializing Nest bridge handler");
130     }
131
132     /**
133      * Clean up the handler.
134      */
135     @Override
136     public void dispose() {
137         logger.debug("Nest bridge disposed");
138         stopStreamingUpdates();
139
140         ScheduledFuture<?> localInitializeJob = initializeJob;
141         if (localInitializeJob != null && !localInitializeJob.isCancelled()) {
142             localInitializeJob.cancel(true);
143             initializeJob = null;
144         }
145
146         ScheduledFuture<?> localTransmitJob = transmitJob;
147         if (localTransmitJob != null && !localTransmitJob.isCancelled()) {
148             localTransmitJob.cancel(true);
149             transmitJob = null;
150         }
151
152         this.authorizer = null;
153         this.redirectUrlSupplier = null;
154         this.streamingRestClient = null;
155     }
156
157     public <T> boolean addThingDataListener(Class<T> dataClass, NestThingDataListener<T> listener) {
158         return updateHandler.addListener(dataClass, listener);
159     }
160
161     public <T> boolean addThingDataListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
162         return updateHandler.addListener(dataClass, nestId, listener);
163     }
164
165     /**
166      * Adds the update request into the queue for doing something with, send immediately if the queue is empty.
167      */
168     public void addUpdateRequest(NestUpdateRequest request) {
169         nestUpdateRequests.add(request);
170         scheduleTransmitJobForPendingRequests();
171     }
172
173     protected NestRedirectUrlSupplier createRedirectUrlSupplier() throws InvalidAccessTokenException {
174         return new NestRedirectUrlSupplier(getHttpHeaders());
175     }
176
177     private String getExistingOrNewAccessToken() throws InvalidAccessTokenException {
178         String accessToken = config.accessToken;
179         if (accessToken == null || accessToken.isEmpty()) {
180             accessToken = authorizer.getNewAccessToken();
181             config.accessToken = accessToken;
182             config.pincode = "";
183             // Update and save the access token in the bridge configuration
184             Configuration configuration = editConfiguration();
185             configuration.put(NestBridgeConfiguration.ACCESS_TOKEN, config.accessToken);
186             configuration.put(NestBridgeConfiguration.PINCODE, config.pincode);
187             updateConfiguration(configuration);
188             logger.debug("Retrieved new access token: {}", config.accessToken);
189             return accessToken;
190         } else {
191             logger.debug("Re-using access token from configuration: {}", accessToken);
192             return accessToken;
193         }
194     }
195
196     protected Properties getHttpHeaders() throws InvalidAccessTokenException {
197         Properties httpHeaders = new Properties();
198         httpHeaders.put("Authorization", "Bearer " + getExistingOrNewAccessToken());
199         httpHeaders.put("Content-Type", JSON_CONTENT_TYPE);
200         return httpHeaders;
201     }
202
203     public @Nullable <T> T getLastUpdate(Class<T> dataClass, String nestId) {
204         return updateHandler.getLastUpdate(dataClass, nestId);
205     }
206
207     public <T> List<T> getLastUpdates(Class<T> dataClass) {
208         return updateHandler.getLastUpdates(dataClass);
209     }
210
211     private NestRedirectUrlSupplier getOrCreateRedirectUrlSupplier() throws InvalidAccessTokenException {
212         NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
213         if (localRedirectUrlSupplier == null) {
214             localRedirectUrlSupplier = createRedirectUrlSupplier();
215             redirectUrlSupplier = localRedirectUrlSupplier;
216         }
217         return localRedirectUrlSupplier;
218     }
219
220     private Set<String> getPresentThingsNestIds() {
221         Set<String> nestIds = new HashSet<>();
222         for (Thing thing : getThing().getThings()) {
223             ThingHandler handler = thing.getHandler();
224             if (handler != null && thing.getStatusInfo().getStatusDetail() != ThingStatusDetail.GONE) {
225                 nestIds.add(((NestIdentifiable) handler).getId());
226             }
227         }
228         return nestIds;
229     }
230
231     /**
232      * Handles an incoming command update
233      */
234     @Override
235     public void handleCommand(ChannelUID channelUID, Command command) {
236         if (command instanceof RefreshType) {
237             logger.debug("Refresh command received");
238             updateHandler.resendLastUpdates();
239         }
240     }
241
242     private void jsonToPutUrl(NestUpdateRequest request)
243             throws FailedSendingNestDataException, InvalidAccessTokenException, FailedResolvingNestUrlException {
244         try {
245             NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
246             if (localRedirectUrlSupplier == null) {
247                 throw new FailedResolvingNestUrlException("redirectUrlSupplier is null");
248             }
249
250             String url = localRedirectUrlSupplier.getRedirectUrl() + request.getUpdatePath();
251             logger.debug("Putting data to: {}", url);
252
253             String jsonContent = NestUtils.toJson(request.getValues());
254             logger.debug("PUT content: {}", jsonContent);
255
256             ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8));
257             String jsonResponse = HttpUtil.executeUrl("PUT", url, getHttpHeaders(), inputStream, JSON_CONTENT_TYPE,
258                     REQUEST_TIMEOUT);
259             logger.debug("PUT response: {}", jsonResponse);
260
261             ErrorData error = NestUtils.fromJson(jsonResponse, ErrorData.class);
262             if (error.getError() != null && !error.getError().isBlank()) {
263                 logger.debug("Nest API error: {}", error);
264                 logger.warn("Nest API error: {}", error.getMessage());
265             }
266         } catch (IOException e) {
267             throw new FailedSendingNestDataException("Failed to send data", e);
268         }
269     }
270
271     @Override
272     public void onAuthorizationRevoked(String token) {
273         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
274                 "Authorization token revoked: " + token);
275     }
276
277     @Override
278     public void onConnected() {
279         updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Streaming data connection established");
280         scheduleTransmitJobForPendingRequests();
281     }
282
283     @Override
284     public void onDisconnected() {
285         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Streaming data disconnected");
286     }
287
288     @Override
289     public void onError(String message) {
290         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
291     }
292
293     @Override
294     public void onNewTopLevelData(TopLevelData data) {
295         updateHandler.handleUpdate(data);
296         updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Receiving streaming data");
297     }
298
299     public <T> boolean removeThingDataListener(Class<T> dataClass, NestThingDataListener<T> listener) {
300         return updateHandler.removeListener(dataClass, listener);
301     }
302
303     public <T> boolean removeThingDataListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
304         return updateHandler.removeListener(dataClass, nestId, listener);
305     }
306
307     private void restartStreamingUpdates() {
308         synchronized (this) {
309             stopStreamingUpdates();
310             startStreamingUpdates();
311         }
312     }
313
314     private void scheduleTransmitJobForPendingRequests() {
315         ScheduledFuture<?> localTransmitJob = transmitJob;
316         if (!nestUpdateRequests.isEmpty() && (localTransmitJob == null || localTransmitJob.isDone())) {
317             transmitJob = scheduler.schedule(this::transmitQueue, 0, SECONDS);
318         }
319     }
320
321     private void startStreamingUpdates() {
322         synchronized (this) {
323             try {
324                 NestStreamingRestClient localStreamingRestClient = new NestStreamingRestClient(
325                         getExistingOrNewAccessToken(), clientBuilder, eventSourceFactory,
326                         getOrCreateRedirectUrlSupplier(), scheduler);
327                 localStreamingRestClient.addStreamingDataListener(this);
328                 localStreamingRestClient.start();
329
330                 streamingRestClient = localStreamingRestClient;
331             } catch (InvalidAccessTokenException e) {
332                 logger.debug("Invalid access token", e);
333                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
334                         "Token is invalid and could not be refreshed: " + e.getMessage());
335             }
336         }
337     }
338
339     private void stopStreamingUpdates() {
340         NestStreamingRestClient localStreamingRestClient = streamingRestClient;
341         if (localStreamingRestClient != null) {
342             synchronized (this) {
343                 localStreamingRestClient.stop();
344                 localStreamingRestClient.removeStreamingDataListener(this);
345                 streamingRestClient = null;
346             }
347         }
348     }
349
350     private void transmitQueue() {
351         if (getThing().getStatus() == ThingStatus.OFFLINE) {
352             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
353                     "Not transmitting events because bridge is OFFLINE");
354             return;
355         }
356
357         try {
358             while (!nestUpdateRequests.isEmpty()) {
359                 // nestUpdateRequests is a CopyOnWriteArrayList so its iterator does not support remove operations
360                 NestUpdateRequest request = nestUpdateRequests.get(0);
361                 jsonToPutUrl(request);
362                 nestUpdateRequests.remove(request);
363             }
364         } catch (InvalidAccessTokenException e) {
365             logger.debug("Invalid access token", e);
366             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
367                     "Token is invalid and could not be refreshed: " + e.getMessage());
368         } catch (FailedResolvingNestUrlException e) {
369             logger.debug("Unable to resolve redirect URL", e);
370             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
371             scheduler.schedule(this::restartStreamingUpdates, 5, SECONDS);
372         } catch (FailedSendingNestDataException e) {
373             logger.debug("Error sending data", e);
374             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
375             scheduler.schedule(this::restartStreamingUpdates, 5, SECONDS);
376
377             NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
378             if (localRedirectUrlSupplier != null) {
379                 localRedirectUrlSupplier.resetCache();
380             }
381         }
382     }
383 }