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