]> git.basschouten.com Git - openhab-addons.git/blob
56f207aa60c83f56a523ccf83eb65d6ce814cc94
[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         } catch (UnsupportedEncodingException e) {
225             logger.debug("Cannot login due to unsupported encoding", e);
226             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unsupported encoding");
227         }
228     }
229
230     private void setTooManyRequests() {
231         logger.debug("Too many requests error, suspending activity for {} seconds", SUSPEND_TIME);
232         tooManyRequests = true;
233         scheduler.schedule(this::enableLogin, SUSPEND_TIME, TimeUnit.SECONDS);
234     }
235
236     private @Nullable String registerEvents() {
237         String url;
238
239         try {
240             url = TAHOMA_EVENTS_URL + "register";
241
242             String line = sendPostToTahomaWithCookie(url, "");
243             SomfyTahomaRegisterEventsResponse response = gson.fromJson(line, SomfyTahomaRegisterEventsResponse.class);
244             return response.getId();
245         } catch (JsonSyntaxException e) {
246             logger.debug("Received invalid data", e);
247             return null;
248         } catch (ExecutionException e) {
249             if (isAuthenticationChallenge(e)) {
250                 reLogin();
251                 return UNAUTHORIZED;
252             } else {
253                 logger.info("Cannot register events!", e);
254                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
255             }
256         } catch (InterruptedException | TimeoutException e) {
257             logger.info("Cannot register events!", e);
258             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
259             if (e instanceof InterruptedException) {
260                 Thread.currentThread().interrupt();
261             }
262         }
263         return null;
264     }
265
266     private String urlEncode(String text) throws UnsupportedEncodingException {
267         return URLEncoder.encode(text, StandardCharsets.UTF_8.toString());
268     }
269
270     private void enableLogin() {
271         tooManyRequests = false;
272     }
273
274     private List<SomfyTahomaEvent> getEvents() {
275         String url;
276         String line = "";
277
278         try {
279             url = TAHOMA_API_URL + "events/" + eventsId + "/fetch";
280
281             line = sendPostToTahomaWithCookie(url, "");
282             SomfyTahomaEvent[] array = gson.fromJson(line, SomfyTahomaEvent[].class);
283             return new ArrayList<>(Arrays.asList(array));
284         } catch (JsonSyntaxException e) {
285             logger.debug("Received data: {} is not JSON", line, e);
286             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
287         } catch (ExecutionException e) {
288             if (isAuthenticationChallenge(e)) {
289                 reLogin();
290             } else {
291                 logger.debug("Cannot get Tahoma events!", e);
292                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
293             }
294         } catch (InterruptedException | TimeoutException e) {
295             logger.debug("Cannot get Tahoma events!", e);
296             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
297             if (e instanceof InterruptedException) {
298                 Thread.currentThread().interrupt();
299             }
300         }
301         return new ArrayList<>();
302     }
303
304     @Override
305     public void handleRemoval() {
306         super.handleRemoval();
307         logout();
308     }
309
310     @Override
311     public Collection<Class<? extends ThingHandlerService>> getServices() {
312         return Collections.singleton(SomfyTahomaItemDiscoveryService.class);
313     }
314
315     @Override
316     public void dispose() {
317         cleanup();
318         super.dispose();
319     }
320
321     private void cleanup() {
322         logger.debug("Doing cleanup");
323         stopPolling();
324         executions.clear();
325         try {
326             httpClient.stop();
327         } catch (Exception e) {
328             logger.debug("Error during http client stopping", e);
329         }
330     }
331
332     @Override
333     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
334         super.bridgeStatusChanged(bridgeStatusInfo);
335         if (ThingStatus.UNINITIALIZED == bridgeStatusInfo.getStatus()) {
336             cleanup();
337         }
338     }
339
340     /**
341      * Stops this thing's polling future
342      */
343     private void stopPolling() {
344         ScheduledFuture<?> localPollFuture = pollFuture;
345         if (localPollFuture != null && !localPollFuture.isCancelled()) {
346             localPollFuture.cancel(true);
347         }
348         ScheduledFuture<?> localStatusFuture = statusFuture;
349         if (localStatusFuture != null && !localStatusFuture.isCancelled()) {
350             localStatusFuture.cancel(true);
351         }
352         ScheduledFuture<?> localReconciliationFuture = reconciliationFuture;
353         if (localReconciliationFuture != null && !localReconciliationFuture.isCancelled()) {
354             localReconciliationFuture.cancel(true);
355         }
356     }
357
358     public List<SomfyTahomaActionGroup> listActionGroups() {
359         String groups = getGroups();
360         if (StringUtils.equals(groups, UNAUTHORIZED)) {
361             groups = getGroups();
362         }
363
364         if (groups == null || groups.equals(UNAUTHORIZED)) {
365             return Collections.emptyList();
366         }
367
368         try {
369             SomfyTahomaActionGroup[] list = gson.fromJson(groups, SomfyTahomaActionGroup[].class);
370             return Arrays.asList(list);
371         } catch (JsonSyntaxException e) {
372             logger.debug("Received data: {} is not JSON", groups, e);
373             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
374         }
375         return new ArrayList<>();
376     }
377
378     private @Nullable String getGroups() {
379         String url;
380
381         try {
382             url = TAHOMA_API_URL + "actionGroups";
383             return sendGetToTahomaWithCookie(url);
384         } catch (ExecutionException e) {
385             if (isAuthenticationChallenge(e)) {
386                 reLogin();
387                 return UNAUTHORIZED;
388             } else {
389                 logger.debug("Cannot send getActionGroups command!", e);
390                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
391             }
392         } catch (InterruptedException | TimeoutException e) {
393             logger.debug("Cannot send getActionGroups command!", e);
394             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
395             if (e instanceof InterruptedException) {
396                 Thread.currentThread().interrupt();
397             }
398         }
399         return null;
400     }
401
402     public @Nullable SomfyTahomaSetup getSetup() {
403         String url;
404         String line = "";
405
406         try {
407             url = TAHOMA_API_URL + "setup";
408             line = sendGetToTahomaWithCookie(url);
409             return gson.fromJson(line, SomfyTahomaSetup.class);
410         } catch (JsonSyntaxException e) {
411             logger.debug("Received data: {} is not JSON", line, e);
412             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
413         } catch (ExecutionException e) {
414             if (isAuthenticationChallenge(e)) {
415                 reLogin();
416             } else {
417                 logger.debug("Cannot send getSetup command!", e);
418                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
419             }
420         } catch (InterruptedException | TimeoutException e) {
421             logger.debug("Cannot send getSetup command!", e);
422             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
423             if (e instanceof InterruptedException) {
424                 Thread.currentThread().interrupt();
425             }
426         }
427         return null;
428     }
429
430     public List<SomfyTahomaDevice> getDevices() {
431         String url;
432         String line = "";
433
434         try {
435             url = SETUP_URL + "devices";
436             line = sendGetToTahomaWithCookie(url);
437             return Arrays.asList(gson.fromJson(line, SomfyTahomaDevice[].class));
438         } catch (JsonSyntaxException e) {
439             logger.debug("Received data: {} is not JSON", line, e);
440             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
441         } catch (ExecutionException e) {
442             if (isAuthenticationChallenge(e)) {
443                 reLogin();
444             } else {
445                 logger.debug("Cannot send get devices command!", e);
446                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
447             }
448         } catch (InterruptedException | TimeoutException e) {
449             logger.debug("Cannot send get devices command!", e);
450             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
451             if (e instanceof InterruptedException) {
452                 Thread.currentThread().interrupt();
453             }
454         }
455         return Collections.emptyList();
456     }
457
458     private void getTahomaUpdates() {
459         logger.debug("Getting Tahoma Updates...");
460         if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
461             return;
462         }
463
464         List<SomfyTahomaEvent> events = getEvents();
465         logger.trace("Got total of {} events", events.size());
466         for (SomfyTahomaEvent event : events) {
467             processEvent(event);
468         }
469     }
470
471     private void processEvent(SomfyTahomaEvent event) {
472         logger.debug("Got event: {}", event.getName());
473         switch (event.getName()) {
474             case "ExecutionRegisteredEvent":
475                 processExecutionRegisteredEvent(event);
476                 break;
477             case "ExecutionStateChangedEvent":
478                 processExecutionChangedEvent(event);
479                 break;
480             case "DeviceStateChangedEvent":
481                 processStateChangedEvent(event);
482                 break;
483             case "RefreshAllDevicesStatesCompletedEvent":
484                 scheduler.schedule(this::updateThings, 1, TimeUnit.SECONDS);
485                 break;
486             case "GatewayAliveEvent":
487             case "GatewayDownEvent":
488                 processGatewayEvent(event);
489                 break;
490             default:
491                 // ignore other states
492         }
493     }
494
495     private synchronized void updateThings() {
496         boolean needsUpdate = reconciliation;
497
498         for (Thing th : getThing().getThings()) {
499             if (ThingStatus.ONLINE != th.getStatus()) {
500                 needsUpdate = true;
501             }
502         }
503
504         // update all states only if necessary
505         if (needsUpdate) {
506             updateAllStates();
507             reconciliation = false;
508         }
509     }
510
511     private void processExecutionRegisteredEvent(SomfyTahomaEvent event) {
512         JsonElement el = event.getAction();
513         if (el.isJsonArray()) {
514             SomfyTahomaAction[] actions = gson.fromJson(el, SomfyTahomaAction[].class);
515             for (SomfyTahomaAction action : actions) {
516                 registerExecution(action.getDeviceURL(), event.getExecId());
517             }
518         } else {
519             SomfyTahomaAction action = gson.fromJson(el, SomfyTahomaAction.class);
520             registerExecution(action.getDeviceURL(), event.getExecId());
521         }
522     }
523
524     private void processExecutionChangedEvent(SomfyTahomaEvent event) {
525         if (FAILED_EVENT.equals(event.getNewState()) || COMPLETED_EVENT.equals(event.getNewState())) {
526             logger.debug("Removing execution id: {}", event.getExecId());
527             unregisterExecution(event.getExecId());
528         }
529     }
530
531     private void registerExecution(String url, String execId) {
532         if (executions.containsKey(url)) {
533             executions.remove(url);
534             logger.debug("Previous execution exists for url: {}", url);
535         }
536         executions.put(url, execId);
537     }
538
539     private void unregisterExecution(String execId) {
540         if (executions.containsValue(execId)) {
541             executions.values().removeAll(Collections.singleton(execId));
542         } else {
543             logger.debug("Cannot remove execution id: {}, because it is not registered", execId);
544         }
545     }
546
547     private void processGatewayEvent(SomfyTahomaEvent event) {
548         // update gateway status
549         for (Thing th : getThing().getThings()) {
550             if (THING_TYPE_GATEWAY.equals(th.getThingTypeUID())) {
551                 SomfyTahomaGatewayHandler gatewayHandler = (SomfyTahomaGatewayHandler) th.getHandler();
552                 if (gatewayHandler != null && gatewayHandler.getGateWayId().equals(event.getGatewayId())) {
553                     gatewayHandler.refresh(STATUS);
554                 }
555             }
556         }
557     }
558
559     private synchronized void updateAllStates() {
560         logger.debug("Updating all states");
561         getDevices().forEach(device -> updateDevice(device));
562     }
563
564     private void updateDevice(SomfyTahomaDevice device) {
565         String url = device.getDeviceURL();
566         List<SomfyTahomaState> states = device.getStates();
567         updateDevice(url, states);
568     }
569
570     private void updateDevice(String url, List<SomfyTahomaState> states) {
571         Thing th = getThingByDeviceUrl(url);
572         if (th == null) {
573             return;
574         }
575         SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) th.getHandler();
576         if (handler != null) {
577             handler.updateThingStatus(states);
578             handler.updateThingChannels(states);
579         }
580     }
581
582     private void processStateChangedEvent(SomfyTahomaEvent event) {
583         String deviceUrl = event.getDeviceUrl();
584         List<SomfyTahomaState> states = event.getDeviceStates();
585         logger.debug("States for device {} : {}", deviceUrl, states);
586         Thing thing = getThingByDeviceUrl(deviceUrl);
587
588         if (thing != null) {
589             logger.debug("Updating status of thing: {}", thing.getLabel());
590             SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) thing.getHandler();
591
592             if (handler != null) {
593                 // update thing status
594                 handler.updateThingStatus(states);
595                 handler.updateThingChannels(states);
596             }
597         } else {
598             logger.debug("Thing handler is null, probably not bound thing.");
599         }
600     }
601
602     private void enableReconciliation() {
603         logger.debug("Enabling reconciliation");
604         reconciliation = true;
605     }
606
607     private void refreshTahomaStates() {
608         logger.debug("Refreshing Tahoma states...");
609         if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
610             return;
611         }
612
613         // force Tahoma to ask for actual states
614         forceGatewaySync();
615     }
616
617     private @Nullable Thing getThingByDeviceUrl(String deviceUrl) {
618         for (Thing th : getThing().getThings()) {
619             String url = (String) th.getConfiguration().get("url");
620             if (deviceUrl.equals(url)) {
621                 return th;
622             }
623         }
624         return null;
625     }
626
627     private void logout() {
628         try {
629             eventsId = "";
630             sendGetToTahomaWithCookie(TAHOMA_API_URL + "logout");
631         } catch (InterruptedException | ExecutionException | TimeoutException e) {
632             logger.debug("Cannot send logout command!", e);
633             if (e instanceof InterruptedException) {
634                 Thread.currentThread().interrupt();
635             }
636         }
637     }
638
639     private String sendPostToTahomaWithCookie(String url, String urlParameters)
640             throws InterruptedException, ExecutionException, TimeoutException {
641         return sendMethodToTahomaWithCookie(url, HttpMethod.POST, urlParameters);
642     }
643
644     private String sendGetToTahomaWithCookie(String url)
645             throws InterruptedException, ExecutionException, TimeoutException {
646         return sendMethodToTahomaWithCookie(url, HttpMethod.GET);
647     }
648
649     private String sendPutToTahomaWithCookie(String url)
650             throws InterruptedException, ExecutionException, TimeoutException {
651         return sendMethodToTahomaWithCookie(url, HttpMethod.PUT);
652     }
653
654     private String sendDeleteToTahomaWithCookie(String url)
655             throws InterruptedException, ExecutionException, TimeoutException {
656         return sendMethodToTahomaWithCookie(url, HttpMethod.DELETE);
657     }
658
659     private String sendMethodToTahomaWithCookie(String url, HttpMethod method)
660             throws InterruptedException, ExecutionException, TimeoutException {
661         return sendMethodToTahomaWithCookie(url, method, "");
662     }
663
664     private String sendMethodToTahomaWithCookie(String url, HttpMethod method, String urlParameters)
665             throws InterruptedException, ExecutionException, TimeoutException {
666         logger.trace("Sending {} to url: {} with data: {}", method.asString(), url, urlParameters);
667         Request request = sendRequestBuilder(url, method);
668         if (StringUtils.isNotEmpty(urlParameters)) {
669             request = request.content(new StringContentProvider(urlParameters), "application/json;charset=UTF-8");
670         }
671
672         ContentResponse response = request.send();
673
674         if (logger.isTraceEnabled()) {
675             logger.trace("Response: {}", response.getContentAsString());
676         }
677
678         if (response.getStatus() < 200 || response.getStatus() >= 300) {
679             logger.debug("Received unexpected status code: {}", response.getStatus());
680         }
681         return response.getContentAsString();
682     }
683
684     private Request sendRequestBuilder(String url, HttpMethod method) {
685         return httpClient.newRequest(url).method(method).header(HttpHeader.ACCEPT_LANGUAGE, "en-US,en")
686                 .header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate").header("X-Requested-With", "XMLHttpRequest")
687                 .timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS).agent(TAHOMA_AGENT);
688     }
689
690     public void sendCommand(String io, String command, String params) {
691         if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
692             return;
693         }
694
695         Boolean result = sendCommandInternal(io, command, params);
696         if (result != null && !result) {
697             sendCommandInternal(io, command, params);
698         }
699     }
700
701     private @Nullable Boolean sendCommandInternal(String io, String command, String params) {
702         String url;
703         String line = "";
704
705         try {
706             url = EXEC_URL + "apply";
707
708             String value = params.equals("[]") ? command : params.replace("\"", "");
709             String urlParameters = "{\"label\":\"" + getThingLabelByURL(io) + " - " + value
710                     + " - OH2\",\"actions\":[{\"deviceURL\":\"" + io + "\",\"commands\":[{\"name\":\"" + command
711                     + "\",\"parameters\":" + params + "}]}]}";
712
713             line = sendPostToTahomaWithCookie(url, urlParameters);
714
715             SomfyTahomaApplyResponse data = gson.fromJson(line, SomfyTahomaApplyResponse.class);
716
717             if (!StringUtils.isEmpty(data.getExecId())) {
718                 logger.debug("Exec id: {}", data.getExecId());
719                 registerExecution(io, data.getExecId());
720             } else {
721                 logger.warn("Apply command response: {}", line);
722                 return false;
723             }
724             return true;
725         } catch (JsonSyntaxException e) {
726             logger.debug("Received data: {} is not JSON", line, e);
727             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
728         } catch (ExecutionException e) {
729             if (isAuthenticationChallenge(e)) {
730                 reLogin();
731                 return false;
732             } else {
733                 logger.debug("Cannot send apply command {} with params {}!", command, params, e);
734                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
735             }
736         } catch (InterruptedException | TimeoutException e) {
737             logger.debug("Cannot send apply command {} with params {}!", command, params, e);
738             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
739             if (e instanceof InterruptedException) {
740                 Thread.currentThread().interrupt();
741             }
742         }
743         return null;
744     }
745
746     private String getThingLabelByURL(String io) {
747         Thing th = getThingByDeviceUrl(io);
748         if (th != null) {
749             if (th.getProperties().containsKey(NAME_STATE)) {
750                 // Return label from Tahoma
751                 return th.getProperties().get(NAME_STATE).replace("\"", "");
752             }
753             // Return label from OH2
754             String label = th.getLabel();
755             return label != null ? label.replace("\"", "") : "";
756         }
757         return "null";
758     }
759
760     public @Nullable String getCurrentExecutions(String io) {
761         if (executions.containsKey(io)) {
762             return executions.get(io);
763         }
764         return null;
765     }
766
767     public void cancelExecution(String executionId) {
768         Boolean result = cancelExecutionInternal(executionId);
769         if (result != null && !result) {
770             cancelExecutionInternal(executionId);
771         }
772     }
773
774     private @Nullable Boolean cancelExecutionInternal(String executionId) {
775         String url;
776
777         try {
778             url = DELETE_URL + executionId;
779             sendDeleteToTahomaWithCookie(url);
780             return true;
781         } catch (ExecutionException e) {
782             if (isAuthenticationChallenge(e)) {
783                 reLogin();
784                 return false;
785             } else {
786                 logger.debug("Cannot cancel execution!", e);
787                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
788             }
789         } catch (InterruptedException | TimeoutException e) {
790             logger.debug("Cannot cancel execution!", e);
791             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
792             if (e instanceof InterruptedException) {
793                 Thread.currentThread().interrupt();
794             }
795         }
796         return null;
797     }
798
799     public void executeActionGroup(String id) {
800         if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
801             return;
802         }
803         String execId = executeActionGroupInternal(id);
804         if (UNAUTHORIZED.equals(execId)) {
805             execId = executeActionGroupInternal(id);
806         }
807         if (!UNAUTHORIZED.equals(execId) && execId != null) {
808             registerExecution(id, execId);
809         }
810     }
811
812     private boolean reLogin() {
813         logger.debug("Doing relogin");
814         reLoginNeeded = true;
815         login();
816         return ThingStatus.OFFLINE != thing.getStatus();
817     }
818
819     public @Nullable String executeActionGroupInternal(String id) {
820         String line = "";
821         try {
822             String url = EXEC_URL + id;
823
824             line = sendPostToTahomaWithCookie(url, "");
825             SomfyTahomaApplyResponse data = gson.fromJson(line, SomfyTahomaApplyResponse.class);
826             if (data.getExecId().isEmpty()) {
827                 logger.debug("Got empty exec response");
828             }
829             return data.getExecId();
830         } catch (JsonSyntaxException e) {
831             logger.debug("Received data: {} is not JSON", line, e);
832             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
833         } catch (ExecutionException e) {
834             if (isAuthenticationChallenge(e)) {
835                 reLogin();
836                 return UNAUTHORIZED;
837             } else {
838                 logger.debug("Cannot exec execution group!", e);
839                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
840             }
841         } catch (InterruptedException | TimeoutException e) {
842             logger.debug("Cannot exec execution group!", e);
843             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
844             if (e instanceof InterruptedException) {
845                 Thread.currentThread().interrupt();
846             }
847         }
848         return null;
849     }
850
851     public void forceGatewaySync() {
852         try {
853             sendPutToTahomaWithCookie(REFRESH_URL);
854         } catch (ExecutionException e) {
855             if (isAuthenticationChallenge(e)) {
856                 reLogin();
857             } else {
858                 logger.debug("Cannot sync gateway!", e);
859                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
860             }
861         } catch (InterruptedException | TimeoutException e) {
862             logger.debug("Cannot sync gateway!", e);
863             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
864             if (e instanceof InterruptedException) {
865                 Thread.currentThread().interrupt();
866             }
867         }
868     }
869
870     public SomfyTahomaStatus getTahomaStatus(String gatewayId) {
871         String line = "";
872         try {
873             String url = GATEWAYS_URL + gatewayId;
874             line = sendGetToTahomaWithCookie(url);
875             SomfyTahomaStatusResponse data = gson.fromJson(line, SomfyTahomaStatusResponse.class);
876             logger.debug("Tahoma status: {}", data.getConnectivity().getStatus());
877             logger.debug("Tahoma protocol version: {}", data.getConnectivity().getProtocolVersion());
878             return data.getConnectivity();
879         } catch (JsonSyntaxException e) {
880             logger.debug("Received data: {} is not JSON", line, e);
881             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
882         } catch (ExecutionException e) {
883             if (isAuthenticationChallenge(e)) {
884                 reLogin();
885                 return new SomfyTahomaStatus();
886             } else {
887                 logger.debug("Cannot get Tahoma gateway status!", e);
888                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
889             }
890         } catch (InterruptedException | TimeoutException e) {
891             logger.debug("Cannot get Tahoma gateway status!", e);
892             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
893             if (e instanceof InterruptedException) {
894                 Thread.currentThread().interrupt();
895             }
896         }
897         return new SomfyTahomaStatus();
898     }
899
900     private boolean isAuthenticationChallenge(Exception ex) {
901         return ex.getMessage().contains(AUTHENTICATION_CHALLENGE);
902     }
903
904     @Override
905     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
906         super.handleConfigurationUpdate(configurationParameters);
907         if (configurationParameters.containsKey("email")) {
908             thingConfig.setEmail(configurationParameters.get("email").toString());
909         }
910         if (configurationParameters.containsKey("password")) {
911             thingConfig.setPassword(configurationParameters.get("password").toString());
912         }
913     }
914
915     public synchronized void refresh(String url, String stateName) {
916         try {
917             String line = sendGetToTahomaWithCookie(DEVICES_URL + urlEncode(url) + "/states/" + stateName);
918             SomfyTahomaState state = gson.fromJson(line, SomfyTahomaState.class);
919             if (StringUtils.isNotEmpty(state.getName())) {
920                 updateDevice(url, Arrays.asList(state));
921             }
922         } catch (UnsupportedEncodingException e) {
923             logger.debug("Unsupported encoding!", e);
924             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
925         } catch (ExecutionException e) {
926             if (isAuthenticationChallenge(e)) {
927                 reLogin();
928             } else {
929                 logger.debug("Cannot refresh device states!", e);
930                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
931             }
932         } catch (InterruptedException | TimeoutException e) {
933             logger.debug("Cannot refresh device states!", e);
934             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
935             if (e instanceof InterruptedException) {
936                 Thread.currentThread().interrupt();
937             }
938         }
939     }
940 }