]> git.basschouten.com Git - openhab-addons.git/blob
3c98821df7a08d85ee0eb4fbf6fde160a3a8b302
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.wolfsmartset.internal.api;
14
15 import java.net.URLEncoder;
16 import java.nio.charset.StandardCharsets;
17 import java.time.Instant;
18 import java.time.LocalDateTime;
19 import java.time.format.DateTimeFormatter;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Map.Entry;
26 import java.util.concurrent.CancellationException;
27 import java.util.concurrent.CompletableFuture;
28 import java.util.concurrent.ExecutionException;
29 import java.util.concurrent.LinkedBlockingQueue;
30 import java.util.concurrent.RejectedExecutionException;
31 import java.util.concurrent.ScheduledExecutorService;
32 import java.util.concurrent.ScheduledFuture;
33 import java.util.concurrent.TimeUnit;
34 import java.util.concurrent.TimeoutException;
35
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.eclipse.jetty.client.HttpClient;
39 import org.eclipse.jetty.client.HttpResponseException;
40 import org.eclipse.jetty.client.api.ContentResponse;
41 import org.eclipse.jetty.client.api.Request;
42 import org.eclipse.jetty.client.util.StringContentProvider;
43 import org.eclipse.jetty.http.HttpHeader;
44 import org.eclipse.jetty.http.HttpMethod;
45 import org.eclipse.jetty.http.HttpStatus;
46 import org.openhab.binding.wolfsmartset.internal.dto.CreateSession2DTO;
47 import org.openhab.binding.wolfsmartset.internal.dto.GetGuiDescriptionForGatewayDTO;
48 import org.openhab.binding.wolfsmartset.internal.dto.GetParameterValuesDTO;
49 import org.openhab.binding.wolfsmartset.internal.dto.GetSystemListDTO;
50 import org.openhab.binding.wolfsmartset.internal.dto.GetSystemStateListDTO;
51 import org.openhab.binding.wolfsmartset.internal.dto.LoginResponseDTO;
52 import org.openhab.binding.wolfsmartset.internal.dto.ReadFaultMessagesDTO;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 import com.google.gson.Gson;
57 import com.google.gson.GsonBuilder;
58 import com.google.gson.JsonArray;
59 import com.google.gson.JsonElement;
60 import com.google.gson.JsonObject;
61 import com.google.gson.JsonParseException;
62 import com.google.gson.JsonSyntaxException;
63
64 /**
65  * The {@link WolfSmartsetApi} class is used for connecting to the Wolf Smartset cloud service
66  *
67  * @author Bo Biene - Initial contribution
68  */
69 @NonNullByDefault
70 public class WolfSmartsetApi {
71     private static final int MAX_QUEUE_SIZE = 1000; // maximum queue size
72     private static final int REQUEST_TIMEOUT_SECONDS = 10;
73
74     private static final DateTimeFormatter SESSION_TIME_STAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
75     private static final String WOLF_API_URL = "https://www.wolf-smartset.com/portal/";
76
77     private Instant blockRequestsUntil = Instant.now();
78     private String username;
79     private String password;
80     private String serviceToken = "";
81     private @Nullable CreateSession2DTO session = null;
82     private int loginFailedCounter = 0;
83     private HttpClient httpClient;
84     private int delay = 500; // in ms
85     private final ScheduledExecutorService scheduler;
86     private final Gson gson = new GsonBuilder().serializeNulls().create();
87     private final LinkedBlockingQueue<RequestQueueEntry> requestQueue = new LinkedBlockingQueue<>(MAX_QUEUE_SIZE);
88     private @Nullable ScheduledFuture<?> processJob;
89
90     private final Logger logger = LoggerFactory.getLogger(WolfSmartsetApi.class);
91
92     public WolfSmartsetApi(String username, String password, HttpClient httpClient, ScheduledExecutorService scheduler)
93             throws WolfSmartsetCloudException {
94         this.username = username;
95         this.password = password;
96         this.httpClient = httpClient;
97         this.scheduler = scheduler;
98         if (!checkCredentials()) {
99             throw new WolfSmartsetCloudException("username or password can't be empty");
100         }
101     }
102
103     /**
104      * Validate Login to wolf smartset. Returns true if valid token is available, otherwise tries to authenticate with
105      * wolf smartset portal
106      */
107     public synchronized boolean login() {
108         if (!checkCredentials()) {
109             return false;
110         }
111         if (!serviceToken.isEmpty()) {
112             return true;
113         }
114         logger.debug("Wolf Smartset login with username {}", username);
115         try {
116             loginRequest();
117             loginFailedCounter = 0;
118             this.session = getCreateSession();
119             if (this.session != null) {
120                 logger.debug("login successful, browserSessionId {}", session.getBrowserSessionId());
121                 return true;
122             } else {
123                 loginFailedCounter++;
124                 this.session = null;
125                 logger.trace("Login succeeded but failed to create session {}", loginFailedCounter);
126                 return false;
127             }
128         } catch (WolfSmartsetCloudException e) {
129             logger.debug("Error logging on to Wolf Smartset ({}): {}", loginFailedCounter, e.getMessage());
130             loginFailedCounter++;
131             serviceToken = "";
132             loginFailedCounterCheck();
133             return false;
134         }
135     }
136
137     /**
138      * Request the systems available for the authenticated account
139      * 
140      * @return a list of the available systems
141      */
142     public List<GetSystemListDTO> getSystems() {
143         final String response = getSystemString();
144         List<GetSystemListDTO> devicesList = new ArrayList<>();
145         try {
146             GetSystemListDTO[] cdl = gson.fromJson(response, GetSystemListDTO[].class);
147             if (cdl != null) {
148                 for (GetSystemListDTO system : cdl) {
149                     devicesList.add(system);
150                 }
151             }
152         } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
153             loginFailedCounter++;
154             logger.warn("Error while parsing devices: {}", e.getMessage());
155         }
156         return devicesList;
157     }
158
159     /**
160      * Request the description of the given system
161      * 
162      * @param systemId the id of the system
163      * @param gatewayId the id of the gateway the system relates to
164      * @return dto describing the requested system
165      */
166     public @Nullable GetGuiDescriptionForGatewayDTO getSystemDescription(Integer systemId, Integer gatewayId) {
167         final String response = getSystemDescriptionString(systemId, gatewayId);
168         GetGuiDescriptionForGatewayDTO deviceDescription = null;
169         try {
170             deviceDescription = gson.fromJson(response, GetGuiDescriptionForGatewayDTO.class);
171         } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
172             loginFailedCounter++;
173             logger.warn("Error while parsing device descriptions: {}", e.getMessage());
174         }
175         return deviceDescription;
176     }
177
178     /**
179      * Request the system state of the given systems
180      * 
181      * @param systems a list of {@link GetSystemListDTO}
182      * @return the {@link GetSystemStateListDTO} descibing the state of the given {@link GetSystemListDTO} items
183      */
184     public @Nullable GetSystemStateListDTO @Nullable [] getSystemState(Collection<@Nullable GetSystemListDTO> systems) {
185         final String response = getSystemStateString(systems);
186         GetSystemStateListDTO[] systemState = null;
187         try {
188             systemState = gson.fromJson(response, GetSystemStateListDTO[].class);
189         } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
190             loginFailedCounter++;
191             logger.warn("Error while parsing device descriptions: {}", e.getMessage());
192         }
193         if (systemState != null && systemState.length >= 1) {
194             return systemState;
195         } else {
196             return null;
197         }
198     }
199
200     /**
201      * Request the fault messages of the given system
202      * 
203      * @param systemId the id of the system
204      * @param gatewayId the id of the gateway the system relates to
205      * @return {@link ReadFaultMessagesDTO} containing the faultmessages
206      */
207     public @Nullable ReadFaultMessagesDTO getFaultMessages(Integer systemId, Integer gatewayId) {
208         final String response = getFaultMessagesString(systemId, gatewayId);
209         ReadFaultMessagesDTO faultMessages = null;
210         try {
211             faultMessages = gson.fromJson(response, ReadFaultMessagesDTO.class);
212         } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
213             loginFailedCounter++;
214             logger.warn("Error while parsing faultmessages: {}", e.getMessage());
215         }
216         return faultMessages;
217     }
218
219     /**
220      * Request the current values for a unit associated with the given system.
221      * if lastAccess is not null, only value changes newer than the given timestamp are returned
222      * 
223      * @param systemId the id of the system
224      * @param gatewayId the id of the gateway the system relates to
225      * @param bundleId the id of the Unit
226      * @param valueIdList list of the values to request
227      * @param lastAccess timestamp of the last valid value request
228      * @return {@link GetParameterValuesDTO} containing the requested values
229      */
230     public @Nullable GetParameterValuesDTO getGetParameterValues(Integer systemId, Integer gatewayId, Long bundleId,
231             List<Long> valueIdList, @Nullable Instant lastAccess) {
232         final String response = getGetParameterValuesString(systemId, gatewayId, bundleId, valueIdList, lastAccess);
233         GetParameterValuesDTO parameterValues = null;
234         try {
235             parameterValues = gson.fromJson(response, GetParameterValuesDTO.class);
236         } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
237             loginFailedCounter++;
238             logger.warn("Error while parsing device parameter values: {}", e.getMessage());
239         }
240         return parameterValues;
241     }
242
243     public void stopRequestQueue() {
244         try {
245             stopProcessJob();
246             requestQueue.forEach(queueEntry -> queueEntry.future.completeExceptionally(new CancellationException()));
247         } catch (Exception e) {
248             logger.debug("Error stopping request queue background processing:{}", e.getMessage(), e);
249         }
250     }
251
252     /**
253      * Set a new delay
254      *
255      * @param delay in ms between to requests
256      */
257     private void setDelay(int delay) {
258         if (delay < 0) {
259             throw new IllegalArgumentException("Delay needs to be larger or equal to zero");
260         }
261         this.delay = delay;
262         stopProcessJob();
263         if (delay != 0) {
264             processJob = scheduler.scheduleWithFixedDelay(() -> processQueue(), 0, delay, TimeUnit.MILLISECONDS);
265         }
266     }
267
268     private boolean checkCredentials() {
269         if (username.trim().isEmpty() || password.trim().isEmpty()) {
270             logger.debug("Wolf Smartset: username or password missing.");
271             return false;
272         }
273         return true;
274     }
275
276     private String getCreateSessionString() {
277         String resp = "";
278         try {
279             JsonObject json = new JsonObject();
280             json.addProperty("Timestamp", SESSION_TIME_STAMP.format(LocalDateTime.now()));
281             resp = requestPOST("api/portal/CreateSession2", json).get();
282             logger.trace("api/portal/CreateSession2 response: {}", resp);
283         } catch (InterruptedException | ExecutionException e) {
284             logger.warn("getSystemStateString failed with {}: {}", e.getCause(), e.getMessage());
285             loginFailedCounter++;
286         } catch (WolfSmartsetCloudException e) {
287             logger.debug("getSystemStateString failed with {}: {}", e.getCause(), e.getMessage());
288             loginFailedCounter++;
289         }
290
291         return resp;
292     }
293
294     private @Nullable CreateSession2DTO getCreateSession() {
295         final String response = getCreateSessionString();
296         CreateSession2DTO session = null;
297         try {
298             session = gson.fromJson(response, CreateSession2DTO.class);
299         } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
300             loginFailedCounter++;
301             logger.warn("getCreateSession failed with {}: {}", e.getCause(), e.getMessage());
302         }
303         return session;
304     }
305
306     private String getSystemString() {
307         String resp = "";
308         try {
309             resp = requestGET("api/portal/GetSystemList").get();
310             logger.trace("api/portal/GetSystemList response: {}", resp);
311         } catch (InterruptedException | ExecutionException | WolfSmartsetCloudException e) {
312             logger.warn("getSystemString failed with {}: {}", e.getCause(), e.getMessage());
313             loginFailedCounter++;
314         }
315         return resp;
316     }
317
318     private String getSystemDescriptionString(Integer systemId, Integer gatewayId) {
319         String resp = "";
320         try {
321             Map<String, String> params = new HashMap<String, String>();
322             params.put("SystemId", systemId.toString());
323             params.put("GatewayId", gatewayId.toString());
324             resp = requestGET("api/portal/GetGuiDescriptionForGateway", params).get();
325             logger.trace("api/portal/GetGuiDescriptionForGateway response: {}", resp);
326         } catch (InterruptedException | ExecutionException | WolfSmartsetCloudException e) {
327             logger.warn("getSystemDescriptionString failed with {}: {}", e.getCause(), e.getMessage());
328             loginFailedCounter++;
329         }
330         return resp;
331     }
332
333     private String getSystemStateString(Collection<@Nullable GetSystemListDTO> systems) {
334         String resp = "";
335         try {
336             JsonArray jsonSystemList = new JsonArray();
337
338             for (@Nullable
339             GetSystemListDTO system : systems) {
340                 if (system != null) {
341                     JsonObject jsonSystem = new JsonObject();
342                     jsonSystem.addProperty("SystemId", system.getId());
343                     jsonSystem.addProperty("GatewayId", system.getGatewayId());
344
345                     if (system.getSystemShareId() != null) {
346                         jsonSystem.addProperty("SystemShareId", system.getSystemShareId());
347                     }
348                     jsonSystemList.add(jsonSystem);
349                 }
350             }
351
352             JsonObject json = new JsonObject();
353
354             json.add("SystemList", jsonSystemList);
355             resp = requestPOST("api/portal/GetSystemStateList", json).get();
356             logger.trace("api/portal/GetSystemStateList response: {}", resp);
357         } catch (InterruptedException | ExecutionException | WolfSmartsetCloudException e) {
358             logger.warn("getSystemStateString failed with {}: {}", e.getCause(), e.getMessage());
359             loginFailedCounter++;
360         }
361         return resp;
362     }
363
364     private String getFaultMessagesString(Integer systemId, Integer gatewayId) {
365         String resp = "";
366         try {
367             JsonObject json = new JsonObject();
368
369             json.addProperty("SystemId", systemId);
370             json.addProperty("GatewayId", gatewayId);
371             resp = requestPOST("api/portal/ReadFaultMessages", json).get();
372             logger.trace("api/portal/ReadFaultMessages response: {}", resp);
373         } catch (InterruptedException | ExecutionException | WolfSmartsetCloudException e) {
374             logger.warn("getFaultMessagesString failed with {}: {}", e.getCause(), e.getMessage());
375             loginFailedCounter++;
376         }
377         return resp;
378     }
379
380     private String getGetParameterValuesString(Integer systemId, Integer gatewayId, Long bundleId,
381             List<Long> valueIdList, @Nullable Instant lastAccess) {
382         String resp = "";
383         try {
384             JsonObject json = new JsonObject();
385             json.addProperty("SystemId", systemId);
386             json.addProperty("GatewayId", gatewayId);
387             json.addProperty("BundleId", bundleId);
388             json.addProperty("IsSubBundle", false);
389             json.add("ValueIdList", gson.toJsonTree(valueIdList));
390             if (lastAccess != null) {
391                 json.addProperty("LastAccess", DateTimeFormatter.ISO_INSTANT.format(lastAccess));
392             } else {
393                 json.addProperty("LastAccess", (String) null);
394             }
395             json.addProperty("GuiIdChanged", false);
396             if (session != null) {
397                 json.addProperty("SessionId", session.getBrowserSessionId());
398             }
399             resp = requestPOST("api/portal/GetParameterValues", json).get();
400             logger.trace("api/portal/GetParameterValues response: {}", resp);
401         } catch (InterruptedException | ExecutionException | WolfSmartsetCloudException e) {
402             logger.warn("getGetParameterValuesString failed with {}: {}", e.getCause(), e.getMessage());
403             loginFailedCounter++;
404         }
405         return resp;
406     }
407
408     private CompletableFuture<String> requestGET(String url) throws WolfSmartsetCloudException {
409         return requestGET(url, new HashMap<String, String>());
410     }
411
412     private CompletableFuture<String> requestGET(String url, Map<String, String> params)
413             throws WolfSmartsetCloudException {
414         return rateLimtedRequest(() -> {
415             if (this.serviceToken.isEmpty()) {
416                 throw new WolfSmartsetCloudException("Cannot execute request. service token missing");
417             }
418             loginFailedCounterCheck();
419
420             var requestUrl = WOLF_API_URL + url;
421             Request request = httpClient.newRequest(requestUrl).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
422
423             // using HTTP GET with ContentType application/x-www-form-urlencoded like the iOS App does
424             request.header(HttpHeader.AUTHORIZATION, serviceToken);
425             request.method(HttpMethod.GET);
426             request.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
427
428             for (Entry<String, String> entry : params.entrySet()) {
429                 logger.debug("Send request param: {}={} to {}", entry.getKey(), entry.getValue().toString(), url);
430                 request.param(entry.getKey(), entry.getValue());
431             }
432
433             return request;
434         });
435     }
436
437     private CompletableFuture<String> requestPOST(String url, JsonElement json) throws WolfSmartsetCloudException {
438         return rateLimtedRequest(() -> {
439             if (this.serviceToken.isEmpty()) {
440                 throw new WolfSmartsetCloudException("Cannot execute request. service token missing");
441             }
442             loginFailedCounterCheck();
443
444             var request = createPOSTRequest(url, json);
445             request.header(HttpHeader.AUTHORIZATION, serviceToken);
446             return request;
447         });
448     }
449
450     private Request createPOSTRequest(String url, JsonElement json) {
451         var requestUrl = WOLF_API_URL + url;
452         Request request = httpClient.newRequest(requestUrl).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
453
454         request.header(HttpHeader.ACCEPT, "application/json");
455         request.header(HttpHeader.CONTENT_TYPE, "application/json");
456         request.method(HttpMethod.POST);
457
458         request.content(new StringContentProvider(json.toString()), "application/json");
459         return request;
460     }
461
462     private CompletableFuture<String> rateLimtedRequest(SupplyRequestFunctionalInterface buildRequest) {
463         // if no delay is set, return a completed CompletableFuture
464         CompletableFuture<String> future = new CompletableFuture<>();
465         RequestQueueEntry queueEntry = new RequestQueueEntry(buildRequest, future);
466
467         if (delay == 0) {
468             queueEntry.completeFuture((r) -> this.getResponse(r));
469         } else {
470             if (!requestQueue.offer(queueEntry)) {
471                 future.completeExceptionally(new RejectedExecutionException("Maximum queue size exceeded."));
472             }
473         }
474         return future;
475     }
476
477     private void stopProcessJob() {
478         ScheduledFuture<?> processJob = this.processJob;
479         if (processJob != null) {
480             processJob.cancel(false);
481             this.processJob = null;
482         }
483     }
484
485     private void processQueue() {
486         // No new Requests until blockRequestsUntil, is set when recieved HttpStatus.TOO_MANY_REQUESTS_429
487         if (blockRequestsUntil.isBefore(Instant.now())) {
488             RequestQueueEntry queueEntry = requestQueue.poll();
489             if (queueEntry != null) {
490                 queueEntry.completeFuture((r) -> this.getResponse(r));
491             }
492         }
493     }
494
495     @FunctionalInterface
496     interface SupplyRequestFunctionalInterface {
497         Request get() throws WolfSmartsetCloudException;
498     }
499
500     @FunctionalInterface
501     interface GetResponseFunctionalInterface {
502         String get(Request request) throws WolfSmartsetCloudException;
503     }
504
505     private String getResponse(Request request) throws WolfSmartsetCloudException {
506         try {
507             logger.debug("execute request {} {}", request.getMethod(), request.getURI());
508             final ContentResponse response = request.send();
509             if (response.getStatus() == HttpStatus.NOT_FOUND_404) {
510                 throw new WolfSmartsetCloudException("Invalid request, not found " + request.getURI());
511             } else if (response.getStatus() == HttpStatus.TOO_MANY_REQUESTS_429) {
512                 blockRequestsUntil = Instant.now().plusSeconds(30);
513                 throw new WolfSmartsetCloudException("Error too many requests: " + response.getContentAsString());
514             } else if (response.getStatus() >= HttpStatus.BAD_REQUEST_400
515                     && response.getStatus() < HttpStatus.INTERNAL_SERVER_ERROR_500) {
516                 this.serviceToken = "";
517                 logger.debug("Status {} while executing request to {} :{}", response.getStatus(), request.getURI(),
518                         response.getContentAsString());
519             } else {
520                 return response.getContentAsString();
521             }
522         } catch (HttpResponseException e) {
523             serviceToken = "";
524             logger.debug("Error while executing request to {} :{}", request.getURI(), e.getMessage());
525             loginFailedCounter++;
526         } catch (InterruptedException | TimeoutException | ExecutionException /* | IOException */ e) {
527             logger.debug("Error while executing request to {} :{}", request.getURI(), e.getMessage());
528             loginFailedCounter++;
529         }
530         return "";
531     }
532
533     void loginFailedCounterCheck() {
534         if (loginFailedCounter > 10) {
535             logger.debug("Repeated errors logging on to Wolf Smartset");
536             serviceToken = "";
537             loginFailedCounter = 0;
538         }
539     }
540
541     protected void loginRequest() throws WolfSmartsetCloudException {
542         try {
543             setDelay(delay);
544             logger.trace("Wolf Smartset Login");
545
546             String url = WOLF_API_URL + "connect/token";
547             Request request = httpClient.POST(url).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
548             request.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
549
550             // Building Request body exacly the way the iOS App did this
551             var encodedUser = URLEncoder.encode(username, StandardCharsets.UTF_8);
552             var encodedPassword = URLEncoder.encode(password, StandardCharsets.UTF_8);
553             var authRequestBody = "grant_type=password&username=" + encodedUser + "&password=" + encodedPassword;
554
555             request.content(new StringContentProvider("application/x-www-form-urlencoded", authRequestBody,
556                     StandardCharsets.UTF_8));
557
558             final ContentResponse response;
559             response = request.send();
560
561             final String content = response.getContentAsString();
562             logger.trace("Wolf smartset Login response= {}", response);
563             logger.trace("Wolf smartset Login content= {}", content);
564
565             switch (response.getStatus()) {
566                 case HttpStatus.FORBIDDEN_403:
567                     throw new WolfSmartsetCloudException(
568                             "Access denied. Did you set the correct password and/or username?");
569                 case HttpStatus.OK_200:
570                     LoginResponseDTO jsonResp = gson.fromJson(content, LoginResponseDTO.class);
571                     if (jsonResp == null) {
572                         throw new WolfSmartsetCloudException("Error getting logon details: " + content);
573                     }
574
575                     serviceToken = jsonResp.getTokenType() + " " + jsonResp.getAccessToken();
576
577                     logger.trace("Wolf Smartset login scope = {}", jsonResp.getScope());
578                     logger.trace("Wolf Smartset login expiresIn = {}", jsonResp.getExpiresIn());
579                     logger.trace("Wolf Smartset login tokenType = {}", jsonResp.getTokenType());
580                     return;
581                 default:
582                     logger.trace("request returned status '{}', reason: {}, content = {}", response.getStatus(),
583                             response.getReason(), response.getContentAsString());
584                     throw new WolfSmartsetCloudException(response.getStatus() + response.getReason());
585             }
586         } catch (InterruptedException | TimeoutException | ExecutionException | JsonParseException e) {
587             throw new WolfSmartsetCloudException("Cannot logon to Wolf Smartset cloud: " + e.getMessage(), e);
588         }
589     }
590
591     private static class RequestQueueEntry {
592         private SupplyRequestFunctionalInterface buildRequest;
593         private CompletableFuture<String> future;
594
595         public RequestQueueEntry(SupplyRequestFunctionalInterface buildRequest, CompletableFuture<String> future) {
596             this.buildRequest = buildRequest;
597             this.future = future;
598         }
599
600         public void completeFuture(GetResponseFunctionalInterface getResponse) {
601             try {
602                 String response = getResponse.get(this.buildRequest.get());
603                 future.complete(response);
604             } catch (WolfSmartsetCloudException e) {
605                 future.completeExceptionally(e);
606             }
607         }
608     }
609 }