2 * Copyright (c) 2010-2021 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;
20 import java.time.Duration;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.List;
26 import java.util.concurrent.ConcurrentLinkedQueue;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.TimeoutException;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.eclipse.jetty.client.HttpClient;
35 import org.eclipse.jetty.client.api.ContentResponse;
36 import org.eclipse.jetty.client.api.Request;
37 import org.eclipse.jetty.client.util.StringContentProvider;
38 import org.eclipse.jetty.http.HttpHeader;
39 import org.eclipse.jetty.http.HttpMethod;
40 import org.openhab.binding.somfytahoma.internal.config.SomfyTahomaConfig;
41 import org.openhab.binding.somfytahoma.internal.discovery.SomfyTahomaItemDiscoveryService;
42 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaAction;
43 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaActionGroup;
44 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaApplyResponse;
45 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaDevice;
46 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaEvent;
47 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaLoginResponse;
48 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaRegisterEventsResponse;
49 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaSetup;
50 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaState;
51 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaStatus;
52 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaStatusResponse;
53 import org.openhab.core.cache.ExpiringCache;
54 import org.openhab.core.io.net.http.HttpClientFactory;
55 import org.openhab.core.thing.Bridge;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.ThingStatusInfo;
61 import org.openhab.core.thing.binding.BaseBridgeHandler;
62 import org.openhab.core.thing.binding.ThingHandlerService;
63 import org.openhab.core.types.Command;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
67 import com.google.gson.Gson;
68 import com.google.gson.JsonElement;
69 import com.google.gson.JsonSyntaxException;
72 * The {@link SomfyTahomaBridgeHandler} is responsible for handling commands, which are
73 * sent to one of the channels.
75 * @author Ondrej Pecta - Initial contribution
76 * @author Laurent Garnier - Other portals integration
79 public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
81 private final Logger logger = LoggerFactory.getLogger(SomfyTahomaBridgeHandler.class);
84 * The shared HttpClient
86 private final HttpClient httpClient;
89 * Future to poll for updates
91 private @Nullable ScheduledFuture<?> pollFuture;
94 * Future to poll for status
96 private @Nullable ScheduledFuture<?> statusFuture;
99 * Future to set reconciliation flag
101 private @Nullable ScheduledFuture<?> reconciliationFuture;
103 // List of futures used for command retries
104 private Collection<ScheduledFuture<?>> retryFutures = new ConcurrentLinkedQueue<ScheduledFuture<?>>();
109 private Map<String, String> executions = new HashMap<>();
111 // Too many requests flag
112 private boolean tooManyRequests = false;
114 // Silent relogin flag
115 private boolean reLoginNeeded = false;
117 // Reconciliation flag
118 private boolean reconciliation = false;
123 protected SomfyTahomaConfig thingConfig = new SomfyTahomaConfig();
126 * Id of registered events
128 private String eventsId = "";
130 private Map<String, SomfyTahomaDevice> devicePlaces = new HashMap<>();
132 private ExpiringCache<List<SomfyTahomaDevice>> cachedDevices = new ExpiringCache<>(Duration.ofSeconds(30),
136 private final Gson gson = new Gson();
138 public SomfyTahomaBridgeHandler(Bridge thing, HttpClientFactory httpClientFactory) {
140 this.httpClient = httpClientFactory.createHttpClient("somfy_" + thing.getUID().getId());
144 public void handleCommand(ChannelUID channelUID, Command command) {
148 public void initialize() {
149 thingConfig = getConfigAs(SomfyTahomaConfig.class);
153 } catch (Exception e) {
154 logger.debug("Cannot start http client for: {}", thing.getBridgeUID().getId(), e);
158 scheduler.execute(() -> {
161 logger.debug("Initialize done...");
166 * starts this things polling future
168 private void initPolling() {
170 scheduleGetUpdates(10);
172 statusFuture = scheduler.scheduleWithFixedDelay(() -> {
173 refreshTahomaStates();
174 }, 60, thingConfig.getStatusTimeout(), TimeUnit.SECONDS);
176 reconciliationFuture = scheduler.scheduleWithFixedDelay(() -> {
177 enableReconciliation();
178 }, RECONCILIATION_TIME, RECONCILIATION_TIME, TimeUnit.SECONDS);
181 private void scheduleGetUpdates(long delay) {
182 pollFuture = scheduler.schedule(() -> {
184 scheduleNextGetUpdates();
185 }, delay, TimeUnit.SECONDS);
188 private void scheduleNextGetUpdates() {
189 ScheduledFuture<?> localPollFuture = pollFuture;
190 if (localPollFuture != null) {
191 localPollFuture.cancel(false);
193 scheduleGetUpdates(executions.isEmpty() ? thingConfig.getRefresh() : 2);
196 public synchronized void login() {
197 if (thingConfig.getEmail().isEmpty() || thingConfig.getPassword().isEmpty()) {
198 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
199 "Can not access device as username and/or password are null");
203 if (tooManyRequests) {
204 logger.debug("Skipping login due to too many requests");
208 if (ThingStatus.ONLINE == thing.getStatus() && !reLoginNeeded) {
209 logger.debug("No need to log in, because already logged in");
213 reLoginNeeded = false;
216 String urlParameters = "userId=" + urlEncode(thingConfig.getEmail()) + "&userPassword="
217 + urlEncode(thingConfig.getPassword());
219 ContentResponse response = sendRequestBuilder("login", HttpMethod.POST)
220 .content(new StringContentProvider(urlParameters),
221 "application/x-www-form-urlencoded; charset=UTF-8")
224 if (logger.isTraceEnabled()) {
225 logger.trace("Login response: {}", response.getContentAsString());
228 SomfyTahomaLoginResponse data = gson.fromJson(response.getContentAsString(),
229 SomfyTahomaLoginResponse.class);
231 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
232 "Received invalid data (login)");
233 } else if (data.isSuccess()) {
234 logger.debug("SomfyTahoma version: {}", data.getVersion());
235 String id = registerEvents();
236 if (id != null && !UNAUTHORIZED.equals(id)) {
238 logger.debug("Events id: {}", eventsId);
239 updateStatus(ThingStatus.ONLINE);
241 logger.debug("Events id error: {}", id);
244 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
245 "Error logging in: " + data.getError());
246 if (data.getError().startsWith(TOO_MANY_REQUESTS)) {
247 setTooManyRequests();
250 } catch (JsonSyntaxException e) {
251 logger.debug("Received invalid data (login)", e);
252 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data (login)");
253 } catch (ExecutionException e) {
254 if (isAuthenticationChallenge(e)) {
255 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
256 "Error logging in (check your credentials)");
257 setTooManyRequests();
259 logger.debug("Cannot get login cookie", e);
260 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot get login cookie");
262 } catch (TimeoutException e) {
263 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Getting login cookie timeout");
264 } catch (InterruptedException e) {
265 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
266 "Getting login cookie interrupted");
267 Thread.currentThread().interrupt();
271 private void setTooManyRequests() {
272 logger.debug("Too many requests or bad credentials for the cloud portal, suspending activity for {} seconds",
274 tooManyRequests = true;
275 scheduler.schedule(this::enableLogin, SUSPEND_TIME, TimeUnit.SECONDS);
278 private @Nullable String registerEvents() {
279 SomfyTahomaRegisterEventsResponse response = invokeCallToURL(EVENTS_URL + "register", "", HttpMethod.POST,
280 SomfyTahomaRegisterEventsResponse.class);
281 return response != null ? response.getId() : null;
284 private String urlEncode(String text) {
286 return URLEncoder.encode(text, StandardCharsets.UTF_8.toString());
287 } catch (UnsupportedEncodingException e) {
292 private void enableLogin() {
293 tooManyRequests = false;
296 private List<SomfyTahomaEvent> getEvents() {
297 SomfyTahomaEvent[] response = invokeCallToURL(EVENTS_URL + eventsId + "/fetch", "", HttpMethod.POST,
298 SomfyTahomaEvent[].class);
299 return response != null ? List.of(response) : List.of();
303 public void handleRemoval() {
304 super.handleRemoval();
309 public Collection<Class<? extends ThingHandlerService>> getServices() {
310 return Collections.singleton(SomfyTahomaItemDiscoveryService.class);
314 public void dispose() {
319 private void cleanup() {
320 logger.debug("Doing cleanup");
323 // cancel all scheduled retries
324 retryFutures.forEach(x -> x.cancel(false));
328 } catch (Exception e) {
329 logger.debug("Error during http client stopping", e);
334 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
335 super.bridgeStatusChanged(bridgeStatusInfo);
336 if (ThingStatus.UNINITIALIZED == bridgeStatusInfo.getStatus()) {
342 * Stops this thing's polling future
344 private void stopPolling() {
345 ScheduledFuture<?> localPollFuture = pollFuture;
346 if (localPollFuture != null && !localPollFuture.isCancelled()) {
347 localPollFuture.cancel(true);
349 ScheduledFuture<?> localStatusFuture = statusFuture;
350 if (localStatusFuture != null && !localStatusFuture.isCancelled()) {
351 localStatusFuture.cancel(true);
353 ScheduledFuture<?> localReconciliationFuture = reconciliationFuture;
354 if (localReconciliationFuture != null && !localReconciliationFuture.isCancelled()) {
355 localReconciliationFuture.cancel(true);
359 public List<SomfyTahomaActionGroup> listActionGroups() {
360 SomfyTahomaActionGroup[] list = invokeCallToURL("actionGroups", "", HttpMethod.GET,
361 SomfyTahomaActionGroup[].class);
362 return list != null ? List.of(list) : List.of();
365 public @Nullable SomfyTahomaSetup getSetup() {
366 SomfyTahomaSetup setup = invokeCallToURL("setup", "", HttpMethod.GET, SomfyTahomaSetup.class);
368 saveDevicePlaces(setup.getDevices());
373 public List<SomfyTahomaDevice> getDevices() {
374 SomfyTahomaDevice[] response = invokeCallToURL(SETUP_URL + "devices", "", HttpMethod.GET,
375 SomfyTahomaDevice[].class);
376 List<SomfyTahomaDevice> devices = response != null ? List.of(response) : List.of();
377 saveDevicePlaces(devices);
381 public synchronized @Nullable SomfyTahomaDevice getCachedDevice(String url) {
382 List<SomfyTahomaDevice> devices = cachedDevices.getValue();
383 if (devices != null) {
384 for (SomfyTahomaDevice device : devices) {
385 if (url.equals(device.getDeviceURL())) {
393 private void saveDevicePlaces(List<SomfyTahomaDevice> devices) {
394 devicePlaces.clear();
395 for (SomfyTahomaDevice device : devices) {
396 if (!device.getPlaceOID().isEmpty()) {
397 SomfyTahomaDevice newDevice = new SomfyTahomaDevice();
398 newDevice.setPlaceOID(device.getPlaceOID());
399 newDevice.setWidget(device.getWidget());
400 devicePlaces.put(device.getDeviceURL(), newDevice);
405 private void getTahomaUpdates() {
406 logger.debug("Getting Tahoma Updates...");
407 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
411 List<SomfyTahomaEvent> events = getEvents();
412 logger.trace("Got total of {} events", events.size());
413 for (SomfyTahomaEvent event : events) {
418 private void processEvent(SomfyTahomaEvent event) {
419 logger.debug("Got event: {}", event.getName());
420 switch (event.getName()) {
421 case "ExecutionRegisteredEvent":
422 processExecutionRegisteredEvent(event);
424 case "ExecutionStateChangedEvent":
425 processExecutionChangedEvent(event);
427 case "DeviceStateChangedEvent":
428 processStateChangedEvent(event);
430 case "RefreshAllDevicesStatesCompletedEvent":
431 scheduler.schedule(this::updateThings, 1, TimeUnit.SECONDS);
433 case "GatewayAliveEvent":
434 case "GatewayDownEvent":
435 processGatewayEvent(event);
438 // ignore other states
442 private synchronized void updateThings() {
443 boolean needsUpdate = reconciliation;
445 for (Thing th : getThing().getThings()) {
446 if (ThingStatus.ONLINE != th.getStatus()) {
451 // update all states only if necessary
454 reconciliation = false;
458 private void processExecutionRegisteredEvent(SomfyTahomaEvent event) {
459 boolean invalidData = false;
461 JsonElement el = event.getAction();
462 if (el.isJsonArray()) {
463 SomfyTahomaAction[] actions = gson.fromJson(el, SomfyTahomaAction[].class);
464 if (actions == null) {
467 for (SomfyTahomaAction action : actions) {
468 registerExecution(action.getDeviceURL(), event.getExecId());
472 SomfyTahomaAction action = gson.fromJson(el, SomfyTahomaAction.class);
473 if (action == null) {
476 registerExecution(action.getDeviceURL(), event.getExecId());
479 } catch (JsonSyntaxException e) {
483 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
484 "Received invalid data (execution registered)");
488 private void processExecutionChangedEvent(SomfyTahomaEvent event) {
489 if (FAILED_EVENT.equals(event.getNewState()) || COMPLETED_EVENT.equals(event.getNewState())) {
490 logger.debug("Removing execution id: {}", event.getExecId());
491 unregisterExecution(event.getExecId());
495 private void registerExecution(String url, String execId) {
496 if (executions.containsKey(url)) {
497 executions.remove(url);
498 logger.debug("Previous execution exists for url: {}", url);
500 executions.put(url, execId);
503 private void unregisterExecution(String execId) {
504 if (executions.containsValue(execId)) {
505 executions.values().removeAll(Collections.singleton(execId));
507 logger.debug("Cannot remove execution id: {}, because it is not registered", execId);
511 private void processGatewayEvent(SomfyTahomaEvent event) {
512 // update gateway status
513 for (Thing th : getThing().getThings()) {
514 if (THING_TYPE_GATEWAY.equals(th.getThingTypeUID())) {
515 SomfyTahomaGatewayHandler gatewayHandler = (SomfyTahomaGatewayHandler) th.getHandler();
516 if (gatewayHandler != null && gatewayHandler.getGateWayId().equals(event.getGatewayId())) {
517 gatewayHandler.refresh(STATUS);
523 private synchronized void updateAllStates() {
524 logger.debug("Updating all states");
525 getDevices().forEach(device -> updateDevice(device));
528 private void updateDevice(SomfyTahomaDevice device) {
529 String url = device.getDeviceURL();
530 List<SomfyTahomaState> states = device.getStates();
531 updateDevice(url, states);
534 private void updateDevice(String url, List<SomfyTahomaState> states) {
535 Thing th = getThingByDeviceUrl(url);
539 SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) th.getHandler();
540 if (handler != null) {
541 handler.updateThingStatus(states);
542 handler.updateThingChannels(states);
546 private void processStateChangedEvent(SomfyTahomaEvent event) {
547 String deviceUrl = event.getDeviceUrl();
548 List<SomfyTahomaState> states = event.getDeviceStates();
549 logger.debug("States for device {} : {}", deviceUrl, states);
550 Thing thing = getThingByDeviceUrl(deviceUrl);
553 logger.debug("Updating status of thing: {}", thing.getLabel());
554 SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) thing.getHandler();
556 if (handler != null) {
557 // update thing status
558 handler.updateThingStatus(states);
559 handler.updateThingChannels(states);
562 logger.debug("Thing handler is null, probably not bound thing.");
566 private void enableReconciliation() {
567 logger.debug("Enabling reconciliation");
568 reconciliation = true;
571 private void refreshTahomaStates() {
572 logger.debug("Refreshing Tahoma states...");
573 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
577 // force Tahoma to ask for actual states
581 private @Nullable Thing getThingByDeviceUrl(String deviceUrl) {
582 for (Thing th : getThing().getThings()) {
583 String url = (String) th.getConfiguration().get("url");
584 if (deviceUrl.equals(url)) {
591 private void logout() {
594 sendGetToTahomaWithCookie("logout");
595 } catch (ExecutionException | TimeoutException e) {
596 logger.debug("Cannot send logout command!", e);
597 } catch (InterruptedException e) {
598 Thread.currentThread().interrupt();
602 private String sendPostToTahomaWithCookie(String url, String urlParameters)
603 throws InterruptedException, ExecutionException, TimeoutException {
604 return sendMethodToTahomaWithCookie(url, HttpMethod.POST, urlParameters);
607 private String sendGetToTahomaWithCookie(String url)
608 throws InterruptedException, ExecutionException, TimeoutException {
609 return sendMethodToTahomaWithCookie(url, HttpMethod.GET);
612 private String sendPutToTahomaWithCookie(String url)
613 throws InterruptedException, ExecutionException, TimeoutException {
614 return sendMethodToTahomaWithCookie(url, HttpMethod.PUT);
617 private String sendDeleteToTahomaWithCookie(String url)
618 throws InterruptedException, ExecutionException, TimeoutException {
619 return sendMethodToTahomaWithCookie(url, HttpMethod.DELETE);
622 private String sendMethodToTahomaWithCookie(String url, HttpMethod method)
623 throws InterruptedException, ExecutionException, TimeoutException {
624 return sendMethodToTahomaWithCookie(url, method, "");
627 private String sendMethodToTahomaWithCookie(String url, HttpMethod method, String urlParameters)
628 throws InterruptedException, ExecutionException, TimeoutException {
629 logger.trace("Sending {} to url: {} with data: {}", method.asString(), getApiFullUrl(url), urlParameters);
630 Request request = sendRequestBuilder(url, method);
631 if (!urlParameters.isEmpty()) {
632 request = request.content(new StringContentProvider(urlParameters), "application/json;charset=UTF-8");
635 ContentResponse response = request.send();
637 if (logger.isTraceEnabled()) {
638 logger.trace("Response: {}", response.getContentAsString());
641 if (response.getStatus() < 200 || response.getStatus() >= 300) {
642 logger.debug("Received unexpected status code: {}", response.getStatus());
644 return response.getContentAsString();
647 private Request sendRequestBuilder(String subUrl, HttpMethod method) {
648 return httpClient.newRequest(getApiFullUrl(subUrl)).method(method)
649 .header(HttpHeader.ACCEPT_LANGUAGE, "en-US,en").header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate")
650 .header("X-Requested-With", "XMLHttpRequest").timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS)
651 .agent(TAHOMA_AGENT);
654 private String getApiFullUrl(String subUrl) {
655 return "https://" + thingConfig.getCloudPortal() + API_BASE_URL + subUrl;
658 public void sendCommand(String io, String command, String params, String url) {
659 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
663 removeFinishedRetries();
665 boolean result = sendCommandInternal(io, command, params, url);
667 scheduleRetry(io, command, params, url, thingConfig.getRetries());
671 private void repeatSendCommandInternal(String io, String command, String params, String url, int retries) {
672 logger.debug("Retrying command, retries left: {}", retries);
673 boolean result = sendCommandInternal(io, command, params, url);
674 if (!result && (retries > 0)) {
675 scheduleRetry(io, command, params, url, retries - 1);
679 private boolean sendCommandInternal(String io, String command, String params, String url) {
680 String value = "[]".equals(params) ? command : command + " " + params.replace("\"", "");
681 String urlParameters = "{\"label\":\"" + getThingLabelByURL(io) + " - " + value
682 + " - openHAB\",\"actions\":[{\"deviceURL\":\"" + io + "\",\"commands\":[{\"name\":\"" + command
683 + "\",\"parameters\":" + params + "}]}]}";
684 SomfyTahomaApplyResponse response = invokeCallToURL(url, urlParameters, HttpMethod.POST,
685 SomfyTahomaApplyResponse.class);
686 if (response != null) {
687 if (!response.getExecId().isEmpty()) {
688 logger.debug("Exec id: {}", response.getExecId());
689 registerExecution(io, response.getExecId());
690 scheduleNextGetUpdates();
692 logger.debug("ExecId is empty!");
700 private void removeFinishedRetries() {
701 retryFutures.removeIf(x -> x.isDone());
702 logger.debug("Currently {} retries are scheduled.", retryFutures.size());
705 private void scheduleRetry(String io, String command, String params, String url, int retries) {
706 retryFutures.add(scheduler.schedule(() -> {
707 repeatSendCommandInternal(io, command, params, url, retries);
708 }, thingConfig.getRetryDelay(), TimeUnit.MILLISECONDS));
711 public void sendCommandToSameDevicesInPlace(String io, String command, String params, String url) {
712 SomfyTahomaDevice device = devicePlaces.get(io);
713 if (device != null && !device.getPlaceOID().isEmpty()) {
714 devicePlaces.forEach((deviceUrl, devicePlace) -> {
715 if (device.getPlaceOID().equals(devicePlace.getPlaceOID())
716 && device.getWidget().equals(devicePlace.getWidget())) {
717 sendCommand(deviceUrl, command, params, url);
721 sendCommand(io, command, params, url);
725 private String getThingLabelByURL(String io) {
726 Thing th = getThingByDeviceUrl(io);
728 if (th.getProperties().containsKey(NAME_STATE)) {
729 // Return label from Tahoma
730 return th.getProperties().get(NAME_STATE).replace("\"", "");
732 // Return label from the thing
733 String label = th.getLabel();
734 return label != null ? label.replace("\"", "") : "";
739 public @Nullable String getCurrentExecutions(String io) {
740 if (executions.containsKey(io)) {
741 return executions.get(io);
746 public void cancelExecution(String executionId) {
747 invokeCallToURL(DELETE_URL + executionId, "", HttpMethod.DELETE, null);
750 public void executeActionGroup(String id) {
751 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
754 String execId = executeActionGroupInternal(id);
755 if (execId == null) {
756 execId = executeActionGroupInternal(id);
758 if (execId != null) {
759 registerExecution(id, execId);
760 scheduleNextGetUpdates();
764 private boolean reLogin() {
765 logger.debug("Doing relogin");
766 reLoginNeeded = true;
768 return ThingStatus.OFFLINE != thing.getStatus();
771 public @Nullable String executeActionGroupInternal(String id) {
772 SomfyTahomaApplyResponse response = invokeCallToURL(EXEC_URL + id, "", HttpMethod.POST,
773 SomfyTahomaApplyResponse.class);
774 if (response != null) {
775 if (response.getExecId().isEmpty()) {
776 logger.debug("Got empty exec response");
779 return response.getExecId();
784 public void forceGatewaySync() {
785 invokeCallToURL(REFRESH_URL, "", HttpMethod.PUT, null);
788 public SomfyTahomaStatus getTahomaStatus(String gatewayId) {
789 SomfyTahomaStatusResponse data = invokeCallToURL(GATEWAYS_URL + gatewayId, "", HttpMethod.GET,
790 SomfyTahomaStatusResponse.class);
792 logger.debug("Tahoma status: {}", data.getConnectivity().getStatus());
793 logger.debug("Tahoma protocol version: {}", data.getConnectivity().getProtocolVersion());
794 return data.getConnectivity();
796 return new SomfyTahomaStatus();
799 private boolean isAuthenticationChallenge(Exception ex) {
800 String msg = ex.getMessage();
801 return msg != null && msg.contains(AUTHENTICATION_CHALLENGE);
805 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
806 super.handleConfigurationUpdate(configurationParameters);
807 if (configurationParameters.containsKey("email") || configurationParameters.containsKey("password")
808 || configurationParameters.containsKey("portalUrl")) {
809 reLoginNeeded = true;
810 tooManyRequests = false;
814 public synchronized void refresh(String url, String stateName) {
815 SomfyTahomaState state = invokeCallToURL(DEVICES_URL + urlEncode(url) + "/states/" + stateName, "",
816 HttpMethod.GET, SomfyTahomaState.class);
817 if (state != null && !state.getName().isEmpty()) {
818 updateDevice(url, List.of(state));
822 private @Nullable <T> T invokeCallToURL(String url, String urlParameters, HttpMethod method,
823 @Nullable Class<T> classOfT) {
824 String response = "";
828 response = sendGetToTahomaWithCookie(url);
831 response = sendPutToTahomaWithCookie(url);
834 response = sendPostToTahomaWithCookie(url, urlParameters);
837 response = sendDeleteToTahomaWithCookie(url);
840 return classOfT != null ? gson.fromJson(response, classOfT) : null;
841 } catch (JsonSyntaxException e) {
842 logger.debug("Received data: {} is not JSON", response, e);
843 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
844 } catch (ExecutionException e) {
845 if (isAuthenticationChallenge(e)) {
848 logger.debug("Cannot call url: {} with params: {}!", getApiFullUrl(url), urlParameters, e);
849 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
851 } catch (TimeoutException e) {
852 logger.debug("Timeout when calling url: {} with params: {}!", getApiFullUrl(url), urlParameters, e);
853 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
854 } catch (InterruptedException e) {
855 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
856 Thread.currentThread().interrupt();