]> git.basschouten.com Git - openhab-addons.git/blob
c71acdfdc143987ce2543399d6caf0140eb2fbae
[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.somfytahoma.internal.handler;
14
15 import static org.openhab.binding.somfytahoma.internal.SomfyTahomaBindingConstants.*;
16
17 import java.io.UnsupportedEncodingException;
18 import java.net.URLEncoder;
19 import java.nio.charset.StandardCharsets;
20 import java.util.*;
21 import java.util.concurrent.ConcurrentLinkedQueue;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.TimeoutException;
26
27 import org.apache.commons.lang.StringUtils;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.eclipse.jetty.client.api.ContentResponse;
32 import org.eclipse.jetty.client.api.Request;
33 import org.eclipse.jetty.client.util.StringContentProvider;
34 import org.eclipse.jetty.http.HttpHeader;
35 import org.eclipse.jetty.http.HttpMethod;
36 import org.openhab.binding.somfytahoma.internal.config.SomfyTahomaConfig;
37 import org.openhab.binding.somfytahoma.internal.discovery.SomfyTahomaItemDiscoveryService;
38 import org.openhab.binding.somfytahoma.internal.model.*;
39 import org.openhab.core.io.net.http.HttpClientFactory;
40 import org.openhab.core.thing.*;
41 import org.openhab.core.thing.binding.BaseBridgeHandler;
42 import org.openhab.core.thing.binding.ThingHandlerService;
43 import org.openhab.core.types.Command;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 import com.google.gson.Gson;
48 import com.google.gson.JsonElement;
49 import com.google.gson.JsonSyntaxException;
50
51 /**
52  * The {@link SomfyTahomaBridgeHandler} is responsible for handling commands, which are
53  * sent to one of the channels.
54  *
55  * @author Ondrej Pecta - Initial contribution
56  */
57 @NonNullByDefault
58 public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
59
60     private final Logger logger = LoggerFactory.getLogger(SomfyTahomaBridgeHandler.class);
61
62     /**
63      * The shared HttpClient
64      */
65     private final HttpClient httpClient;
66
67     /**
68      * Future to poll for updates
69      */
70     private @Nullable ScheduledFuture<?> pollFuture;
71
72     /**
73      * Future to poll for status
74      */
75     private @Nullable ScheduledFuture<?> statusFuture;
76
77     /**
78      * Future to set reconciliation flag
79      */
80     private @Nullable ScheduledFuture<?> reconciliationFuture;
81
82     // List of futures used for command retries
83     private Collection<ScheduledFuture<?>> retryFutures = new ConcurrentLinkedQueue<ScheduledFuture<?>>();
84
85     /**
86      * List of executions
87      */
88     private Map<String, String> executions = new HashMap<>();
89
90     // Too many requests flag
91     private boolean tooManyRequests = false;
92
93     // Silent relogin flag
94     private boolean reLoginNeeded = false;
95
96     // Reconciliation flag
97     private boolean reconciliation = false;
98
99     /**
100      * Our configuration
101      */
102     protected SomfyTahomaConfig thingConfig = new SomfyTahomaConfig();
103
104     /**
105      * Id of registered events
106      */
107     private String eventsId = "";
108
109     // Gson & parser
110     private final Gson gson = new Gson();
111
112     public SomfyTahomaBridgeHandler(Bridge thing, HttpClientFactory httpClientFactory) {
113         super(thing);
114         this.httpClient = httpClientFactory.createHttpClient("somfy_" + thing.getUID().getId());
115     }
116
117     @Override
118     public void handleCommand(ChannelUID channelUID, Command command) {
119     }
120
121     @Override
122     public void initialize() {
123         thingConfig = getConfigAs(SomfyTahomaConfig.class);
124
125         try {
126             httpClient.start();
127         } catch (Exception e) {
128             logger.debug("Cannot start http client for: {}", thing.getBridgeUID().getId(), e);
129             return;
130         }
131
132         scheduler.execute(() -> {
133             login();
134             initPolling();
135             logger.debug("Initialize done...");
136         });
137     }
138
139     /**
140      * starts this things polling future
141      */
142     private void initPolling() {
143         stopPolling();
144         pollFuture = scheduler.scheduleWithFixedDelay(() -> {
145             getTahomaUpdates();
146         }, 10, thingConfig.getRefresh(), TimeUnit.SECONDS);
147
148         statusFuture = scheduler.scheduleWithFixedDelay(() -> {
149             refreshTahomaStates();
150         }, 60, thingConfig.getStatusTimeout(), TimeUnit.SECONDS);
151
152         reconciliationFuture = scheduler.scheduleWithFixedDelay(() -> {
153             enableReconciliation();
154         }, RECONCILIATION_TIME, RECONCILIATION_TIME, TimeUnit.SECONDS);
155     }
156
157     public synchronized void login() {
158         String url;
159
160         if (StringUtils.isEmpty(thingConfig.getEmail()) || StringUtils.isEmpty(thingConfig.getPassword())) {
161             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
162                     "Can not access device as username and/or password are null");
163             return;
164         }
165
166         if (tooManyRequests) {
167             logger.debug("Skipping login due to too many requests");
168             return;
169         }
170
171         if (ThingStatus.ONLINE == thing.getStatus() && !reLoginNeeded) {
172             logger.debug("No need to log in, because already logged in");
173             return;
174         }
175
176         reLoginNeeded = false;
177
178         try {
179             url = TAHOMA_API_URL + "login";
180             String urlParameters = "userId=" + urlEncode(thingConfig.getEmail()) + "&userPassword="
181                     + urlEncode(thingConfig.getPassword());
182
183             ContentResponse response = sendRequestBuilder(url, HttpMethod.POST)
184                     .content(new StringContentProvider(urlParameters),
185                             "application/x-www-form-urlencoded; charset=UTF-8")
186                     .send();
187
188             if (logger.isTraceEnabled()) {
189                 logger.trace("Login response: {}", response.getContentAsString());
190             }
191
192             SomfyTahomaLoginResponse data = gson.fromJson(response.getContentAsString(),
193                     SomfyTahomaLoginResponse.class);
194             if (data.isSuccess()) {
195                 logger.debug("SomfyTahoma version: {}", data.getVersion());
196                 String id = registerEvents();
197                 if (id != null && !id.equals(UNAUTHORIZED)) {
198                     eventsId = id;
199                     logger.debug("Events id: {}", eventsId);
200                     updateStatus(ThingStatus.ONLINE);
201                 } else {
202                     logger.debug("Events id error: {}", id);
203                 }
204             } else {
205                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
206                         "Error logging in: " + data.getError());
207                 if (data.getError().startsWith(TOO_MANY_REQUESTS)) {
208                     setTooManyRequests();
209                 }
210             }
211         } catch (JsonSyntaxException e) {
212             logger.debug("Received invalid data", e);
213             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
214         } catch (InterruptedException | ExecutionException | TimeoutException e) {
215             if (e instanceof ExecutionException) {
216                 if (e.getMessage().contains(AUTHENTICATION_CHALLENGE)) {
217                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
218                             "Authentication challenge");
219                     setTooManyRequests();
220                     return;
221                 }
222             }
223             logger.debug("Cannot get login cookie!", e);
224             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot get login cookie");
225             if (e instanceof InterruptedException) {
226                 Thread.currentThread().interrupt();
227             }
228         }
229     }
230
231     private void setTooManyRequests() {
232         logger.debug("Too many requests error, suspending activity for {} seconds", SUSPEND_TIME);
233         tooManyRequests = true;
234         scheduler.schedule(this::enableLogin, SUSPEND_TIME, TimeUnit.SECONDS);
235     }
236
237     private @Nullable String registerEvents() {
238         SomfyTahomaRegisterEventsResponse response = invokeCallToURL(TAHOMA_EVENTS_URL + "register", "",
239                 HttpMethod.POST, SomfyTahomaRegisterEventsResponse.class);
240         return response != null ? response.getId() : null;
241     }
242
243     private String urlEncode(String text) {
244         try {
245             return URLEncoder.encode(text, StandardCharsets.UTF_8.toString());
246         } catch (UnsupportedEncodingException e) {
247             return text;
248         }
249     }
250
251     private void enableLogin() {
252         tooManyRequests = false;
253     }
254
255     private List<SomfyTahomaEvent> getEvents() {
256         SomfyTahomaEvent[] response = invokeCallToURL(TAHOMA_API_URL + "events/" + eventsId + "/fetch", "",
257                 HttpMethod.POST, SomfyTahomaEvent[].class);
258         return response != null ? List.of(response) : List.of();
259     }
260
261     @Override
262     public void handleRemoval() {
263         super.handleRemoval();
264         logout();
265     }
266
267     @Override
268     public Collection<Class<? extends ThingHandlerService>> getServices() {
269         return Collections.singleton(SomfyTahomaItemDiscoveryService.class);
270     }
271
272     @Override
273     public void dispose() {
274         cleanup();
275         super.dispose();
276     }
277
278     private void cleanup() {
279         logger.debug("Doing cleanup");
280         stopPolling();
281         executions.clear();
282         // cancel all scheduled retries
283         retryFutures.forEach(x -> x.cancel(false));
284
285         try {
286             httpClient.stop();
287         } catch (Exception e) {
288             logger.debug("Error during http client stopping", e);
289         }
290     }
291
292     @Override
293     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
294         super.bridgeStatusChanged(bridgeStatusInfo);
295         if (ThingStatus.UNINITIALIZED == bridgeStatusInfo.getStatus()) {
296             cleanup();
297         }
298     }
299
300     /**
301      * Stops this thing's polling future
302      */
303     private void stopPolling() {
304         ScheduledFuture<?> localPollFuture = pollFuture;
305         if (localPollFuture != null && !localPollFuture.isCancelled()) {
306             localPollFuture.cancel(true);
307         }
308         ScheduledFuture<?> localStatusFuture = statusFuture;
309         if (localStatusFuture != null && !localStatusFuture.isCancelled()) {
310             localStatusFuture.cancel(true);
311         }
312         ScheduledFuture<?> localReconciliationFuture = reconciliationFuture;
313         if (localReconciliationFuture != null && !localReconciliationFuture.isCancelled()) {
314             localReconciliationFuture.cancel(true);
315         }
316     }
317
318     public List<SomfyTahomaActionGroup> listActionGroups() {
319         SomfyTahomaActionGroup[] list = invokeCallToURL(TAHOMA_API_URL + "actionGroups", "", HttpMethod.GET,
320                 SomfyTahomaActionGroup[].class);
321         return list != null ? List.of(list) : List.of();
322     }
323
324     public @Nullable SomfyTahomaSetup getSetup() {
325         return invokeCallToURL(TAHOMA_API_URL + "setup", "", HttpMethod.GET, SomfyTahomaSetup.class);
326     }
327
328     public List<SomfyTahomaDevice> getDevices() {
329         SomfyTahomaDevice[] response = invokeCallToURL(SETUP_URL + "devices", "", HttpMethod.GET,
330                 SomfyTahomaDevice[].class);
331         return response != null ? List.of(response) : List.of();
332     }
333
334     private void getTahomaUpdates() {
335         logger.debug("Getting Tahoma Updates...");
336         if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
337             return;
338         }
339
340         List<SomfyTahomaEvent> events = getEvents();
341         logger.trace("Got total of {} events", events.size());
342         for (SomfyTahomaEvent event : events) {
343             processEvent(event);
344         }
345     }
346
347     private void processEvent(SomfyTahomaEvent event) {
348         logger.debug("Got event: {}", event.getName());
349         switch (event.getName()) {
350             case "ExecutionRegisteredEvent":
351                 processExecutionRegisteredEvent(event);
352                 break;
353             case "ExecutionStateChangedEvent":
354                 processExecutionChangedEvent(event);
355                 break;
356             case "DeviceStateChangedEvent":
357                 processStateChangedEvent(event);
358                 break;
359             case "RefreshAllDevicesStatesCompletedEvent":
360                 scheduler.schedule(this::updateThings, 1, TimeUnit.SECONDS);
361                 break;
362             case "GatewayAliveEvent":
363             case "GatewayDownEvent":
364                 processGatewayEvent(event);
365                 break;
366             default:
367                 // ignore other states
368         }
369     }
370
371     private synchronized void updateThings() {
372         boolean needsUpdate = reconciliation;
373
374         for (Thing th : getThing().getThings()) {
375             if (ThingStatus.ONLINE != th.getStatus()) {
376                 needsUpdate = true;
377             }
378         }
379
380         // update all states only if necessary
381         if (needsUpdate) {
382             updateAllStates();
383             reconciliation = false;
384         }
385     }
386
387     private void processExecutionRegisteredEvent(SomfyTahomaEvent event) {
388         JsonElement el = event.getAction();
389         if (el.isJsonArray()) {
390             SomfyTahomaAction[] actions = gson.fromJson(el, SomfyTahomaAction[].class);
391             for (SomfyTahomaAction action : actions) {
392                 registerExecution(action.getDeviceURL(), event.getExecId());
393             }
394         } else {
395             SomfyTahomaAction action = gson.fromJson(el, SomfyTahomaAction.class);
396             registerExecution(action.getDeviceURL(), event.getExecId());
397         }
398     }
399
400     private void processExecutionChangedEvent(SomfyTahomaEvent event) {
401         if (FAILED_EVENT.equals(event.getNewState()) || COMPLETED_EVENT.equals(event.getNewState())) {
402             logger.debug("Removing execution id: {}", event.getExecId());
403             unregisterExecution(event.getExecId());
404         }
405     }
406
407     private void registerExecution(String url, String execId) {
408         if (executions.containsKey(url)) {
409             executions.remove(url);
410             logger.debug("Previous execution exists for url: {}", url);
411         }
412         executions.put(url, execId);
413     }
414
415     private void unregisterExecution(String execId) {
416         if (executions.containsValue(execId)) {
417             executions.values().removeAll(Collections.singleton(execId));
418         } else {
419             logger.debug("Cannot remove execution id: {}, because it is not registered", execId);
420         }
421     }
422
423     private void processGatewayEvent(SomfyTahomaEvent event) {
424         // update gateway status
425         for (Thing th : getThing().getThings()) {
426             if (THING_TYPE_GATEWAY.equals(th.getThingTypeUID())) {
427                 SomfyTahomaGatewayHandler gatewayHandler = (SomfyTahomaGatewayHandler) th.getHandler();
428                 if (gatewayHandler != null && gatewayHandler.getGateWayId().equals(event.getGatewayId())) {
429                     gatewayHandler.refresh(STATUS);
430                 }
431             }
432         }
433     }
434
435     private synchronized void updateAllStates() {
436         logger.debug("Updating all states");
437         getDevices().forEach(device -> updateDevice(device));
438     }
439
440     private void updateDevice(SomfyTahomaDevice device) {
441         String url = device.getDeviceURL();
442         List<SomfyTahomaState> states = device.getStates();
443         updateDevice(url, states);
444     }
445
446     private void updateDevice(String url, List<SomfyTahomaState> states) {
447         Thing th = getThingByDeviceUrl(url);
448         if (th == null) {
449             return;
450         }
451         SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) th.getHandler();
452         if (handler != null) {
453             handler.updateThingStatus(states);
454             handler.updateThingChannels(states);
455         }
456     }
457
458     private void processStateChangedEvent(SomfyTahomaEvent event) {
459         String deviceUrl = event.getDeviceUrl();
460         List<SomfyTahomaState> states = event.getDeviceStates();
461         logger.debug("States for device {} : {}", deviceUrl, states);
462         Thing thing = getThingByDeviceUrl(deviceUrl);
463
464         if (thing != null) {
465             logger.debug("Updating status of thing: {}", thing.getLabel());
466             SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) thing.getHandler();
467
468             if (handler != null) {
469                 // update thing status
470                 handler.updateThingStatus(states);
471                 handler.updateThingChannels(states);
472             }
473         } else {
474             logger.debug("Thing handler is null, probably not bound thing.");
475         }
476     }
477
478     private void enableReconciliation() {
479         logger.debug("Enabling reconciliation");
480         reconciliation = true;
481     }
482
483     private void refreshTahomaStates() {
484         logger.debug("Refreshing Tahoma states...");
485         if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
486             return;
487         }
488
489         // force Tahoma to ask for actual states
490         forceGatewaySync();
491     }
492
493     private @Nullable Thing getThingByDeviceUrl(String deviceUrl) {
494         for (Thing th : getThing().getThings()) {
495             String url = (String) th.getConfiguration().get("url");
496             if (deviceUrl.equals(url)) {
497                 return th;
498             }
499         }
500         return null;
501     }
502
503     private void logout() {
504         try {
505             eventsId = "";
506             sendGetToTahomaWithCookie(TAHOMA_API_URL + "logout");
507         } catch (InterruptedException | ExecutionException | TimeoutException e) {
508             logger.debug("Cannot send logout command!", e);
509             if (e instanceof InterruptedException) {
510                 Thread.currentThread().interrupt();
511             }
512         }
513     }
514
515     private String sendPostToTahomaWithCookie(String url, String urlParameters)
516             throws InterruptedException, ExecutionException, TimeoutException {
517         return sendMethodToTahomaWithCookie(url, HttpMethod.POST, urlParameters);
518     }
519
520     private String sendGetToTahomaWithCookie(String url)
521             throws InterruptedException, ExecutionException, TimeoutException {
522         return sendMethodToTahomaWithCookie(url, HttpMethod.GET);
523     }
524
525     private String sendPutToTahomaWithCookie(String url)
526             throws InterruptedException, ExecutionException, TimeoutException {
527         return sendMethodToTahomaWithCookie(url, HttpMethod.PUT);
528     }
529
530     private String sendDeleteToTahomaWithCookie(String url)
531             throws InterruptedException, ExecutionException, TimeoutException {
532         return sendMethodToTahomaWithCookie(url, HttpMethod.DELETE);
533     }
534
535     private String sendMethodToTahomaWithCookie(String url, HttpMethod method)
536             throws InterruptedException, ExecutionException, TimeoutException {
537         return sendMethodToTahomaWithCookie(url, method, "");
538     }
539
540     private String sendMethodToTahomaWithCookie(String url, HttpMethod method, String urlParameters)
541             throws InterruptedException, ExecutionException, TimeoutException {
542         logger.trace("Sending {} to url: {} with data: {}", method.asString(), url, urlParameters);
543         Request request = sendRequestBuilder(url, method);
544         if (StringUtils.isNotEmpty(urlParameters)) {
545             request = request.content(new StringContentProvider(urlParameters), "application/json;charset=UTF-8");
546         }
547
548         ContentResponse response = request.send();
549
550         if (logger.isTraceEnabled()) {
551             logger.trace("Response: {}", response.getContentAsString());
552         }
553
554         if (response.getStatus() < 200 || response.getStatus() >= 300) {
555             logger.debug("Received unexpected status code: {}", response.getStatus());
556         }
557         return response.getContentAsString();
558     }
559
560     private Request sendRequestBuilder(String url, HttpMethod method) {
561         return httpClient.newRequest(url).method(method).header(HttpHeader.ACCEPT_LANGUAGE, "en-US,en")
562                 .header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate").header("X-Requested-With", "XMLHttpRequest")
563                 .timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS).agent(TAHOMA_AGENT);
564     }
565
566     public void sendCommand(String io, String command, String params, String url) {
567         if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
568             return;
569         }
570
571         removeFinishedRetries();
572
573         boolean result = sendCommandInternal(io, command, params, url);
574         if (!result) {
575             scheduleRetry(io, command, params, url, thingConfig.getRetries());
576         }
577     }
578
579     private void repeatSendCommandInternal(String io, String command, String params, String url, int retries) {
580         logger.debug("Retrying command, retries left: {}", retries);
581         boolean result = sendCommandInternal(io, command, params, url);
582         if (!result && (retries > 0)) {
583             scheduleRetry(io, command, params, url, retries - 1);
584         }
585     }
586
587     private boolean sendCommandInternal(String io, String command, String params, String url) {
588         String value = params.equals("[]") ? command : params.replace("\"", "");
589         String urlParameters = "{\"label\":\"" + getThingLabelByURL(io) + " - " + value
590                 + " - OH2\",\"actions\":[{\"deviceURL\":\"" + io + "\",\"commands\":[{\"name\":\"" + command
591                 + "\",\"parameters\":" + params + "}]}]}";
592         SomfyTahomaApplyResponse response = invokeCallToURL(url, urlParameters, HttpMethod.POST,
593                 SomfyTahomaApplyResponse.class);
594         if (response != null) {
595             if (!response.getExecId().isEmpty()) {
596                 logger.debug("Exec id: {}", response.getExecId());
597                 registerExecution(io, response.getExecId());
598             } else {
599                 logger.debug("ExecId is empty!");
600                 return false;
601             }
602             return true;
603         }
604         return false;
605     }
606
607     private void removeFinishedRetries() {
608         retryFutures.removeIf(x -> x.isDone());
609         logger.debug("Currently {} retries are scheduled.", retryFutures.size());
610     }
611
612     private void scheduleRetry(String io, String command, String params, String url, int retries) {
613         retryFutures.add(scheduler.schedule(() -> {
614             repeatSendCommandInternal(io, command, params, url, retries);
615         }, thingConfig.getRetryDelay(), TimeUnit.MILLISECONDS));
616     }
617
618     private String getThingLabelByURL(String io) {
619         Thing th = getThingByDeviceUrl(io);
620         if (th != null) {
621             if (th.getProperties().containsKey(NAME_STATE)) {
622                 // Return label from Tahoma
623                 return th.getProperties().get(NAME_STATE).replace("\"", "");
624             }
625             // Return label from OH2
626             String label = th.getLabel();
627             return label != null ? label.replace("\"", "") : "";
628         }
629         return "null";
630     }
631
632     public @Nullable String getCurrentExecutions(String io) {
633         if (executions.containsKey(io)) {
634             return executions.get(io);
635         }
636         return null;
637     }
638
639     public void cancelExecution(String executionId) {
640         invokeCallToURL(DELETE_URL + executionId, "", HttpMethod.DELETE, null);
641     }
642
643     public void executeActionGroup(String id) {
644         if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
645             return;
646         }
647         String execId = executeActionGroupInternal(id);
648         if (execId == null) {
649             execId = executeActionGroupInternal(id);
650         }
651         if (execId != null) {
652             registerExecution(id, execId);
653         }
654     }
655
656     private boolean reLogin() {
657         logger.debug("Doing relogin");
658         reLoginNeeded = true;
659         login();
660         return ThingStatus.OFFLINE != thing.getStatus();
661     }
662
663     public @Nullable String executeActionGroupInternal(String id) {
664         SomfyTahomaApplyResponse response = invokeCallToURL(EXEC_URL + id, "", HttpMethod.POST,
665                 SomfyTahomaApplyResponse.class);
666         if (response != null) {
667             if (response.getExecId().isEmpty()) {
668                 logger.debug("Got empty exec response");
669                 return null;
670             }
671             return response.getExecId();
672         }
673         return null;
674     }
675
676     public void forceGatewaySync() {
677         invokeCallToURL(REFRESH_URL, "", HttpMethod.PUT, null);
678     }
679
680     public SomfyTahomaStatus getTahomaStatus(String gatewayId) {
681         SomfyTahomaStatusResponse data = invokeCallToURL(GATEWAYS_URL + gatewayId, "", HttpMethod.GET,
682                 SomfyTahomaStatusResponse.class);
683         if (data != null) {
684             logger.debug("Tahoma status: {}", data.getConnectivity().getStatus());
685             logger.debug("Tahoma protocol version: {}", data.getConnectivity().getProtocolVersion());
686             return data.getConnectivity();
687         }
688         return new SomfyTahomaStatus();
689     }
690
691     private boolean isAuthenticationChallenge(Exception ex) {
692         return ex.getMessage().contains(AUTHENTICATION_CHALLENGE);
693     }
694
695     @Override
696     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
697         super.handleConfigurationUpdate(configurationParameters);
698         if (configurationParameters.containsKey("email")) {
699             thingConfig.setEmail(configurationParameters.get("email").toString());
700         }
701         if (configurationParameters.containsKey("password")) {
702             thingConfig.setPassword(configurationParameters.get("password").toString());
703         }
704     }
705
706     public synchronized void refresh(String url, String stateName) {
707         SomfyTahomaState state = invokeCallToURL(DEVICES_URL + urlEncode(url) + "/states/" + stateName, "",
708                 HttpMethod.GET, SomfyTahomaState.class);
709         if (state != null && !state.getName().isEmpty()) {
710             updateDevice(url, List.of(state));
711         }
712     }
713
714     private @Nullable <T> T invokeCallToURL(String url, String urlParameters, HttpMethod method,
715             @Nullable Class<T> classOfT) {
716         String response = "";
717         try {
718             switch (method) {
719                 case GET:
720                     response = sendGetToTahomaWithCookie(url);
721                     break;
722                 case PUT:
723                     response = sendPutToTahomaWithCookie(url);
724                     break;
725                 case POST:
726                     response = sendPostToTahomaWithCookie(url, urlParameters);
727                     break;
728                 case DELETE:
729                     response = sendDeleteToTahomaWithCookie(url);
730                 default:
731             }
732             return classOfT != null ? gson.fromJson(response, classOfT) : null;
733         } catch (JsonSyntaxException e) {
734             logger.debug("Received data: {} is not JSON", response, e);
735             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
736         } catch (ExecutionException e) {
737             if (isAuthenticationChallenge(e)) {
738                 reLogin();
739             } else {
740                 logger.debug("Cannot call url: {} with params: {}!", url, urlParameters, e);
741                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
742             }
743         } catch (InterruptedException | TimeoutException e) {
744             logger.debug("Cannot call url: {} with params: {}!", url, urlParameters, e);
745             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
746             if (e instanceof InterruptedException) {
747                 Thread.currentThread().interrupt();
748             }
749         }
750         return null;
751     }
752 }