2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.somfytahoma.internal.handler;
15 import static org.openhab.binding.somfytahoma.internal.SomfyTahomaBindingConstants.*;
17 import java.io.UnsupportedEncodingException;
18 import java.net.URLEncoder;
19 import java.nio.charset.StandardCharsets;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
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;
46 import com.google.gson.Gson;
47 import com.google.gson.JsonElement;
48 import com.google.gson.JsonSyntaxException;
51 * The {@link SomfyTahomaBridgeHandler} is responsible for handling commands, which are
52 * sent to one of the channels.
54 * @author Ondrej Pecta - Initial contribution
57 public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
59 private final Logger logger = LoggerFactory.getLogger(SomfyTahomaBridgeHandler.class);
62 * The shared HttpClient
64 private final HttpClient httpClient;
67 * Future to poll for updates
69 private @Nullable ScheduledFuture<?> pollFuture;
72 * Future to poll for status
74 private @Nullable ScheduledFuture<?> statusFuture;
77 * Future to set reconciliation flag
79 private @Nullable ScheduledFuture<?> reconciliationFuture;
84 private Map<String, String> executions = new HashMap<>();
86 // Too many requests flag
87 private boolean tooManyRequests = false;
89 // Silent relogin flag
90 private boolean reLoginNeeded = false;
92 // Reconciliation flag
93 private boolean reconciliation = false;
98 protected SomfyTahomaConfig thingConfig = new SomfyTahomaConfig();
101 * Id of registered events
103 private String eventsId = "";
106 private final Gson gson = new Gson();
108 public SomfyTahomaBridgeHandler(Bridge thing, HttpClientFactory httpClientFactory) {
110 this.httpClient = httpClientFactory.createHttpClient("somfy_" + thing.getUID().getId());
114 public void handleCommand(ChannelUID channelUID, Command command) {
118 public void initialize() {
119 thingConfig = getConfigAs(SomfyTahomaConfig.class);
123 } catch (Exception e) {
124 logger.debug("Cannot start http client for: {}", thing.getBridgeUID().getId(), e);
128 scheduler.execute(() -> {
131 logger.debug("Initialize done...");
136 * starts this things polling future
138 private void initPolling() {
140 pollFuture = scheduler.scheduleWithFixedDelay(() -> {
142 }, 10, thingConfig.getRefresh(), TimeUnit.SECONDS);
144 statusFuture = scheduler.scheduleWithFixedDelay(() -> {
145 refreshTahomaStates();
146 }, 60, thingConfig.getStatusTimeout(), TimeUnit.SECONDS);
148 reconciliationFuture = scheduler.scheduleWithFixedDelay(() -> {
149 enableReconciliation();
150 }, RECONCILIATION_TIME, RECONCILIATION_TIME, TimeUnit.SECONDS);
153 public synchronized void login() {
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");
162 if (tooManyRequests) {
163 logger.debug("Skipping login due to too many requests");
167 if (ThingStatus.ONLINE == thing.getStatus() && !reLoginNeeded) {
168 logger.debug("No need to log in, because already logged in");
172 reLoginNeeded = false;
175 url = TAHOMA_API_URL + "login";
176 String urlParameters = "userId=" + urlEncode(thingConfig.getEmail()) + "&userPassword="
177 + urlEncode(thingConfig.getPassword());
179 ContentResponse response = sendRequestBuilder(url, HttpMethod.POST)
180 .content(new StringContentProvider(urlParameters),
181 "application/x-www-form-urlencoded; charset=UTF-8")
184 if (logger.isTraceEnabled()) {
185 logger.trace("Login response: {}", response.getContentAsString());
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)) {
195 logger.debug("Events id: {}", eventsId);
196 updateStatus(ThingStatus.ONLINE);
198 logger.debug("Events id error: {}", id);
201 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
202 "Error logging in: " + data.getError());
203 if (data.getError().startsWith(TOO_MANY_REQUESTS)) {
204 setTooManyRequests();
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();
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();
224 } catch (UnsupportedEncodingException e) {
225 logger.debug("Cannot login due to unsupported encoding", e);
226 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unsupported encoding");
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);
236 private @Nullable String registerEvents() {
240 url = TAHOMA_EVENTS_URL + "register";
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);
248 } catch (ExecutionException e) {
249 if (isAuthenticationChallenge(e)) {
253 logger.info("Cannot register events!", e);
254 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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();
266 private String urlEncode(String text) throws UnsupportedEncodingException {
267 return URLEncoder.encode(text, StandardCharsets.UTF_8.toString());
270 private void enableLogin() {
271 tooManyRequests = false;
274 private List<SomfyTahomaEvent> getEvents() {
279 url = TAHOMA_API_URL + "events/" + eventsId + "/fetch";
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)) {
291 logger.debug("Cannot get Tahoma events!", e);
292 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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();
301 return new ArrayList<>();
305 public void handleRemoval() {
306 super.handleRemoval();
311 public Collection<Class<? extends ThingHandlerService>> getServices() {
312 return Collections.singleton(SomfyTahomaItemDiscoveryService.class);
316 public void dispose() {
321 private void cleanup() {
322 logger.debug("Doing cleanup");
327 } catch (Exception e) {
328 logger.debug("Error during http client stopping", e);
333 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
334 super.bridgeStatusChanged(bridgeStatusInfo);
335 if (ThingStatus.UNINITIALIZED == bridgeStatusInfo.getStatus()) {
341 * Stops this thing's polling future
343 private void stopPolling() {
344 ScheduledFuture<?> localPollFuture = pollFuture;
345 if (localPollFuture != null && !localPollFuture.isCancelled()) {
346 localPollFuture.cancel(true);
348 ScheduledFuture<?> localStatusFuture = statusFuture;
349 if (localStatusFuture != null && !localStatusFuture.isCancelled()) {
350 localStatusFuture.cancel(true);
352 ScheduledFuture<?> localReconciliationFuture = reconciliationFuture;
353 if (localReconciliationFuture != null && !localReconciliationFuture.isCancelled()) {
354 localReconciliationFuture.cancel(true);
358 public List<SomfyTahomaActionGroup> listActionGroups() {
359 String groups = getGroups();
360 if (StringUtils.equals(groups, UNAUTHORIZED)) {
361 groups = getGroups();
364 if (groups == null || groups.equals(UNAUTHORIZED)) {
365 return Collections.emptyList();
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");
375 return new ArrayList<>();
378 private @Nullable String getGroups() {
382 url = TAHOMA_API_URL + "actionGroups";
383 return sendGetToTahomaWithCookie(url);
384 } catch (ExecutionException e) {
385 if (isAuthenticationChallenge(e)) {
389 logger.debug("Cannot send getActionGroups command!", e);
390 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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();
402 public @Nullable SomfyTahomaSetup getSetup() {
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)) {
417 logger.debug("Cannot send getSetup command!", e);
418 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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();
430 public List<SomfyTahomaDevice> getDevices() {
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)) {
445 logger.debug("Cannot send get devices command!", e);
446 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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();
455 return Collections.emptyList();
458 private void getTahomaUpdates() {
459 logger.debug("Getting Tahoma Updates...");
460 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
464 List<SomfyTahomaEvent> events = getEvents();
465 logger.trace("Got total of {} events", events.size());
466 for (SomfyTahomaEvent event : events) {
471 private void processEvent(SomfyTahomaEvent event) {
472 logger.debug("Got event: {}", event.getName());
473 switch (event.getName()) {
474 case "ExecutionRegisteredEvent":
475 processExecutionRegisteredEvent(event);
477 case "ExecutionStateChangedEvent":
478 processExecutionChangedEvent(event);
480 case "DeviceStateChangedEvent":
481 processStateChangedEvent(event);
483 case "RefreshAllDevicesStatesCompletedEvent":
484 scheduler.schedule(this::updateThings, 1, TimeUnit.SECONDS);
486 case "GatewayAliveEvent":
487 case "GatewayDownEvent":
488 processGatewayEvent(event);
491 // ignore other states
495 private synchronized void updateThings() {
496 boolean needsUpdate = reconciliation;
498 for (Thing th : getThing().getThings()) {
499 if (ThingStatus.ONLINE != th.getStatus()) {
504 // update all states only if necessary
507 reconciliation = false;
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());
519 SomfyTahomaAction action = gson.fromJson(el, SomfyTahomaAction.class);
520 registerExecution(action.getDeviceURL(), event.getExecId());
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());
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);
536 executions.put(url, execId);
539 private void unregisterExecution(String execId) {
540 if (executions.containsValue(execId)) {
541 executions.values().removeAll(Collections.singleton(execId));
543 logger.debug("Cannot remove execution id: {}, because it is not registered", execId);
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);
559 private synchronized void updateAllStates() {
560 logger.debug("Updating all states");
561 getDevices().forEach(device -> updateDevice(device));
564 private void updateDevice(SomfyTahomaDevice device) {
565 String url = device.getDeviceURL();
566 List<SomfyTahomaState> states = device.getStates();
567 updateDevice(url, states);
570 private void updateDevice(String url, List<SomfyTahomaState> states) {
571 Thing th = getThingByDeviceUrl(url);
575 SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) th.getHandler();
576 if (handler != null) {
577 handler.updateThingStatus(states);
578 handler.updateThingChannels(states);
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);
589 logger.debug("Updating status of thing: {}", thing.getLabel());
590 SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) thing.getHandler();
592 if (handler != null) {
593 // update thing status
594 handler.updateThingStatus(states);
595 handler.updateThingChannels(states);
598 logger.debug("Thing handler is null, probably not bound thing.");
602 private void enableReconciliation() {
603 logger.debug("Enabling reconciliation");
604 reconciliation = true;
607 private void refreshTahomaStates() {
608 logger.debug("Refreshing Tahoma states...");
609 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
613 // force Tahoma to ask for actual states
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)) {
627 private void logout() {
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();
639 private String sendPostToTahomaWithCookie(String url, String urlParameters)
640 throws InterruptedException, ExecutionException, TimeoutException {
641 return sendMethodToTahomaWithCookie(url, HttpMethod.POST, urlParameters);
644 private String sendGetToTahomaWithCookie(String url)
645 throws InterruptedException, ExecutionException, TimeoutException {
646 return sendMethodToTahomaWithCookie(url, HttpMethod.GET);
649 private String sendPutToTahomaWithCookie(String url)
650 throws InterruptedException, ExecutionException, TimeoutException {
651 return sendMethodToTahomaWithCookie(url, HttpMethod.PUT);
654 private String sendDeleteToTahomaWithCookie(String url)
655 throws InterruptedException, ExecutionException, TimeoutException {
656 return sendMethodToTahomaWithCookie(url, HttpMethod.DELETE);
659 private String sendMethodToTahomaWithCookie(String url, HttpMethod method)
660 throws InterruptedException, ExecutionException, TimeoutException {
661 return sendMethodToTahomaWithCookie(url, method, "");
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");
672 ContentResponse response = request.send();
674 if (logger.isTraceEnabled()) {
675 logger.trace("Response: {}", response.getContentAsString());
678 if (response.getStatus() < 200 || response.getStatus() >= 300) {
679 logger.debug("Received unexpected status code: {}", response.getStatus());
681 return response.getContentAsString();
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);
690 public void sendCommand(String io, String command, String params) {
691 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
695 Boolean result = sendCommandInternal(io, command, params);
696 if (result != null && !result) {
697 sendCommandInternal(io, command, params);
701 private @Nullable Boolean sendCommandInternal(String io, String command, String params) {
706 url = EXEC_URL + "apply";
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 + "}]}]}";
713 line = sendPostToTahomaWithCookie(url, urlParameters);
715 SomfyTahomaApplyResponse data = gson.fromJson(line, SomfyTahomaApplyResponse.class);
717 if (!StringUtils.isEmpty(data.getExecId())) {
718 logger.debug("Exec id: {}", data.getExecId());
719 registerExecution(io, data.getExecId());
721 logger.warn("Apply command response: {}", line);
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)) {
733 logger.debug("Cannot send apply command {} with params {}!", command, params, e);
734 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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();
746 private String getThingLabelByURL(String io) {
747 Thing th = getThingByDeviceUrl(io);
749 if (th.getProperties().containsKey(NAME_STATE)) {
750 // Return label from Tahoma
751 return th.getProperties().get(NAME_STATE).replace("\"", "");
753 // Return label from OH2
754 String label = th.getLabel();
755 return label != null ? label.replace("\"", "") : "";
760 public @Nullable String getCurrentExecutions(String io) {
761 if (executions.containsKey(io)) {
762 return executions.get(io);
767 public void cancelExecution(String executionId) {
768 Boolean result = cancelExecutionInternal(executionId);
769 if (result != null && !result) {
770 cancelExecutionInternal(executionId);
774 private @Nullable Boolean cancelExecutionInternal(String executionId) {
778 url = DELETE_URL + executionId;
779 sendDeleteToTahomaWithCookie(url);
781 } catch (ExecutionException e) {
782 if (isAuthenticationChallenge(e)) {
786 logger.debug("Cannot cancel execution!", e);
787 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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();
799 public void executeActionGroup(String id) {
800 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
803 String execId = executeActionGroupInternal(id);
804 if (UNAUTHORIZED.equals(execId)) {
805 execId = executeActionGroupInternal(id);
807 if (!UNAUTHORIZED.equals(execId) && execId != null) {
808 registerExecution(id, execId);
812 private boolean reLogin() {
813 logger.debug("Doing relogin");
814 reLoginNeeded = true;
816 return ThingStatus.OFFLINE != thing.getStatus();
819 public @Nullable String executeActionGroupInternal(String id) {
822 String url = EXEC_URL + id;
824 line = sendPostToTahomaWithCookie(url, "");
825 SomfyTahomaApplyResponse data = gson.fromJson(line, SomfyTahomaApplyResponse.class);
826 if (data.getExecId().isEmpty()) {
827 logger.debug("Got empty exec response");
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)) {
838 logger.debug("Cannot exec execution group!", e);
839 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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();
851 public void forceGatewaySync() {
853 sendPutToTahomaWithCookie(REFRESH_URL);
854 } catch (ExecutionException e) {
855 if (isAuthenticationChallenge(e)) {
858 logger.debug("Cannot sync gateway!", e);
859 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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();
870 public SomfyTahomaStatus getTahomaStatus(String gatewayId) {
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)) {
885 return new SomfyTahomaStatus();
887 logger.debug("Cannot get Tahoma gateway status!", e);
888 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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();
897 return new SomfyTahomaStatus();
900 private boolean isAuthenticationChallenge(Exception ex) {
901 return ex.getMessage().contains(AUTHENTICATION_CHALLENGE);
905 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
906 super.handleConfigurationUpdate(configurationParameters);
907 if (configurationParameters.containsKey("email")) {
908 thingConfig.setEmail(configurationParameters.get("email").toString());
910 if (configurationParameters.containsKey("password")) {
911 thingConfig.setPassword(configurationParameters.get("password").toString());
915 public synchronized void refresh(String url, String stateName) {
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));
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)) {
929 logger.debug("Cannot refresh device states!", e);
930 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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();