2 * Copyright (c) 2010-2021 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.nest.internal.handler;
15 import static java.util.concurrent.TimeUnit.SECONDS;
16 import static org.openhab.binding.nest.internal.NestBindingConstants.JSON_CONTENT_TYPE;
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;
25 import java.util.concurrent.CopyOnWriteArrayList;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
29 import javax.ws.rs.client.ClientBuilder;
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;
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.
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
72 public class NestBridgeHandler extends BaseBridgeHandler implements NestStreamingDataListener {
74 private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30);
76 private final Logger logger = LoggerFactory.getLogger(NestBridgeHandler.class);
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);
84 private @NonNullByDefault({}) NestAuthorizer authorizer;
85 private @NonNullByDefault({}) NestBridgeConfiguration config;
87 private @Nullable ScheduledFuture<?> initializeJob;
88 private @Nullable ScheduledFuture<?> transmitJob;
89 private @Nullable NestRedirectUrlSupplier redirectUrlSupplier;
90 private @Nullable NestStreamingRestClient streamingRestClient;
93 * Creates the bridge handler to connect to Nest.
95 * @param bridge The bridge to connect to Nest with.
97 public NestBridgeHandler(Bridge bridge, ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory) {
99 this.clientBuilder = clientBuilder;
100 this.eventSourceFactory = eventSourceFactory;
104 * Initialize the connection to Nest.
107 public void initialize() {
108 logger.debug("Initializing Nest bridge handler");
110 config = getConfigAs(NestBridgeConfiguration.class);
111 authorizer = new NestAuthorizer(config);
112 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Starting poll query");
114 initializeJob = scheduler.schedule(() -> {
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());
127 }, 0, TimeUnit.SECONDS);
129 logger.debug("Finished initializing Nest bridge handler");
133 * Clean up the handler.
136 public void dispose() {
137 logger.debug("Nest bridge disposed");
138 stopStreamingUpdates();
140 ScheduledFuture<?> localInitializeJob = initializeJob;
141 if (localInitializeJob != null && !localInitializeJob.isCancelled()) {
142 localInitializeJob.cancel(true);
143 initializeJob = null;
146 ScheduledFuture<?> localTransmitJob = transmitJob;
147 if (localTransmitJob != null && !localTransmitJob.isCancelled()) {
148 localTransmitJob.cancel(true);
152 this.authorizer = null;
153 this.redirectUrlSupplier = null;
154 this.streamingRestClient = null;
157 public <T> boolean addThingDataListener(Class<T> dataClass, NestThingDataListener<T> listener) {
158 return updateHandler.addListener(dataClass, listener);
161 public <T> boolean addThingDataListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
162 return updateHandler.addListener(dataClass, nestId, listener);
166 * Adds the update request into the queue for doing something with, send immediately if the queue is empty.
168 public void addUpdateRequest(NestUpdateRequest request) {
169 nestUpdateRequests.add(request);
170 scheduleTransmitJobForPendingRequests();
173 protected NestRedirectUrlSupplier createRedirectUrlSupplier() throws InvalidAccessTokenException {
174 return new NestRedirectUrlSupplier(getHttpHeaders());
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;
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);
191 logger.debug("Re-using access token from configuration: {}", accessToken);
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);
203 public @Nullable <T> T getLastUpdate(Class<T> dataClass, String nestId) {
204 return updateHandler.getLastUpdate(dataClass, nestId);
207 public <T> List<T> getLastUpdates(Class<T> dataClass) {
208 return updateHandler.getLastUpdates(dataClass);
211 private NestRedirectUrlSupplier getOrCreateRedirectUrlSupplier() throws InvalidAccessTokenException {
212 NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
213 if (localRedirectUrlSupplier == null) {
214 localRedirectUrlSupplier = createRedirectUrlSupplier();
215 redirectUrlSupplier = localRedirectUrlSupplier;
217 return localRedirectUrlSupplier;
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());
232 * Handles an incoming command update
235 public void handleCommand(ChannelUID channelUID, Command command) {
236 if (command instanceof RefreshType) {
237 logger.debug("Refresh command received");
238 updateHandler.resendLastUpdates();
242 private void jsonToPutUrl(NestUpdateRequest request)
243 throws FailedSendingNestDataException, InvalidAccessTokenException, FailedResolvingNestUrlException {
245 NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
246 if (localRedirectUrlSupplier == null) {
247 throw new FailedResolvingNestUrlException("redirectUrlSupplier is null");
250 String url = localRedirectUrlSupplier.getRedirectUrl() + request.getUpdatePath();
251 logger.debug("Putting data to: {}", url);
253 String jsonContent = NestUtils.toJson(request.getValues());
254 logger.debug("PUT content: {}", jsonContent);
256 ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8));
257 String jsonResponse = HttpUtil.executeUrl("PUT", url, getHttpHeaders(), inputStream, JSON_CONTENT_TYPE,
259 logger.debug("PUT response: {}", jsonResponse);
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());
266 } catch (IOException e) {
267 throw new FailedSendingNestDataException("Failed to send data", e);
272 public void onAuthorizationRevoked(String token) {
273 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
274 "Authorization token revoked: " + token);
278 public void onConnected() {
279 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Streaming data connection established");
280 scheduleTransmitJobForPendingRequests();
284 public void onDisconnected() {
285 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Streaming data disconnected");
289 public void onError(String message) {
290 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
294 public void onNewTopLevelData(TopLevelData data) {
295 updateHandler.handleUpdate(data);
296 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Receiving streaming data");
299 public <T> boolean removeThingDataListener(Class<T> dataClass, NestThingDataListener<T> listener) {
300 return updateHandler.removeListener(dataClass, listener);
303 public <T> boolean removeThingDataListener(Class<T> dataClass, String nestId, NestThingDataListener<T> listener) {
304 return updateHandler.removeListener(dataClass, nestId, listener);
307 private void restartStreamingUpdates() {
308 synchronized (this) {
309 stopStreamingUpdates();
310 startStreamingUpdates();
314 private void scheduleTransmitJobForPendingRequests() {
315 ScheduledFuture<?> localTransmitJob = transmitJob;
316 if (!nestUpdateRequests.isEmpty() && (localTransmitJob == null || localTransmitJob.isDone())) {
317 transmitJob = scheduler.schedule(this::transmitQueue, 0, SECONDS);
321 private void startStreamingUpdates() {
322 synchronized (this) {
324 NestStreamingRestClient localStreamingRestClient = new NestStreamingRestClient(
325 getExistingOrNewAccessToken(), clientBuilder, eventSourceFactory,
326 getOrCreateRedirectUrlSupplier(), scheduler);
327 localStreamingRestClient.addStreamingDataListener(this);
328 localStreamingRestClient.start();
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());
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;
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");
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);
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);
377 NestRedirectUrlSupplier localRedirectUrlSupplier = redirectUrlSupplier;
378 if (localRedirectUrlSupplier != null) {
379 localRedirectUrlSupplier.resetCache();