]> git.basschouten.com Git - openhab-addons.git/blob
b6ebe5d1a330717bdc62657fd396f22e3acfb644
[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         Client client;
277         // Avoid a timeout exception after 1 minute by setting the read timeout to 0 (infinite)
278         if (trustedCertificate) {
279             client = clientBuilder.sslContext(httpClient.getSslContextFactory().getSslContext())
280                     .hostnameVerifier(new HostnameVerifier() {
281                         @Override
282                         public boolean verify(@Nullable String hostname, @Nullable SSLSession session) {
283                             return true;
284                         }
285                     }).readTimeout(0, TimeUnit.SECONDS)
286                     .register(new RemoteopenhabStreamingRequestFilter(credentialToken)).build();
287         } else {
288             client = clientBuilder.readTimeout(0, TimeUnit.SECONDS)
289                     .register(new RemoteopenhabStreamingRequestFilter(credentialToken)).build();
290         }
291         SseEventSource eventSource = eventSourceFactory.newSource(client.target(restSseUrl));
292         eventSource.register(this::onEvent, this::onError, this::onComplete);
293         return eventSource;
294     }
295
296     private void reopenEventSource() {
297         logger.debug("Reopening EventSource");
298
299         String url;
300         try {
301             url = String.format("%s?topics=%s/items/*/*,%s/things/*/*,%s/channels/*/triggered", getRestApiUrl("events"),
302                     getTopicNamespace(), getTopicNamespace(), getTopicNamespace());
303         } catch (RemoteopenhabException e) {
304             logger.debug("{}", e.getMessage());
305             return;
306         }
307
308         closeEventSource(true);
309
310         logger.debug("Opening new EventSource {}", url);
311         SseEventSource localEventSource = createEventSource(url);
312         localEventSource.open();
313
314         eventSource = localEventSource;
315     }
316
317     private void closeEventSource(boolean waitingForCompletion) {
318         SseEventSource localEventSource = eventSource;
319         if (localEventSource != null) {
320             if (!localEventSource.isOpen() || completed) {
321                 logger.debug("Existing EventSource is already closed");
322             } else if (localEventSource.close(waitingForCompletion ? 10 : 0, TimeUnit.SECONDS)) {
323                 logger.debug("Succesfully closed existing EventSource");
324             } else {
325                 logger.debug("Failed to close existing EventSource");
326             }
327             eventSource = null;
328         }
329         connected = false;
330     }
331
332     public boolean addStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
333         return listeners.add(listener);
334     }
335
336     public boolean removeStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
337         return listeners.remove(listener);
338     }
339
340     public boolean addItemsDataListener(RemoteopenhabItemsDataListener listener) {
341         return itemsListeners.add(listener);
342     }
343
344     public boolean removeItemsDataListener(RemoteopenhabItemsDataListener listener) {
345         return itemsListeners.remove(listener);
346     }
347
348     public boolean addThingsDataListener(RemoteopenhabThingsDataListener listener) {
349         return thingsListeners.add(listener);
350     }
351
352     public boolean removeThingsDataListener(RemoteopenhabThingsDataListener listener) {
353         return thingsListeners.remove(listener);
354     }
355
356     public long getLastEventTimestamp() {
357         return lastEventTimestamp;
358     }
359
360     private void onEvent(InboundSseEvent inboundEvent) {
361         String name = inboundEvent.getName();
362         String data = inboundEvent.readData();
363         logger.trace("Received event name {} date {}", name, data);
364
365         lastEventTimestamp = System.currentTimeMillis();
366         if (!connected) {
367             logger.debug("Connected to streaming events");
368             connected = true;
369             listeners.forEach(listener -> listener.onConnected());
370         }
371
372         if (!"message".equals(name)) {
373             logger.debug("Received unhandled event with name '{}' and data '{}'", name, data);
374             return;
375         }
376
377         try {
378             RemoteopenhabEvent event = jsonParser.fromJson(data, RemoteopenhabEvent.class);
379             String itemName;
380             String thingUID;
381             RemoteopenhabEventPayload payload;
382             RemoteopenhabItem item;
383             RemoteopenhabThing thing;
384             switch (event.type) {
385                 case "ItemStateEvent":
386                     itemName = extractItemNameFromTopic(event.topic, event.type, "state");
387                     payload = jsonParser.fromJson(event.payload, RemoteopenhabEventPayload.class);
388                     itemsListeners.forEach(
389                             listener -> listener.onItemStateEvent(itemName, payload.type, payload.value, false));
390                     break;
391                 case "ItemStateChangedEvent":
392                     itemName = extractItemNameFromTopic(event.topic, event.type, "statechanged");
393                     payload = jsonParser.fromJson(event.payload, RemoteopenhabEventPayload.class);
394                     itemsListeners.forEach(
395                             listener -> listener.onItemStateEvent(itemName, payload.type, payload.value, true));
396                     break;
397                 case "GroupItemStateChangedEvent":
398                     itemName = extractItemNameFromTopic(event.topic, event.type, "statechanged");
399                     payload = jsonParser.fromJson(event.payload, RemoteopenhabEventPayload.class);
400                     itemsListeners.forEach(
401                             listener -> listener.onItemStateEvent(itemName, payload.type, payload.value, false));
402                     break;
403                 case "ItemAddedEvent":
404                     itemName = extractItemNameFromTopic(event.topic, event.type, "added");
405                     item = Objects.requireNonNull(jsonParser.fromJson(event.payload, RemoteopenhabItem.class));
406                     itemsListeners.forEach(listener -> listener.onItemAdded(item));
407                     break;
408                 case "ItemRemovedEvent":
409                     itemName = extractItemNameFromTopic(event.topic, event.type, "removed");
410                     item = Objects.requireNonNull(jsonParser.fromJson(event.payload, RemoteopenhabItem.class));
411                     itemsListeners.forEach(listener -> listener.onItemRemoved(item));
412                     break;
413                 case "ItemUpdatedEvent":
414                     itemName = extractItemNameFromTopic(event.topic, event.type, "updated");
415                     RemoteopenhabItem[] updItem = jsonParser.fromJson(event.payload, RemoteopenhabItem[].class);
416                     if (updItem.length == 2) {
417                         itemsListeners.forEach(listener -> listener.onItemUpdated(updItem[0], updItem[1]));
418                     } else {
419                         logger.debug("Invalid payload for event type {} for topic {}", event.type, event.topic);
420                     }
421                     break;
422                 case "ThingStatusInfoChangedEvent":
423                     thingUID = extractThingUIDFromTopic(event.topic, event.type, "statuschanged");
424                     RemoteopenhabStatusInfo[] updStatus = jsonParser.fromJson(event.payload,
425                             RemoteopenhabStatusInfo[].class);
426                     if (updStatus.length == 2) {
427                         thingsListeners.forEach(listener -> listener.onThingStatusUpdated(thingUID, updStatus[0]));
428                     } else {
429                         logger.debug("Invalid payload for event type {} for topic {}", event.type, event.topic);
430                     }
431                     break;
432                 case "ThingAddedEvent":
433                     thingUID = extractThingUIDFromTopic(event.topic, event.type, "added");
434                     thing = Objects.requireNonNull(jsonParser.fromJson(event.payload, RemoteopenhabThing.class));
435                     thingsListeners.forEach(listener -> listener.onThingAdded(thing));
436                     break;
437                 case "ThingRemovedEvent":
438                     thingUID = extractThingUIDFromTopic(event.topic, event.type, "removed");
439                     thing = Objects.requireNonNull(jsonParser.fromJson(event.payload, RemoteopenhabThing.class));
440                     thingsListeners.forEach(listener -> listener.onThingRemoved(thing));
441                     break;
442                 case "ChannelTriggeredEvent":
443                     RemoteopenhabChannelTriggerEvent triggerEvent = jsonParser.fromJson(event.payload,
444                             RemoteopenhabChannelTriggerEvent.class);
445                     thingsListeners
446                             .forEach(listener -> listener.onChannelTriggered(triggerEvent.channel, triggerEvent.event));
447                     break;
448                 case "ItemStatePredictedEvent":
449                 case "ItemCommandEvent":
450                 case "ThingStatusInfoEvent":
451                 case "ThingUpdatedEvent":
452                     logger.trace("Ignored event type {} for topic {}", event.type, event.topic);
453                     break;
454                 default:
455                     logger.debug("Unexpected event type {} for topic {}", event.type, event.topic);
456                     break;
457             }
458         } catch (RemoteopenhabException | JsonSyntaxException e) {
459             logger.debug("An exception occurred while processing the inbound '{}' event containg data: {}", name, data,
460                     e);
461         }
462     }
463
464     private void onComplete() {
465         logger.debug("Disconnected from streaming events");
466         completed = true;
467         listeners.forEach(listener -> listener.onDisconnected());
468     }
469
470     private void onError(Throwable error) {
471         logger.debug("Error occurred while receiving events", error);
472         listeners.forEach(listener -> listener.onError("Error occurred while receiving events"));
473     }
474
475     private String extractItemNameFromTopic(String topic, String eventType, String finalPart)
476             throws RemoteopenhabException {
477         String[] parts = topic.split("/");
478         int expectedNbParts = "GroupItemStateChangedEvent".equals(eventType) ? 5 : 4;
479         if (parts.length != expectedNbParts || !getTopicNamespace().equals(parts[0]) || !"items".equals(parts[1])
480                 || !finalPart.equals(parts[parts.length - 1])) {
481             throw new RemoteopenhabException("Invalid event topic " + topic + " for event type " + eventType);
482         }
483         return parts[2];
484     }
485
486     private String extractThingUIDFromTopic(String topic, String eventType, String finalPart)
487             throws RemoteopenhabException {
488         String[] parts = topic.split("/");
489         int expectedNbParts = 4;
490         if (parts.length != expectedNbParts || !getTopicNamespace().equals(parts[0]) || !"things".equals(parts[1])
491                 || !finalPart.equals(parts[parts.length - 1])) {
492             throw new RemoteopenhabException("Invalid event topic " + topic + " for event type " + eventType);
493         }
494         return parts[2];
495     }
496
497     public String executeGetUrl(String url, String acceptHeader, boolean provideAccessToken, boolean asyncReading)
498             throws RemoteopenhabException {
499         return executeUrl(HttpMethod.GET, url, acceptHeader, null, null, provideAccessToken, asyncReading, true);
500     }
501
502     public String executeUrl(HttpMethod httpMethod, String url, String acceptHeader, @Nullable String content,
503             @Nullable String contentType, boolean provideAccessToken, boolean asyncReading, boolean retryIfEOF)
504             throws RemoteopenhabException {
505         final Request request = httpClient.newRequest(url).method(httpMethod)
506                 .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).followRedirects(false)
507                 .header(HttpHeaders.ACCEPT, acceptHeader);
508
509         if (url.startsWith("https:") || authenticateAnyway) {
510             boolean useAlternativeHeader = false;
511             if (!credentialToken.isEmpty()) {
512                 request.header(HttpHeaders.AUTHORIZATION, "Basic " + credentialToken);
513                 useAlternativeHeader = true;
514             }
515             if (provideAccessToken && !accessToken.isEmpty()) {
516                 if (useAlternativeHeader) {
517                     request.header("X-OPENHAB-TOKEN", accessToken);
518                 } else {
519                     request.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
520                 }
521             }
522         }
523
524         if (content != null && (HttpMethod.POST.equals(httpMethod) || HttpMethod.PUT.equals(httpMethod))
525                 && contentType != null) {
526             request.content(new StringContentProvider(content), contentType);
527         }
528
529         logger.debug("Request {} {}", request.getMethod(), request.getURI());
530
531         try {
532             if (asyncReading) {
533                 InputStreamResponseListener listener = new InputStreamResponseListener();
534                 request.send(listener);
535                 Response response = listener.get(5, TimeUnit.SECONDS);
536                 int statusCode = response.getStatus();
537                 if (statusCode != HttpStatus.OK_200) {
538                     response.abort(new Exception(response.getReason()));
539                     String statusLine = statusCode + " " + response.getReason();
540                     throw new RemoteopenhabException("HTTP call failed: " + statusLine);
541                 }
542                 ByteArrayOutputStream responseContent = new ByteArrayOutputStream();
543                 try (InputStream input = listener.getInputStream()) {
544                     input.transferTo(responseContent);
545                 }
546                 return new String(responseContent.toByteArray(), StandardCharsets.UTF_8.name());
547             } else {
548                 ContentResponse response = request.send();
549                 int statusCode = response.getStatus();
550                 if (statusCode == HttpStatus.MOVED_PERMANENTLY_301 || statusCode == HttpStatus.FOUND_302) {
551                     String locationHeader = response.getHeaders().get(HttpHeaders.LOCATION);
552                     if (locationHeader != null && !locationHeader.isBlank()) {
553                         logger.debug("The remopte server redirected the request to this URL: {}", locationHeader);
554                         return executeUrl(httpMethod, locationHeader, acceptHeader, content, contentType,
555                                 provideAccessToken, asyncReading, retryIfEOF);
556                     } else {
557                         String statusLine = statusCode + " " + response.getReason();
558                         throw new RemoteopenhabException("HTTP call failed: " + statusLine);
559                     }
560                 } else if (statusCode >= HttpStatus.BAD_REQUEST_400) {
561                     String statusLine = statusCode + " " + response.getReason();
562                     throw new RemoteopenhabException("HTTP call failed: " + statusLine);
563                 }
564                 String encoding = response.getEncoding() != null ? response.getEncoding().replaceAll("\"", "").trim()
565                         : StandardCharsets.UTF_8.name();
566                 return new String(response.getContent(), encoding);
567             }
568         } catch (RemoteopenhabException e) {
569             throw e;
570         } catch (ExecutionException e) {
571             // After a long network outage, the first HTTP request will fail with an EOFException exception.
572             // We retry the request a second time in this case.
573             Throwable cause = e.getCause();
574             if (retryIfEOF && cause instanceof EOFException) {
575                 logger.debug("EOFException - retry the request");
576                 return executeUrl(httpMethod, url, acceptHeader, content, contentType, provideAccessToken, asyncReading,
577                         false);
578             } else {
579                 throw new RemoteopenhabException(e);
580             }
581         } catch (IOException | TimeoutException e) {
582             throw new RemoteopenhabException(e);
583         } catch (InterruptedException e) {
584             Thread.currentThread().interrupt();
585             throw new RemoteopenhabException(e);
586         }
587     }
588 }