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.boschshc.internal.devices.bridge;
15 import static org.eclipse.jetty.http.HttpMethod.POST;
17 import java.util.concurrent.ExecutionException;
18 import java.util.concurrent.ScheduledExecutorService;
19 import java.util.concurrent.TimeUnit;
20 import java.util.concurrent.TimeoutException;
21 import java.util.function.Consumer;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.eclipse.jetty.client.api.Request;
26 import org.eclipse.jetty.client.api.Result;
27 import org.eclipse.jetty.client.util.BufferingResponseListener;
28 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollError;
29 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
30 import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
31 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
32 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
36 import com.google.gson.Gson;
39 * Handles the long polling to the Smart Home Controller.
41 * @author Christian Oeing - Initial contribution
44 public class LongPolling {
46 private final Logger logger = LoggerFactory.getLogger(LongPolling.class);
49 * gson instance to convert a class to json string and back.
51 private final Gson gson = new Gson();
54 * Executor to schedule long polls.
56 private final ScheduledExecutorService scheduler;
59 * Handler for long poll results.
61 private final Consumer<LongPollResult> handleResult;
64 * Handler for unrecoverable.
66 private final Consumer<Throwable> handleFailure;
69 * Current running long polling request.
71 private @Nullable Request request;
74 * Indicates if long polling was aborted.
76 private boolean aborted = false;
78 public LongPolling(ScheduledExecutorService scheduler, Consumer<LongPollResult> handleResult,
79 Consumer<Throwable> handleFailure) {
80 this.scheduler = scheduler;
81 this.handleResult = handleResult;
82 this.handleFailure = handleFailure;
85 public void start(BoschHttpClient httpClient) throws LongPollingFailedException {
86 // Subscribe to state updates.
87 String subscriptionId = this.subscribe(httpClient);
88 this.executeLongPoll(httpClient, subscriptionId);
92 // Abort long polling.
94 Request request = this.request;
95 if (request != null) {
96 request.abort(new AbortLongPolling());
102 * Subscribe to events and store the subscription ID needed for long polling.
104 * @param httpClient Http client to use for sending subscription request
105 * @return Subscription id
107 private String subscribe(BoschHttpClient httpClient) throws LongPollingFailedException {
109 String url = httpClient.getBoschShcUrl("remote/json-rpc");
110 JsonRpcRequest request = new JsonRpcRequest("2.0", "RE/subscribe",
111 new String[] { "com/bosch/sh/remote/*", null });
112 logger.debug("Subscribe: Sending request: {} - using httpClient {}", gson.toJson(request), httpClient);
113 Request httpRequest = httpClient.createRequest(url, POST, request);
114 SubscribeResult response = httpClient.sendRequest(httpRequest, SubscribeResult.class);
116 logger.debug("Subscribe: Got subscription ID: {} {}", response.getResult(), response.getJsonrpc());
117 String subscriptionId = response.getResult();
118 return subscriptionId;
119 } catch (TimeoutException | ExecutionException | InterruptedException e) {
120 throw new LongPollingFailedException("Error on subscribe request", e);
124 private void executeLongPoll(BoschHttpClient httpClient, String subscriptionId) {
125 scheduler.execute(() -> this.longPoll(httpClient, subscriptionId));
129 * Start long polling the home controller. Once a long poll resolves, a new one is started.
131 private void longPoll(BoschHttpClient httpClient, String subscriptionId) {
132 logger.debug("Sending long poll request");
134 JsonRpcRequest requestContent = new JsonRpcRequest("2.0", "RE/longPoll", new String[] { subscriptionId, "20" });
135 String url = httpClient.getBoschShcUrl("remote/json-rpc");
136 Request request = httpClient.createRequest(url, POST, requestContent);
138 // Long polling responds after 20 seconds with an empty response if no update has happened.
139 // 10 second threshold was added to not time out if response from controller takes a bit longer than 20 seconds.
140 request.timeout(30, TimeUnit.SECONDS);
142 this.request = request;
143 LongPolling longPolling = this;
144 request.send(new BufferingResponseListener() {
146 public void onComplete(@Nullable Result result) {
147 Throwable failure = result != null ? result.getFailure() : null;
148 if (failure != null) {
149 if (failure instanceof ExecutionException) {
150 if (failure.getCause() instanceof AbortLongPolling) {
151 logger.debug("Canceling long polling for subscription id {} because it was aborted",
154 longPolling.handleFailure.accept(new LongPollingFailedException(
155 "Unexpected exception during long polling request", failure));
158 longPolling.handleFailure.accept(new LongPollingFailedException(
159 "Unexpected exception during long polling request", failure));
162 longPolling.onLongPollResponse(httpClient, subscriptionId, this.getContentAsString());
168 private void onLongPollResponse(BoschHttpClient httpClient, String subscriptionId, String content) {
169 // Check if thing is still online
171 logger.debug("Canceling long polling for subscription id {} because it was aborted", subscriptionId);
175 logger.debug("Long poll response: {}", content);
177 String nextSubscriptionId = subscriptionId;
179 LongPollResult longPollResult = gson.fromJson(content, LongPollResult.class);
180 if (longPollResult != null && longPollResult.result != null) {
181 this.handleResult.accept(longPollResult);
183 logger.warn("Long poll response contained no results: {}", content);
185 // Check if we got a proper result from the SHC
186 LongPollError longPollError = gson.fromJson(content, LongPollError.class);
188 if (longPollError != null && longPollError.error != null) {
189 logger.warn("Got long poll error: {} (code: {})", longPollError.error.message,
190 longPollError.error.code);
192 if (longPollError.error.code == LongPollError.SUBSCRIPTION_INVALID) {
193 logger.warn("Subscription {} became invalid, subscribing again", subscriptionId);
195 nextSubscriptionId = this.subscribe(httpClient);
196 } catch (LongPollingFailedException e) {
197 this.handleFailure.accept(e);
205 this.executeLongPoll(httpClient, nextSubscriptionId);
208 @SuppressWarnings("serial")
209 private class AbortLongPolling extends BoschSHCException {