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