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