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.apache.commons.lang.StringUtils;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.eclipse.jetty.client.HttpClient;
36 import org.eclipse.jetty.client.api.ContentResponse;
37 import org.eclipse.jetty.client.api.Request;
38 import org.eclipse.jetty.client.util.StringContentProvider;
39 import org.eclipse.jetty.http.HttpHeader;
40 import org.eclipse.jetty.http.HttpMethod;
41 import org.openhab.binding.somfytahoma.internal.config.SomfyTahomaConfig;
42 import org.openhab.binding.somfytahoma.internal.discovery.SomfyTahomaItemDiscoveryService;
43 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaAction;
44 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaActionGroup;
45 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaApplyResponse;
46 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaDevice;
47 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaEvent;
48 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaLoginResponse;
49 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaRegisterEventsResponse;
50 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaSetup;
51 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaState;
52 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaStatus;
53 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaStatusResponse;
54 import org.openhab.core.cache.ExpiringCache;
55 import org.openhab.core.io.net.http.HttpClientFactory;
56 import org.openhab.core.thing.Bridge;
57 import org.openhab.core.thing.ChannelUID;
58 import org.openhab.core.thing.Thing;
59 import org.openhab.core.thing.ThingStatus;
60 import org.openhab.core.thing.ThingStatusDetail;
61 import org.openhab.core.thing.ThingStatusInfo;
62 import org.openhab.core.thing.binding.BaseBridgeHandler;
63 import org.openhab.core.thing.binding.ThingHandlerService;
64 import org.openhab.core.types.Command;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
68 import com.google.gson.Gson;
69 import com.google.gson.JsonElement;
70 import com.google.gson.JsonSyntaxException;
73 * The {@link SomfyTahomaBridgeHandler} is responsible for handling commands, which are
74 * sent to one of the channels.
76 * @author Ondrej Pecta - Initial contribution
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 ExpiringCache<List<SomfyTahomaDevice>> cachedDevices = new ExpiringCache<>(Duration.ofSeconds(30),
134 private final Gson gson = new Gson();
136 public SomfyTahomaBridgeHandler(Bridge thing, HttpClientFactory httpClientFactory) {
138 this.httpClient = httpClientFactory.createHttpClient("somfy_" + thing.getUID().getId());
142 public void handleCommand(ChannelUID channelUID, Command command) {
146 public void initialize() {
147 thingConfig = getConfigAs(SomfyTahomaConfig.class);
151 } catch (Exception e) {
152 logger.debug("Cannot start http client for: {}", thing.getBridgeUID().getId(), e);
156 scheduler.execute(() -> {
159 logger.debug("Initialize done...");
164 * starts this things polling future
166 private void initPolling() {
168 pollFuture = scheduler.scheduleWithFixedDelay(() -> {
170 }, 10, thingConfig.getRefresh(), TimeUnit.SECONDS);
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 public synchronized void login() {
184 if (StringUtils.isEmpty(thingConfig.getEmail()) || StringUtils.isEmpty(thingConfig.getPassword())) {
185 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
186 "Can not access device as username and/or password are null");
190 if (tooManyRequests) {
191 logger.debug("Skipping login due to too many requests");
195 if (ThingStatus.ONLINE == thing.getStatus() && !reLoginNeeded) {
196 logger.debug("No need to log in, because already logged in");
200 reLoginNeeded = false;
203 url = TAHOMA_API_URL + "login";
204 String urlParameters = "userId=" + urlEncode(thingConfig.getEmail()) + "&userPassword="
205 + urlEncode(thingConfig.getPassword());
207 ContentResponse response = sendRequestBuilder(url, HttpMethod.POST)
208 .content(new StringContentProvider(urlParameters),
209 "application/x-www-form-urlencoded; charset=UTF-8")
212 if (logger.isTraceEnabled()) {
213 logger.trace("Login response: {}", response.getContentAsString());
216 SomfyTahomaLoginResponse data = gson.fromJson(response.getContentAsString(),
217 SomfyTahomaLoginResponse.class);
218 if (data.isSuccess()) {
219 logger.debug("SomfyTahoma version: {}", data.getVersion());
220 String id = registerEvents();
221 if (id != null && !id.equals(UNAUTHORIZED)) {
223 logger.debug("Events id: {}", eventsId);
224 updateStatus(ThingStatus.ONLINE);
226 logger.debug("Events id error: {}", id);
229 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
230 "Error logging in: " + data.getError());
231 if (data.getError().startsWith(TOO_MANY_REQUESTS)) {
232 setTooManyRequests();
235 } catch (JsonSyntaxException e) {
236 logger.debug("Received invalid data", e);
237 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
238 } catch (InterruptedException | ExecutionException | TimeoutException e) {
239 if (e instanceof ExecutionException) {
240 if (e.getMessage().contains(AUTHENTICATION_CHALLENGE)) {
241 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
242 "Authentication challenge");
243 setTooManyRequests();
247 logger.debug("Cannot get login cookie!", e);
248 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot get login cookie");
249 if (e instanceof InterruptedException) {
250 Thread.currentThread().interrupt();
255 private void setTooManyRequests() {
256 logger.debug("Too many requests error, suspending activity for {} seconds", SUSPEND_TIME);
257 tooManyRequests = true;
258 scheduler.schedule(this::enableLogin, SUSPEND_TIME, TimeUnit.SECONDS);
261 private @Nullable String registerEvents() {
262 SomfyTahomaRegisterEventsResponse response = invokeCallToURL(TAHOMA_EVENTS_URL + "register", "",
263 HttpMethod.POST, SomfyTahomaRegisterEventsResponse.class);
264 return response != null ? response.getId() : null;
267 private String urlEncode(String text) {
269 return URLEncoder.encode(text, StandardCharsets.UTF_8.toString());
270 } catch (UnsupportedEncodingException e) {
275 private void enableLogin() {
276 tooManyRequests = false;
279 private List<SomfyTahomaEvent> getEvents() {
280 SomfyTahomaEvent[] response = invokeCallToURL(TAHOMA_API_URL + "events/" + eventsId + "/fetch", "",
281 HttpMethod.POST, SomfyTahomaEvent[].class);
282 return response != null ? List.of(response) : List.of();
286 public void handleRemoval() {
287 super.handleRemoval();
292 public Collection<Class<? extends ThingHandlerService>> getServices() {
293 return Collections.singleton(SomfyTahomaItemDiscoveryService.class);
297 public void dispose() {
302 private void cleanup() {
303 logger.debug("Doing cleanup");
306 // cancel all scheduled retries
307 retryFutures.forEach(x -> x.cancel(false));
311 } catch (Exception e) {
312 logger.debug("Error during http client stopping", e);
317 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
318 super.bridgeStatusChanged(bridgeStatusInfo);
319 if (ThingStatus.UNINITIALIZED == bridgeStatusInfo.getStatus()) {
325 * Stops this thing's polling future
327 private void stopPolling() {
328 ScheduledFuture<?> localPollFuture = pollFuture;
329 if (localPollFuture != null && !localPollFuture.isCancelled()) {
330 localPollFuture.cancel(true);
332 ScheduledFuture<?> localStatusFuture = statusFuture;
333 if (localStatusFuture != null && !localStatusFuture.isCancelled()) {
334 localStatusFuture.cancel(true);
336 ScheduledFuture<?> localReconciliationFuture = reconciliationFuture;
337 if (localReconciliationFuture != null && !localReconciliationFuture.isCancelled()) {
338 localReconciliationFuture.cancel(true);
342 public List<SomfyTahomaActionGroup> listActionGroups() {
343 SomfyTahomaActionGroup[] list = invokeCallToURL(TAHOMA_API_URL + "actionGroups", "", HttpMethod.GET,
344 SomfyTahomaActionGroup[].class);
345 return list != null ? List.of(list) : List.of();
348 public @Nullable SomfyTahomaSetup getSetup() {
349 return invokeCallToURL(TAHOMA_API_URL + "setup", "", HttpMethod.GET, SomfyTahomaSetup.class);
352 public List<SomfyTahomaDevice> getDevices() {
353 SomfyTahomaDevice[] response = invokeCallToURL(SETUP_URL + "devices", "", HttpMethod.GET,
354 SomfyTahomaDevice[].class);
355 return response != null ? List.of(response) : List.of();
358 public synchronized @Nullable SomfyTahomaDevice getCachedDevice(String url) {
359 List<SomfyTahomaDevice> devices = cachedDevices.getValue();
360 for (SomfyTahomaDevice device : devices) {
361 if (url.equals(device.getDeviceURL())) {
368 private void getTahomaUpdates() {
369 logger.debug("Getting Tahoma Updates...");
370 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
374 List<SomfyTahomaEvent> events = getEvents();
375 logger.trace("Got total of {} events", events.size());
376 for (SomfyTahomaEvent event : events) {
381 private void processEvent(SomfyTahomaEvent event) {
382 logger.debug("Got event: {}", event.getName());
383 switch (event.getName()) {
384 case "ExecutionRegisteredEvent":
385 processExecutionRegisteredEvent(event);
387 case "ExecutionStateChangedEvent":
388 processExecutionChangedEvent(event);
390 case "DeviceStateChangedEvent":
391 processStateChangedEvent(event);
393 case "RefreshAllDevicesStatesCompletedEvent":
394 scheduler.schedule(this::updateThings, 1, TimeUnit.SECONDS);
396 case "GatewayAliveEvent":
397 case "GatewayDownEvent":
398 processGatewayEvent(event);
401 // ignore other states
405 private synchronized void updateThings() {
406 boolean needsUpdate = reconciliation;
408 for (Thing th : getThing().getThings()) {
409 if (ThingStatus.ONLINE != th.getStatus()) {
414 // update all states only if necessary
417 reconciliation = false;
421 private void processExecutionRegisteredEvent(SomfyTahomaEvent event) {
422 JsonElement el = event.getAction();
423 if (el.isJsonArray()) {
424 SomfyTahomaAction[] actions = gson.fromJson(el, SomfyTahomaAction[].class);
425 for (SomfyTahomaAction action : actions) {
426 registerExecution(action.getDeviceURL(), event.getExecId());
429 SomfyTahomaAction action = gson.fromJson(el, SomfyTahomaAction.class);
430 registerExecution(action.getDeviceURL(), event.getExecId());
434 private void processExecutionChangedEvent(SomfyTahomaEvent event) {
435 if (FAILED_EVENT.equals(event.getNewState()) || COMPLETED_EVENT.equals(event.getNewState())) {
436 logger.debug("Removing execution id: {}", event.getExecId());
437 unregisterExecution(event.getExecId());
441 private void registerExecution(String url, String execId) {
442 if (executions.containsKey(url)) {
443 executions.remove(url);
444 logger.debug("Previous execution exists for url: {}", url);
446 executions.put(url, execId);
449 private void unregisterExecution(String execId) {
450 if (executions.containsValue(execId)) {
451 executions.values().removeAll(Collections.singleton(execId));
453 logger.debug("Cannot remove execution id: {}, because it is not registered", execId);
457 private void processGatewayEvent(SomfyTahomaEvent event) {
458 // update gateway status
459 for (Thing th : getThing().getThings()) {
460 if (THING_TYPE_GATEWAY.equals(th.getThingTypeUID())) {
461 SomfyTahomaGatewayHandler gatewayHandler = (SomfyTahomaGatewayHandler) th.getHandler();
462 if (gatewayHandler != null && gatewayHandler.getGateWayId().equals(event.getGatewayId())) {
463 gatewayHandler.refresh(STATUS);
469 private synchronized void updateAllStates() {
470 logger.debug("Updating all states");
471 getDevices().forEach(device -> updateDevice(device));
474 private void updateDevice(SomfyTahomaDevice device) {
475 String url = device.getDeviceURL();
476 List<SomfyTahomaState> states = device.getStates();
477 updateDevice(url, states);
480 private void updateDevice(String url, List<SomfyTahomaState> states) {
481 Thing th = getThingByDeviceUrl(url);
485 SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) th.getHandler();
486 if (handler != null) {
487 handler.updateThingStatus(states);
488 handler.updateThingChannels(states);
492 private void processStateChangedEvent(SomfyTahomaEvent event) {
493 String deviceUrl = event.getDeviceUrl();
494 List<SomfyTahomaState> states = event.getDeviceStates();
495 logger.debug("States for device {} : {}", deviceUrl, states);
496 Thing thing = getThingByDeviceUrl(deviceUrl);
499 logger.debug("Updating status of thing: {}", thing.getLabel());
500 SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) thing.getHandler();
502 if (handler != null) {
503 // update thing status
504 handler.updateThingStatus(states);
505 handler.updateThingChannels(states);
508 logger.debug("Thing handler is null, probably not bound thing.");
512 private void enableReconciliation() {
513 logger.debug("Enabling reconciliation");
514 reconciliation = true;
517 private void refreshTahomaStates() {
518 logger.debug("Refreshing Tahoma states...");
519 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
523 // force Tahoma to ask for actual states
527 private @Nullable Thing getThingByDeviceUrl(String deviceUrl) {
528 for (Thing th : getThing().getThings()) {
529 String url = (String) th.getConfiguration().get("url");
530 if (deviceUrl.equals(url)) {
537 private void logout() {
540 sendGetToTahomaWithCookie(TAHOMA_API_URL + "logout");
541 } catch (InterruptedException | ExecutionException | TimeoutException e) {
542 logger.debug("Cannot send logout command!", e);
543 if (e instanceof InterruptedException) {
544 Thread.currentThread().interrupt();
549 private String sendPostToTahomaWithCookie(String url, String urlParameters)
550 throws InterruptedException, ExecutionException, TimeoutException {
551 return sendMethodToTahomaWithCookie(url, HttpMethod.POST, urlParameters);
554 private String sendGetToTahomaWithCookie(String url)
555 throws InterruptedException, ExecutionException, TimeoutException {
556 return sendMethodToTahomaWithCookie(url, HttpMethod.GET);
559 private String sendPutToTahomaWithCookie(String url)
560 throws InterruptedException, ExecutionException, TimeoutException {
561 return sendMethodToTahomaWithCookie(url, HttpMethod.PUT);
564 private String sendDeleteToTahomaWithCookie(String url)
565 throws InterruptedException, ExecutionException, TimeoutException {
566 return sendMethodToTahomaWithCookie(url, HttpMethod.DELETE);
569 private String sendMethodToTahomaWithCookie(String url, HttpMethod method)
570 throws InterruptedException, ExecutionException, TimeoutException {
571 return sendMethodToTahomaWithCookie(url, method, "");
574 private String sendMethodToTahomaWithCookie(String url, HttpMethod method, String urlParameters)
575 throws InterruptedException, ExecutionException, TimeoutException {
576 logger.trace("Sending {} to url: {} with data: {}", method.asString(), url, urlParameters);
577 Request request = sendRequestBuilder(url, method);
578 if (StringUtils.isNotEmpty(urlParameters)) {
579 request = request.content(new StringContentProvider(urlParameters), "application/json;charset=UTF-8");
582 ContentResponse response = request.send();
584 if (logger.isTraceEnabled()) {
585 logger.trace("Response: {}", response.getContentAsString());
588 if (response.getStatus() < 200 || response.getStatus() >= 300) {
589 logger.debug("Received unexpected status code: {}", response.getStatus());
591 return response.getContentAsString();
594 private Request sendRequestBuilder(String url, HttpMethod method) {
595 return httpClient.newRequest(url).method(method).header(HttpHeader.ACCEPT_LANGUAGE, "en-US,en")
596 .header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate").header("X-Requested-With", "XMLHttpRequest")
597 .timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS).agent(TAHOMA_AGENT);
600 public void sendCommand(String io, String command, String params, String url) {
601 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
605 removeFinishedRetries();
607 boolean result = sendCommandInternal(io, command, params, url);
609 scheduleRetry(io, command, params, url, thingConfig.getRetries());
613 private void repeatSendCommandInternal(String io, String command, String params, String url, int retries) {
614 logger.debug("Retrying command, retries left: {}", retries);
615 boolean result = sendCommandInternal(io, command, params, url);
616 if (!result && (retries > 0)) {
617 scheduleRetry(io, command, params, url, retries - 1);
621 private boolean sendCommandInternal(String io, String command, String params, String url) {
622 String value = params.equals("[]") ? command : params.replace("\"", "");
623 String urlParameters = "{\"label\":\"" + getThingLabelByURL(io) + " - " + value
624 + " - OH2\",\"actions\":[{\"deviceURL\":\"" + io + "\",\"commands\":[{\"name\":\"" + command
625 + "\",\"parameters\":" + params + "}]}]}";
626 SomfyTahomaApplyResponse response = invokeCallToURL(url, urlParameters, HttpMethod.POST,
627 SomfyTahomaApplyResponse.class);
628 if (response != null) {
629 if (!response.getExecId().isEmpty()) {
630 logger.debug("Exec id: {}", response.getExecId());
631 registerExecution(io, response.getExecId());
633 logger.debug("ExecId is empty!");
641 private void removeFinishedRetries() {
642 retryFutures.removeIf(x -> x.isDone());
643 logger.debug("Currently {} retries are scheduled.", retryFutures.size());
646 private void scheduleRetry(String io, String command, String params, String url, int retries) {
647 retryFutures.add(scheduler.schedule(() -> {
648 repeatSendCommandInternal(io, command, params, url, retries);
649 }, thingConfig.getRetryDelay(), TimeUnit.MILLISECONDS));
652 private String getThingLabelByURL(String io) {
653 Thing th = getThingByDeviceUrl(io);
655 if (th.getProperties().containsKey(NAME_STATE)) {
656 // Return label from Tahoma
657 return th.getProperties().get(NAME_STATE).replace("\"", "");
659 // Return label from OH2
660 String label = th.getLabel();
661 return label != null ? label.replace("\"", "") : "";
666 public @Nullable String getCurrentExecutions(String io) {
667 if (executions.containsKey(io)) {
668 return executions.get(io);
673 public void cancelExecution(String executionId) {
674 invokeCallToURL(DELETE_URL + executionId, "", HttpMethod.DELETE, null);
677 public void executeActionGroup(String id) {
678 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
681 String execId = executeActionGroupInternal(id);
682 if (execId == null) {
683 execId = executeActionGroupInternal(id);
685 if (execId != null) {
686 registerExecution(id, execId);
690 private boolean reLogin() {
691 logger.debug("Doing relogin");
692 reLoginNeeded = true;
694 return ThingStatus.OFFLINE != thing.getStatus();
697 public @Nullable String executeActionGroupInternal(String id) {
698 SomfyTahomaApplyResponse response = invokeCallToURL(EXEC_URL + id, "", HttpMethod.POST,
699 SomfyTahomaApplyResponse.class);
700 if (response != null) {
701 if (response.getExecId().isEmpty()) {
702 logger.debug("Got empty exec response");
705 return response.getExecId();
710 public void forceGatewaySync() {
711 invokeCallToURL(REFRESH_URL, "", HttpMethod.PUT, null);
714 public SomfyTahomaStatus getTahomaStatus(String gatewayId) {
715 SomfyTahomaStatusResponse data = invokeCallToURL(GATEWAYS_URL + gatewayId, "", HttpMethod.GET,
716 SomfyTahomaStatusResponse.class);
718 logger.debug("Tahoma status: {}", data.getConnectivity().getStatus());
719 logger.debug("Tahoma protocol version: {}", data.getConnectivity().getProtocolVersion());
720 return data.getConnectivity();
722 return new SomfyTahomaStatus();
725 private boolean isAuthenticationChallenge(Exception ex) {
726 return ex.getMessage().contains(AUTHENTICATION_CHALLENGE);
730 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
731 super.handleConfigurationUpdate(configurationParameters);
732 if (configurationParameters.containsKey("email")) {
733 thingConfig.setEmail(configurationParameters.get("email").toString());
735 if (configurationParameters.containsKey("password")) {
736 thingConfig.setPassword(configurationParameters.get("password").toString());
740 public synchronized void refresh(String url, String stateName) {
741 SomfyTahomaState state = invokeCallToURL(DEVICES_URL + urlEncode(url) + "/states/" + stateName, "",
742 HttpMethod.GET, SomfyTahomaState.class);
743 if (state != null && !state.getName().isEmpty()) {
744 updateDevice(url, List.of(state));
748 private @Nullable <T> T invokeCallToURL(String url, String urlParameters, HttpMethod method,
749 @Nullable Class<T> classOfT) {
750 String response = "";
754 response = sendGetToTahomaWithCookie(url);
757 response = sendPutToTahomaWithCookie(url);
760 response = sendPostToTahomaWithCookie(url, urlParameters);
763 response = sendDeleteToTahomaWithCookie(url);
766 return classOfT != null ? gson.fromJson(response, classOfT) : null;
767 } catch (JsonSyntaxException e) {
768 logger.debug("Received data: {} is not JSON", response, e);
769 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
770 } catch (ExecutionException e) {
771 if (isAuthenticationChallenge(e)) {
774 logger.debug("Cannot call url: {} with params: {}!", url, urlParameters, e);
775 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
777 } catch (InterruptedException | TimeoutException e) {
778 logger.debug("Cannot call url: {} with params: {}!", url, urlParameters, e);
779 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
780 if (e instanceof InterruptedException) {
781 Thread.currentThread().interrupt();