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
78 public class SomfyTahomaBridgeHandler extends BaseBridgeHandler {
80 private final Logger logger = LoggerFactory.getLogger(SomfyTahomaBridgeHandler.class);
83 * The shared HttpClient
85 private final HttpClient httpClient;
88 * Future to poll for updates
90 private @Nullable ScheduledFuture<?> pollFuture;
93 * Future to poll for status
95 private @Nullable ScheduledFuture<?> statusFuture;
98 * Future to set reconciliation flag
100 private @Nullable ScheduledFuture<?> reconciliationFuture;
102 // List of futures used for command retries
103 private Collection<ScheduledFuture<?>> retryFutures = new ConcurrentLinkedQueue<ScheduledFuture<?>>();
108 private Map<String, String> executions = new HashMap<>();
110 // Too many requests flag
111 private boolean tooManyRequests = false;
113 // Silent relogin flag
114 private boolean reLoginNeeded = false;
116 // Reconciliation flag
117 private boolean reconciliation = false;
122 protected SomfyTahomaConfig thingConfig = new SomfyTahomaConfig();
125 * Id of registered events
127 private String eventsId = "";
129 private Map<String, SomfyTahomaDevice> devicePlaces = new HashMap<>();
131 private ExpiringCache<List<SomfyTahomaDevice>> cachedDevices = new ExpiringCache<>(Duration.ofSeconds(30),
135 private final Gson gson = new Gson();
137 public SomfyTahomaBridgeHandler(Bridge thing, HttpClientFactory httpClientFactory) {
139 this.httpClient = httpClientFactory.createHttpClient("somfy_" + thing.getUID().getId());
143 public void handleCommand(ChannelUID channelUID, Command command) {
147 public void initialize() {
148 thingConfig = getConfigAs(SomfyTahomaConfig.class);
152 } catch (Exception e) {
153 logger.debug("Cannot start http client for: {}", thing.getBridgeUID().getId(), e);
157 scheduler.execute(() -> {
160 logger.debug("Initialize done...");
165 * starts this things polling future
167 private void initPolling() {
169 scheduleGetUpdates(10);
171 statusFuture = scheduler.scheduleWithFixedDelay(() -> {
172 refreshTahomaStates();
173 }, 60, thingConfig.getStatusTimeout(), TimeUnit.SECONDS);
175 reconciliationFuture = scheduler.scheduleWithFixedDelay(() -> {
176 enableReconciliation();
177 }, RECONCILIATION_TIME, RECONCILIATION_TIME, TimeUnit.SECONDS);
180 private void scheduleGetUpdates(long delay) {
181 pollFuture = scheduler.schedule(() -> {
183 scheduleNextGetUpdates();
184 }, delay, TimeUnit.SECONDS);
187 private void scheduleNextGetUpdates() {
188 ScheduledFuture<?> localPollFuture = pollFuture;
189 if (localPollFuture != null) {
190 localPollFuture.cancel(false);
192 scheduleGetUpdates(executions.isEmpty() ? thingConfig.getRefresh() : 2);
195 public synchronized void login() {
198 if (thingConfig.getEmail().isEmpty() || thingConfig.getPassword().isEmpty()) {
199 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
200 "Can not access device as username and/or password are null");
204 if (tooManyRequests) {
205 logger.debug("Skipping login due to too many requests");
209 if (ThingStatus.ONLINE == thing.getStatus() && !reLoginNeeded) {
210 logger.debug("No need to log in, because already logged in");
214 reLoginNeeded = false;
217 url = TAHOMA_API_URL + "login";
218 String urlParameters = "userId=" + urlEncode(thingConfig.getEmail()) + "&userPassword="
219 + urlEncode(thingConfig.getPassword());
221 ContentResponse response = sendRequestBuilder(url, HttpMethod.POST)
222 .content(new StringContentProvider(urlParameters),
223 "application/x-www-form-urlencoded; charset=UTF-8")
226 if (logger.isTraceEnabled()) {
227 logger.trace("Login response: {}", response.getContentAsString());
230 SomfyTahomaLoginResponse data = gson.fromJson(response.getContentAsString(),
231 SomfyTahomaLoginResponse.class);
232 if (data.isSuccess()) {
233 logger.debug("SomfyTahoma version: {}", data.getVersion());
234 String id = registerEvents();
235 if (id != null && !id.equals(UNAUTHORIZED)) {
237 logger.debug("Events id: {}", eventsId);
238 updateStatus(ThingStatus.ONLINE);
240 logger.debug("Events id error: {}", id);
243 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
244 "Error logging in: " + data.getError());
245 if (data.getError().startsWith(TOO_MANY_REQUESTS)) {
246 setTooManyRequests();
249 } catch (JsonSyntaxException e) {
250 logger.debug("Received invalid data", e);
251 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
252 } catch (InterruptedException | ExecutionException | TimeoutException e) {
253 if (e instanceof ExecutionException) {
254 if (e.getMessage().contains(AUTHENTICATION_CHALLENGE)) {
255 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
256 "Authentication challenge");
257 setTooManyRequests();
261 logger.debug("Cannot get login cookie!", e);
262 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot get login cookie");
263 if (e instanceof InterruptedException) {
264 Thread.currentThread().interrupt();
269 private void setTooManyRequests() {
270 logger.debug("Too many requests error, suspending activity for {} seconds", SUSPEND_TIME);
271 tooManyRequests = true;
272 scheduler.schedule(this::enableLogin, SUSPEND_TIME, TimeUnit.SECONDS);
275 private @Nullable String registerEvents() {
276 SomfyTahomaRegisterEventsResponse response = invokeCallToURL(TAHOMA_EVENTS_URL + "register", "",
277 HttpMethod.POST, SomfyTahomaRegisterEventsResponse.class);
278 return response != null ? response.getId() : null;
281 private String urlEncode(String text) {
283 return URLEncoder.encode(text, StandardCharsets.UTF_8.toString());
284 } catch (UnsupportedEncodingException e) {
289 private void enableLogin() {
290 tooManyRequests = false;
293 private List<SomfyTahomaEvent> getEvents() {
294 SomfyTahomaEvent[] response = invokeCallToURL(TAHOMA_API_URL + "events/" + eventsId + "/fetch", "",
295 HttpMethod.POST, SomfyTahomaEvent[].class);
296 return response != null ? List.of(response) : List.of();
300 public void handleRemoval() {
301 super.handleRemoval();
306 public Collection<Class<? extends ThingHandlerService>> getServices() {
307 return Collections.singleton(SomfyTahomaItemDiscoveryService.class);
311 public void dispose() {
316 private void cleanup() {
317 logger.debug("Doing cleanup");
320 // cancel all scheduled retries
321 retryFutures.forEach(x -> x.cancel(false));
325 } catch (Exception e) {
326 logger.debug("Error during http client stopping", e);
331 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
332 super.bridgeStatusChanged(bridgeStatusInfo);
333 if (ThingStatus.UNINITIALIZED == bridgeStatusInfo.getStatus()) {
339 * Stops this thing's polling future
341 private void stopPolling() {
342 ScheduledFuture<?> localPollFuture = pollFuture;
343 if (localPollFuture != null && !localPollFuture.isCancelled()) {
344 localPollFuture.cancel(true);
346 ScheduledFuture<?> localStatusFuture = statusFuture;
347 if (localStatusFuture != null && !localStatusFuture.isCancelled()) {
348 localStatusFuture.cancel(true);
350 ScheduledFuture<?> localReconciliationFuture = reconciliationFuture;
351 if (localReconciliationFuture != null && !localReconciliationFuture.isCancelled()) {
352 localReconciliationFuture.cancel(true);
356 public List<SomfyTahomaActionGroup> listActionGroups() {
357 SomfyTahomaActionGroup[] list = invokeCallToURL(TAHOMA_API_URL + "actionGroups", "", HttpMethod.GET,
358 SomfyTahomaActionGroup[].class);
359 return list != null ? List.of(list) : List.of();
362 public @Nullable SomfyTahomaSetup getSetup() {
363 SomfyTahomaSetup setup = invokeCallToURL(TAHOMA_API_URL + "setup", "", HttpMethod.GET, SomfyTahomaSetup.class);
365 saveDevicePlaces(setup.getDevices());
370 public List<SomfyTahomaDevice> getDevices() {
371 SomfyTahomaDevice[] response = invokeCallToURL(SETUP_URL + "devices", "", HttpMethod.GET,
372 SomfyTahomaDevice[].class);
373 List<SomfyTahomaDevice> devices = response != null ? List.of(response) : List.of();
374 saveDevicePlaces(devices);
378 public synchronized @Nullable SomfyTahomaDevice getCachedDevice(String url) {
379 List<SomfyTahomaDevice> devices = cachedDevices.getValue();
380 for (SomfyTahomaDevice device : devices) {
381 if (url.equals(device.getDeviceURL())) {
388 private void saveDevicePlaces(List<SomfyTahomaDevice> devices) {
389 devicePlaces.clear();
390 for (SomfyTahomaDevice device : devices) {
391 if (!device.getPlaceOID().isEmpty()) {
392 SomfyTahomaDevice newDevice = new SomfyTahomaDevice();
393 newDevice.setPlaceOID(device.getPlaceOID());
394 newDevice.setWidget(device.getWidget());
395 devicePlaces.put(device.getDeviceURL(), newDevice);
400 private void getTahomaUpdates() {
401 logger.debug("Getting Tahoma Updates...");
402 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
406 List<SomfyTahomaEvent> events = getEvents();
407 logger.trace("Got total of {} events", events.size());
408 for (SomfyTahomaEvent event : events) {
413 private void processEvent(SomfyTahomaEvent event) {
414 logger.debug("Got event: {}", event.getName());
415 switch (event.getName()) {
416 case "ExecutionRegisteredEvent":
417 processExecutionRegisteredEvent(event);
419 case "ExecutionStateChangedEvent":
420 processExecutionChangedEvent(event);
422 case "DeviceStateChangedEvent":
423 processStateChangedEvent(event);
425 case "RefreshAllDevicesStatesCompletedEvent":
426 scheduler.schedule(this::updateThings, 1, TimeUnit.SECONDS);
428 case "GatewayAliveEvent":
429 case "GatewayDownEvent":
430 processGatewayEvent(event);
433 // ignore other states
437 private synchronized void updateThings() {
438 boolean needsUpdate = reconciliation;
440 for (Thing th : getThing().getThings()) {
441 if (ThingStatus.ONLINE != th.getStatus()) {
446 // update all states only if necessary
449 reconciliation = false;
453 private void processExecutionRegisteredEvent(SomfyTahomaEvent event) {
454 JsonElement el = event.getAction();
455 if (el.isJsonArray()) {
456 SomfyTahomaAction[] actions = gson.fromJson(el, SomfyTahomaAction[].class);
457 for (SomfyTahomaAction action : actions) {
458 registerExecution(action.getDeviceURL(), event.getExecId());
461 SomfyTahomaAction action = gson.fromJson(el, SomfyTahomaAction.class);
462 registerExecution(action.getDeviceURL(), event.getExecId());
466 private void processExecutionChangedEvent(SomfyTahomaEvent event) {
467 if (FAILED_EVENT.equals(event.getNewState()) || COMPLETED_EVENT.equals(event.getNewState())) {
468 logger.debug("Removing execution id: {}", event.getExecId());
469 unregisterExecution(event.getExecId());
473 private void registerExecution(String url, String execId) {
474 if (executions.containsKey(url)) {
475 executions.remove(url);
476 logger.debug("Previous execution exists for url: {}", url);
478 executions.put(url, execId);
481 private void unregisterExecution(String execId) {
482 if (executions.containsValue(execId)) {
483 executions.values().removeAll(Collections.singleton(execId));
485 logger.debug("Cannot remove execution id: {}, because it is not registered", execId);
489 private void processGatewayEvent(SomfyTahomaEvent event) {
490 // update gateway status
491 for (Thing th : getThing().getThings()) {
492 if (THING_TYPE_GATEWAY.equals(th.getThingTypeUID())) {
493 SomfyTahomaGatewayHandler gatewayHandler = (SomfyTahomaGatewayHandler) th.getHandler();
494 if (gatewayHandler != null && gatewayHandler.getGateWayId().equals(event.getGatewayId())) {
495 gatewayHandler.refresh(STATUS);
501 private synchronized void updateAllStates() {
502 logger.debug("Updating all states");
503 getDevices().forEach(device -> updateDevice(device));
506 private void updateDevice(SomfyTahomaDevice device) {
507 String url = device.getDeviceURL();
508 List<SomfyTahomaState> states = device.getStates();
509 updateDevice(url, states);
512 private void updateDevice(String url, List<SomfyTahomaState> states) {
513 Thing th = getThingByDeviceUrl(url);
517 SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) th.getHandler();
518 if (handler != null) {
519 handler.updateThingStatus(states);
520 handler.updateThingChannels(states);
524 private void processStateChangedEvent(SomfyTahomaEvent event) {
525 String deviceUrl = event.getDeviceUrl();
526 List<SomfyTahomaState> states = event.getDeviceStates();
527 logger.debug("States for device {} : {}", deviceUrl, states);
528 Thing thing = getThingByDeviceUrl(deviceUrl);
531 logger.debug("Updating status of thing: {}", thing.getLabel());
532 SomfyTahomaBaseThingHandler handler = (SomfyTahomaBaseThingHandler) thing.getHandler();
534 if (handler != null) {
535 // update thing status
536 handler.updateThingStatus(states);
537 handler.updateThingChannels(states);
540 logger.debug("Thing handler is null, probably not bound thing.");
544 private void enableReconciliation() {
545 logger.debug("Enabling reconciliation");
546 reconciliation = true;
549 private void refreshTahomaStates() {
550 logger.debug("Refreshing Tahoma states...");
551 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
555 // force Tahoma to ask for actual states
559 private @Nullable Thing getThingByDeviceUrl(String deviceUrl) {
560 for (Thing th : getThing().getThings()) {
561 String url = (String) th.getConfiguration().get("url");
562 if (deviceUrl.equals(url)) {
569 private void logout() {
572 sendGetToTahomaWithCookie(TAHOMA_API_URL + "logout");
573 } catch (InterruptedException | ExecutionException | TimeoutException e) {
574 logger.debug("Cannot send logout command!", e);
575 if (e instanceof InterruptedException) {
576 Thread.currentThread().interrupt();
581 private String sendPostToTahomaWithCookie(String url, String urlParameters)
582 throws InterruptedException, ExecutionException, TimeoutException {
583 return sendMethodToTahomaWithCookie(url, HttpMethod.POST, urlParameters);
586 private String sendGetToTahomaWithCookie(String url)
587 throws InterruptedException, ExecutionException, TimeoutException {
588 return sendMethodToTahomaWithCookie(url, HttpMethod.GET);
591 private String sendPutToTahomaWithCookie(String url)
592 throws InterruptedException, ExecutionException, TimeoutException {
593 return sendMethodToTahomaWithCookie(url, HttpMethod.PUT);
596 private String sendDeleteToTahomaWithCookie(String url)
597 throws InterruptedException, ExecutionException, TimeoutException {
598 return sendMethodToTahomaWithCookie(url, HttpMethod.DELETE);
601 private String sendMethodToTahomaWithCookie(String url, HttpMethod method)
602 throws InterruptedException, ExecutionException, TimeoutException {
603 return sendMethodToTahomaWithCookie(url, method, "");
606 private String sendMethodToTahomaWithCookie(String url, HttpMethod method, String urlParameters)
607 throws InterruptedException, ExecutionException, TimeoutException {
608 logger.trace("Sending {} to url: {} with data: {}", method.asString(), url, urlParameters);
609 Request request = sendRequestBuilder(url, method);
610 if (!urlParameters.isEmpty()) {
611 request = request.content(new StringContentProvider(urlParameters), "application/json;charset=UTF-8");
614 ContentResponse response = request.send();
616 if (logger.isTraceEnabled()) {
617 logger.trace("Response: {}", response.getContentAsString());
620 if (response.getStatus() < 200 || response.getStatus() >= 300) {
621 logger.debug("Received unexpected status code: {}", response.getStatus());
623 return response.getContentAsString();
626 private Request sendRequestBuilder(String url, HttpMethod method) {
627 return httpClient.newRequest(url).method(method).header(HttpHeader.ACCEPT_LANGUAGE, "en-US,en")
628 .header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate").header("X-Requested-With", "XMLHttpRequest")
629 .timeout(TAHOMA_TIMEOUT, TimeUnit.SECONDS).agent(TAHOMA_AGENT);
632 public void sendCommand(String io, String command, String params, String url) {
633 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
637 removeFinishedRetries();
639 boolean result = sendCommandInternal(io, command, params, url);
641 scheduleRetry(io, command, params, url, thingConfig.getRetries());
645 private void repeatSendCommandInternal(String io, String command, String params, String url, int retries) {
646 logger.debug("Retrying command, retries left: {}", retries);
647 boolean result = sendCommandInternal(io, command, params, url);
648 if (!result && (retries > 0)) {
649 scheduleRetry(io, command, params, url, retries - 1);
653 private boolean sendCommandInternal(String io, String command, String params, String url) {
654 String value = params.equals("[]") ? command : command + " " + params.replace("\"", "");
655 String urlParameters = "{\"label\":\"" + getThingLabelByURL(io) + " - " + value
656 + " - openHAB\",\"actions\":[{\"deviceURL\":\"" + io + "\",\"commands\":[{\"name\":\"" + command
657 + "\",\"parameters\":" + params + "}]}]}";
658 SomfyTahomaApplyResponse response = invokeCallToURL(url, urlParameters, HttpMethod.POST,
659 SomfyTahomaApplyResponse.class);
660 if (response != null) {
661 if (!response.getExecId().isEmpty()) {
662 logger.debug("Exec id: {}", response.getExecId());
663 registerExecution(io, response.getExecId());
664 scheduleNextGetUpdates();
666 logger.debug("ExecId is empty!");
674 private void removeFinishedRetries() {
675 retryFutures.removeIf(x -> x.isDone());
676 logger.debug("Currently {} retries are scheduled.", retryFutures.size());
679 private void scheduleRetry(String io, String command, String params, String url, int retries) {
680 retryFutures.add(scheduler.schedule(() -> {
681 repeatSendCommandInternal(io, command, params, url, retries);
682 }, thingConfig.getRetryDelay(), TimeUnit.MILLISECONDS));
685 public void sendCommandToSameDevicesInPlace(String io, String command, String params, String url) {
686 SomfyTahomaDevice device = devicePlaces.get(io);
687 if (device != null && !device.getPlaceOID().isEmpty()) {
688 devicePlaces.forEach((deviceUrl, devicePlace) -> {
689 if (device.getPlaceOID().equals(devicePlace.getPlaceOID())
690 && device.getWidget().equals(devicePlace.getWidget())) {
691 sendCommand(deviceUrl, command, params, url);
695 sendCommand(io, command, params, url);
699 private String getThingLabelByURL(String io) {
700 Thing th = getThingByDeviceUrl(io);
702 if (th.getProperties().containsKey(NAME_STATE)) {
703 // Return label from Tahoma
704 return th.getProperties().get(NAME_STATE).replace("\"", "");
706 // Return label from the thing
707 String label = th.getLabel();
708 return label != null ? label.replace("\"", "") : "";
713 public @Nullable String getCurrentExecutions(String io) {
714 if (executions.containsKey(io)) {
715 return executions.get(io);
720 public void cancelExecution(String executionId) {
721 invokeCallToURL(DELETE_URL + executionId, "", HttpMethod.DELETE, null);
724 public void executeActionGroup(String id) {
725 if (ThingStatus.OFFLINE == thing.getStatus() && !reLogin()) {
728 String execId = executeActionGroupInternal(id);
729 if (execId == null) {
730 execId = executeActionGroupInternal(id);
732 if (execId != null) {
733 registerExecution(id, execId);
734 scheduleNextGetUpdates();
738 private boolean reLogin() {
739 logger.debug("Doing relogin");
740 reLoginNeeded = true;
742 return ThingStatus.OFFLINE != thing.getStatus();
745 public @Nullable String executeActionGroupInternal(String id) {
746 SomfyTahomaApplyResponse response = invokeCallToURL(EXEC_URL + id, "", HttpMethod.POST,
747 SomfyTahomaApplyResponse.class);
748 if (response != null) {
749 if (response.getExecId().isEmpty()) {
750 logger.debug("Got empty exec response");
753 return response.getExecId();
758 public void forceGatewaySync() {
759 invokeCallToURL(REFRESH_URL, "", HttpMethod.PUT, null);
762 public SomfyTahomaStatus getTahomaStatus(String gatewayId) {
763 SomfyTahomaStatusResponse data = invokeCallToURL(GATEWAYS_URL + gatewayId, "", HttpMethod.GET,
764 SomfyTahomaStatusResponse.class);
766 logger.debug("Tahoma status: {}", data.getConnectivity().getStatus());
767 logger.debug("Tahoma protocol version: {}", data.getConnectivity().getProtocolVersion());
768 return data.getConnectivity();
770 return new SomfyTahomaStatus();
773 private boolean isAuthenticationChallenge(Exception ex) {
774 return ex.getMessage().contains(AUTHENTICATION_CHALLENGE);
778 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
779 super.handleConfigurationUpdate(configurationParameters);
780 if (configurationParameters.containsKey("email")) {
781 thingConfig.setEmail(configurationParameters.get("email").toString());
783 if (configurationParameters.containsKey("password")) {
784 thingConfig.setPassword(configurationParameters.get("password").toString());
788 public synchronized void refresh(String url, String stateName) {
789 SomfyTahomaState state = invokeCallToURL(DEVICES_URL + urlEncode(url) + "/states/" + stateName, "",
790 HttpMethod.GET, SomfyTahomaState.class);
791 if (state != null && !state.getName().isEmpty()) {
792 updateDevice(url, List.of(state));
796 private @Nullable <T> T invokeCallToURL(String url, String urlParameters, HttpMethod method,
797 @Nullable Class<T> classOfT) {
798 String response = "";
802 response = sendGetToTahomaWithCookie(url);
805 response = sendPutToTahomaWithCookie(url);
808 response = sendPostToTahomaWithCookie(url, urlParameters);
811 response = sendDeleteToTahomaWithCookie(url);
814 return classOfT != null ? gson.fromJson(response, classOfT) : null;
815 } catch (JsonSyntaxException e) {
816 logger.debug("Received data: {} is not JSON", response, e);
817 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Received invalid data");
818 } catch (ExecutionException e) {
819 if (isAuthenticationChallenge(e)) {
822 logger.debug("Cannot call url: {} with params: {}!", url, urlParameters, e);
823 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
825 } catch (InterruptedException | TimeoutException e) {
826 logger.debug("Cannot call url: {} with params: {}!", url, urlParameters, e);
827 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
828 if (e instanceof InterruptedException) {
829 Thread.currentThread().interrupt();