]> git.basschouten.com Git - openhab-addons.git/blob
636217d73beb8183b93e289116e9acb1b14ecffc
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.IOException;
18 import java.net.InetAddress;
19 import java.net.URLEncoder;
20 import java.nio.charset.StandardCharsets;
21 import java.time.Duration;
22 import java.time.Instant;
23 import java.util.Collection;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.concurrent.ConcurrentLinkedQueue;
29 import java.util.concurrent.ExecutionException;
30 import java.util.concurrent.ScheduledFuture;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.TimeoutException;
33
34 import javax.jmdns.JmDNS;
35 import javax.ws.rs.core.MediaType;
36
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.eclipse.jetty.client.HttpClient;
40 import org.eclipse.jetty.client.WWWAuthenticationProtocolHandler;
41 import org.eclipse.jetty.client.api.ContentResponse;
42 import org.eclipse.jetty.client.api.Request;
43 import org.eclipse.jetty.client.util.StringContentProvider;
44 import org.eclipse.jetty.http.HttpHeader;
45 import org.eclipse.jetty.http.HttpMethod;
46 import org.eclipse.jetty.util.ssl.SslContextFactory;
47 import org.openhab.binding.somfytahoma.internal.config.SomfyTahomaConfig;
48 import org.openhab.binding.somfytahoma.internal.discovery.SomfyTahomaItemDiscoveryService;
49 import org.openhab.binding.somfytahoma.internal.discovery.SomfyTahomaMDNSDiscoveryListener;
50 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaAction;
51 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaActionGroup;
52 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaApplyResponse;
53 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaDevice;
54 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaError;
55 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaEvent;
56 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaLocalToken;
57 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaLoginResponse;
58 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaOauth2Error;
59 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaOauth2Reponse;
60 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaRegisterEventsResponse;
61 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaSetup;
62 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaState;
63 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaStatus;
64 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaStatusResponse;
65 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaTokenReponse;
66 import org.openhab.core.cache.ExpiringCache;
67 import org.openhab.core.config.core.Configuration;
68 import org.openhab.core.io.net.http.HttpClientFactory;
69 import org.openhab.core.thing.Bridge;
70 import org.openhab.core.thing.ChannelUID;
71 import org.openhab.core.thing.Thing;
72 import org.openhab.core.thing.ThingStatus;
73 import org.openhab.core.thing.ThingStatusDetail;
74 import org.openhab.core.thing.ThingStatusInfo;
75 import org.openhab.core.thing.binding.BaseBridgeHandler;
76 import org.openhab.core.thing.binding.ThingHandlerService;
77 import org.openhab.core.thing.util.ThingWebClientUtil;
78 import org.openhab.core.types.Command;
79 import org.slf4j.Logger;
80 import org.slf4j.LoggerFactory;
81
82 import com.google.gson.Gson;
83 import com.google.gson.JsonElement;
84 import com.google.gson.JsonSyntaxException;
85
86 /**
87  * The {@link SomfyTahomaBridgeHandler} is responsible for handling commands, which are
88  * sent to one of the channels.
89  *
90  * @author Ondrej Pecta - Initial contribution
91  * @author Laurent Garnier - Other portals integration
92  */
93 @NonNullByDefault
94 public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
95
96     private final Logger logger = LoggerFactory.getLogger(SomfyTahomaBridgeHandler.class);
97
98     /**
99      * The shared HttpClient
100      */
101     private @Nullable HttpClient httpClient;
102
103     /**
104      * Future to poll for updates
105      */
106     private @Nullable ScheduledFuture<?> pollFuture;
107
108     /**
109      * Future to poll for status
110      */
111     private @Nullable ScheduledFuture<?> statusFuture;
112
113     /**
114      * Future to set reconciliation flag
115      */
116     private @Nullable ScheduledFuture<?> reconciliationFuture;
117
118     /**
119      * Future for postponed login
120      */
121     private @Nullable ScheduledFuture<?> loginFuture;
122
123     // List of futures used for command retries
124     private Collection<ScheduledFuture<?>> retryFutures = new ConcurrentLinkedQueue<>();
125
126     /**
127      * List of executions
128      */
129     private Map<String, String> executions = new HashMap<>();
130
131     // Too many requests flag
132     private boolean tooManyRequests = false;
133
134     // Silent relogin flag
135     private boolean reLoginNeeded = false;
136
137     // Reconciliation flag
138     private boolean reconciliation = false;
139
140     // Cloud fallback
141     private boolean cloudFallback = false;
142
143     // Communication errors counter
144     private int errorsCounter = 0;
145
146     // Last login timestamp
147     private Instant lastLoginTimestamp = Instant.MIN;
148
149     /**
150      * Our configuration
151      */
152     protected SomfyTahomaConfig thingConfig = new SomfyTahomaConfig();
153
154     /**
155      * Id of registered events
156      */
157     private String eventsId = "";
158
159     private String localToken = "";
160
161     private String accessToken = "";
162
163     private Map<String, SomfyTahomaDevice> devicePlaces = new HashMap<>();
164
165     private ExpiringCache<List<SomfyTahomaDevice>> cachedDevices = new ExpiringCache<>(Duration.ofSeconds(30),
166             this::getDevices);
167
168     // Gson & parser
169     private final Gson gson = new Gson();
170
171     private final HttpClientFactory httpClientFactory;
172
173     public SomfyTahomaBridgeHandler(Bridge thing, HttpClientFactory httpClientFactory) {
174         super(thing);
175         this.httpClientFactory = httpClientFactory;
176     }
177
178     @Override
179     public void handleCommand(ChannelUID channelUID, Command command) {
180     }
181
182     @Override
183     public void initialize() {
184         updateStatus(ThingStatus.UNKNOWN);
185         thingConfig = getConfigAs(SomfyTahomaConfig.class);
186         createHttpClient();
187
188         scheduler.execute(() -> {
189             login();
190             initPolling();
191             logger.debug("Initialize done...");
192         });
193     }
194
195     private void createHttpClient() {
196         // let's create the right http client
197         String clientName = ThingWebClientUtil.buildWebClientConsumerName(thing.getUID(), null);
198         if (thingConfig.isDevMode()) {
199             this.httpClient = httpClientFactory.createHttpClient(clientName, new SslContextFactory.Client(true));
200         } else {
201             this.httpClient = httpClientFactory.createHttpClient(clientName);
202         }
203
204         try {
205             httpClient.start();
206         } catch (Exception e) {
207             logger.debug("Cannot start http client for: {}", thing.getUID(), e);
208             return;
209         }
210         // Remove the WWWAuth protocol handler since Tahoma is not fully compliant
211         httpClient.getProtocolHandlers().remove(WWWAuthenticationProtocolHandler.NAME);
212     }
213
214     /**
215      * starts this things polling future
216      */
217     private void initPolling() {
218         stopPolling();
219         scheduleGetUpdates(10);
220
221         statusFuture = scheduler.scheduleWithFixedDelay(() -> {
222             refreshTahomaStates();
223         }, 60, thingConfig.getStatusTimeout(), TimeUnit.SECONDS);
224
225         reconciliationFuture = scheduler.scheduleWithFixedDelay(() -> {
226             enableReconciliation();
227         }, RECONCILIATION_TIME, RECONCILIATION_TIME, TimeUnit.SECONDS);
228     }
229
230     private void scheduleGetUpdates(long delay) {
231         pollFuture = scheduler.schedule(() -> {
232             getTahomaUpdates();
233             scheduleNextGetUpdates();
234         }, delay, TimeUnit.SECONDS);
235     }
236
237     private void scheduleNextGetUpdates() {
238         ScheduledFuture<?> localPollFuture = pollFuture;
239         if (localPollFuture != null) {
240             localPollFuture.cancel(false);
241         }
242         scheduleGetUpdates(executions.isEmpty() ? thingConfig.getRefresh() : 2);
243     }
244
245     public synchronized void login() {
246         if (thingConfig.getEmail().isEmpty() || thingConfig.getPassword().isEmpty()) {
247             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
248                     "Can not access device as username and/or password are null");
249             return;
250         }
251
252         if (tooManyRequests || Instant.now().minusSeconds(LOGIN_LIMIT_TIME).isBefore(lastLoginTimestamp)) {
253             logger.debug("Postponing login to avoid throttling");
254             return;
255         }
256
257         if (ThingStatus.ONLINE == thing.getStatus() && !reLoginNeeded) {
258             logger.debug("No need to log in, because already logged in");
259             return;
260         }
261
262         reLoginNeeded = false;
263         cloudFallback = false;
264
265         try {
266             lastLoginTimestamp = Instant.now();
267
268             if (thingConfig.getCloudPortal().equalsIgnoreCase(COZYTOUCH_PORTAL)) {
269                 if (!loginCozyTouch()) {
270                     return;
271                 }
272             } else {
273                 loginOAUTH();
274             }
275
276             if (thingConfig.isDevMode()) {
277                 initializeLocalMode();
278             }
279
280             String id = registerEvents();
281             if (id != null && !UNAUTHORIZED.equals(id)) {
282                 eventsId = id;
283                 logger.debug("Events id: {}", eventsId);
284                 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE,
285                         isDevModeReady() ? "LAN mode" : cloudFallback ? "Cloud mode fallback" : "Cloud mode");
286             } else {
287                 logger.debug("Events id error: {}", id);
288                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "unable to register events");
289             }
290         } catch (JsonSyntaxException e) {
291             logger.debug("Received invalid data (login)", e);
292             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data (login)");
293         } catch (ExecutionException e) {
294             if (isOAuthGrantError(e)) {
295                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
296                         "Error logging in (check your credentials)");
297                 setTooManyRequests();
298             } else {
299                 logger.debug("Cannot get login cookie", e);
300                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot get login cookie");
301             }
302         } catch (TimeoutException e) {
303             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Getting login cookie timeout");
304         } catch (InterruptedException e) {
305             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
306                     "Getting login cookie interrupted");
307             Thread.currentThread().interrupt();
308         }
309     }
310
311     private boolean loginCozyTouch()
312             throws ExecutionException, InterruptedException, TimeoutException, JsonSyntaxException {
313         logger.debug("CozyTouch Oauth2 authentication flow");
314         String urlParameters = "jwt=" + getCozytouchJWT();
315         ContentResponse response = sendRequestBuilder("login", HttpMethod.POST)
316                 .content(new StringContentProvider(urlParameters), "application/x-www-form-urlencoded; charset=UTF-8")
317                 .send();
318
319         if (logger.isTraceEnabled()) {
320             logger.trace("Login response: {}", response.getContentAsString());
321         }
322
323         SomfyTahomaLoginResponse data = gson.fromJson(response.getContentAsString(), SomfyTahomaLoginResponse.class);
324
325         if (data == null) {
326             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data (login)");
327             return false;
328         } else if (!data.getErrorCode().isEmpty()) {
329             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, data.getError());
330             if (data.getError().startsWith(TOO_MANY_REQUESTS)) {
331                 setTooManyRequests();
332             }
333             return false;
334         }
335         return true;
336     }
337
338     public boolean isDevModeReady() {
339         return thingConfig.isDevMode() && !localToken.isEmpty() && !cloudFallback;
340     }
341
342     private void initializeLocalMode() {
343         if (thingConfig.getIp().isEmpty() || thingConfig.getPin().isEmpty()) {
344             discoverGateway();
345         }
346
347         if (!thingConfig.getIp().isEmpty() && !thingConfig.getPin().isEmpty()) {
348             try {
349                 if (thingConfig.getToken().isEmpty()) {
350                     localToken = getNewLocalToken();
351                     logger.debug("Local token retrieved");
352                     activateLocalToken();
353                     updateConfiguration();
354                 } else {
355                     localToken = thingConfig.getToken();
356                     activateLocalToken();
357                 }
358                 logger.debug("Local mode initialized, waiting for cloud sync");
359                 Thread.sleep(3000);
360             } catch (InterruptedException ex) {
361                 logger.debug("Interruption during local mode initialization, falling back to cloud mode", ex);
362                 Thread.currentThread().interrupt();
363             } catch (ExecutionException | TimeoutException ex) {
364                 logger.debug("Exception during local mode initialization, falling back to cloud mode", ex);
365                 cloudFallback = true;
366             }
367         } else {
368             logger.debug("Cannot switch to developer mode - gateway not found on LAN");
369             cloudFallback = true;
370         }
371     }
372
373     private String getNewLocalToken() throws ExecutionException, InterruptedException, TimeoutException {
374         // Get list of local tokens
375         SomfyTahomaLocalToken[] tokens = invokeCallToURL(
376                 CONFIG_URL + thingConfig.getPin() + LOCAL_TOKENS_URL + "devmode", "", HttpMethod.GET,
377                 SomfyTahomaLocalToken[].class);
378
379         // Delete old OH tokens
380         for (SomfyTahomaLocalToken token : tokens) {
381             if (OPENHAB_TOKEN.equals(token.getLabel())) {
382                 logger.debug("Deleting token: {}", token.getUuid());
383                 sendDeleteToTahomaWithCookie(CONFIG_URL + thingConfig.getPin() + LOCAL_TOKENS_URL + token.getUuid());
384             }
385         }
386
387         // Generate a new token
388         SomfyTahomaTokenReponse tokenResponse = invokeCallToURL(
389                 CONFIG_URL + thingConfig.getPin() + LOCAL_TOKENS_URL + "generate", "", HttpMethod.GET,
390                 SomfyTahomaTokenReponse.class);
391
392         return tokenResponse.getToken();
393     }
394
395     private void discoverGateway() {
396         logger.debug("Starting mDNS discovery...");
397         JmDNS jmdns = null;
398
399         try {
400             // Create a JmDNS instance
401             jmdns = JmDNS.create(InetAddress.getLocalHost());
402             jmdns.addServiceListener("_kizboxdev._tcp.local.", new SomfyTahomaMDNSDiscoveryListener(this));
403
404             // Wait a bit
405             Thread.sleep(TAHOMA_TIMEOUT * 1000);
406         } catch (InterruptedException e) {
407             logger.debug("mDNS discovery interrupted", e);
408             Thread.currentThread().interrupt();
409         } catch (IOException e) {
410             logger.debug("Exception during mDNS discovery", e);
411         }
412
413         if (jmdns != null) {
414             jmdns.unregisterAllServices();
415             try {
416                 jmdns.close();
417             } catch (IOException e) {
418                 // ignore
419             }
420         }
421     }
422
423     private void activateLocalToken() throws ExecutionException, InterruptedException, TimeoutException {
424         String param = "{\"label\" : \"" + OPENHAB_TOKEN + "\",\"token\" : \"" + localToken
425                 + "\",\"scope\" : \"devmode\"}";
426         String response = sendPostToTahomaWithCookie(CONFIG_URL + thingConfig.getPin() + "/local/tokens", param);
427         logger.trace("Local token activation: {}", response);
428     }
429
430     private void setTooManyRequests() {
431         if (!tooManyRequests) {
432             logger.debug(
433                     "Too many requests or bad credentials for the cloud portal, suspending activity for {} seconds",
434                     SUSPEND_TIME);
435             tooManyRequests = true;
436             loginFuture = scheduler.schedule(this::enableLogin, SUSPEND_TIME, TimeUnit.SECONDS);
437         }
438     }
439
440     private @Nullable String registerEvents() {
441         SomfyTahomaRegisterEventsResponse response = invokeCallToURL(EVENTS_URL + "register", "", HttpMethod.POST,
442                 SomfyTahomaRegisterEventsResponse.class);
443         return response != null ? response.getId() : null;
444     }
445
446     private String urlEncode(String text) {
447         return URLEncoder.encode(text, StandardCharsets.UTF_8);
448     }
449
450     private void enableLogin() {
451         tooManyRequests = false;
452     }
453
454     private List<SomfyTahomaEvent> getEvents() {
455         if (eventsId.isEmpty()) {
456             return List.of();
457         }
458
459         SomfyTahomaEvent[] response = invokeCallToURL(EVENTS_URL + eventsId + "/fetch", "", HttpMethod.POST,
460                 SomfyTahomaEvent[].class);
461         return response != null ? List.of(response) : List.of();
462     }
463
464     @Override
465     public void handleRemoval() {
466         super.handleRemoval();
467         logout();
468     }
469
470     @Override
471     public Collection<Class<? extends ThingHandlerService>> getServices() {
472         return Set.of(SomfyTahomaItemDiscoveryService.class);
473     }
474
475     @Override
476     public void dispose() {
477         cleanup();
478         super.dispose();
479     }
480
481     private void cleanup() {
482         logger.debug("Doing cleanup");
483         stopPolling();
484         executions.clear();
485         // cancel all scheduled retries
486         retryFutures.forEach(x -> x.cancel(false));
487
488         ScheduledFuture<?> localLoginFuture = loginFuture;
489         if (localLoginFuture != null) {
490             localLoginFuture.cancel(true);
491             loginFuture = null;
492         }
493
494         HttpClient localHttpClient = httpClient;
495         if (localHttpClient != null) {
496             try {
497                 localHttpClient.stop();
498             } catch (Exception e) {
499                 logger.debug("Error during http client stopping", e);
500             }
501             httpClient = null;
502         }
503
504         // Clean access data
505         localToken = "";
506         accessToken = "";
507     }
508
509     @Override
510     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
511         super.bridgeStatusChanged(bridgeStatusInfo);
512         if (ThingStatus.UNINITIALIZED == bridgeStatusInfo.getStatus()) {
513             cleanup();
514         }
515     }
516
517     /**
518      * Stops this thing's polling future
519      */
520     private void stopPolling() {
521         ScheduledFuture<?> localPollFuture = pollFuture;
522         if (localPollFuture != null) {
523             localPollFuture.cancel(true);
524             pollFuture = null;
525         }
526         ScheduledFuture<?> localStatusFuture = statusFuture;
527         if (localStatusFuture != null) {
528             localStatusFuture.cancel(true);
529             statusFuture = null;
530         }
531         ScheduledFuture<?> localReconciliationFuture = reconciliationFuture;
532         if (localReconciliationFuture != null) {
533             localReconciliationFuture.cancel(true);
534             reconciliationFuture = null;
535         }
536     }
537
538     public List<SomfyTahomaActionGroup> listActionGroups() {
539         SomfyTahomaActionGroup[] list = invokeCallToURL("actionGroups", "", HttpMethod.GET,
540                 SomfyTahomaActionGroup[].class);
541         return list != null ? List.of(list) : List.of();
542     }
543
544     public @Nullable SomfyTahomaSetup getSetup() {
545         SomfyTahomaSetup setup = invokeCallToURL("setup", "", HttpMethod.GET, SomfyTahomaSetup.class);
546         if (setup != null) {
547             saveDevicePlaces(setup.getDevices());
548         }
549         return setup;
550     }
551
552     public List<SomfyTahomaDevice> getDevices() {
553         SomfyTahomaDevice[] response = invokeCallToURL(SETUP_URL + "devices", "", HttpMethod.GET,
554                 SomfyTahomaDevice[].class);
555         List<SomfyTahomaDevice> devices = response != null ? List.of(response) : List.of();
556         saveDevicePlaces(devices);
557         return devices;
558     }
559
560     public synchronized @Nullable SomfyTahomaDevice getCachedDevice(String url) {
561         List<SomfyTahomaDevice> devices = cachedDevices.getValue();
562         if (devices != null) {
563             for (SomfyTahomaDevice device : devices) {
564                 if (url.equals(device.getDeviceURL())) {
565                     return device;
566                 }
567             }
568         }
569         return null;
570     }
571
572     private void saveDevicePlaces(List<SomfyTahomaDevice> devices) {
573         devicePlaces.clear();
574         for (SomfyTahomaDevice device : devices) {
575             if (!device.getPlaceOID().isEmpty()) {
576                 SomfyTahomaDevice newDevice = new SomfyTahomaDevice();
577                 newDevice.setPlaceOID(device.getPlaceOID());
578                 newDevice.getDefinition().setWidgetName(device.getDefinition().getWidgetName());
579                 devicePlaces.put(device.getDeviceURL(), newDevice);
580             }
581         }
582     }
583
584     private void getTahomaUpdates() {
585         logger.debug("Getting Tahoma Updates...");
586         if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
587             return;
588         }
589
590         List<SomfyTahomaEvent> events = getEvents();
591         logger.trace("Got total of {} events", events.size());
592         for (SomfyTahomaEvent event : events) {
593             processEvent(event);
594         }
595     }
596
597     private void processEvent(SomfyTahomaEvent event) {
598         logger.debug("Got event: {}", event.getName());
599         switch (event.getName()) {
600             case "ExecutionRegisteredEvent":
601                 processExecutionRegisteredEvent(event);
602                 break;
603             case "ExecutionStateChangedEvent":
604                 processExecutionChangedEvent(event);
605                 break;
606             case "DeviceStateChangedEvent":
607                 processStateChangedEvent(event);
608                 break;
609             case "RefreshAllDevicesStatesCompletedEvent":
610                 scheduler.schedule(this::updateThings, 1, TimeUnit.SECONDS);
611                 break;
612             case "GatewayAliveEvent":
613             case "GatewayDownEvent":
614                 processGatewayEvent(event);
615                 break;
616             default:
617                 // ignore other states
618         }
619     }
620
621     private synchronized void updateThings() {
622         boolean needsUpdate = reconciliation;
623
624         for (Thing th : getThing().getThings()) {
625             if (th.isEnabled() && ThingStatus.ONLINE != th.getStatus()) {
626                 needsUpdate = true;
627             }
628         }
629
630         // update all states only if necessary
631         if (needsUpdate) {
632             updateAllStates();
633             reconciliation = false;
634         }
635     }
636
637     private void processExecutionRegisteredEvent(SomfyTahomaEvent event) {
638         boolean invalidData = false;
639         try {
640             JsonElement el = event.getAction();
641             if (el.isJsonArray()) {
642                 SomfyTahomaAction[] actions = gson.fromJson(el, SomfyTahomaAction[].class);
643                 if (actions == null) {
644                     invalidData = true;
645                 } else {
646                     for (SomfyTahomaAction action : actions) {
647                         registerExecution(action.getDeviceURL(), event.getExecId());
648                     }
649                 }
650             } else {
651                 SomfyTahomaAction action = gson.fromJson(el, SomfyTahomaAction.class);
652                 if (action == null) {
653                     invalidData = true;
654                 } else {
655                     registerExecution(action.getDeviceURL(), event.getExecId());
656                 }
657             }
658         } catch (JsonSyntaxException e) {
659             invalidData = true;
660         }
661         if (invalidData) {
662             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
663                     "Received invalid data (execution registered)");
664         }
665     }
666
667     private void processExecutionChangedEvent(SomfyTahomaEvent event) {
668         if (FAILED_EVENT.equals(event.getNewState()) || COMPLETED_EVENT.equals(event.getNewState())) {
669             logger.debug("Removing execution id: {}", event.getExecId());
670             unregisterExecution(event.getExecId());
671         }
672     }
673
674     private void registerExecution(String url, String execId) {
675         if (executions.containsKey(url)) {
676             executions.remove(url);
677             logger.debug("Previous execution exists for url: {}", url);
678         }
679         executions.put(url, execId);
680     }
681
682     private void unregisterExecution(String execId) {
683         if (executions.containsValue(execId)) {
684             executions.values().removeAll(Set.of(execId));
685         } else {
686             logger.debug("Cannot remove execution id: {}, because it is not registered", execId);
687         }
688     }
689
690     private void processGatewayEvent(SomfyTahomaEvent event) {
691         // update gateway status
692         for (Thing th : getThing().getThings()) {
693             if (th.isEnabled() && THING_TYPE_GATEWAY.equals(th.getThingTypeUID())) {
694                 SomfyTahomaGatewayHandler gatewayHandler = (SomfyTahomaGatewayHandler) th.getHandler();
695                 if (gatewayHandler != null && gatewayHandler.getGateWayId().equals(event.getGatewayId())) {
696                     gatewayHandler.refresh(STATUS);
697                 }
698             }
699         }
700     }
701
702     private synchronized void updateAllStates() {
703         logger.debug("Updating all states");
704         getDevices().forEach(device -> updateDevice(device));
705     }
706
707     private void updateDevice(SomfyTahomaDevice device) {
708         String url = device.getDeviceURL();
709         List<SomfyTahomaState> states = device.getStates();
710         updateDevice(url, states);
711     }
712
713     private void updateDevice(String url, List<SomfyTahomaState> states) {
714         Thing th = getThingByDeviceUrl(url);
715         if (th == null) {
716             return;
717         }
718         SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) th.getHandler();
719         if (handler != null) {
720             handler.updateThingStatus(states);
721             handler.updateThingChannels(states);
722         }
723     }
724
725     private void processStateChangedEvent(SomfyTahomaEvent event) {
726         String deviceUrl = event.getDeviceUrl();
727         List<SomfyTahomaState> states = event.getDeviceStates();
728         logger.debug("States for device {} : {}", deviceUrl, states);
729         Thing thing = getThingByDeviceUrl(deviceUrl);
730
731         if (thing != null) {
732             logger.debug("Updating status of thing: {}", thing.getLabel());
733             SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) thing.getHandler();
734
735             if (handler != null) {
736                 // update thing status
737                 handler.updateThingStatus(states);
738                 handler.updateThingChannels(states);
739             }
740         } else {
741             logger.debug("Thing is disabled or handler is null, probably not bound thing.");
742         }
743     }
744
745     private void enableReconciliation() {
746         logger.debug("Enabling reconciliation");
747         reconciliation = true;
748     }
749
750     private void refreshTahomaStates() {
751         logger.debug("Refreshing Tahoma states...");
752         if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
753             return;
754         }
755
756         // force Tahoma to ask for actual states
757         forceGatewaySync();
758     }
759
760     private @Nullable Thing getThingByDeviceUrl(String deviceUrl) {
761         for (Thing th : getThing().getThings()) {
762             if (!th.isEnabled()) {
763                 continue;
764             }
765             String url = (String) th.getConfiguration().get("url");
766             if (deviceUrl.equals(url)) {
767                 return th;
768             }
769         }
770         return null;
771     }
772
773     private void logout() {
774         try {
775             eventsId = "";
776             sendGetToTahomaWithCookie("logout");
777         } catch (ExecutionException | TimeoutException e) {
778             logger.debug("Cannot send logout command!", e);
779         } catch (InterruptedException e) {
780             Thread.currentThread().interrupt();
781         }
782     }
783
784     private String sendPostToTahomaWithCookie(String url, String urlParameters)
785             throws InterruptedException, ExecutionException, TimeoutException {
786         return sendMethodToTahomaWithCookie(url, HttpMethod.POST, urlParameters);
787     }
788
789     private String sendGetToTahomaWithCookie(String url)
790             throws InterruptedException, ExecutionException, TimeoutException {
791         return sendMethodToTahomaWithCookie(url, HttpMethod.GET);
792     }
793
794     private String sendPutToTahomaWithCookie(String url)
795             throws InterruptedException, ExecutionException, TimeoutException {
796         return sendMethodToTahomaWithCookie(url, HttpMethod.PUT);
797     }
798
799     private String sendDeleteToTahomaWithCookie(String url)
800             throws InterruptedException, ExecutionException, TimeoutException {
801         return sendMethodToTahomaWithCookie(url, HttpMethod.DELETE);
802     }
803
804     private String sendMethodToTahomaWithCookie(String url, HttpMethod method)
805             throws InterruptedException, ExecutionException, TimeoutException {
806         return sendMethodToTahomaWithCookie(url, method, "");
807     }
808
809     private String sendMethodToTahomaWithCookie(String url, HttpMethod method, String urlParameters)
810             throws InterruptedException, ExecutionException, TimeoutException {
811         logger.debug("Sending {} to url: {} with data: {}", method.asString(), getApiFullUrl(url), urlParameters);
812         Request request = sendRequestBuilder(url, method);
813         if (!urlParameters.isEmpty()) {
814             request = request.content(new StringContentProvider(urlParameters), "application/json");
815         }
816
817         ContentResponse response = request.send();
818
819         if (logger.isTraceEnabled()) {
820             logger.trace("Response: {}", response.getContentAsString());
821         }
822
823         if (response.getStatus() < 200 || response.getStatus() >= 300) {
824             logger.debug("Received unexpected status code: {}", response.getStatus());
825             if (response.getHeaders().contains(HttpHeader.CONTENT_TYPE)) {
826                 if (response.getHeaders().getField(HttpHeader.CONTENT_TYPE).getValue()
827                         .equalsIgnoreCase(MediaType.APPLICATION_JSON)) {
828                     try {
829                         SomfyTahomaError error = gson.fromJson(response.getContentAsString(), SomfyTahomaError.class);
830                         throw new ExecutionException(error.getError(), null);
831                     } catch (JsonSyntaxException e) {
832
833                     }
834                 }
835             }
836             throw new ExecutionException(
837                     "Unknown http error " + response.getStatus() + " while attempting to send a message.", null);
838         }
839         return response.getContentAsString();
840     }
841
842     private Request sendRequestBuilder(String subUrl, HttpMethod method) {
843         return isLocalRequest(subUrl) ? sendRequestBuilderLocal(subUrl, method)
844                 : sendRequestBuilderCloud(subUrl, method);
845     }
846
847     private boolean isLocalRequest(String subUrl) {
848         return isDevModeReady() && !subUrl.startsWith(CONFIG_URL);
849     }
850
851     private Request sendRequestBuilderCloud(String subUrl, HttpMethod method) {
852         Request request = httpClient.newRequest(getApiFullUrl(subUrl)).method(method)
853                 .header(HttpHeader.ACCEPT_LANGUAGE, "en-US,en").header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate")
854                 .header("X-Requested-With", "XMLHttpRequest").timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS)
855                 .agent(TAHOMA_AGENT);
856
857         if (!thingConfig.getCloudPortal().equalsIgnoreCase(COZYTOUCH_PORTAL)) {
858             // user OAuth token if not cozytouch
859             request = request.header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken);
860         }
861         return request;
862     }
863
864     private Request sendRequestBuilderLocal(String subUrl, HttpMethod method) {
865         return httpClient.newRequest(getApiFullUrl(subUrl)).method(method).accept("application/json")
866                 .timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS).header(HttpHeader.AUTHORIZATION, "Bearer " + localToken);
867     }
868
869     /**
870      * Performs the login for Cozytouch using OAUTH2 authorization.
871      *
872      * @return JSESSION ID cookie value.
873      * @throws ExecutionException
874      * @throws TimeoutException
875      * @throws InterruptedException
876      * @throws JsonSyntaxException
877      */
878     private String getCozytouchJWT()
879             throws InterruptedException, TimeoutException, ExecutionException, JsonSyntaxException {
880         String authBaseUrl = "https://" + COZYTOUCH_OAUTH2_URL;
881
882         String urlParameters = "grant_type=password&username=" + urlEncode(thingConfig.getEmail()) + "&password="
883                 + urlEncode(thingConfig.getPassword());
884
885         ContentResponse response = httpClient.newRequest(authBaseUrl + COZYTOUCH_OAUTH2_TOKEN_URL)
886                 .method(HttpMethod.POST).header(HttpHeader.ACCEPT_LANGUAGE, "en-US,en")
887                 .header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate").header("X-Requested-With", "XMLHttpRequest")
888                 .header(HttpHeader.AUTHORIZATION, "Basic " + COZYTOUCH_OAUTH2_BASICAUTH)
889                 .timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS).agent(TAHOMA_AGENT)
890                 .content(new StringContentProvider(urlParameters), "application/x-www-form-urlencoded; charset=UTF-8")
891                 .send();
892
893         if (response.getStatus() != 200) {
894             // Login error
895             if (response.getHeaders().getField(HttpHeader.CONTENT_TYPE).getValue()
896                     .equalsIgnoreCase(MediaType.APPLICATION_JSON)) {
897                 try {
898                     SomfyTahomaOauth2Error error = gson.fromJson(response.getContentAsString(),
899                             SomfyTahomaOauth2Error.class);
900                     throw new ExecutionException(error.getErrorDescription(), null);
901                 } catch (JsonSyntaxException e) {
902
903                 }
904             }
905             throw new ExecutionException("Unknown error while attempting to log in.", null);
906         }
907
908         SomfyTahomaOauth2Reponse oauth2response = gson.fromJson(response.getContentAsString(),
909                 SomfyTahomaOauth2Reponse.class);
910
911         logger.debug("OAuth2 Access Token: {}", oauth2response.getAccessToken());
912
913         response = httpClient.newRequest(authBaseUrl + COZYTOUCH_OAUTH2_JWT_URL).method(HttpMethod.GET)
914                 .header(HttpHeader.AUTHORIZATION, "Bearer " + oauth2response.getAccessToken())
915                 .timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS).send();
916
917         if (response.getStatus() == 200) {
918             String jwt = response.getContentAsString();
919             return jwt.replace("\"", "");
920         } else {
921             throw new ExecutionException(String.format("Failed to retrieve JWT token. ResponseCode=%d, ResponseText=%s",
922                     response.getStatus(), response.getContentAsString()), null);
923         }
924     }
925
926     private void loginOAUTH() throws InterruptedException, TimeoutException, ExecutionException, JsonSyntaxException {
927         String authBaseUrl = "https://" + SOMFY_OAUTH2_URL;
928
929         String urlParameters = "client_id=" + SOMFY_OAUTH2_CLIENT_ID + "&client_secret=" + SOMFY_OAUTH2_CLIENT_SECRET
930                 + "&grant_type=password&username=" + urlEncode(thingConfig.getEmail()) + "&password="
931                 + urlEncode(thingConfig.getPassword());
932
933         ContentResponse response = httpClient.newRequest(authBaseUrl).method(HttpMethod.POST)
934                 .header(HttpHeader.ACCEPT_LANGUAGE, "en-US,en").header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate")
935                 .header("X-Requested-With", "XMLHttpRequest").timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS)
936                 .agent(TAHOMA_AGENT)
937                 .content(new StringContentProvider(urlParameters), "application/x-www-form-urlencoded; charset=UTF-8")
938                 .send();
939
940         if (response.getStatus() != 200) {
941             // Login error
942             if (response.getHeaders().getField(HttpHeader.CONTENT_TYPE).getValue()
943                     .equalsIgnoreCase(MediaType.APPLICATION_JSON)) {
944                 try {
945                     SomfyTahomaOauth2Error error = gson.fromJson(response.getContentAsString(),
946                             SomfyTahomaOauth2Error.class);
947                     throw new ExecutionException(error.getErrorDescription(), null);
948                 } catch (JsonSyntaxException e) {
949                 }
950             }
951             throw new ExecutionException("Unknown error while attempting to log in.", null);
952         }
953
954         SomfyTahomaOauth2Reponse oauth2response = gson.fromJson(response.getContentAsString(),
955                 SomfyTahomaOauth2Reponse.class);
956
957         logger.debug("OAuth2 Access Token: {}", oauth2response.getAccessToken());
958
959         accessToken = oauth2response.getAccessToken();
960     }
961
962     private String getApiFullUrl(String subUrl) {
963         return isLocalRequest(subUrl)
964                 ? "https://" + thingConfig.getIp() + ":8443/enduser-mobile-web/1/enduserAPI/" + subUrl
965                 : "https://" + thingConfig.getCloudPortal() + API_BASE_URL + subUrl;
966     }
967
968     public void sendCommand(String io, String command, String params, String url) {
969         if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
970             return;
971         }
972
973         removeFinishedRetries();
974
975         boolean result = sendCommandInternal(io, command, params, url);
976         if (!result) {
977             scheduleRetry(io, command, params, url, thingConfig.getRetries());
978         }
979     }
980
981     private void repeatSendCommandInternal(String io, String command, String params, String url, int retries) {
982         logger.debug("Retrying command, retries left: {}", retries);
983         boolean result = sendCommandInternal(io, command, params, url);
984         if (!result && (retries > 0)) {
985             scheduleRetry(io, command, params, url, retries - 1);
986         }
987     }
988
989     private boolean sendCommandInternal(String io, String command, String params, String url) {
990         String value = "[]".equals(params) ? command : command + " " + params.replace("\"", "");
991         String urlParameters = "{\"label\":\"" + getThingLabelByURL(io) + " - " + value
992                 + " - openHAB\",\"actions\":[{\"deviceURL\":\"" + io + "\",\"commands\":[{\"name\":\"" + command
993                 + "\",\"parameters\":" + params + "}]}]}";
994         SomfyTahomaApplyResponse response = invokeCallToURL(url, urlParameters, HttpMethod.POST,
995                 SomfyTahomaApplyResponse.class);
996         if (response != null) {
997             if (!response.getExecId().isEmpty()) {
998                 logger.debug("Exec id: {}", response.getExecId());
999                 registerExecution(io, response.getExecId());
1000                 scheduleNextGetUpdates();
1001             } else {
1002                 logger.debug("ExecId is empty!");
1003                 return false;
1004             }
1005             return true;
1006         }
1007         return false;
1008     }
1009
1010     private void removeFinishedRetries() {
1011         retryFutures.removeIf(x -> x.isDone());
1012         logger.debug("Currently {} retries are scheduled.", retryFutures.size());
1013     }
1014
1015     private void scheduleRetry(String io, String command, String params, String url, int retries) {
1016         retryFutures.add(scheduler.schedule(() -> {
1017             repeatSendCommandInternal(io, command, params, url, retries);
1018         }, thingConfig.getRetryDelay(), TimeUnit.MILLISECONDS));
1019     }
1020
1021     public void sendCommandToSameDevicesInPlace(String io, String command, String params, String url) {
1022         SomfyTahomaDevice device = devicePlaces.get(io);
1023         if (device != null && !device.getPlaceOID().isEmpty()) {
1024             devicePlaces.forEach((deviceUrl, devicePlace) -> {
1025                 if (device.getPlaceOID().equals(devicePlace.getPlaceOID())
1026                         && device.getDefinition().getWidgetName().equals(devicePlace.getDefinition().getWidgetName())) {
1027                     sendCommand(deviceUrl, command, params, url);
1028                 }
1029             });
1030         } else {
1031             sendCommand(io, command, params, url);
1032         }
1033     }
1034
1035     private String getThingLabelByURL(String io) {
1036         Thing th = getThingByDeviceUrl(io);
1037         if (th != null) {
1038             if (th.getProperties().containsKey(NAME_STATE)) {
1039                 // Return label from Tahoma
1040                 return th.getProperties().get(NAME_STATE).replace("\"", "");
1041             }
1042             // Return label from the thing
1043             String label = th.getLabel();
1044             return label != null ? label.replace("\"", "") : "";
1045         }
1046         return "null";
1047     }
1048
1049     public @Nullable String getCurrentExecutions(String io) {
1050         if (executions.containsKey(io)) {
1051             return executions.get(io);
1052         }
1053         return null;
1054     }
1055
1056     public void cancelExecution(String executionId) {
1057         invokeCallToURL(DELETE_URL + executionId, "", HttpMethod.DELETE, null);
1058     }
1059
1060     public void executeActionGroup(String id) {
1061         if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
1062             return;
1063         }
1064         String execId = executeActionGroupInternal(id);
1065         if (execId == null) {
1066             execId = executeActionGroupInternal(id);
1067         }
1068         if (execId != null) {
1069             registerExecution(id, execId);
1070             scheduleNextGetUpdates();
1071         }
1072     }
1073
1074     private boolean reLogin() {
1075         logger.debug("Doing relogin");
1076         reLoginNeeded = true;
1077         localToken = "";
1078         accessToken = "";
1079         login();
1080         return ThingStatus.OFFLINE != thing.getStatus();
1081     }
1082
1083     public @Nullable String executeActionGroupInternal(String id) {
1084         SomfyTahomaApplyResponse response = invokeCallToURL(EXEC_URL + id, "", HttpMethod.POST,
1085                 SomfyTahomaApplyResponse.class);
1086         if (response != null) {
1087             if (response.getExecId().isEmpty()) {
1088                 logger.debug("Got empty exec response");
1089                 return null;
1090             }
1091             return response.getExecId();
1092         }
1093         return null;
1094     }
1095
1096     public void forceGatewaySync() {
1097         // refresh is valid only if in a cloud mode
1098         if (!thingConfig.isDevMode() || localToken.isEmpty()) {
1099             invokeCallToURL(REFRESH_URL, "", HttpMethod.PUT, null);
1100         }
1101     }
1102
1103     public SomfyTahomaStatus getTahomaStatus(String gatewayId) {
1104         SomfyTahomaStatusResponse status = null;
1105
1106         if (isDevModeReady()) {
1107             // Local endpoint does not have a method for specific gateway
1108             SomfyTahomaStatusResponse[] data = invokeCallToURL(GATEWAYS_URL, "", HttpMethod.GET,
1109                     SomfyTahomaStatusResponse[].class);
1110             if (data != null) {
1111                 for (SomfyTahomaStatusResponse gatewayStatus : data) {
1112                     if (gatewayStatus.getGatewayId().equals(gatewayId)) {
1113                         status = gatewayStatus;
1114                         break;
1115                     }
1116                 }
1117             }
1118         } else {
1119             status = invokeCallToURL(GATEWAYS_URL + gatewayId, "", HttpMethod.GET, SomfyTahomaStatusResponse.class);
1120         }
1121
1122         if (status != null) {
1123             logger.debug("Tahoma status: {}", status.getConnectivity().getStatus());
1124             logger.debug("Tahoma protocol version: {}", status.getConnectivity().getProtocolVersion());
1125             return status.getConnectivity();
1126         }
1127         return new SomfyTahomaStatus();
1128     }
1129
1130     private boolean isTempBanned(Exception ex) {
1131         String msg = ex.getMessage();
1132         return msg != null && msg.contains(TEMPORARILY_BANNED);
1133     }
1134
1135     private boolean isEventListenerTimeout(Exception ex) {
1136         String msg = ex.getMessage();
1137         return msg != null && msg.contains(EVENT_LISTENER_TIMEOUT);
1138     }
1139
1140     private boolean isOAuthGrantError(Exception ex) {
1141         String msg = ex.getMessage();
1142         return msg != null
1143                 && (msg.contains(AUTHENTICATION_OAUTH_GRANT_ERROR) || msg.contains(AUTHENTICATION_OAUTH_INVALID_GRANT));
1144     }
1145
1146     @Override
1147     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
1148         super.handleConfigurationUpdate(configurationParameters);
1149         if (configurationParameters.containsKey("email") || configurationParameters.containsKey("password")
1150                 || configurationParameters.containsKey("portalUrl")) {
1151             reLoginNeeded = true;
1152             tooManyRequests = false;
1153         }
1154     }
1155
1156     public synchronized void refresh(String url, String stateName) {
1157         SomfyTahomaState state = invokeCallToURL(DEVICES_URL + urlEncode(url) + "/states/" + stateName, "",
1158                 HttpMethod.GET, SomfyTahomaState.class);
1159         if (state != null && !state.getName().isEmpty()) {
1160             updateDevice(url, List.of(state));
1161         }
1162     }
1163
1164     private @Nullable <T> T invokeCallToURL(String url, String urlParameters, HttpMethod method,
1165             @Nullable Class<T> classOfT) {
1166         String response = "";
1167         try {
1168             switch (method) {
1169                 case GET:
1170                     response = sendGetToTahomaWithCookie(url);
1171                     break;
1172                 case PUT:
1173                     response = sendPutToTahomaWithCookie(url);
1174                     break;
1175                 case POST:
1176                     response = sendPostToTahomaWithCookie(url, urlParameters);
1177                     break;
1178                 case DELETE:
1179                     response = sendDeleteToTahomaWithCookie(url);
1180                 default:
1181             }
1182             errorsCounter = 0;
1183             return classOfT != null ? gson.fromJson(response, classOfT) : null;
1184         } catch (JsonSyntaxException e) {
1185             logger.debug("Received data: {} is not JSON", response, e);
1186             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
1187         } catch (ExecutionException e) {
1188             if (isTempBanned(e)) {
1189                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Temporarily banned");
1190                 setTooManyRequests();
1191             } else if (isEventListenerTimeout(e)) {
1192                 logger.debug("Event listener timeout occurred", e);
1193                 reLogin();
1194             } else if (isDevModeReady()) {
1195                 // the local gateway is unreachable
1196                 errorsCounter++;
1197                 logger.debug("Local gateway communication error", e);
1198                 discoverGateway();
1199                 if (errorsCounter > MAX_ERRORS) {
1200                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1201                             "Too many communication errors");
1202                 }
1203             } else {
1204                 logger.debug("Cannot call url: {} with params: {}!", getApiFullUrl(url), urlParameters, e);
1205                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
1206             }
1207         } catch (TimeoutException e) {
1208             errorsCounter++;
1209             logger.debug("Timeout when calling url: {} with params: {}!", getApiFullUrl(url), urlParameters, e);
1210             if (errorsCounter > MAX_ERRORS) {
1211                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Too many timeouts");
1212             }
1213         } catch (InterruptedException e) {
1214             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
1215             Thread.currentThread().interrupt();
1216         }
1217         return null;
1218     }
1219
1220     public void setGatewayIPAddress(String gatewayIPAddress) {
1221         thingConfig.setIp(gatewayIPAddress);
1222     }
1223
1224     public void setGatewayPin(String gatewayPin) {
1225         thingConfig.setPin(gatewayPin);
1226     }
1227
1228     public void updateConfiguration() {
1229         Configuration config = editConfiguration();
1230         config.put("ip", thingConfig.getIp());
1231         config.put("pin", thingConfig.getPin());
1232         if (!localToken.isEmpty()) {
1233             config.put("token", localToken);
1234         }
1235         updateConfiguration(config);
1236     }
1237 }