]> git.basschouten.com Git - openhab-addons.git/blob
f43becaf11c1dac559c8af7aeec9e44ea2cb8233
[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.remoteopenhab.internal.rest;
14
15 import java.io.ByteArrayOutputStream;
16 import java.io.EOFException;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.nio.charset.StandardCharsets;
20 import java.util.Arrays;
21 import java.util.Base64;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Objects;
26 import java.util.concurrent.CopyOnWriteArrayList;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.TimeoutException;
30
31 import javax.net.ssl.HostnameVerifier;
32 import javax.net.ssl.SSLSession;
33 import javax.ws.rs.client.Client;
34 import javax.ws.rs.client.ClientBuilder;
35 import javax.ws.rs.core.HttpHeaders;
36 import javax.ws.rs.sse.InboundSseEvent;
37 import javax.ws.rs.sse.SseEventSource;
38
39 import org.eclipse.jdt.annotation.NonNullByDefault;
40 import org.eclipse.jdt.annotation.Nullable;
41 import org.eclipse.jetty.client.HttpClient;
42 import org.eclipse.jetty.client.api.ContentResponse;
43 import org.eclipse.jetty.client.api.Request;
44 import org.eclipse.jetty.client.api.Response;
45 import org.eclipse.jetty.client.util.InputStreamResponseListener;
46 import org.eclipse.jetty.client.util.StringContentProvider;
47 import org.eclipse.jetty.http.HttpMethod;
48 import org.eclipse.jetty.http.HttpStatus;
49 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabChannelTriggerEvent;
50 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabEvent;
51 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabEventPayload;
52 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabItem;
53 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabRestApi;
54 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabStatusInfo;
55 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabThing;
56 import org.openhab.binding.remoteopenhab.internal.exceptions.RemoteopenhabException;
57 import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabItemsDataListener;
58 import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabStreamingDataListener;
59 import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabThingsDataListener;
60 import org.openhab.core.types.Command;
61 import org.osgi.service.jaxrs.client.SseEventSourceFactory;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 import com.google.gson.Gson;
66 import com.google.gson.JsonSyntaxException;
67
68 /**
69  * A client to use the openHAB REST API and to receive/parse events received from the openHAB REST API Server-Sent
70  * Events (SSE).
71  *
72  * @author Laurent Garnier - Initial contribution
73  */
74 @NonNullByDefault
75 public class RemoteopenhabRestClient {
76
77     private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30);
78
79     private final Logger logger = LoggerFactory.getLogger(RemoteopenhabRestClient.class);
80
81     private final ClientBuilder clientBuilder;
82     private final SseEventSourceFactory eventSourceFactory;
83     private final Gson jsonParser;
84
85     private final Object startStopLock = new Object();
86     private final List<RemoteopenhabStreamingDataListener> listeners = new CopyOnWriteArrayList<>();
87     private final List<RemoteopenhabItemsDataListener> itemsListeners = new CopyOnWriteArrayList<>();
88     private final List<RemoteopenhabThingsDataListener> thingsListeners = new CopyOnWriteArrayList<>();
89
90     private HttpClient httpClient;
91     private @Nullable String restUrl;
92     private @Nullable String restApiVersion;
93     private Map<String, @Nullable String> apiEndPointsUrls = new HashMap<>();
94     private @Nullable String topicNamespace;
95     private boolean authenticateAnyway;
96     private String accessToken;
97     private String credentialToken;
98     private boolean trustedCertificate;
99     private boolean connected;
100     private boolean completed;
101
102     private @Nullable SseEventSource eventSource;
103     private long lastEventTimestamp;
104
105     public RemoteopenhabRestClient(final HttpClient httpClient, final ClientBuilder clientBuilder,
106             final SseEventSourceFactory eventSourceFactory, final Gson jsonParser) {
107         this.httpClient = httpClient;
108         this.clientBuilder = clientBuilder;
109         this.eventSourceFactory = eventSourceFactory;
110         this.jsonParser = jsonParser;
111         this.accessToken = "";
112         this.credentialToken = "";
113     }
114
115     public void setHttpClient(HttpClient httpClient) {
116         this.httpClient = httpClient;
117     }
118
119     public String getRestUrl() throws RemoteopenhabException {
120         String url = restUrl;
121         if (url == null) {
122             throw new RemoteopenhabException("REST client not correctly setup");
123         }
124         return url;
125     }
126
127     public void setRestUrl(String restUrl) {
128         this.restUrl = restUrl;
129     }
130
131     public void setAuthenticationData(boolean authenticateAnyway, String accessToken, String username,
132             String password) {
133         this.authenticateAnyway = authenticateAnyway;
134         this.accessToken = accessToken;
135         if (username.isBlank() || password.isBlank()) {
136             this.credentialToken = "";
137         } else {
138             String token = username + ":" + password;
139             this.credentialToken = Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8));
140         }
141     }
142
143     public void setTrustedCertificate(boolean trustedCertificate) {
144         this.trustedCertificate = trustedCertificate;
145     }
146
147     public void tryApi() throws RemoteopenhabException {
148         try {
149             String jsonResponse = executeGetUrl(getRestUrl(), "application/json", false, false);
150             if (jsonResponse.isEmpty()) {
151                 throw new RemoteopenhabException("JSON response is empty");
152             }
153             RemoteopenhabRestApi restApi = jsonParser.fromJson(jsonResponse, RemoteopenhabRestApi.class);
154             restApiVersion = restApi.version;
155             logger.debug("REST API version = {}", restApiVersion);
156             apiEndPointsUrls.clear();
157             for (int i = 0; i < restApi.links.length; i++) {
158                 apiEndPointsUrls.put(restApi.links[i].type, restApi.links[i].url);
159             }
160             logger.debug("REST API items = {}", apiEndPointsUrls.get("items"));
161             logger.debug("REST API things = {}", apiEndPointsUrls.get("things"));
162             logger.debug("REST API events = {}", apiEndPointsUrls.get("events"));
163             topicNamespace = restApi.runtimeInfo != null ? "openhab" : "smarthome";
164             logger.debug("topic namespace = {}", topicNamespace);
165         } catch (RemoteopenhabException | JsonSyntaxException e) {
166             throw new RemoteopenhabException("Failed to execute the root REST API: " + e.getMessage(), e);
167         }
168     }
169
170     public List<RemoteopenhabItem> getRemoteItems(@Nullable String fields) throws RemoteopenhabException {
171         try {
172             String url = String.format("%s?recursive=false", getRestApiUrl("items"));
173             if (fields != null) {
174                 url += "&fields=" + fields;
175             }
176             boolean asyncReading = fields == null || Arrays.asList(fields.split(",")).contains("state");
177             String jsonResponse = executeGetUrl(url, "application/json", false, asyncReading);
178             if (jsonResponse.isEmpty()) {
179                 throw new RemoteopenhabException("JSON response is empty");
180             }
181             return Arrays.asList(jsonParser.fromJson(jsonResponse, RemoteopenhabItem[].class));
182         } catch (RemoteopenhabException | JsonSyntaxException e) {
183             throw new RemoteopenhabException(
184                     "Failed to get the list of remote items using the items REST API: " + e.getMessage(), e);
185         }
186     }
187
188     public String getRemoteItemState(String itemName) throws RemoteopenhabException {
189         try {
190             String url = String.format("%s/%s/state", getRestApiUrl("items"), itemName);
191             return executeGetUrl(url, "text/plain", false, true);
192         } catch (RemoteopenhabException e) {
193             throw new RemoteopenhabException("Failed to get the state of remote item " + itemName
194                     + " using the items REST API: " + e.getMessage(), e);
195         }
196     }
197
198     public void sendCommandToRemoteItem(String itemName, Command command) throws RemoteopenhabException {
199         try {
200             String url = String.format("%s/%s", getRestApiUrl("items"), itemName);
201             executeUrl(HttpMethod.POST, url, "application/json", command.toFullString(), "text/plain", false, false,
202                     true);
203         } catch (RemoteopenhabException e) {
204             throw new RemoteopenhabException("Failed to send command to the remote item " + itemName
205                     + " using the items REST API: " + e.getMessage(), e);
206         }
207     }
208
209     public List<RemoteopenhabThing> getRemoteThings() throws RemoteopenhabException {
210         try {
211             String jsonResponse = executeGetUrl(getRestApiUrl("things"), "application/json", true, false);
212             if (jsonResponse.isEmpty()) {
213                 throw new RemoteopenhabException("JSON response is empty");
214             }
215             return Arrays.asList(jsonParser.fromJson(jsonResponse, RemoteopenhabThing[].class));
216         } catch (RemoteopenhabException | JsonSyntaxException e) {
217             throw new RemoteopenhabException(
218                     "Failed to get the list of remote things using the things REST API: " + e.getMessage(), e);
219         }
220     }
221
222     public RemoteopenhabThing getRemoteThing(String uid) throws RemoteopenhabException {
223         try {
224             String url = String.format("%s/%s", getRestApiUrl("things"), uid);
225             String jsonResponse = executeGetUrl(url, "application/json", true, false);
226             if (jsonResponse.isEmpty()) {
227                 throw new RemoteopenhabException("JSON response is empty");
228             }
229             return Objects.requireNonNull(jsonParser.fromJson(jsonResponse, RemoteopenhabThing.class));
230         } catch (RemoteopenhabException | JsonSyntaxException e) {
231             throw new RemoteopenhabException(
232                     "Failed to get the remote thing " + uid + " using the things REST API: " + e.getMessage(), e);
233         }
234     }
235
236     public @Nullable String getRestApiVersion() {
237         return restApiVersion;
238     }
239
240     private String getRestApiUrl(String endPoint) throws RemoteopenhabException {
241         String url = apiEndPointsUrls.get(endPoint);
242         if (url == null) {
243             url = getRestUrl();
244             if (!url.endsWith("/")) {
245                 url += "/";
246             }
247             url += endPoint;
248         }
249         return url;
250     }
251
252     public String getTopicNamespace() {
253         String namespace = topicNamespace;
254         return namespace != null ? namespace : "openhab";
255     }
256
257     public void start() {
258         synchronized (startStopLock) {
259             logger.debug("Opening EventSource");
260             reopenEventSource();
261             logger.debug("EventSource started");
262         }
263     }
264
265     public void stop(boolean waitingForCompletion) {
266         synchronized (startStopLock) {
267             logger.debug("Closing EventSource");
268             closeEventSource(waitingForCompletion);
269             logger.debug("EventSource stopped");
270             lastEventTimestamp = 0;
271         }
272     }
273
274     private SseEventSource createEventSource(String restSseUrl) {
275         String credentialToken = restSseUrl.startsWith("https:") || authenticateAnyway ? this.credentialToken : "";
276
277         RemoteopenhabStreamingRequestFilter filter;
278         boolean filterRegistered = clientBuilder.getConfiguration()
279                 .isRegistered(RemoteopenhabStreamingRequestFilter.class);
280         if (filterRegistered) {
281             filter = clientBuilder.getConfiguration().getInstances().stream()
282                     .filter(instance -> instance instanceof RemoteopenhabStreamingRequestFilter)
283                     .map(instance -> (RemoteopenhabStreamingRequestFilter) instance).findAny().orElseThrow();
284         } else {
285             filter = new RemoteopenhabStreamingRequestFilter();
286         }
287         filter.setCredentialToken(restSseUrl, credentialToken);
288
289         Client client;
290         // Avoid a timeout exception after 1 minute by setting the read timeout to 0 (infinite)
291         if (trustedCertificate) {
292             HostnameVerifier alwaysValidHostname = new HostnameVerifier() {
293                 @Override
294                 public boolean verify(@Nullable String hostname, @Nullable SSLSession session) {
295                     return true;
296                 }
297             };
298             if (filterRegistered) {
299                 client = clientBuilder.sslContext(httpClient.getSslContextFactory().getSslContext())
300                         .hostnameVerifier(alwaysValidHostname).readTimeout(0, TimeUnit.SECONDS).build();
301             } else {
302                 client = clientBuilder.sslContext(httpClient.getSslContextFactory().getSslContext())
303                         .hostnameVerifier(alwaysValidHostname).readTimeout(0, TimeUnit.SECONDS).register(filter)
304                         .build();
305             }
306         } else {
307             if (filterRegistered) {
308                 client = clientBuilder.readTimeout(0, TimeUnit.SECONDS).build();
309             } else {
310                 client = clientBuilder.readTimeout(0, TimeUnit.SECONDS).register(filter).build();
311             }
312         }
313
314         SseEventSource eventSource = eventSourceFactory.newSource(client.target(restSseUrl));
315         eventSource.register(this::onEvent, this::onError, this::onComplete);
316         return eventSource;
317     }
318
319     private void reopenEventSource() {
320         logger.debug("Reopening EventSource");
321
322         String url;
323         try {
324             url = String.format("%s?topics=%s/items/*/*,%s/things/*/*,%s/channels/*/triggered", getRestApiUrl("events"),
325                     getTopicNamespace(), getTopicNamespace(), getTopicNamespace());
326         } catch (RemoteopenhabException e) {
327             logger.debug("{}", e.getMessage());
328             return;
329         }
330
331         closeEventSource(true);
332
333         logger.debug("Opening new EventSource {}", url);
334         SseEventSource localEventSource = createEventSource(url);
335         localEventSource.open();
336
337         eventSource = localEventSource;
338     }
339
340     private void closeEventSource(boolean waitingForCompletion) {
341         SseEventSource localEventSource = eventSource;
342         if (localEventSource != null) {
343             if (!localEventSource.isOpen() || completed) {
344                 logger.debug("Existing EventSource is already closed");
345             } else if (localEventSource.close(waitingForCompletion ? 10 : 0, TimeUnit.SECONDS)) {
346                 logger.debug("Succesfully closed existing EventSource");
347             } else {
348                 logger.debug("Failed to close existing EventSource");
349             }
350             eventSource = null;
351         }
352         connected = false;
353     }
354
355     public boolean addStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
356         return listeners.add(listener);
357     }
358
359     public boolean removeStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
360         return listeners.remove(listener);
361     }
362
363     public boolean addItemsDataListener(RemoteopenhabItemsDataListener listener) {
364         return itemsListeners.add(listener);
365     }
366
367     public boolean removeItemsDataListener(RemoteopenhabItemsDataListener listener) {
368         return itemsListeners.remove(listener);
369     }
370
371     public boolean addThingsDataListener(RemoteopenhabThingsDataListener listener) {
372         return thingsListeners.add(listener);
373     }
374
375     public boolean removeThingsDataListener(RemoteopenhabThingsDataListener listener) {
376         return thingsListeners.remove(listener);
377     }
378
379     public long getLastEventTimestamp() {
380         return lastEventTimestamp;
381     }
382
383     private void onEvent(InboundSseEvent inboundEvent) {
384         String name = inboundEvent.getName();
385         String data = inboundEvent.readData();
386         logger.trace("Received event name {} date {}", name, data);
387
388         lastEventTimestamp = System.currentTimeMillis();
389         if (!connected) {
390             logger.debug("Connected to streaming events");
391             connected = true;
392             listeners.forEach(listener -> listener.onConnected());
393         }
394
395         if (!"message".equals(name)) {
396             logger.debug("Received unhandled event with name '{}' and data '{}'", name, data);
397             return;
398         }
399
400         try {
401             RemoteopenhabEvent event = jsonParser.fromJson(data, RemoteopenhabEvent.class);
402             String itemName;
403             String thingUID;
404             RemoteopenhabEventPayload payload;
405             RemoteopenhabItem item;
406             RemoteopenhabThing thing;
407             switch (event.type) {
408                 case "ItemStateEvent":
409                     itemName = extractItemNameFromTopic(event.topic, event.type, "state");
410                     payload = jsonParser.fromJson(event.payload, RemoteopenhabEventPayload.class);
411                     itemsListeners.forEach(
412                             listener -> listener.onItemStateEvent(itemName, payload.type, payload.value, false));
413                     break;
414                 case "ItemStateChangedEvent":
415                     itemName = extractItemNameFromTopic(event.topic, event.type, "statechanged");
416                     payload = jsonParser.fromJson(event.payload, RemoteopenhabEventPayload.class);
417                     itemsListeners.forEach(
418                             listener -> listener.onItemStateEvent(itemName, payload.type, payload.value, true));
419                     break;
420                 case "GroupItemStateChangedEvent":
421                     itemName = extractItemNameFromTopic(event.topic, event.type, "statechanged");
422                     payload = jsonParser.fromJson(event.payload, RemoteopenhabEventPayload.class);
423                     itemsListeners.forEach(
424                             listener -> listener.onItemStateEvent(itemName, payload.type, payload.value, false));
425                     break;
426                 case "ItemAddedEvent":
427                     itemName = extractItemNameFromTopic(event.topic, event.type, "added");
428                     item = Objects.requireNonNull(jsonParser.fromJson(event.payload, RemoteopenhabItem.class));
429                     itemsListeners.forEach(listener -> listener.onItemAdded(item));
430                     break;
431                 case "ItemRemovedEvent":
432                     itemName = extractItemNameFromTopic(event.topic, event.type, "removed");
433                     item = Objects.requireNonNull(jsonParser.fromJson(event.payload, RemoteopenhabItem.class));
434                     itemsListeners.forEach(listener -> listener.onItemRemoved(item));
435                     break;
436                 case "ItemUpdatedEvent":
437                     itemName = extractItemNameFromTopic(event.topic, event.type, "updated");
438                     RemoteopenhabItem[] updItem = jsonParser.fromJson(event.payload, RemoteopenhabItem[].class);
439                     if (updItem.length == 2) {
440                         itemsListeners.forEach(listener -> listener.onItemUpdated(updItem[0], updItem[1]));
441                     } else {
442                         logger.debug("Invalid payload for event type {} for topic {}", event.type, event.topic);
443                     }
444                     break;
445                 case "ThingStatusInfoChangedEvent":
446                     thingUID = extractThingUIDFromTopic(event.topic, event.type, "statuschanged");
447                     RemoteopenhabStatusInfo[] updStatus = jsonParser.fromJson(event.payload,
448                             RemoteopenhabStatusInfo[].class);
449                     if (updStatus.length == 2) {
450                         thingsListeners.forEach(listener -> listener.onThingStatusUpdated(thingUID, updStatus[0]));
451                     } else {
452                         logger.debug("Invalid payload for event type {} for topic {}", event.type, event.topic);
453                     }
454                     break;
455                 case "ThingAddedEvent":
456                     thingUID = extractThingUIDFromTopic(event.topic, event.type, "added");
457                     thing = Objects.requireNonNull(jsonParser.fromJson(event.payload, RemoteopenhabThing.class));
458                     thingsListeners.forEach(listener -> listener.onThingAdded(thing));
459                     break;
460                 case "ThingRemovedEvent":
461                     thingUID = extractThingUIDFromTopic(event.topic, event.type, "removed");
462                     thing = Objects.requireNonNull(jsonParser.fromJson(event.payload, RemoteopenhabThing.class));
463                     thingsListeners.forEach(listener -> listener.onThingRemoved(thing));
464                     break;
465                 case "ChannelTriggeredEvent":
466                     RemoteopenhabChannelTriggerEvent triggerEvent = jsonParser.fromJson(event.payload,
467                             RemoteopenhabChannelTriggerEvent.class);
468                     thingsListeners
469                             .forEach(listener -> listener.onChannelTriggered(triggerEvent.channel, triggerEvent.event));
470                     break;
471                 case "ItemStatePredictedEvent":
472                 case "ItemCommandEvent":
473                 case "ThingStatusInfoEvent":
474                 case "ThingUpdatedEvent":
475                     logger.trace("Ignored event type {} for topic {}", event.type, event.topic);
476                     break;
477                 default:
478                     logger.debug("Unexpected event type {} for topic {}", event.type, event.topic);
479                     break;
480             }
481         } catch (RemoteopenhabException | JsonSyntaxException e) {
482             logger.debug("An exception occurred while processing the inbound '{}' event containg data: {}", name, data,
483                     e);
484         }
485     }
486
487     private void onComplete() {
488         logger.debug("Disconnected from streaming events");
489         completed = true;
490         listeners.forEach(listener -> listener.onDisconnected());
491     }
492
493     private void onError(Throwable error) {
494         logger.debug("Error occurred while receiving events", error);
495         listeners.forEach(listener -> listener.onError("Error occurred while receiving events"));
496     }
497
498     private String extractItemNameFromTopic(String topic, String eventType, String finalPart)
499             throws RemoteopenhabException {
500         String[] parts = topic.split("/");
501         int expectedNbParts = "GroupItemStateChangedEvent".equals(eventType) ? 5 : 4;
502         if (parts.length != expectedNbParts || !getTopicNamespace().equals(parts[0]) || !"items".equals(parts[1])
503                 || !finalPart.equals(parts[parts.length - 1])) {
504             throw new RemoteopenhabException("Invalid event topic " + topic + " for event type " + eventType);
505         }
506         return parts[2];
507     }
508
509     private String extractThingUIDFromTopic(String topic, String eventType, String finalPart)
510             throws RemoteopenhabException {
511         String[] parts = topic.split("/");
512         int expectedNbParts = 4;
513         if (parts.length != expectedNbParts || !getTopicNamespace().equals(parts[0]) || !"things".equals(parts[1])
514                 || !finalPart.equals(parts[parts.length - 1])) {
515             throw new RemoteopenhabException("Invalid event topic " + topic + " for event type " + eventType);
516         }
517         return parts[2];
518     }
519
520     public String executeGetUrl(String url, String acceptHeader, boolean provideAccessToken, boolean asyncReading)
521             throws RemoteopenhabException {
522         return executeUrl(HttpMethod.GET, url, acceptHeader, null, null, provideAccessToken, asyncReading, true);
523     }
524
525     public String executeUrl(HttpMethod httpMethod, String url, String acceptHeader, @Nullable String content,
526             @Nullable String contentType, boolean provideAccessToken, boolean asyncReading, boolean retryIfEOF)
527             throws RemoteopenhabException {
528         final Request request = httpClient.newRequest(url).method(httpMethod)
529                 .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).followRedirects(false)
530                 .header(HttpHeaders.ACCEPT, acceptHeader);
531
532         if (url.startsWith("https:") || authenticateAnyway) {
533             boolean useAlternativeHeader = false;
534             if (!credentialToken.isEmpty()) {
535                 request.header(HttpHeaders.AUTHORIZATION, "Basic " + credentialToken);
536                 useAlternativeHeader = true;
537             }
538             if (provideAccessToken && !accessToken.isEmpty()) {
539                 if (useAlternativeHeader) {
540                     request.header("X-OPENHAB-TOKEN", accessToken);
541                 } else {
542                     request.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
543                 }
544             }
545         }
546
547         if (content != null && (HttpMethod.POST.equals(httpMethod) || HttpMethod.PUT.equals(httpMethod))
548                 && contentType != null) {
549             request.content(new StringContentProvider(content), contentType);
550         }
551
552         logger.debug("Request {} {}", request.getMethod(), request.getURI());
553
554         try {
555             if (asyncReading) {
556                 InputStreamResponseListener listener = new InputStreamResponseListener();
557                 request.send(listener);
558                 Response response = listener.get(5, TimeUnit.SECONDS);
559                 int statusCode = response.getStatus();
560                 if (statusCode != HttpStatus.OK_200) {
561                     response.abort(new Exception(response.getReason()));
562                     String statusLine = statusCode + " " + response.getReason();
563                     throw new RemoteopenhabException("HTTP call failed: " + statusLine);
564                 }
565                 ByteArrayOutputStream responseContent = new ByteArrayOutputStream();
566                 try (InputStream input = listener.getInputStream()) {
567                     input.transferTo(responseContent);
568                 }
569                 return new String(responseContent.toByteArray(), StandardCharsets.UTF_8.name());
570             } else {
571                 ContentResponse response = request.send();
572                 int statusCode = response.getStatus();
573                 if (statusCode == HttpStatus.MOVED_PERMANENTLY_301 || statusCode == HttpStatus.FOUND_302) {
574                     String locationHeader = response.getHeaders().get(HttpHeaders.LOCATION);
575                     if (locationHeader != null && !locationHeader.isBlank()) {
576                         logger.debug("The remopte server redirected the request to this URL: {}", locationHeader);
577                         return executeUrl(httpMethod, locationHeader, acceptHeader, content, contentType,
578                                 provideAccessToken, asyncReading, retryIfEOF);
579                     } else {
580                         String statusLine = statusCode + " " + response.getReason();
581                         throw new RemoteopenhabException("HTTP call failed: " + statusLine);
582                     }
583                 } else if (statusCode >= HttpStatus.BAD_REQUEST_400) {
584                     String statusLine = statusCode + " " + response.getReason();
585                     throw new RemoteopenhabException("HTTP call failed: " + statusLine);
586                 }
587                 String encoding = response.getEncoding() != null ? response.getEncoding().replaceAll("\"", "").trim()
588                         : StandardCharsets.UTF_8.name();
589                 return new String(response.getContent(), encoding);
590             }
591         } catch (RemoteopenhabException e) {
592             throw e;
593         } catch (ExecutionException e) {
594             // After a long network outage, the first HTTP request will fail with an EOFException exception.
595             // We retry the request a second time in this case.
596             Throwable cause = e.getCause();
597             if (retryIfEOF && cause instanceof EOFException) {
598                 logger.debug("EOFException - retry the request");
599                 return executeUrl(httpMethod, url, acceptHeader, content, contentType, provideAccessToken, asyncReading,
600                         false);
601             } else {
602                 throw new RemoteopenhabException(e);
603             }
604         } catch (IOException | TimeoutException e) {
605             throw new RemoteopenhabException(e);
606         } catch (InterruptedException e) {
607             Thread.currentThread().interrupt();
608             throw new RemoteopenhabException(e);
609         }
610     }
611 }