]> git.basschouten.com Git - openhab-addons.git/commitdiff
[amazonechocontrol] improvements and bug fixes (#9057)
authorJ-N-K <J-N-K@users.noreply.github.com>
Wed, 25 Nov 2020 20:15:29 +0000 (21:15 +0100)
committerGitHub <noreply@github.com>
Wed, 25 Nov 2020 20:15:29 +0000 (12:15 -0800)
* fixed: InterrupedException
* changed: single and group queues to device queue
added: standard volume to speak request
* changed: log from info to debug
* fix compile warnings
* remove dependency on StringUtils
* more improvements
* fix HandlerPowerController
* attempt to solve stopping tts
* logging powercontroller
* fix smarthome devices not updating
* finalize smarthome device update fix
* additional device information logging for discovery
* fix color channel for smarthome devices

Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
Co-authored-by: Tom Blum <trinitus01@googlemail.com>
Co-authored-by: Connor Petty <mistercpp2000@gmail.com>
26 files changed:
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoDynamicStateDescriptionProvider.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandler.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerSendMessage.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/FlashBriefingProfileHandler.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevices.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroups.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBase.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java
bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java

index f5476e352cc157bfce56bb8530774018bb5d7921..0a7129787bcf83dce8f80ffc9f692112edec7441 100644 (file)
 package org.openhab.binding.amazonechocontrol.internal;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler;
 
 /**
- * The {@link AccountHandlerConfig} holds the configuration for the {@link AccountHandler}
+ * The {@link AccountHandlerConfig} holds the configuration for the
+ * {@link org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler}
  *
  * @author Jan N. Klug - Initial contribution
  */
index 0d902d36d6ccde675ef8ba2c3d03922594f52304..a865add09da6b2c3c70c3ab22e315a9daad3f684 100644 (file)
@@ -32,7 +32,6 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.commons.lang.StringEscapeUtils;
-import org.apache.commons.lang.StringUtils;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler;
@@ -128,15 +127,18 @@ public class AccountServlet extends HttpServlet {
         doVerb("POST", req, resp);
     }
 
-    void doVerb(String verb, @Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
-            throws ServletException, IOException {
+    void doVerb(String verb, @Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
         if (req == null) {
             return;
         }
         if (resp == null) {
             return;
         }
-        String baseUrl = req.getRequestURI().substring(servletUrl.length());
+        String requestUri = req.getRequestURI();
+        if (requestUri == null) {
+            return;
+        }
+        String baseUrl = requestUri.substring(servletUrl.length());
         String uri = baseUrl;
         String queryString = req.getQueryString();
         if (queryString != null && queryString.length() > 0) {
@@ -146,7 +148,12 @@ public class AccountServlet extends HttpServlet {
         Connection connection = this.account.findConnection();
         if (connection != null && uri.equals("/changedomain")) {
             Map<String, String[]> map = req.getParameterMap();
-            String domain = map.get("domain")[0];
+            String[] domainArray = map.get("domain");
+            if (domainArray == null) {
+                logger.warn("Could not determine domain");
+                return;
+            }
+            String domain = domainArray[0];
             String loginData = connection.serializeLoginData();
             Connection newConnection = new Connection(null, this.gson);
             if (newConnection.tryRestoreLogin(loginData, domain)) {
@@ -192,15 +199,20 @@ public class AccountServlet extends HttpServlet {
 
             postDataBuilder.append(name);
             postDataBuilder.append('=');
-            String value = map.get(name)[0];
+            String value = "";
             if (name.equals("failedSignInCount")) {
                 value = "ape:AA==";
+            } else {
+                String[] strings = map.get(name);
+                if (strings != null && strings.length > 0 && strings[0] != null) {
+                    value = strings[0];
+                }
             }
             postDataBuilder.append(URLEncoder.encode(value, StandardCharsets.UTF_8.name()));
         }
 
         uri = req.getRequestURI();
-        if (!uri.startsWith(servletUrl)) {
+        if (uri == null || !uri.startsWith(servletUrl)) {
             returnError(resp, "Invalid request uri '" + uri + "'");
             return;
         }
@@ -221,15 +233,18 @@ public class AccountServlet extends HttpServlet {
     }
 
     @Override
-    protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
-            throws ServletException, IOException {
+    protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
         if (req == null) {
             return;
         }
         if (resp == null) {
             return;
         }
-        String baseUrl = req.getRequestURI().substring(servletUrl.length());
+        String requestUri = req.getRequestURI();
+        if (requestUri == null) {
+            return;
+        }
+        String baseUrl = requestUri.substring(servletUrl.length());
         String uri = baseUrl;
         String queryString = req.getQueryString();
         if (queryString != null && queryString.length() > 0) {
@@ -312,7 +327,7 @@ public class AccountServlet extends HttpServlet {
 
             String html = connection.getLoginPage();
             returnHtml(connection, resp, html, "amazon.com");
-        } catch (URISyntaxException e) {
+        } catch (URISyntaxException | InterruptedException e) {
             logger.warn("get failed with uri syntax error", e);
         }
     }
@@ -419,7 +434,8 @@ public class AccountServlet extends HttpServlet {
         createPageEndAndSent(resp, html);
     }
 
-    private void handleDevices(HttpServletResponse resp, Connection connection) throws IOException, URISyntaxException {
+    private void handleDevices(HttpServletResponse resp, Connection connection)
+            throws IOException, URISyntaxException, InterruptedException {
         returnHtml(connection, resp,
                 "<html>" + StringEscapeUtils.escapeHtml(connection.getDeviceListJson()) + "</html>");
     }
@@ -435,13 +451,13 @@ public class AccountServlet extends HttpServlet {
         StringBuilder html = new StringBuilder();
         html.append("<html><head><title>"
                 + StringEscapeUtils.escapeHtml(BINDING_NAME + " - " + this.account.getThing().getLabel()));
-        if (StringUtils.isNotEmpty(title)) {
+        if (!title.isEmpty()) {
             html.append(" - ");
             html.append(StringEscapeUtils.escapeHtml(title));
         }
         html.append("</title><head><body>");
         html.append("<h1>" + StringEscapeUtils.escapeHtml(BINDING_NAME + " - " + this.account.getThing().getLabel()));
-        if (StringUtils.isNotEmpty(title)) {
+        if (!title.isEmpty()) {
             html.append(" - ");
             html.append(StringEscapeUtils.escapeHtml(title));
         }
@@ -502,9 +518,9 @@ public class AccountServlet extends HttpServlet {
             List<String> properties = musicProvider.supportedProperties;
             String providerId = musicProvider.id;
             String displayName = musicProvider.displayName;
-            if (properties != null && properties.contains("Alexa.Music.PlaySearchPhrase")
-                    && StringUtils.isNotEmpty(providerId) && StringUtils.equals(musicProvider.availability, "AVAILABLE")
-                    && StringUtils.isNotEmpty(displayName)) {
+            if (properties != null && properties.contains("Alexa.Music.PlaySearchPhrase") && providerId != null
+                    && !providerId.isEmpty() && "AVAILABLE".equals(musicProvider.availability) && displayName != null
+                    && !displayName.isEmpty()) {
                 html.append("<tr><td>");
                 html.append(StringEscapeUtils.escapeHtml(displayName));
                 html.append("</td><td>");
@@ -521,7 +537,8 @@ public class AccountServlet extends HttpServlet {
         String errorMessage = "No notifications sounds found";
         try {
             notificationSounds = connection.getNotificationSounds(device);
-        } catch (IOException | HttpException | URISyntaxException | JsonSyntaxException | ConnectionException e) {
+        } catch (IOException | HttpException | URISyntaxException | JsonSyntaxException | ConnectionException
+                | InterruptedException e) {
             errorMessage = e.getLocalizedMessage();
         }
         if (notificationSounds != null) {
@@ -551,7 +568,8 @@ public class AccountServlet extends HttpServlet {
         String errorMessage = "No playlists found";
         try {
             playLists = connection.getPlaylists(device);
-        } catch (IOException | HttpException | URISyntaxException | JsonSyntaxException | ConnectionException e) {
+        } catch (IOException | HttpException | URISyntaxException | JsonSyntaxException | ConnectionException
+                | InterruptedException e) {
             errorMessage = e.getLocalizedMessage();
         }
 
@@ -595,8 +613,9 @@ public class AccountServlet extends HttpServlet {
             if (state == null) {
                 continue;
             }
-            if ((state.deviceSerialNumber == null && device.serialNumber == null)
-                    || (state.deviceSerialNumber != null && state.deviceSerialNumber.equals(device.serialNumber))) {
+            String stateDeviceSerialNumber = state.deviceSerialNumber;
+            if ((stateDeviceSerialNumber == null && device.serialNumber == null)
+                    || (stateDeviceSerialNumber != null && stateDeviceSerialNumber.equals(device.serialNumber))) {
                 PairedDevice[] pairedDeviceList = state.pairedDeviceList;
                 if (pairedDeviceList != null && pairedDeviceList.length > 0) {
                     html.append("<table><tr><th align='left'>Name</th><th align='left'>Value</th></tr>");
@@ -666,7 +685,7 @@ public class AccountServlet extends HttpServlet {
                     return;
                 }
             }
-        } catch (URISyntaxException | ConnectionException e) {
+        } catch (URISyntaxException | ConnectionException | InterruptedException e) {
             returnError(resp, e.getLocalizedMessage());
             return;
         }
index 26855346c74f5e2fbb2e62d7f8cc84992263ce43..170c151c73b5efe2042cdf30cc85b9c89a1832a3 100644 (file)
@@ -14,13 +14,7 @@ package org.openhab.binding.amazonechocontrol.internal;
 
 import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -117,8 +111,8 @@ public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory {
     }
 
     private synchronized void registerDiscoveryService(AccountHandler bridgeHandler) {
-        List<ServiceRegistration<?>> discoveryServiceRegistration = discoveryServiceRegistrations
-                .computeIfAbsent(bridgeHandler.getThing().getUID(), k -> new ArrayList<>());
+        List<ServiceRegistration<?>> discoveryServiceRegistration = Objects.requireNonNull(discoveryServiceRegistrations
+                .computeIfAbsent(bridgeHandler.getThing().getUID(), k -> new ArrayList<>()));
         SmartHomeDevicesDiscovery smartHomeDevicesDiscovery = new SmartHomeDevicesDiscovery(bridgeHandler);
         smartHomeDevicesDiscovery.activate();
         discoveryServiceRegistration.add(bundleContext.registerService(DiscoveryService.class.getName(),
index 118cabb70e586ad2faeaf520833aa8277019931c..f4d36ae7a8af2e470187a924292a71d2a9a385e0 100644 (file)
@@ -20,7 +20,6 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
-import org.apache.commons.lang.StringUtils;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler;
@@ -220,10 +219,9 @@ public class AmazonEchoDynamicStateDescriptionProvider implements DynamicStateDe
                 List<String> properties = musicProvider.supportedProperties;
                 String providerId = musicProvider.id;
                 String displayName = musicProvider.displayName;
-                if (properties != null && properties.contains("Alexa.Music.PlaySearchPhrase")
-                        && StringUtils.isNotEmpty(providerId)
-                        && StringUtils.equals(musicProvider.availability, "AVAILABLE")
-                        && StringUtils.isNotEmpty(displayName) && providerId != null) {
+                if (properties != null && properties.contains("Alexa.Music.PlaySearchPhrase") && providerId != null
+                        && !providerId.isEmpty() && "AVAILABLE".equals(musicProvider.availability)
+                        && displayName != null && !displayName.isEmpty()) {
                     options.add(new StateOption(providerId, displayName));
                 }
             }
index c34595059aa52a3a8b7bb7712de40099120eac50..a5fee625be18455e2c3c22b8d9952d2e130a40f6 100644 (file)
@@ -40,7 +40,6 @@ import org.slf4j.LoggerFactory;
  */
 @NonNullByDefault
 public class BindingServlet extends HttpServlet {
-
     private static final long serialVersionUID = -1453738923337413163L;
 
     private final Logger logger = LoggerFactory.getLogger(BindingServlet.class);
@@ -87,7 +86,11 @@ public class BindingServlet extends HttpServlet {
         if (resp == null) {
             return;
         }
-        String uri = req.getRequestURI().substring(servletUrl.length());
+        String requestUri = req.getRequestURI();
+        if (requestUri == null) {
+            return;
+        }
+        String uri = requestUri.substring(servletUrl.length());
         String queryString = req.getQueryString();
         if (queryString != null && queryString.length() > 0) {
             uri += "?" + queryString;
index ccc72288b87695f4682609320b2d5fc3faf9817f..48bc57b917fa2152895de61ce5babee1629c92aa 100644 (file)
@@ -14,6 +14,7 @@ package org.openhab.binding.amazonechocontrol.internal;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InterruptedIOException;
 import java.io.OutputStream;
 import java.net.CookieManager;
 import java.net.CookieStore;
@@ -39,11 +40,9 @@ import java.util.Objects;
 import java.util.Random;
 import java.util.Scanner;
 import java.util.Set;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.*;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -51,7 +50,6 @@ import java.util.zip.GZIPInputStream;
 
 import javax.net.ssl.HttpsURLConnection;
 
-import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonActivities;
@@ -110,13 +108,7 @@ import org.openhab.core.util.HexUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonSyntaxException;
+import com.google.gson.*;
 
 /**
  * The {@link Connection} is responsible for the connection to the amazon server
@@ -131,12 +123,15 @@ public class Connection {
     private static final Pattern CHARSET_PATTERN = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)");
     private static final String DEVICE_TYPE = "A2IVLV5VM2W81";
 
-    protected final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(THING_THREADPOOL_NAME);
-
     private final Logger logger = LoggerFactory.getLogger(Connection.class);
 
+    protected final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(THING_THREADPOOL_NAME);
+
     private final Random rand = new Random();
     private final CookieManager cookieManager = new CookieManager();
+    private final Gson gson;
+    private final Gson gsonWithNullSerialization;
+
     private String amazonSite = "amazon.com";
     private String alexaServer = "https://alexa.amazon.com";
     private final String userAgent;
@@ -152,19 +147,20 @@ public class Connection {
     private @Nullable String accountCustomerId;
     private @Nullable String customerName;
 
-    private Map<Integer, Announcement> announcements = new LinkedHashMap<>();
-    private Map<Integer, TextToSpeech> textToSpeeches = new LinkedHashMap<>();
-    private Map<Integer, Volume> volumes = new LinkedHashMap<>();
-    private @Nullable ScheduledFuture<?> announcementTimer;
-    private @Nullable ScheduledFuture<?> textToSpeechTimer;
-    private @Nullable ScheduledFuture<?> volumeTimer;
+    private Map<Integer, Announcement> announcements = Collections.synchronizedMap(new LinkedHashMap<>());
+    private Map<Integer, TextToSpeech> textToSpeeches = Collections.synchronizedMap(new LinkedHashMap<>());
+    private Map<Integer, Volume> volumes = Collections.synchronizedMap(new LinkedHashMap<>());
+    private Map<String, LinkedBlockingQueue<QueueObject>> devices = Collections.synchronizedMap(new LinkedHashMap<>());
 
-    private final Gson gson;
-    private final Gson gsonWithNullSerialization;
+    private final Map<TimerType, ScheduledFuture<?>> timers = new ConcurrentHashMap<>();
+    private final Map<TimerType, Lock> locks = new ConcurrentHashMap<>();
 
-    private Map<Device, QueueObject> singles = Collections.synchronizedMap(new LinkedHashMap<>());
-    private Map<Device, QueueObject> groups = Collections.synchronizedMap(new LinkedHashMap<>());
-    public @Nullable ScheduledFuture<?> singleGroupTimer;
+    private enum TimerType {
+        ANNOUNCEMENT,
+        TTS,
+        VOLUME,
+        DEVICES
+    }
 
     public Connection(@Nullable Connection oldConnection, Gson gson) {
         this.gson = gson;
@@ -200,15 +196,16 @@ public class Connection {
 
         // build user agent
         this.userAgent = "AmazonWebView/Amazon Alexa/2.2.223830.0/iOS/11.4.1/iPhone";
-
-        // setAmazonSite(amazonSite);
         GsonBuilder gsonBuilder = new GsonBuilder();
         gsonWithNullSerialization = gsonBuilder.create();
+
+        replaceTimer(TimerType.DEVICES,
+                scheduler.scheduleWithFixedDelay(this::handleExecuteSequenceNode, 0, 500, TimeUnit.MILLISECONDS));
     }
 
     /**
      * Generate a new device id
-     *
+     * <p>
      * The device id consists of 16 random bytes in upper-case hex format, a # as separator and a fixed DEVICE_TYPE
      *
      * @return a string containing the new device-id
@@ -307,8 +304,8 @@ public class Connection {
     }
 
     public boolean isSequenceNodeQueueRunning() {
-        return singles.values().stream().anyMatch(queueObject -> queueObject.queueRunning.get())
-                || groups.values().stream().anyMatch(queueObject -> queueObject.queueRunning.get());
+        return devices.values().stream().anyMatch(
+                (queueObjects) -> (queueObjects.stream().anyMatch(queueObject -> queueObject.future != null)));
     }
 
     public String serializeLoginData() {
@@ -384,7 +381,7 @@ public class Connection {
                 }
             } catch (IOException e) {
                 return false;
-            } catch (URISyntaxException e) {
+            } catch (URISyntaxException | InterruptedException e) {
             }
         }
         return false;
@@ -458,34 +455,24 @@ public class Connection {
             String accountCustomerId = this.accountCustomerId;
             if (accountCustomerId == null || accountCustomerId.isEmpty()) {
                 List<Device> devices = this.getDeviceList();
-                for (Device device : devices) {
-                    final String serial = this.serial;
-                    if (serial != null && serial.equals(device.serialNumber)) {
-                        this.accountCustomerId = device.deviceOwnerCustomerId;
-                        break;
-                    }
-                }
-                accountCustomerId = this.accountCustomerId;
+                accountCustomerId = devices.stream().filter(device -> serial.equals(device.serialNumber)).findAny()
+                        .map(device -> device.deviceOwnerCustomerId).orElse(null);
                 if (accountCustomerId == null || accountCustomerId.isEmpty()) {
-                    for (Device device : devices) {
-                        if ("This Device".equals(device.accountName)) {
-                            this.accountCustomerId = device.deviceOwnerCustomerId;
-                            String serial = device.serialNumber;
-                            if (serial != null) {
-                                this.serial = serial;
-                            }
-                            break;
-                        }
-                    }
+                    accountCustomerId = devices.stream().filter(device -> "This Device".equals(device.accountName))
+                            .findAny().map(device -> {
+                                serial = Objects.requireNonNullElse(device.serialNumber, serial);
+                                return device.deviceOwnerCustomerId;
+                            }).orElse(null);
                 }
+                this.accountCustomerId = accountCustomerId;
             }
-        } catch (URISyntaxException | IOException | ConnectionException e) {
+        } catch (URISyntaxException | IOException | InterruptedException | ConnectionException e) {
             logger.debug("Getting account customer Id failed", e);
         }
         return loginTime;
     }
 
-    private @Nullable Authentication tryGetBootstrap() throws IOException, URISyntaxException {
+    private @Nullable Authentication tryGetBootstrap() throws IOException, URISyntaxException, InterruptedException {
         HttpsURLConnection connection = makeRequest("GET", alexaServer + "/api/bootstrap", null, false, false, null, 0);
         String contentType = connection.getContentType();
         if (connection.getResponseCode() == 200 && contentType != null
@@ -546,12 +533,12 @@ public class Connection {
         return result;
     }
 
-    public String makeRequestAndReturnString(String url) throws IOException, URISyntaxException {
+    public String makeRequestAndReturnString(String url) throws IOException, URISyntaxException, InterruptedException {
         return makeRequestAndReturnString("GET", url, null, false, null);
     }
 
     public String makeRequestAndReturnString(String verb, String url, @Nullable String postData, boolean json,
-            @Nullable Map<String, String> customHeaders) throws IOException, URISyntaxException {
+            @Nullable Map<String, String> customHeaders) throws IOException, URISyntaxException, InterruptedException {
         HttpsURLConnection connection = makeRequest(verb, url, postData, json, true, customHeaders, 3);
         String result = convertStream(connection);
         logger.debug("Result of {} {}:{}", verb, url, result);
@@ -560,7 +547,7 @@ public class Connection {
 
     public HttpsURLConnection makeRequest(String verb, String url, @Nullable String postData, boolean json,
             boolean autoredirect, @Nullable Map<String, String> customHeaders, int badRequestRepeats)
-            throws IOException, URISyntaxException {
+            throws IOException, URISyntaxException, InterruptedException {
         String currentUrl = url;
         int redirectCounter = 0;
         int retryCounter = 0;
@@ -640,14 +627,14 @@ public class Connection {
                 String location = null;
 
                 // handle response headers
-                Map<String, List<String>> headerFields = connection.getHeaderFields();
-                for (Map.Entry<String, List<String>> header : headerFields.entrySet()) {
+                Map<@Nullable String, List<String>> headerFields = connection.getHeaderFields();
+                for (Map.Entry<@Nullable String, List<String>> header : headerFields.entrySet()) {
                     String key = header.getKey();
                     if (key != null && !key.isEmpty()) {
                         if (key.equalsIgnoreCase("Set-Cookie")) {
                             // store cookie
                             for (String cookieHeader : header.getValue()) {
-                                if (cookieHeader != null && !cookieHeader.isEmpty()) {
+                                if (!cookieHeader.isEmpty()) {
                                     List<HttpCookie> cookies = HttpCookie.parse(cookieHeader);
                                     for (HttpCookie cookie : cookies) {
                                         cookieManager.getCookieStore().add(uri, cookie);
@@ -658,7 +645,7 @@ public class Connection {
                         if (key.equalsIgnoreCase("Location")) {
                             // get redirect location
                             location = header.getValue().get(0);
-                            if (location != null && !location.isEmpty()) {
+                            if (!location.isEmpty()) {
                                 location = uri.resolve(location).toString();
                                 // check for https
                                 if (location.toLowerCase().startsWith("http://")) {
@@ -691,12 +678,14 @@ public class Connection {
                         throw new HttpException(code,
                                 verb + " url '" + url + "' failed: " + connection.getResponseMessage());
                     }
-                    try {
-                        Thread.sleep(2000);
-                    } catch (InterruptedException e) {
-                        logger.warn("Unable to wait for next call to {}", url, e);
-                    }
+                    Thread.sleep(2000);
                 }
+            } catch (InterruptedException | InterruptedIOException e) {
+                if (connection != null) {
+                    connection.disconnect();
+                }
+                logger.warn("Unable to wait for next call to {}", url, e);
+                throw e;
             } catch (IOException e) {
                 if (connection != null) {
                     connection.disconnect();
@@ -713,7 +702,7 @@ public class Connection {
     }
 
     public String registerConnectionAsApp(String oAutRedirectUrl)
-            throws ConnectionException, IOException, URISyntaxException {
+            throws ConnectionException, IOException, URISyntaxException, InterruptedException {
         URI oAutRedirectUri = new URI(oAutRedirectUrl);
 
         Map<String, String> queryParameters = new LinkedHashMap<>();
@@ -749,7 +738,7 @@ public class Connection {
         JsonRegisterAppResponse registerAppResponse = parseJson(registerAppResultJson, JsonRegisterAppResponse.class);
 
         if (registerAppResponse == null) {
-            throw new ConnectionException("Error: No response receivec from register application");
+            throw new ConnectionException("Error: No response received from register application");
         }
         Response response = registerAppResponse.response;
         if (response == null) {
@@ -767,8 +756,9 @@ public class Connection {
         if (bearer == null) {
             throw new ConnectionException("Error: No bearer received from register application");
         }
-        this.refreshToken = bearer.refreshToken;
-        if (this.refreshToken == null || this.refreshToken.isEmpty()) {
+        String refreshToken = bearer.refreshToken;
+        this.refreshToken = refreshToken;
+        if (refreshToken == null || refreshToken.isEmpty()) {
             throw new ConnectionException("Error: No refresh token received");
         }
         try {
@@ -806,7 +796,7 @@ public class Connection {
         return deviceName;
     }
 
-    private void exchangeToken() throws IOException, URISyntaxException {
+    private void exchangeToken() throws IOException, URISyntaxException, InterruptedException {
         this.renewTime = 0;
         String cookiesJson = "{\"cookies\":{\"." + getAmazonSite() + "\":[]}}";
         String cookiesBase64 = Base64.getEncoder().encodeToString(cookiesJson.getBytes());
@@ -821,8 +811,8 @@ public class Connection {
 
         String exchangeTokenJson = makeRequestAndReturnString("POST",
                 "https://www." + getAmazonSite() + "/ap/exchangetoken", exchangePostData, false, exchangeTokenHeader);
-        JsonExchangeTokenResponse exchangeTokenResponse = gson.fromJson(exchangeTokenJson,
-                JsonExchangeTokenResponse.class);
+        JsonExchangeTokenResponse exchangeTokenResponse = Objects
+                .requireNonNull(gson.fromJson(exchangeTokenJson, JsonExchangeTokenResponse.class));
 
         org.openhab.binding.amazonechocontrol.internal.jsons.JsonExchangeTokenResponse.Response response = exchangeTokenResponse.response;
         if (response != null) {
@@ -832,16 +822,18 @@ public class Connection {
                 if (cookiesMap != null) {
                     for (String domain : cookiesMap.keySet()) {
                         Cookie[] cookies = cookiesMap.get(domain);
-                        for (Cookie cookie : cookies) {
-                            if (cookie != null) {
-                                HttpCookie httpCookie = new HttpCookie(cookie.name, cookie.value);
-                                httpCookie.setPath(cookie.path);
-                                httpCookie.setDomain(domain);
-                                Boolean secure = cookie.secure;
-                                if (secure != null) {
-                                    httpCookie.setSecure(secure);
+                        if (cookies != null) {
+                            for (Cookie cookie : cookies) {
+                                if (cookie != null) {
+                                    HttpCookie httpCookie = new HttpCookie(cookie.name, cookie.value);
+                                    httpCookie.setPath(cookie.path);
+                                    httpCookie.setDomain(domain);
+                                    Boolean secure = cookie.secure;
+                                    if (secure != null) {
+                                        httpCookie.setSecure(secure);
+                                    }
+                                    this.cookieManager.getCookieStore().add(null, httpCookie);
                                 }
-                                this.cookieManager.getCookieStore().add(null, httpCookie);
                             }
                         }
                     }
@@ -854,7 +846,7 @@ public class Connection {
         this.renewTime = (long) (System.currentTimeMillis() + Connection.EXPIRES_IN * 1000d / 0.8d); // start renew at
     }
 
-    public boolean checkRenewSession() throws URISyntaxException, IOException {
+    public boolean checkRenewSession() throws URISyntaxException, IOException, InterruptedException {
         if (System.currentTimeMillis() >= this.renewTime) {
             String renewTokenPostData = "app_name=Amazon%20Alexa&app_version=2.2.223830.0&di.sdk.version=6.10.0&source_token="
                     + URLEncoder.encode(refreshToken, StandardCharsets.UTF_8.name())
@@ -873,7 +865,7 @@ public class Connection {
         return loginTime != null;
     }
 
-    public String getLoginPage() throws IOException, URISyntaxException {
+    public String getLoginPage() throws IOException, URISyntaxException, InterruptedException {
         // clear session data
         logout();
 
@@ -902,7 +894,7 @@ public class Connection {
         return loginFormHtml;
     }
 
-    public boolean verifyLogin() throws IOException, URISyntaxException {
+    public boolean verifyLogin() throws IOException, URISyntaxException, InterruptedException {
         if (this.refreshToken == null) {
             return false;
         }
@@ -933,6 +925,15 @@ public class Connection {
         }
     }
 
+    private void replaceTimer(TimerType type, @Nullable ScheduledFuture<?> newTimer) {
+        timers.compute(type, (timerType, oldTimer) -> {
+            if (oldTimer != null) {
+                oldTimer.cancel(true);
+            }
+            return newTimer;
+        });
+    }
+
     public void logout() {
         cookieManager.getCookieStore().removeAll();
         // reset all members
@@ -941,23 +942,23 @@ public class Connection {
         verifyTime = null;
         deviceName = null;
 
-        if (announcementTimer != null) {
-            announcements.clear();
-            announcementTimer.cancel(true);
-        }
-        if (textToSpeechTimer != null) {
-            textToSpeeches.clear();
-            textToSpeechTimer.cancel(true);
-        }
-        if (volumeTimer != null) {
-            volumes.clear();
-            volumeTimer.cancel(true);
-        }
-        singles.values().forEach(queueObject -> queueObject.dispose());
-        groups.values().forEach(queueObject -> queueObject.dispose());
-        if (singleGroupTimer != null) {
-            singleGroupTimer.cancel(true);
-        }
+        replaceTimer(TimerType.ANNOUNCEMENT, null);
+        announcements.clear();
+        replaceTimer(TimerType.TTS, null);
+        textToSpeeches.clear();
+        replaceTimer(TimerType.VOLUME, null);
+        volumes.clear();
+        replaceTimer(TimerType.DEVICES, null);
+
+        devices.values().forEach((queueObjects) -> {
+            queueObjects.forEach((queueObject) -> {
+                Future<?> future = queueObject.future;
+                if (future != null) {
+                    future.cancel(true);
+                    queueObject.future = null;
+                }
+            });
+        });
     }
 
     // parser
@@ -965,8 +966,7 @@ public class Connection {
         try {
             return gson.fromJson(json, type);
         } catch (JsonParseException | IllegalStateException e) {
-            logger.warn("Parsing json failed", e);
-            logger.warn("Illegal json: {}", json);
+            logger.warn("Parsing json failed: {}", json, e);
             throw e;
         }
     }
@@ -983,13 +983,14 @@ public class Connection {
                     return result;
                 }
             }
-        } catch (IOException | URISyntaxException e) {
+        } catch (IOException | URISyntaxException | InterruptedException e) {
             logger.info("getting wakewords failed", e);
         }
         return new WakeWord[0];
     }
 
-    public List<SmartHomeBaseDevice> getSmarthomeDeviceList() throws IOException, URISyntaxException {
+    public List<SmartHomeBaseDevice> getSmarthomeDeviceList()
+            throws IOException, URISyntaxException, InterruptedException {
         try {
             String json = makeRequestAndReturnString(alexaServer + "/api/phoenix");
             logger.debug("getSmartHomeDevices result: {}", json);
@@ -1012,7 +1013,7 @@ public class Connection {
     private void searchSmartHomeDevicesRecursive(@Nullable Object jsonNode, List<SmartHomeBaseDevice> devices) {
         if (jsonNode instanceof Map) {
             @SuppressWarnings("rawtypes")
-            Map map = (Map) jsonNode;
+            Map<String, Object> map = (Map) jsonNode;
             if (map.containsKey("entityId") && map.containsKey("friendlyName") && map.containsKey("actions")) {
                 // device node found, create type element and add it to the results
                 JsonElement element = gson.toJsonTree(jsonNode);
@@ -1032,7 +1033,7 @@ public class Connection {
         }
     }
 
-    public List<Device> getDeviceList() throws IOException, URISyntaxException {
+    public List<Device> getDeviceList() throws IOException, URISyntaxException, InterruptedException {
         String json = getDeviceListJson();
         JsonDevices devices = parseJson(json, JsonDevices.class);
         if (devices != null) {
@@ -1044,13 +1045,13 @@ public class Connection {
         return Collections.emptyList();
     }
 
-    public String getDeviceListJson() throws IOException, URISyntaxException {
+    public String getDeviceListJson() throws IOException, URISyntaxException, InterruptedException {
         String json = makeRequestAndReturnString(alexaServer + "/api/devices-v2/device?cached=false");
         return json;
     }
 
     public Map<String, JsonArray> getSmartHomeDeviceStatesJson(Set<String> applianceIds)
-            throws IOException, URISyntaxException {
+            throws IOException, URISyntaxException, InterruptedException {
         JsonObject requestObject = new JsonObject();
         JsonArray stateRequests = new JsonArray();
         for (String applianceId : applianceIds) {
@@ -1064,7 +1065,7 @@ public class Connection {
         String json = makeRequestAndReturnString("POST", alexaServer + "/api/phoenix/state", requestBody, true, null);
         logger.trace("Requested {} and received {}", requestBody, json);
 
-        JsonObject responseObject = this.gson.fromJson(json, JsonObject.class);
+        JsonObject responseObject = Objects.requireNonNull(gson.fromJson(json, JsonObject.class));
         JsonArray deviceStates = (JsonArray) responseObject.get("deviceStates");
         Map<String, JsonArray> result = new HashMap<>();
         for (JsonElement deviceState : deviceStates) {
@@ -1079,14 +1080,16 @@ public class Connection {
         return result;
     }
 
-    public @Nullable JsonPlayerState getPlayer(Device device) throws IOException, URISyntaxException {
+    public @Nullable JsonPlayerState getPlayer(Device device)
+            throws IOException, URISyntaxException, InterruptedException {
         String json = makeRequestAndReturnString(alexaServer + "/api/np/player?deviceSerialNumber="
                 + device.serialNumber + "&deviceType=" + device.deviceType + "&screenWidth=1440");
         JsonPlayerState playerState = parseJson(json, JsonPlayerState.class);
         return playerState;
     }
 
-    public @Nullable JsonMediaState getMediaState(Device device) throws IOException, URISyntaxException {
+    public @Nullable JsonMediaState getMediaState(Device device)
+            throws IOException, URISyntaxException, InterruptedException {
         String json = makeRequestAndReturnString(alexaServer + "/api/media/state?deviceSerialNumber="
                 + device.serialNumber + "&deviceType=" + device.deviceType);
         JsonMediaState mediaState = parseJson(json, JsonMediaState.class);
@@ -1105,7 +1108,7 @@ public class Connection {
                     return activiesArray;
                 }
             }
-        } catch (IOException | URISyntaxException e) {
+        } catch (IOException | URISyntaxException | InterruptedException e) {
             logger.info("getting activities failed", e);
         }
         return new Activity[0];
@@ -1115,7 +1118,7 @@ public class Connection {
         String json;
         try {
             json = makeRequestAndReturnString(alexaServer + "/api/bluetooth?cached=true");
-        } catch (IOException | URISyntaxException e) {
+        } catch (IOException | URISyntaxException | InterruptedException e) {
             logger.debug("failed to get bluetooth state: {}", e.getMessage());
             return new JsonBluetoothStates();
         }
@@ -1123,27 +1126,27 @@ public class Connection {
         return bluetoothStates;
     }
 
-    public @Nullable JsonPlaylists getPlaylists(Device device) throws IOException, URISyntaxException {
-        String json = makeRequestAndReturnString(alexaServer + "/api/cloudplayer/playlists?deviceSerialNumber="
-                + device.serialNumber + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId="
-                + (this.accountCustomerId == null || this.accountCustomerId.isEmpty() ? device.deviceOwnerCustomerId
-                        : this.accountCustomerId));
+    public @Nullable JsonPlaylists getPlaylists(Device device)
+            throws IOException, URISyntaxException, InterruptedException {
+        String json = makeRequestAndReturnString(
+                alexaServer + "/api/cloudplayer/playlists?deviceSerialNumber=" + device.serialNumber + "&deviceType="
+                        + device.deviceType + "&mediaOwnerCustomerId=" + getCustomerId(device.deviceOwnerCustomerId));
         JsonPlaylists playlists = parseJson(json, JsonPlaylists.class);
         return playlists;
     }
 
-    public void command(Device device, String command) throws IOException, URISyntaxException {
+    public void command(Device device, String command) throws IOException, URISyntaxException, InterruptedException {
         String url = alexaServer + "/api/np/command?deviceSerialNumber=" + device.serialNumber + "&deviceType="
                 + device.deviceType;
         makeRequest("POST", url, command, true, true, null, 0);
     }
 
-    public void smartHomeCommand(String entityId, String action) throws IOException {
+    public void smartHomeCommand(String entityId, String action) throws IOException, InterruptedException {
         smartHomeCommand(entityId, action, null, null);
     }
 
     public void smartHomeCommand(String entityId, String action, @Nullable String property, @Nullable Object value)
-            throws IOException {
+            throws IOException, InterruptedException {
         String url = alexaServer + "/api/phoenix/state";
 
         JsonObject json = new JsonObject();
@@ -1195,7 +1198,8 @@ public class Connection {
         }
     }
 
-    public void notificationVolume(Device device, int volume) throws IOException, URISyntaxException {
+    public void notificationVolume(Device device, int volume)
+            throws IOException, URISyntaxException, InterruptedException {
         String url = alexaServer + "/api/device-notification-state/" + device.deviceType + "/" + device.softwareVersion
                 + "/" + device.serialNumber;
         String command = "{\"deviceSerialNumber\":\"" + device.serialNumber + "\",\"deviceType\":\"" + device.deviceType
@@ -1203,7 +1207,8 @@ public class Connection {
         makeRequest("PUT", url, command, true, true, null, 0);
     }
 
-    public void ascendingAlarm(Device device, boolean ascendingAlarm) throws IOException, URISyntaxException {
+    public void ascendingAlarm(Device device, boolean ascendingAlarm)
+            throws IOException, URISyntaxException, InterruptedException {
         String url = alexaServer + "/api/ascending-alarm/" + device.serialNumber;
         String command = "{\"ascendingAlarmEnabled\":" + (ascendingAlarm ? "true" : "false")
                 + ",\"deviceSerialNumber\":\"" + device.serialNumber + "\",\"deviceType\":\"" + device.deviceType
@@ -1222,7 +1227,7 @@ public class Connection {
                     return deviceNotificationStates;
                 }
             }
-        } catch (IOException | URISyntaxException e) {
+        } catch (IOException | URISyntaxException | InterruptedException e) {
             logger.info("Error getting device notification states", e);
         }
         return new DeviceNotificationState[0];
@@ -1239,13 +1244,14 @@ public class Connection {
                     return ascendingAlarmModelList;
                 }
             }
-        } catch (IOException | URISyntaxException e) {
+        } catch (IOException | URISyntaxException | InterruptedException e) {
             logger.info("Error getting device notification states", e);
         }
         return new AscendingAlarmModel[0];
     }
 
-    public void bluetooth(Device device, @Nullable String address) throws IOException, URISyntaxException {
+    public void bluetooth(Device device, @Nullable String address)
+            throws IOException, URISyntaxException, InterruptedException {
         if (address == null || address.isEmpty()) {
             // disconnect
             makeRequest("POST",
@@ -1258,7 +1264,13 @@ public class Connection {
         }
     }
 
-    public void playRadio(Device device, @Nullable String stationId) throws IOException, URISyntaxException {
+    private @Nullable String getCustomerId(@Nullable String defaultId) {
+        String accountCustomerId = this.accountCustomerId;
+        return accountCustomerId == null || accountCustomerId.isEmpty() ? defaultId : accountCustomerId;
+    }
+
+    public void playRadio(Device device, @Nullable String stationId)
+            throws IOException, URISyntaxException, InterruptedException {
         if (stationId == null || stationId.isEmpty()) {
             command(device, "{\"type\":\"PauseCommand\"}");
         } else {
@@ -1266,14 +1278,13 @@ public class Connection {
                     alexaServer + "/api/tunein/queue-and-play?deviceSerialNumber=" + device.serialNumber
                             + "&deviceType=" + device.deviceType + "&guideId=" + stationId
                             + "&contentType=station&callSign=&mediaOwnerCustomerId="
-                            + (this.accountCustomerId == null || this.accountCustomerId.isEmpty()
-                                    ? device.deviceOwnerCustomerId
-                                    : this.accountCustomerId),
+                            + getCustomerId(device.deviceOwnerCustomerId),
                     "", true, true, null, 0);
         }
     }
 
-    public void playAmazonMusicTrack(Device device, @Nullable String trackId) throws IOException, URISyntaxException {
+    public void playAmazonMusicTrack(Device device, @Nullable String trackId)
+            throws IOException, URISyntaxException, InterruptedException {
         if (trackId == null || trackId.isEmpty()) {
             command(device, "{\"type\":\"PauseCommand\"}");
         } else {
@@ -1281,16 +1292,13 @@ public class Connection {
             makeRequest("POST",
                     alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber
                             + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId="
-                            + (this.accountCustomerId == null || this.accountCustomerId.isEmpty()
-                                    ? device.deviceOwnerCustomerId
-                                    : this.accountCustomerId)
-                            + "&shuffle=false",
+                            + getCustomerId(device.deviceOwnerCustomerId) + "&shuffle=false",
                     command, true, true, null, 0);
         }
     }
 
     public void playAmazonMusicPlayList(Device device, @Nullable String playListId)
-            throws IOException, URISyntaxException {
+            throws IOException, URISyntaxException, InterruptedException {
         if (playListId == null || playListId.isEmpty()) {
             command(device, "{\"type\":\"PauseCommand\"}");
         } else {
@@ -1298,63 +1306,49 @@ public class Connection {
             makeRequest("POST",
                     alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber
                             + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId="
-                            + (this.accountCustomerId == null || this.accountCustomerId.isEmpty()
-                                    ? device.deviceOwnerCustomerId
-                                    : this.accountCustomerId)
-                            + "&shuffle=false",
+                            + getCustomerId(device.deviceOwnerCustomerId) + "&shuffle=false",
                     command, true, true, null, 0);
         }
     }
 
-    public void sendNotificationToMobileApp(String customerId, String text, @Nullable String title)
-            throws IOException, URISyntaxException {
-        Map<String, Object> parameters = new HashMap<>();
-        parameters.put("notificationMessage", text);
-        parameters.put("alexaUrl", "#v2/behaviors");
-        if (title != null && !title.isEmpty()) {
-            parameters.put("title", title);
-        } else {
-            parameters.put("title", "OpenHAB");
-        }
-        parameters.put("customerId", customerId);
-        executeSequenceCommand(null, "Alexa.Notifications.SendMobilePush", parameters);
-    }
-
-    public synchronized void announcement(Device device, String speak, String bodyText, @Nullable String title,
+    public void announcement(Device device, String speak, String bodyText, @Nullable String title,
             @Nullable Integer ttsVolume, @Nullable Integer standardVolume) {
-        if (speak == null || speak.replaceAll("<.+?>", " ").replaceAll("\\s+", " ").trim().isEmpty()) {
+        if (speak.replaceAll("<.+?>", " ").replaceAll("\\s+", " ").trim().isEmpty()) {
             return;
         }
-        if (announcementTimer != null) {
-            announcementTimer.cancel(true);
-            announcementTimer = null;
+
+        // we lock announcements until we have finished adding this one
+        Lock lock = locks.computeIfAbsent(TimerType.ANNOUNCEMENT, k -> new ReentrantLock());
+        lock.lock();
+        try {
+            Announcement announcement = Objects.requireNonNull(announcements.computeIfAbsent(
+                    Objects.hash(speak, bodyText, title), k -> new Announcement(speak, bodyText, title)));
+            announcement.devices.add(device);
+            announcement.ttsVolumes.add(ttsVolume);
+            announcement.standardVolumes.add(standardVolume);
+
+            // schedule an announcement only if it has not been scheduled before
+            timers.computeIfAbsent(TimerType.ANNOUNCEMENT,
+                    k -> scheduler.schedule(this::sendAnnouncement, 500, TimeUnit.MILLISECONDS));
+        } finally {
+            lock.unlock();
         }
-        Announcement announcement = announcements.computeIfAbsent(Objects.hash(speak, bodyText, title),
-                k -> new Announcement(speak, bodyText, title));
-        announcement.devices.add(device);
-        announcement.ttsVolumes.add(ttsVolume);
-        announcement.standardVolumes.add(standardVolume);
-        announcementTimer = scheduler.schedule(this::sendAnnouncement, 500, TimeUnit.MILLISECONDS);
     }
 
-    private synchronized void sendAnnouncement() {
-        // NECESSARY TO CANCEL AND NULL TIMER?
-        if (announcementTimer != null) {
-            announcementTimer.cancel(true);
-            announcementTimer = null;
-        }
-        Iterator<Announcement> iterator = announcements.values().iterator();
-        while (iterator.hasNext()) {
-            Announcement announcement = iterator.next();
-            if (announcement != null) {
+    private void sendAnnouncement() {
+        // we lock new announcements until we have dispatched everything
+        Lock lock = locks.computeIfAbsent(TimerType.ANNOUNCEMENT, k -> new ReentrantLock());
+        lock.lock();
+        try {
+            Iterator<Announcement> iterator = announcements.values().iterator();
+            while (iterator.hasNext()) {
+                Announcement announcement = iterator.next();
                 try {
                     List<Device> devices = announcement.devices;
-                    if (devices != null && !devices.isEmpty()) {
+                    if (!devices.isEmpty()) {
                         String speak = announcement.speak;
                         String bodyText = announcement.bodyText;
                         String title = announcement.title;
-                        List<@Nullable Integer> ttsVolumes = announcement.ttsVolumes;
-                        List<@Nullable Integer> standardVolumes = announcement.standardVolumes;
 
                         Map<String, Object> parameters = new HashMap<>();
                         parameters.put("expireAfter", "PT5S");
@@ -1379,143 +1373,148 @@ public class Connection {
                         target.devices = targetDevices;
                         parameters.put("target", target);
 
-                        String accountCustomerId = this.accountCustomerId;
-                        String customerId = accountCustomerId == null || accountCustomerId.isEmpty()
-                                ? devices.toArray(new Device[0])[0].deviceOwnerCustomerId
-                                : accountCustomerId;
-
+                        String customerId = getCustomerId(devices.get(0).deviceOwnerCustomerId);
                         if (customerId != null) {
                             parameters.put("customerId", customerId);
                         }
-                        executeSequenceCommandWithVolume(devices.toArray(new Device[0]), "AlexaAnnouncement",
-                                parameters, ttsVolumes.toArray(new Integer[0]),
-                                standardVolumes.toArray(new Integer[0]));
+                        executeSequenceCommandWithVolume(devices, "AlexaAnnouncement", parameters,
+                                announcement.ttsVolumes, announcement.standardVolumes);
                     }
                 } catch (Exception e) {
                     logger.warn("send announcement fails with unexpected error", e);
                 }
+                iterator.remove();
             }
-            iterator.remove();
+        } finally {
+            // the timer is done anyway immediately after we unlock
+            timers.remove(TimerType.ANNOUNCEMENT);
+            lock.unlock();
         }
     }
 
-    public synchronized void textToSpeech(Device device, String text, @Nullable Integer ttsVolume,
+    public void textToSpeech(Device device, String text, @Nullable Integer ttsVolume,
             @Nullable Integer standardVolume) {
-        if (text == null || text.replaceAll("<.+?>", "").replaceAll("\\s+", " ").trim().isEmpty()) {
+        if (text.replaceAll("<.+?>", "").replaceAll("\\s+", " ").trim().isEmpty()) {
             return;
         }
-        if (textToSpeechTimer != null) {
-            textToSpeechTimer.cancel(true);
-            textToSpeechTimer = null;
-        }
-        TextToSpeech textToSpeech = textToSpeeches.computeIfAbsent(Objects.hash(text), k -> new TextToSpeech(text));
-        textToSpeech.devices.add(device);
-        textToSpeech.ttsVolumes.add(ttsVolume);
-        textToSpeech.standardVolumes.add(standardVolume);
-        textToSpeechTimer = scheduler.schedule(this::sendTextToSpeech, 500, TimeUnit.MILLISECONDS);
-    }
 
-    private synchronized void sendTextToSpeech() {
-        // NECESSARY TO CANCEL AND NULL TIMER?
-        if (textToSpeechTimer != null) {
-            textToSpeechTimer.cancel(true);
-            textToSpeechTimer = null;
-        }
-        Iterator<TextToSpeech> iterator = textToSpeeches.values().iterator();
-        while (iterator.hasNext()) {
-            TextToSpeech textToSpeech = iterator.next();
-            if (textToSpeech != null) {
+        // we lock TTS until we have finished adding this one
+        Lock lock = locks.computeIfAbsent(TimerType.TTS, k -> new ReentrantLock());
+        lock.lock();
+        try {
+            TextToSpeech textToSpeech = Objects
+                    .requireNonNull(textToSpeeches.computeIfAbsent(Objects.hash(text), k -> new TextToSpeech(text)));
+            textToSpeech.devices.add(device);
+            textToSpeech.ttsVolumes.add(ttsVolume);
+            textToSpeech.standardVolumes.add(standardVolume);
+            // schedule a TTS only if it has not been scheduled before
+            timers.computeIfAbsent(TimerType.TTS,
+                    k -> scheduler.schedule(this::sendTextToSpeech, 500, TimeUnit.MILLISECONDS));
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    private void sendTextToSpeech() {
+        // we lock new TTS until we have dispatched everything
+        Lock lock = locks.computeIfAbsent(TimerType.TTS, k -> new ReentrantLock());
+        lock.lock();
+        try {
+            Iterator<TextToSpeech> iterator = textToSpeeches.values().iterator();
+            while (iterator.hasNext()) {
+                TextToSpeech textToSpeech = iterator.next();
                 try {
                     List<Device> devices = textToSpeech.devices;
-                    if (devices != null && !devices.isEmpty()) {
+                    if (!devices.isEmpty()) {
                         String text = textToSpeech.text;
-                        List<@Nullable Integer> ttsVolumes = textToSpeech.ttsVolumes;
-                        List<@Nullable Integer> standardVolumes = textToSpeech.standardVolumes;
-
                         Map<String, Object> parameters = new HashMap<>();
                         parameters.put("textToSpeak", text);
-                        executeSequenceCommandWithVolume(devices.toArray(new Device[0]), "Alexa.Speak", parameters,
-                                ttsVolumes.toArray(new Integer[0]), standardVolumes.toArray(new Integer[0]));
+                        executeSequenceCommandWithVolume(devices, "Alexa.Speak", parameters, textToSpeech.ttsVolumes,
+                                textToSpeech.standardVolumes);
                     }
                 } catch (Exception e) {
                     logger.warn("send textToSpeech fails with unexpected error", e);
                 }
+                iterator.remove();
             }
-            iterator.remove();
+        } finally {
+            // the timer is done anyway immediately after we unlock
+            timers.remove(TimerType.TTS);
+            lock.unlock();
         }
     }
 
-    public synchronized void volume(Device device, int vol) {
-        if (volumeTimer != null) {
-            volumeTimer.cancel(true);
-            volumeTimer = null;
+    public void volume(Device device, int vol) {
+        // we lock volume until we have finished adding this one
+        Lock lock = locks.computeIfAbsent(TimerType.VOLUME, k -> new ReentrantLock());
+        lock.lock();
+        try {
+            Volume volume = Objects.requireNonNull(volumes.computeIfAbsent(vol, k -> new Volume(vol)));
+            volume.devices.add(device);
+            volume.volumes.add(vol);
+            // schedule a TTS only if it has not been scheduled before
+            timers.computeIfAbsent(TimerType.VOLUME,
+                    k -> scheduler.schedule(this::sendVolume, 500, TimeUnit.MILLISECONDS));
+        } finally {
+            lock.unlock();
         }
-        Volume volume = volumes.computeIfAbsent(vol, k -> new Volume(vol));
-        volume.devices.add(device);
-        volume.volumes.add(vol);
-        volumeTimer = scheduler.schedule(this::sendVolume, 500, TimeUnit.MILLISECONDS);
     }
 
-    private synchronized void sendVolume() {
-        // NECESSARY TO CANCEL AND NULL TIMER?
-        if (volumeTimer != null) {
-            volumeTimer.cancel(true);
-            volumeTimer = null;
-        }
-        Iterator<Volume> iterator = volumes.values().iterator();
-        while (iterator.hasNext()) {
-            Volume volume = iterator.next();
-            if (volume != null) {
+    private void sendVolume() {
+        // we lock new volume until we have dispatched everything
+        Lock lock = locks.computeIfAbsent(TimerType.VOLUME, k -> new ReentrantLock());
+        lock.lock();
+        try {
+            Iterator<Volume> iterator = volumes.values().iterator();
+            while (iterator.hasNext()) {
+                Volume volume = iterator.next();
                 try {
                     List<Device> devices = volume.devices;
-                    if (devices != null && !devices.isEmpty()) {
-                        List<@Nullable Integer> volumes = volume.volumes;
-
-                        executeSequenceCommandWithVolume(devices.toArray(new Device[0]), null, null,
-                                volumes.toArray(new Integer[0]), null);
+                    if (!devices.isEmpty()) {
+                        executeSequenceCommandWithVolume(devices, null, Map.of(), volume.volumes, List.of());
                     }
                 } catch (Exception e) {
                     logger.warn("send volume fails with unexpected error", e);
                 }
+                iterator.remove();
             }
-            iterator.remove();
+        } finally {
+            // the timer is done anyway immediately after we unlock
+            timers.remove(TimerType.VOLUME);
+            lock.unlock();
         }
     }
 
-    private void executeSequenceCommandWithVolume(@Nullable Device[] devices, @Nullable String command,
-            @Nullable Map<String, Object> parameters, @NonNull Integer[] ttsVolumes,
-            @Nullable Integer @Nullable [] standardVolumes) throws IOException, URISyntaxException {
+    private void executeSequenceCommandWithVolume(List<Device> devices, @Nullable String command,
+            Map<String, Object> parameters, List<@Nullable Integer> ttsVolumes,
+            List<@Nullable Integer> standardVolumes) {
         JsonArray serialNodesToExecute = new JsonArray();
-        if (ttsVolumes != null) {
-            JsonArray ttsVolumeNodesToExecute = new JsonArray();
-            for (int i = 0; i < devices.length; i++) {
-                if (ttsVolumes[i] != null && (standardVolumes == null || !ttsVolumes[i].equals(standardVolumes[i]))) {
-                    Map<String, Object> volumeParameters = new HashMap<>();
-                    volumeParameters.put("value", ttsVolumes[i]);
-                    ttsVolumeNodesToExecute
-                            .add(createExecutionNode(devices[i], "Alexa.DeviceControls.Volume", volumeParameters));
-                }
-            }
-            if (ttsVolumeNodesToExecute.size() > 0) {
-                // executeSequenceNodes(devices, ttsVolumeNodesToExecute, true);
-                JsonObject parallelNodesToExecute = new JsonObject();
-                parallelNodesToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode");
-                parallelNodesToExecute.add("nodesToExecute", ttsVolumeNodesToExecute);
-                serialNodesToExecute.add(parallelNodesToExecute);
+        JsonArray ttsVolumeNodesToExecute = new JsonArray();
+        for (int i = 0; i < devices.size(); i++) {
+            Integer ttsVolume = ttsVolumes.size() > i ? ttsVolumes.get(i) : null;
+            Integer standardVolume = standardVolumes.size() > i ? standardVolumes.get(i) : null;
+            if (ttsVolume != null && (standardVolume != null || !ttsVolume.equals(standardVolume))) {
+                ttsVolumeNodesToExecute.add(
+                        createExecutionNode(devices.get(i), "Alexa.DeviceControls.Volume", Map.of("value", ttsVolume)));
             }
         }
+        if (ttsVolumeNodesToExecute.size() > 0) {
+            JsonObject parallelNodesToExecute = new JsonObject();
+            parallelNodesToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode");
+            parallelNodesToExecute.add("nodesToExecute", ttsVolumeNodesToExecute);
+            serialNodesToExecute.add(parallelNodesToExecute);
+        }
 
-        if (command != null && parameters != null) {
+        if (command != null && !parameters.isEmpty()) {
             JsonArray commandNodesToExecute = new JsonArray();
             if ("Alexa.Speak".equals(command)) {
                 for (Device device : devices) {
                     commandNodesToExecute.add(createExecutionNode(device, command, parameters));
                 }
             } else {
-                commandNodesToExecute.add(createExecutionNode(devices[0], command, parameters));
+                commandNodesToExecute.add(createExecutionNode(devices.get(0), command, parameters));
             }
             if (commandNodesToExecute.size() > 0) {
-                // executeSequenceNodes(devices, nodesToExecute, true);
                 JsonObject parallelNodesToExecute = new JsonObject();
                 parallelNodesToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode");
                 parallelNodesToExecute.add("nodesToExecute", commandNodesToExecute);
@@ -1523,122 +1522,156 @@ public class Connection {
             }
         }
 
-        if (serialNodesToExecute.size() > 0) {
-            executeSequenceNodes(devices, serialNodesToExecute, false);
+        JsonArray standardVolumeNodesToExecute = new JsonArray();
+        for (int i = 0; i < devices.size(); i++) {
+            Integer ttsVolume = ttsVolumes.size() > i ? ttsVolumes.get(i) : null;
+            Integer standardVolume = standardVolumes.size() > i ? standardVolumes.get(i) : null;
+            if (ttsVolume != null && standardVolume != null && !ttsVolume.equals(standardVolume)) {
+                standardVolumeNodesToExecute.add(createExecutionNode(devices.get(i), "Alexa.DeviceControls.Volume",
+                        Map.of("value", standardVolume)));
+            }
+        }
+        if (standardVolumeNodesToExecute.size() > 0) {
+            JsonObject parallelNodesToExecute = new JsonObject();
+            parallelNodesToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode");
+            parallelNodesToExecute.add("nodesToExecute", standardVolumeNodesToExecute);
+            serialNodesToExecute.add(parallelNodesToExecute);
         }
 
-        if (standardVolumes != null) {
-            JsonArray standardVolumeNodesToExecute = new JsonArray();
-            for (int i = 0; i < devices.length; i++) {
-                if (ttsVolumes[i] != null && standardVolumes[i] != null && !ttsVolumes[i].equals(standardVolumes[i])) {
-                    Map<String, @Nullable Object> volumeParameters = new HashMap<>();
-                    volumeParameters.put("value", standardVolumes[i]);
-                    standardVolumeNodesToExecute
-                            .add(createExecutionNode(devices[i], "Alexa.DeviceControls.Volume", volumeParameters));
-                }
-            }
-            if (standardVolumeNodesToExecute.size() > 0) {
-                executeSequenceNodes(devices, standardVolumeNodesToExecute, true);
-            }
+        if (serialNodesToExecute.size() > 0) {
+            executeSequenceNodes(devices, serialNodesToExecute, false);
         }
     }
 
     // commands: Alexa.Weather.Play, Alexa.Traffic.Play, Alexa.FlashBriefing.Play,
     // Alexa.GoodMorning.Play,
     // Alexa.SingASong.Play, Alexa.TellStory.Play, Alexa.Speak (textToSpeach)
-    public void executeSequenceCommand(@Nullable Device device, String command,
-            @Nullable Map<String, Object> parameters) throws IOException, URISyntaxException {
+    public void executeSequenceCommand(Device device, String command, Map<String, Object> parameters) {
         JsonObject nodeToExecute = createExecutionNode(device, command, parameters);
-        executeSequenceNode(new Device[] { device }, nodeToExecute);
+        executeSequenceNode(List.of(device), nodeToExecute);
+    }
+
+    private void executeSequenceNode(List<Device> devices, JsonObject nodeToExecute) {
+        QueueObject queueObject = new QueueObject();
+        queueObject.devices = devices;
+        queueObject.nodeToExecute = nodeToExecute;
+        String serialNumbers = "";
+        for (Device device : devices) {
+            String serialNumber = device.serialNumber;
+            if (serialNumber != null) {
+                Objects.requireNonNull(this.devices.computeIfAbsent(serialNumber, k -> new LinkedBlockingQueue<>()))
+                        .offer(queueObject);
+                serialNumbers = serialNumbers + device.serialNumber + " ";
+            }
+        }
+        logger.debug("added {} device {}", queueObject.hashCode(), serialNumbers);
     }
 
-    private void executeSequenceNode(Device[] devices, JsonObject nodeToExecute) {
-        if (devices.length == 1 && groups.values().stream().anyMatch(queueObject -> queueObject.queueRunning.get())
-                || devices.length > 1
-                        && singles.values().stream().anyMatch(queueObject -> queueObject.queueRunning.get())) {
-            if (singleGroupTimer != null) {
-                singleGroupTimer.cancel(true);
-                singleGroupTimer = null;
+    private void handleExecuteSequenceNode() {
+        Lock lock = locks.computeIfAbsent(TimerType.DEVICES, k -> new ReentrantLock());
+        if (lock.tryLock()) {
+            try {
+                for (String serialNumber : devices.keySet()) {
+                    LinkedBlockingQueue<QueueObject> queueObjects = devices.get(serialNumber);
+                    if (queueObjects != null) {
+                        QueueObject queueObject = queueObjects.peek();
+                        if (queueObject != null) {
+                            Future<?> future = queueObject.future;
+                            if (future == null || future.isDone()) {
+                                boolean execute = true;
+                                String serial = "";
+                                for (Device tmpDevice : queueObject.devices) {
+                                    if (!serialNumber.equals(tmpDevice.serialNumber)) {
+                                        LinkedBlockingQueue<QueueObject> tmpQueueObjects = devices
+                                                .get(tmpDevice.serialNumber);
+                                        if (tmpQueueObjects != null) {
+                                            QueueObject tmpQueueObject = tmpQueueObjects.peek();
+                                            Future<?> tmpFuture = tmpQueueObject.future;
+                                            if (!queueObject.equals(tmpQueueObject)
+                                                    || (tmpFuture != null && !tmpFuture.isDone())) {
+                                                execute = false;
+                                                break;
+                                            }
+                                            serial = serial + tmpDevice.serialNumber + " ";
+                                        }
+                                    }
+                                }
+                                if (execute) {
+                                    queueObject.future = scheduler.submit(() -> queuedExecuteSequenceNode(queueObject));
+                                    logger.debug("thread {} device {}", queueObject.hashCode(), serial);
+                                }
+                            }
+                        }
+                    }
+                }
+            } finally {
+                lock.unlock();
             }
-            singleGroupTimer = scheduler.schedule(() -> executeSequenceNode(devices, nodeToExecute), 500,
-                    TimeUnit.MILLISECONDS);
+        }
+    }
 
+    private void queuedExecuteSequenceNode(QueueObject queueObject) {
+        JsonObject nodeToExecute = queueObject.nodeToExecute;
+        ExecutionNodeObject executionNodeObject = getExecutionNodeObject(nodeToExecute);
+        if (executionNodeObject == null) {
+            logger.debug("executionNodeObject empty, removing without execution");
+            removeObjectFromQueueAfterExecutionCompletion(queueObject);
             return;
         }
-
-        if (devices.length == 1) {
-            if (!singles.containsKey(devices[0])) {
-                singles.put(devices[0], new QueueObject());
-            }
-            singles.get(devices[0]).queue.add(nodeToExecute);
+        List<String> types = executionNodeObject.types;
+        long delay = 0;
+        if (types.contains("Alexa.DeviceControls.Volume")) {
+            delay += 2000;
+        }
+        if (types.contains("Announcement")) {
+            delay += 3000;
         } else {
-            if (!groups.containsKey(devices[0])) {
-                groups.put(devices[0], new QueueObject());
-            }
-            groups.get(devices[0]).queue.add(nodeToExecute);
+            delay += 2000;
         }
+        try {
+            JsonObject sequenceJson = new JsonObject();
+            sequenceJson.addProperty("@type", "com.amazon.alexa.behaviors.model.Sequence");
+            sequenceJson.add("startNode", nodeToExecute);
 
-        if (devices.length == 1 && singles.get(devices[0]).queueRunning.compareAndSet(false, true)) {
-            queuedExecuteSequenceNode(devices[0], true);
-        } else if (devices.length > 1 && groups.get(devices[0]).queueRunning.compareAndSet(false, true)) {
-            queuedExecuteSequenceNode(devices[0], false);
-        }
-    }
+            JsonStartRoutineRequest request = new JsonStartRoutineRequest();
+            request.sequenceJson = gson.toJson(sequenceJson);
+            String json = gson.toJson(request);
 
-    private void queuedExecuteSequenceNode(Device device, boolean single) {
-        QueueObject queueObject = single ? singles.get(device) : groups.get(device);
-        JsonObject nodeToExecute = queueObject.queue.poll();
-        if (nodeToExecute != null) {
-            ExecutionNodeObject executionNodeObject = getExecutionNodeObject(nodeToExecute);
-            List<String> types = executionNodeObject.types;
-            long delay = 0;
-            if (types.contains("Alexa.DeviceControls.Volume")) {
-                delay += 2000;
-            }
-            if (types.contains("Announcement")) {
-                delay += 3000;
-            } else {
-                delay += 2000;
+            Map<String, String> headers = new HashMap<>();
+            headers.put("Routines-Version", "1.1.218665");
+
+            String text = executionNodeObject.text;
+            if (text != null) {
+                text = text.replaceAll("<.+?>", " ").replaceAll("\\s+", " ").trim();
+                delay += text.length() * 150;
             }
-            try {
-                JsonObject sequenceJson = new JsonObject();
-                sequenceJson.addProperty("@type", "com.amazon.alexa.behaviors.model.Sequence");
-                sequenceJson.add("startNode", nodeToExecute);
 
-                JsonStartRoutineRequest request = new JsonStartRoutineRequest();
-                request.sequenceJson = gson.toJson(sequenceJson);
-                String json = gson.toJson(request);
+            makeRequest("POST", alexaServer + "/api/behaviors/preview", json, true, true, null, 3);
 
-                Map<String, String> headers = new HashMap<>();
-                headers.put("Routines-Version", "1.1.218665");
+            Thread.sleep(delay);
+        } catch (IOException | URISyntaxException | InterruptedException e) {
+            logger.warn("execute sequence node fails with unexpected error", e);
+        } finally {
+            removeObjectFromQueueAfterExecutionCompletion(queueObject);
+        }
+    }
 
-                String text = executionNodeObject.text;
-                if (text != null && !text.isEmpty()) {
-                    text = text.replaceAll("<.+?>", " ").replaceAll("\\s+", " ").trim();
-                    delay += text.length() * 150;
-                }
-
-                makeRequest("POST", alexaServer + "/api/behaviors/preview", json, true, true, null, 3);
-            } catch (IOException | URISyntaxException e) {
-                logger.warn("execute sequence node fails with unexpected error", e);
-            } finally {
-                queueObject.senderUnblockFuture = scheduler.schedule(() -> queuedExecuteSequenceNode(device, single),
-                        delay, TimeUnit.MILLISECONDS);
-            }
-        } else {
-            queueObject.dispose();
-            // NECESSARY TO CANCEL AND NULL TIMER?
-            if (!isSequenceNodeQueueRunning()) {
-                if (singleGroupTimer != null) {
-                    singleGroupTimer.cancel(true);
-                    singleGroupTimer = null;
+    private void removeObjectFromQueueAfterExecutionCompletion(QueueObject queueObject) {
+        String serial = "";
+        for (Device device : queueObject.devices) {
+            String serialNumber = device.serialNumber;
+            if (serialNumber != null) {
+                LinkedBlockingQueue<?> queue = devices.get(serialNumber);
+                if (queue != null) {
+                    queue.remove(queueObject);
                 }
+                serial = serial + serialNumber + " ";
             }
         }
+        logger.debug("removed {} device {}", queueObject.hashCode(), serial);
     }
 
-    private void executeSequenceNodes(Device[] devices, JsonArray nodesToExecute, boolean parallel)
-            throws IOException, URISyntaxException {
+    private void executeSequenceNodes(List<Device> devices, JsonArray nodesToExecute, boolean parallel) {
         JsonObject serialNode = new JsonObject();
         if (parallel) {
             serialNode.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode");
@@ -1651,31 +1684,26 @@ public class Connection {
         executeSequenceNode(devices, serialNode);
     }
 
-    private JsonObject createExecutionNode(@Nullable Device device, String command,
-            @Nullable Map<String, Object> parameters) {
+    private JsonObject createExecutionNode(@Nullable Device device, String command, Map<String, Object> parameters) {
         JsonObject operationPayload = new JsonObject();
         if (device != null) {
             operationPayload.addProperty("deviceType", device.deviceType);
             operationPayload.addProperty("deviceSerialNumber", device.serialNumber);
             operationPayload.addProperty("locale", "");
-            operationPayload.addProperty("customerId",
-                    this.accountCustomerId == null || this.accountCustomerId.isEmpty() ? device.deviceOwnerCustomerId
-                            : this.accountCustomerId);
-        }
-        if (parameters != null) {
-            for (String key : parameters.keySet()) {
-                Object value = parameters.get(key);
-                if (value instanceof String) {
-                    operationPayload.addProperty(key, (String) value);
-                } else if (value instanceof Number) {
-                    operationPayload.addProperty(key, (Number) value);
-                } else if (value instanceof Boolean) {
-                    operationPayload.addProperty(key, (Boolean) value);
-                } else if (value instanceof Character) {
-                    operationPayload.addProperty(key, (Character) value);
-                } else {
-                    operationPayload.add(key, gson.toJsonTree(value));
-                }
+            operationPayload.addProperty("customerId", getCustomerId(device.deviceOwnerCustomerId));
+        }
+        for (String key : parameters.keySet()) {
+            Object value = parameters.get(key);
+            if (value instanceof String) {
+                operationPayload.addProperty(key, (String) value);
+            } else if (value instanceof Number) {
+                operationPayload.addProperty(key, (Number) value);
+            } else if (value instanceof Boolean) {
+                operationPayload.addProperty(key, (Boolean) value);
+            } else if (value instanceof Character) {
+                operationPayload.addProperty(key, (Character) value);
+            } else {
+                operationPayload.add(key, gson.toJsonTree(value));
             }
         }
 
@@ -1700,61 +1728,14 @@ public class Connection {
                         if (parallelNodesToExecute != null && parallelNodesToExecute.size() > 0) {
                             JsonObject parallelNodesToExecuteJsonObject = parallelNodesToExecute.get(0)
                                     .getAsJsonObject();
-                            if (parallelNodesToExecuteJsonObject != null) {
-                                if (parallelNodesToExecuteJsonObject.has("type")) {
-                                    executionNodeObject.types
-                                            .add(parallelNodesToExecuteJsonObject.get("type").getAsString());
-                                    if (parallelNodesToExecuteJsonObject.has("operationPayload")) {
-                                        JsonObject operationPayload = parallelNodesToExecuteJsonObject
-                                                .getAsJsonObject("operationPayload");
-                                        if (operationPayload != null) {
-                                            if (operationPayload.has("textToSpeak")) {
-                                                executionNodeObject.text = operationPayload.get("textToSpeak")
-                                                        .getAsString();
-                                                break;
-                                            } else if (operationPayload.has("content")) {
-                                                JsonArray content = operationPayload.getAsJsonArray("content");
-                                                if (content != null && content.size() > 0) {
-                                                    JsonObject contentJsonObject = content.get(0).getAsJsonObject();
-                                                    if (contentJsonObject != null && contentJsonObject.has("speak")) {
-                                                        JsonObject speak = contentJsonObject.getAsJsonObject("speak");
-                                                        if (speak != null && speak.has("value")) {
-                                                            executionNodeObject.text = speak.get("value").getAsString();
-                                                            break;
-                                                        }
-                                                    }
-                                                }
-                                            }
-                                        }
-                                    }
-                                }
+                            if (processNodesToExecuteJsonObject(executionNodeObject,
+                                    parallelNodesToExecuteJsonObject)) {
+                                break;
                             }
                         }
                     } else {
-                        if (serialNodesToExecuteJsonObject.has("type")) {
-                            executionNodeObject.types.add(serialNodesToExecuteJsonObject.get("type").getAsString());
-                            if (serialNodesToExecuteJsonObject.has("operationPayload")) {
-                                JsonObject operationPayload = serialNodesToExecuteJsonObject
-                                        .getAsJsonObject("operationPayload");
-                                if (operationPayload != null) {
-                                    if (operationPayload.has("textToSpeak")) {
-                                        executionNodeObject.text = operationPayload.get("textToSpeak").getAsString();
-                                        break;
-                                    } else if (operationPayload.has("content")) {
-                                        JsonArray content = operationPayload.getAsJsonArray("content");
-                                        if (content != null && content.size() > 0) {
-                                            JsonObject contentJsonObject = content.get(0).getAsJsonObject();
-                                            if (contentJsonObject != null && contentJsonObject.has("speak")) {
-                                                JsonObject speak = contentJsonObject.getAsJsonObject("speak");
-                                                if (speak != null && speak.has("value")) {
-                                                    executionNodeObject.text = speak.get("value").getAsString();
-                                                    break;
-                                                }
-                                            }
-                                        }
-                                    }
-                                }
-                            }
+                        if (processNodesToExecuteJsonObject(executionNodeObject, serialNodesToExecuteJsonObject)) {
+                            break;
                         }
                     }
                 }
@@ -1764,14 +1745,44 @@ public class Connection {
         return executionNodeObject;
     }
 
-    public void startRoutine(Device device, String utterance) throws IOException, URISyntaxException {
+    private boolean processNodesToExecuteJsonObject(ExecutionNodeObject executionNodeObject,
+            JsonObject nodesToExecuteJsonObject) {
+        if (nodesToExecuteJsonObject.has("type")) {
+            executionNodeObject.types.add(nodesToExecuteJsonObject.get("type").getAsString());
+            if (nodesToExecuteJsonObject.has("operationPayload")) {
+                JsonObject operationPayload = nodesToExecuteJsonObject.getAsJsonObject("operationPayload");
+                if (operationPayload != null) {
+                    if (operationPayload.has("textToSpeak")) {
+                        executionNodeObject.text = operationPayload.get("textToSpeak").getAsString();
+                        return true;
+                    } else if (operationPayload.has("content")) {
+                        JsonArray content = operationPayload.getAsJsonArray("content");
+                        if (content != null && content.size() > 0) {
+                            JsonObject contentJsonObject = content.get(0).getAsJsonObject();
+                            if (contentJsonObject.has("speak")) {
+                                JsonObject speak = contentJsonObject.getAsJsonObject("speak");
+                                if (speak != null && speak.has("value")) {
+                                    executionNodeObject.text = speak.get("value").getAsString();
+                                    return true;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    public void startRoutine(Device device, String utterance)
+            throws IOException, URISyntaxException, InterruptedException {
         JsonAutomation found = null;
         String deviceLocale = "";
         JsonAutomation[] routines = getRoutines();
         if (routines == null) {
             return;
         }
-        for (JsonAutomation routine : getRoutines()) {
+        for (JsonAutomation routine : routines) {
             if (routine != null) {
                 Trigger[] triggers = routine.triggers;
                 if (triggers != null && routine.sequence != null) {
@@ -1783,7 +1794,8 @@ public class Connection {
                         if (payload == null) {
                             continue;
                         }
-                        if (payload.utterance != null && payload.utterance.equalsIgnoreCase(utterance)) {
+                        String payloadUtterance = payload.utterance;
+                        if (payloadUtterance != null && payloadUtterance.equalsIgnoreCase(utterance)) {
                             found = routine;
                             deviceLocale = payload.locale;
                             break;
@@ -1813,10 +1825,7 @@ public class Connection {
 
             // "customerId": "ALEXA_CUSTOMER_ID"
             String customerId = "\"customerId\":\"ALEXA_CUSTOMER_ID\"";
-            String newCustomerId = "\"customerId\":\""
-                    + (this.accountCustomerId == null || this.accountCustomerId.isEmpty() ? device.deviceOwnerCustomerId
-                            : this.accountCustomerId)
-                    + "\"";
+            String newCustomerId = "\"customerId\":\"" + getCustomerId(device.deviceOwnerCustomerId) + "\"";
             sequenceJson = sequenceJson.replace(customerId.subSequence(0, customerId.length()),
                     newCustomerId.subSequence(0, newCustomerId.length()));
 
@@ -1836,13 +1845,14 @@ public class Connection {
         }
     }
 
-    public @Nullable JsonAutomation @Nullable [] getRoutines() throws IOException, URISyntaxException {
+    public @Nullable JsonAutomation @Nullable [] getRoutines()
+            throws IOException, URISyntaxException, InterruptedException {
         String json = makeRequestAndReturnString(alexaServer + "/api/behaviors/automations?limit=2000");
         JsonAutomation[] result = parseJson(json, JsonAutomation[].class);
         return result;
     }
 
-    public JsonFeed[] getEnabledFlashBriefings() throws IOException, URISyntaxException {
+    public JsonFeed[] getEnabledFlashBriefings() throws IOException, URISyntaxException, InterruptedException {
         String json = makeRequestAndReturnString(alexaServer + "/api/content-skills/enabled-feeds");
         JsonEnabledFeeds result = parseJson(json, JsonEnabledFeeds.class);
         if (result == null) {
@@ -1855,14 +1865,16 @@ public class Connection {
         return new JsonFeed[0];
     }
 
-    public void setEnabledFlashBriefings(JsonFeed[] enabledFlashBriefing) throws IOException, URISyntaxException {
+    public void setEnabledFlashBriefings(JsonFeed[] enabledFlashBriefing)
+            throws IOException, URISyntaxException, InterruptedException {
         JsonEnabledFeeds enabled = new JsonEnabledFeeds();
         enabled.enabledFeeds = enabledFlashBriefing;
         String json = gsonWithNullSerialization.toJson(enabled);
         makeRequest("POST", alexaServer + "/api/content-skills/enabled-feeds", json, true, true, null, 0);
     }
 
-    public JsonNotificationSound[] getNotificationSounds(Device device) throws IOException, URISyntaxException {
+    public JsonNotificationSound[] getNotificationSounds(Device device)
+            throws IOException, URISyntaxException, InterruptedException {
         String json = makeRequestAndReturnString(
                 alexaServer + "/api/notification/sounds?deviceSerialNumber=" + device.serialNumber + "&deviceType="
                         + device.deviceType + "&softwareVersion=" + device.softwareVersion);
@@ -1877,7 +1889,7 @@ public class Connection {
         return new JsonNotificationSound[0];
     }
 
-    public JsonNotificationResponse[] notifications() throws IOException, URISyntaxException {
+    public JsonNotificationResponse[] notifications() throws IOException, URISyntaxException, InterruptedException {
         String response = makeRequestAndReturnString(alexaServer + "/api/notifications");
         JsonNotificationsResponse result = parseJson(response, JsonNotificationsResponse.class);
         if (result == null) {
@@ -1891,7 +1903,7 @@ public class Connection {
     }
 
     public @Nullable JsonNotificationResponse notification(Device device, String type, @Nullable String label,
-            @Nullable JsonNotificationSound sound) throws IOException, URISyntaxException {
+            @Nullable JsonNotificationSound sound) throws IOException, URISyntaxException, InterruptedException {
         Date date = new Date(new Date().getTime());
         long createdDate = date.getTime();
         Date alarm = new Date(createdDate + 5000); // add 5 seconds, because amazon does not except calls for times in
@@ -1918,12 +1930,13 @@ public class Connection {
         return result;
     }
 
-    public void stopNotification(JsonNotificationResponse notification) throws IOException, URISyntaxException {
+    public void stopNotification(JsonNotificationResponse notification)
+            throws IOException, URISyntaxException, InterruptedException {
         makeRequestAndReturnString("DELETE", alexaServer + "/api/notifications/" + notification.id, null, true, null);
     }
 
     public @Nullable JsonNotificationResponse getNotificationState(JsonNotificationResponse notification)
-            throws IOException, URISyntaxException {
+            throws IOException, URISyntaxException, InterruptedException {
         String response = makeRequestAndReturnString("GET", alexaServer + "/api/notifications/" + notification.id, null,
                 true, null);
         JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class);
@@ -1931,29 +1944,25 @@ public class Connection {
     }
 
     public List<JsonMusicProvider> getMusicProviders() {
-        String response;
         try {
             Map<String, String> headers = new HashMap<>();
             headers.put("Routines-Version", "1.1.218665");
-            response = makeRequestAndReturnString("GET",
+            String response = makeRequestAndReturnString("GET",
                     alexaServer + "/api/behaviors/entities?skillId=amzn1.ask.1p.music", null, true, headers);
-        } catch (IOException | URISyntaxException e) {
+            if (!response.isEmpty()) {
+                JsonMusicProvider[] result = parseJson(response, JsonMusicProvider[].class);
+                return Arrays.asList(result);
+            }
+        } catch (IOException | URISyntaxException | InterruptedException e) {
             logger.warn("getMusicProviders fails: {}", e.getMessage());
-            return new ArrayList<>();
         }
-        if (response == null || response.isEmpty()) {
-            return new ArrayList<>();
-        }
-        JsonMusicProvider[] result = parseJson(response, JsonMusicProvider[].class);
-        return Arrays.asList(result);
+        return List.of();
     }
 
     public void playMusicVoiceCommand(Device device, String providerId, String voiceCommand)
-            throws IOException, URISyntaxException {
+            throws IOException, URISyntaxException, InterruptedException {
         JsonPlaySearchPhraseOperationPayload payload = new JsonPlaySearchPhraseOperationPayload();
-        payload.customerId = (this.accountCustomerId == null || this.accountCustomerId.isEmpty()
-                ? device.deviceOwnerCustomerId
-                : this.accountCustomerId);
+        payload.customerId = getCustomerId(device.deviceOwnerCustomerId);
         payload.locale = "ALEXA_CURRENT_LOCALE";
         payload.musicProviderId = providerId;
         payload.searchPhrase = voiceCommand;
@@ -1970,7 +1979,7 @@ public class Connection {
         String validateResultJson = makeRequestAndReturnString("POST",
                 alexaServer + "/api/behaviors/operation/validate", postDataValidate, true, null);
 
-        if (validateResultJson != null && !validateResultJson.isEmpty()) {
+        if (!validateResultJson.isEmpty()) {
             JsonPlayValidationResult validationResult = parseJson(validateResultJson, JsonPlayValidationResult.class);
             if (validationResult != null) {
                 JsonPlaySearchPhraseOperationPayload validatedOperationPayload = validationResult.operationPayload;
@@ -2001,19 +2010,20 @@ public class Connection {
         makeRequest("POST", alexaServer + "/api/behaviors/preview", postData, true, true, null, 3);
     }
 
-    public @Nullable JsonEqualizer getEqualizer(Device device) throws IOException, URISyntaxException {
+    public @Nullable JsonEqualizer getEqualizer(Device device)
+            throws IOException, URISyntaxException, InterruptedException {
         String json = makeRequestAndReturnString(
                 alexaServer + "/api/equalizer/" + device.serialNumber + "/" + device.deviceType);
         return parseJson(json, JsonEqualizer.class);
     }
 
-    public void setEqualizer(Device device, JsonEqualizer settings) throws IOException, URISyntaxException {
+    public void setEqualizer(Device device, JsonEqualizer settings)
+            throws IOException, URISyntaxException, InterruptedException {
         String postData = gson.toJson(settings);
         makeRequest("POST", alexaServer + "/api/equalizer/" + device.serialNumber + "/" + device.deviceType, postData,
                 true, true, null, 0);
     }
 
-    @NonNullByDefault
     private static class Announcement {
         public List<Device> devices = new ArrayList<>();
         public String speak;
@@ -2029,7 +2039,6 @@ public class Connection {
         }
     }
 
-    @NonNullByDefault
     private static class TextToSpeech {
         public List<Device> devices = new ArrayList<>();
         public String text;
@@ -2041,7 +2050,6 @@ public class Connection {
         }
     }
 
-    @NonNullByDefault
     private static class Volume {
         public List<Device> devices = new ArrayList<>();
         public int volume;
@@ -2052,22 +2060,12 @@ public class Connection {
         }
     }
 
-    @NonNullByDefault
     private static class QueueObject {
-        public LinkedBlockingQueue<JsonObject> queue = new LinkedBlockingQueue<>();
-        public AtomicBoolean queueRunning = new AtomicBoolean();
-        public @Nullable ScheduledFuture<?> senderUnblockFuture;
-
-        public void dispose() {
-            queue.clear();
-            queueRunning.set(false);
-            if (senderUnblockFuture != null) {
-                senderUnblockFuture.cancel(true);
-            }
-        }
+        public @Nullable Future<?> future;
+        public List<Device> devices = List.of();
+        public JsonObject nodeToExecute = new JsonObject();
     }
 
-    @NonNullByDefault
     private static class ExecutionNodeObject {
         public List<String> types = new ArrayList<>();
         @Nullable
index ad7670b67df9298e3c5335dc9c873739cc202493..afcbc72574e4f50a1fcf5e3b74be7153169bcf64 100644 (file)
@@ -28,7 +28,6 @@ import java.util.UUID;
 import java.util.concurrent.Future;
 import java.util.concurrent.ThreadLocalRandom;
 
-import org.apache.commons.lang.StringUtils;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
@@ -79,7 +78,7 @@ public class WebSocketConnection {
         webSocketClient = new WebSocketClient(sslContextFactory);
         try {
             String host;
-            if (StringUtils.equalsIgnoreCase(amazonSite, "amazon.com")) {
+            if (amazonSite.equalsIgnoreCase("amazon.com")) {
                 host = "dp-gw-na-js." + amazonSite;
             } else {
                 host = "dp-gw-na." + amazonSite;
@@ -196,7 +195,6 @@ public class WebSocketConnection {
     }
 
     @WebSocket(maxTextMessageSize = 64 * 1024, maxBinaryMessageSize = 64 * 1024)
-    @SuppressWarnings("unused")
     public class AmazonEchoControlWebSocket {
         int msgCounter = -1;
         int messageId;
@@ -349,19 +347,17 @@ public class WebSocketConnection {
                             if (idDataElements.length == 2) {
                                 payload = idDataElements[1];
                             }
-                            if (message.content.payload == null) {
+                            if (payload == null) {
                                 payload = readString(data, idx, data.length - 4 - idx);
                             }
-                            message.content.payload = payload;
-                            if (StringUtils.isNotEmpty(payload)) {
+                            if (!payload.isEmpty()) {
                                 try {
-                                    message.content.pushCommand = gson.fromJson(message.content.payload,
-                                            JsonPushCommand.class);
+                                    message.content.pushCommand = gson.fromJson(payload, JsonPushCommand.class);
                                 } catch (JsonSyntaxException e) {
-                                    logger.info("Parsing json failed", e);
-                                    logger.info("Illegal json: {}", payload);
+                                    logger.info("Parsing json failed, illegal JSON: {}", payload, e);
                                 }
                             }
+                            message.content.payload = payload;
                         }
                     }
                 } else if (message.channel == 0x65) { // CHANNEL_FOR_HEARTBEAT
index 9118cc8f244b12795c9370f3c39e959762187976..00779b7f51736c43eed1d2ce8cd619a2ea7875f5 100644 (file)
@@ -35,7 +35,7 @@ import com.google.gson.JsonSyntaxException;
 public abstract class ChannelHandler {
 
     public abstract boolean tryHandleCommand(Device device, Connection connection, String channelId, Command command)
-            throws IOException, URISyntaxException;
+            throws IOException, URISyntaxException, InterruptedException;
 
     protected final IAmazonThingHandler thingHandler;
     protected final Gson gson;
index 09b3ab82b1635b420f09f75016f8772bbb1e61d2..03e6722575c92577f27c97f51ff2eb992755a390 100644 (file)
@@ -44,7 +44,7 @@ public class ChannelHandlerSendMessage extends ChannelHandler {
 
     @Override
     public boolean tryHandleCommand(Device device, Connection connection, String channelId, Command command)
-            throws IOException, URISyntaxException {
+            throws IOException, URISyntaxException, InterruptedException {
         if (channelId.equals(CHANNEL_NAME)) {
             if (command instanceof StringType) {
                 String commandValue = ((StringType) command).toFullString();
index a3df33c8cf3b61db7005fce81da1928ecfd983ee..da0984b24f4b3a957914c134aa794d2e32b133cb 100644 (file)
@@ -30,6 +30,7 @@ import org.openhab.binding.amazonechocontrol.internal.Connection;
 import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler;
 import org.openhab.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler;
 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities;
+import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDeviceAlias;
 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.DriverIdentity;
 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice;
 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroups.SmartHomeGroup;
@@ -145,6 +146,7 @@ public class SmartHomeDevicesDiscovery extends AbstractDiscoveryService {
 
             if (smartHomeDevice instanceof SmartHomeDevice) {
                 SmartHomeDevice shd = (SmartHomeDevice) smartHomeDevice;
+                logger.trace("Found SmartHome device: {}", shd);
 
                 String entityId = shd.entityId;
                 if (entityId == null) {
@@ -178,23 +180,24 @@ public class SmartHomeDevicesDiscovery extends AbstractDiscoveryService {
 
                 thingUID = new ThingUID(THING_TYPE_SMART_HOME_DEVICE, bridgeThingUID, entityId.replace(".", "-"));
 
+                JsonSmartHomeDeviceAlias[] aliases = shd.aliases;
                 if ("Amazon".equals(shd.manufacturerName) && driverIdentity != null
                         && "SonarCloudService".equals(driverIdentity.identifier)) {
                     deviceName = "Alexa Guard on " + shd.friendlyName;
                 } else if ("Amazon".equals(shd.manufacturerName) && driverIdentity != null
                         && "OnGuardSmartHomeBridgeService".equals(driverIdentity.identifier)) {
                     deviceName = "Alexa Guard";
-                } else if (shd.aliases != null && shd.aliases.length > 0 && shd.aliases[0] != null
-                        && shd.aliases[0].friendlyName != null) {
-                    deviceName = shd.aliases[0].friendlyName;
+                } else if (aliases != null && aliases.length > 0 && aliases[0] != null
+                        && aliases[0].friendlyName != null) {
+                    deviceName = aliases[0].friendlyName;
                 } else {
                     deviceName = shd.friendlyName;
                 }
                 props.put(DEVICE_PROPERTY_ID, id);
-            }
-
-            if (smartHomeDevice instanceof SmartHomeGroup) {
+            } else if (smartHomeDevice instanceof SmartHomeGroup) {
                 SmartHomeGroup shg = (SmartHomeGroup) smartHomeDevice;
+                logger.trace("Found SmartHome device: {}", shg);
+
                 String id = shg.findId();
                 if (id == null) {
                     // No id
index f3c2497b90a761506b6438c5f26ef0412b5b1812..09713617fc04cc4bb71757dfdd2790e59a245484 100644 (file)
@@ -104,7 +104,6 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
     private final Object synchronizeConnection = new Object();
     private Map<String, Device> jsonSerialNumberDeviceMapping = new HashMap<>();
     private Map<String, SmartHomeBaseDevice> jsonIdSmartHomeDeviceMapping = new HashMap<>();
-    private Map<String, SmartHomeDevice> jsonSerialNumberSmartHomeDeviceMapping = new HashMap<>();
 
     private @Nullable ScheduledFuture<?> checkDataJob;
     private @Nullable ScheduledFuture<?> checkLoginJob;
@@ -193,7 +192,7 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
             if (command instanceof RefreshType) {
                 refreshData();
             }
-        } catch (IOException | URISyntaxException e) {
+        } catch (IOException | URISyntaxException | InterruptedException e) {
             logger.info("handleCommand fails", e);
         }
     }
@@ -479,7 +478,7 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
         ZonedDateTime timeStamp = ZonedDateTime.now();
         try {
             notifications = currentConnection.notifications();
-        } catch (IOException | URISyntaxException e) {
+        } catch (IOException | URISyntaxException | InterruptedException e) {
             logger.debug("refreshNotifications failed", e);
             return;
         }
@@ -632,7 +631,7 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
             if (currentConnection.getIsLoggedIn()) {
                 devices = currentConnection.getDeviceList();
             }
-        } catch (IOException | URISyntaxException e) {
+        } catch (IOException | URISyntaxException | InterruptedException e) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
         }
         if (devices != null) {
@@ -655,7 +654,7 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
             String deviceWakeWord = null;
             for (WakeWord wakeWord : wakeWords) {
                 if (wakeWord != null) {
-                    if (serialNumber != null && serialNumber.equals(wakeWord.deviceSerialNumber)) {
+                    if (serialNumber.equals(wakeWord.deviceSerialNumber)) {
                         deviceWakeWord = wakeWord.wakeWord;
                         break;
                     }
@@ -676,7 +675,7 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
         if (currentConnection != null && feeds != null) {
             try {
                 currentConnection.setEnabledFlashBriefings(feeds);
-            } catch (IOException | URISyntaxException e) {
+            } catch (IOException | URISyntaxException | InterruptedException e) {
                 logger.warn("Set flashbriefing profile failed", e);
             }
         }
@@ -736,7 +735,8 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
                 forSerializer[i] = copy;
             }
             this.currentFlashBriefingJson = gson.toJson(forSerializer);
-        } catch (HttpException | JsonSyntaxException | IOException | URISyntaxException | ConnectionException e) {
+        } catch (HttpException | JsonSyntaxException | IOException | URISyntaxException | ConnectionException
+                | InterruptedException e) {
             logger.warn("get flash briefing profiles fails", e);
         }
     }
@@ -780,8 +780,8 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
                 default:
                     String payload = pushCommand.payload;
                     if (payload != null && payload.startsWith("{") && payload.endsWith("}")) {
-                        JsonCommandPayloadPushDevice devicePayload = gson.fromJson(payload,
-                                JsonCommandPayloadPushDevice.class);
+                        JsonCommandPayloadPushDevice devicePayload = Objects
+                                .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushDevice.class));
                         DopplerId dopplerId = devicePayload.dopplerId;
                         if (dopplerId != null) {
                             handlePushDeviceCommand(dopplerId, command, payload);
@@ -800,7 +800,11 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
     }
 
     private void handlePushActivity(@Nullable String payload) {
-        JsonCommandPayloadPushActivity pushActivity = gson.fromJson(payload, JsonCommandPayloadPushActivity.class);
+        if (payload == null) {
+            return;
+        }
+        JsonCommandPayloadPushActivity pushActivity = Objects
+                .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushActivity.class));
 
         Key key = pushActivity.key;
         if (key == null) {
@@ -820,8 +824,8 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
                     if (sourceDeviceIds != null) {
                         Arrays.stream(sourceDeviceIds).filter(Objects::nonNull)
                                 .map(sourceDeviceId -> findEchoHandlerBySerialNumber(sourceDeviceId.serialNumber))
-                                .filter(Objects::nonNull)
-                                .forEach(echoHandler -> echoHandler.handlePushActivity(currentActivity));
+                                .filter(Objects::nonNull).forEach(echoHandler -> Objects.requireNonNull(echoHandler)
+                                        .handlePushActivity(currentActivity));
                     }
                 });
     }
@@ -857,7 +861,7 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
             if (currentConnection.getIsLoggedIn()) {
                 smartHomeDevices = currentConnection.getSmarthomeDeviceList();
             }
-        } catch (IOException | URISyntaxException e) {
+        } catch (IOException | URISyntaxException | InterruptedException e) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
         }
         if (smartHomeDevices != null) {
@@ -878,19 +882,6 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
         smartHomeDeviceHandlers
                 .forEach(child -> child.setDeviceAndUpdateThingState(this, findSmartDeviceHomeJson(child)));
 
-        if (smartHomeDevices != null) {
-            Map<String, SmartHomeDevice> newJsonSerialNumberSmartHomeDeviceMapping = new HashMap<>();
-            for (Object smartDevice : smartHomeDevices) {
-                if (smartDevice instanceof SmartHomeDevice) {
-                    SmartHomeDevice shd = (SmartHomeDevice) smartDevice;
-                    String entityId = shd.entityId;
-                    if (entityId != null) {
-                        newJsonSerialNumberSmartHomeDeviceMapping.put(entityId, shd);
-                    }
-                }
-            }
-            jsonSerialNumberSmartHomeDeviceMapping = newJsonSerialNumberSmartHomeDeviceMapping;
-        }
         if (smartHomeDevices != null) {
             return smartHomeDevices;
         }
@@ -932,7 +923,7 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
 
     private synchronized void updateSmartHomeState(@Nullable String deviceFilterId) {
         try {
-            logger.debug("updateSmartHomeState started");
+            logger.debug("updateSmartHomeState started with deviceFilterId={}", deviceFilterId);
             Connection connection = this.connection;
             if (connection == null || !connection.getIsLoggedIn()) {
                 return;
@@ -976,8 +967,10 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma
                     logger.debug("Device update {} suspended", id);
                     continue;
                 }
-                if (id.equals(deviceFilterId)) {
+                if (deviceFilterId == null || id.equals(deviceFilterId)) {
                     smartHomeDeviceHandler.updateChannelStates(allDevices, applianceIdToCapabilityStates);
+                } else {
+                    logger.trace("Id {} not matching filter {}", id, deviceFilterId);
                 }
             }
 
index fb52230170247a74d49cec0b0ef0b9770aca8ffa..25c74419cfabeb16c0685d494366b1ee250540ad 100644 (file)
@@ -20,17 +20,12 @@ import java.time.Instant;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Objects;
-import java.util.Set;
+import java.util.*;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import org.apache.commons.lang.StringUtils;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.amazonechocontrol.internal.Connection;
@@ -96,7 +91,6 @@ import com.google.gson.Gson;
  */
 @NonNullByDefault
 public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
-
     private final Logger logger = LoggerFactory.getLogger(EchoHandler.class);
     private Gson gson;
     private @Nullable Device device;
@@ -376,7 +370,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                     }
                 }
                 if (volume != null) {
-                    if (StringUtils.equals(device.deviceFamily, "WHA")) {
+                    if ("WHA".equals(device.deviceFamily)) {
                         connection.command(device, "{\"type\":\"VolumeLevelCommand\",\"volumeLevel\":" + volume
                                 + ",\"contentFocusClientId\":\"Default\"}");
                     } else {
@@ -409,8 +403,8 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             if (channelId.equals(CHANNEL_MUSIC_PROVIDER_ID)) {
                 if (command instanceof StringType) {
                     waitForUpdate = 0;
-                    String musicProviderId = ((StringType) command).toFullString();
-                    if (!StringUtils.equals(musicProviderId, this.musicProviderId)) {
+                    String musicProviderId = command.toFullString();
+                    if (!musicProviderId.equals(this.musicProviderId)) {
                         this.musicProviderId = musicProviderId;
                         if (this.isPlaying) {
                             connection.playMusicVoiceCommand(device, this.musicProviderId, "!");
@@ -421,7 +415,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             }
             if (channelId.equals(CHANNEL_PLAY_MUSIC_VOICE_COMMAND)) {
                 if (command instanceof StringType) {
-                    String voiceCommand = ((StringType) command).toFullString();
+                    String voiceCommand = command.toFullString();
                     if (!this.musicProviderId.isEmpty()) {
                         connection.playMusicVoiceCommand(device, this.musicProviderId, voiceCommand);
                         waitForUpdate = 3000;
@@ -447,21 +441,22 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                     waitForUpdate = 4000;
                     String bluetoothId = lastKnownBluetoothMAC;
                     BluetoothState state = bluetoothState;
-                    if (state != null && (StringUtils.isEmpty(bluetoothId))) {
+                    if (state != null && (bluetoothId == null || bluetoothId.isEmpty())) {
                         PairedDevice[] pairedDeviceList = state.pairedDeviceList;
                         if (pairedDeviceList != null) {
                             for (PairedDevice paired : pairedDeviceList) {
                                 if (paired == null) {
                                     continue;
                                 }
-                                if (StringUtils.isNotEmpty(paired.address)) {
-                                    lastKnownBluetoothMAC = paired.address;
+                                String pairedAddress = paired.address;
+                                if (pairedAddress != null && !pairedAddress.isEmpty()) {
+                                    lastKnownBluetoothMAC = pairedAddress;
                                     break;
                                 }
                             }
                         }
                     }
-                    if (StringUtils.isNotEmpty(lastKnownBluetoothMAC)) {
+                    if (lastKnownBluetoothMAC != null && !lastKnownBluetoothMAC.isEmpty()) {
                         connection.bluetooth(device, lastKnownBluetoothMAC);
                     }
                 } else if (command == OnOffType.OFF) {
@@ -474,8 +469,8 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             // amazon music commands
             if (channelId.equals(CHANNEL_AMAZON_MUSIC_TRACK_ID)) {
                 if (command instanceof StringType) {
-                    String trackId = ((StringType) command).toFullString();
-                    if (StringUtils.isNotEmpty(trackId)) {
+                    String trackId = command.toFullString();
+                    if (!trackId.isEmpty()) {
                         waitForUpdate = 3000;
                     }
                     connection.playAmazonMusicTrack(device, trackId);
@@ -483,8 +478,8 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             }
             if (channelId.equals(CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID)) {
                 if (command instanceof StringType) {
-                    String playListId = ((StringType) command).toFullString();
-                    if (StringUtils.isNotEmpty(playListId)) {
+                    String playListId = command.toFullString();
+                    if (!playListId.isEmpty()) {
                         waitForUpdate = 3000;
                     }
                     connection.playAmazonMusicPlayList(device, playListId);
@@ -493,7 +488,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             if (channelId.equals(CHANNEL_AMAZON_MUSIC)) {
                 if (command == OnOffType.ON) {
                     String lastKnownAmazonMusicId = this.lastKnownAmazonMusicId;
-                    if (StringUtils.isNotEmpty(lastKnownAmazonMusicId)) {
+                    if (lastKnownAmazonMusicId != null && !lastKnownAmazonMusicId.isEmpty()) {
                         waitForUpdate = 3000;
                     }
                     connection.playAmazonMusicTrack(device, lastKnownAmazonMusicId);
@@ -505,8 +500,8 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             // radio commands
             if (channelId.equals(CHANNEL_RADIO_STATION_ID)) {
                 if (command instanceof StringType) {
-                    String stationId = ((StringType) command).toFullString();
-                    if (StringUtils.isNotEmpty(stationId)) {
+                    String stationId = command.toFullString();
+                    if (!stationId.isEmpty()) {
                         waitForUpdate = 3000;
                     }
                     connection.playRadio(device, stationId);
@@ -515,7 +510,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             if (channelId.equals(CHANNEL_RADIO)) {
                 if (command == OnOffType.ON) {
                     String lastKnownRadioStationId = this.lastKnownRadioStationId;
-                    if (StringUtils.isNotEmpty(lastKnownRadioStationId)) {
+                    if (lastKnownRadioStationId != null && !lastKnownRadioStationId.isEmpty()) {
                         waitForUpdate = 3000;
                     }
                     connection.playRadio(device, lastKnownRadioStationId);
@@ -528,8 +523,8 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             if (channelId.equals(CHANNEL_REMIND)) {
                 if (command instanceof StringType) {
                     stopCurrentNotification();
-                    String reminder = ((StringType) command).toFullString();
-                    if (StringUtils.isNotEmpty(reminder)) {
+                    String reminder = command.toFullString();
+                    if (!reminder.isEmpty()) {
                         waitForUpdate = 3000;
                         updateRemind = true;
                         currentNotification = connection.notification(device, "Reminder", reminder, null);
@@ -542,8 +537,8 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             if (channelId.equals(CHANNEL_PLAY_ALARM_SOUND)) {
                 if (command instanceof StringType) {
                     stopCurrentNotification();
-                    String alarmSound = ((StringType) command).toFullString();
-                    if (StringUtils.isNotEmpty(alarmSound)) {
+                    String alarmSound = command.toFullString();
+                    if (!alarmSound.isEmpty()) {
                         waitForUpdate = 3000;
                         updateAlarm = true;
                         String[] parts = alarmSound.split(":", 2);
@@ -566,8 +561,8 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             // routine commands
             if (channelId.equals(CHANNEL_TEXT_TO_SPEECH)) {
                 if (command instanceof StringType) {
-                    String text = ((StringType) command).toFullString();
-                    if (StringUtils.isNotEmpty(text)) {
+                    String text = command.toFullString();
+                    if (!text.isEmpty()) {
                         waitForUpdate = 1000;
                         updateTextToSpeech = true;
                         startTextToSpeech(connection, device, text);
@@ -595,8 +590,8 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             }
             if (channelId.equals(CHANNEL_LAST_VOICE_COMMAND)) {
                 if (command instanceof StringType) {
-                    String text = ((StringType) command).toFullString();
-                    if (StringUtils.isNotEmpty(text)) {
+                    String text = command.toFullString();
+                    if (!text.isEmpty()) {
                         waitForUpdate = -1;
                         startTextToSpeech(connection, device, text);
                     }
@@ -604,18 +599,18 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             }
             if (channelId.equals(CHANNEL_START_COMMAND)) {
                 if (command instanceof StringType) {
-                    String commandText = ((StringType) command).toFullString();
-                    if (StringUtils.isNotEmpty(commandText)) {
+                    String commandText = command.toFullString();
+                    if (!commandText.isEmpty()) {
                         updateStartCommand = true;
                         if (commandText.startsWith(FLASH_BRIEFING_COMMAND_PREFIX)) {
                             // Handle custom flashbriefings commands
-                            String flashbriefing = commandText.substring(FLASH_BRIEFING_COMMAND_PREFIX.length());
-
-                            for (FlashBriefingProfileHandler flashBriefing : account
+                            String flashBriefingId = commandText.substring(FLASH_BRIEFING_COMMAND_PREFIX.length());
+                            for (FlashBriefingProfileHandler flashBriefingHandler : account
                                     .getFlashBriefingProfileHandlers()) {
-                                ThingUID flashBriefingId = flashBriefing.getThing().getUID();
-                                if (StringUtils.equals(flashBriefing.getThing().getUID().getId(), flashbriefing)) {
-                                    flashBriefing.handleCommand(new ChannelUID(flashBriefingId, CHANNEL_PLAY_ON_DEVICE),
+                                ThingUID flashBriefingUid = flashBriefingHandler.getThing().getUID();
+                                if (flashBriefingId.equals(flashBriefingHandler.getThing().getUID().getId())) {
+                                    flashBriefingHandler.handleCommand(
+                                            new ChannelUID(flashBriefingUid, CHANNEL_PLAY_ON_DEVICE),
                                             new StringType(device.serialNumber));
                                     break;
                                 }
@@ -626,15 +621,15 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                                 commandText = "Alexa." + commandText + ".Play";
                             }
                             waitForUpdate = 1000;
-                            connection.executeSequenceCommand(device, commandText, null);
+                            connection.executeSequenceCommand(device, commandText, Map.of());
                         }
                     }
                 }
             }
             if (channelId.equals(CHANNEL_START_ROUTINE)) {
                 if (command instanceof StringType) {
-                    String utterance = ((StringType) command).toFullString();
-                    if (StringUtils.isNotEmpty(utterance)) {
+                    String utterance = command.toFullString();
+                    if (!utterance.isEmpty()) {
                         waitForUpdate = 1000;
                         updateRoutine = true;
                         connection.startRoutine(device, utterance);
@@ -669,7 +664,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             } else {
                 this.updateStateJob = scheduler.schedule(doRefresh, waitForUpdate, TimeUnit.MILLISECONDS);
             }
-        } catch (IOException | URISyntaxException e) {
+        } catch (IOException | URISyntaxException | InterruptedException e) {
             logger.info("handleCommand fails", e);
         }
     }
@@ -699,7 +694,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                 try {
                     connection.setEqualizer(device, newEqualizerSetting);
                     return true;
-                } catch (HttpException | IOException | ConnectionException e) {
+                } catch (HttpException | IOException | ConnectionException | InterruptedException e) {
                     logger.debug("Update equalizer failed", e);
                     this.lastKnownEqualizer = null;
                 }
@@ -746,7 +741,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             if (currentConnection != null) {
                 try {
                     currentConnection.stopNotification(currentNotification);
-                } catch (IOException | URISyntaxException e) {
+                } catch (IOException | URISyntaxException | InterruptedException e) {
                     logger.warn("Stop notification failed", e);
                 }
             }
@@ -766,7 +761,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                     }
                 }
             }
-        } catch (IOException | URISyntaxException e) {
+        } catch (IOException | URISyntaxException | InterruptedException e) {
             logger.warn("update notification state fails", e);
         }
         if (stopCurrentNotification) {
@@ -855,20 +850,19 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                             if (musicProviderId != null) {
                                 musicProviderId = musicProviderId.toUpperCase();
 
-                                if (StringUtils.equals(musicProviderId, "AMAZON MUSIC")) {
+                                if (musicProviderId.equals("AMAZON MUSIC")) {
                                     musicProviderId = "AMAZON_MUSIC";
                                 }
-                                if (StringUtils.equals(musicProviderId, "CLOUD_PLAYER")) {
+                                if (musicProviderId.equals("CLOUD_PLAYER")) {
                                     musicProviderId = "AMAZON_MUSIC";
                                 }
-                                if (StringUtils.startsWith(musicProviderId, "TUNEIN")) {
+                                if (musicProviderId.startsWith("TUNEIN")) {
                                     musicProviderId = "TUNEIN";
                                 }
-                                if (StringUtils.startsWithIgnoreCase(musicProviderId, "iHeartRadio")) {
+                                if (musicProviderId.startsWith("IHEARTRADIO")) {
                                     musicProviderId = "I_HEART_RADIO";
                                 }
-                                if (StringUtils.containsIgnoreCase(musicProviderId, "Apple")
-                                        && StringUtils.containsIgnoreCase(musicProviderId, "Music")) {
+                                if (musicProviderId.equals("APPLE") && musicProviderId.contains("MUSIC")) {
                                     musicProviderId = "APPLE_MUSIC";
                                 }
                             }
@@ -880,13 +874,13 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                 if (e.getCode() != 400) {
                     logger.info("getPlayer fails", e);
                 }
-            } catch (IOException | URISyntaxException e) {
+            } catch (IOException | URISyntaxException | InterruptedException e) {
                 logger.info("getPlayer fails", e);
             }
             // check playing
-            isPlaying = (playerInfo != null && StringUtils.equals(playerInfo.state, "PLAYING"));
+            isPlaying = (playerInfo != null && "PLAYING".equals(playerInfo.state));
 
-            isPaused = (playerInfo != null && StringUtils.equals(playerInfo.state, "PAUSED"));
+            isPaused = (playerInfo != null && "PAUSED".equals(playerInfo.state));
             synchronized (progressLock) {
                 Boolean showTime = null;
                 Long mediaLength = null;
@@ -919,8 +913,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
 
             JsonMediaState mediaState = null;
             try {
-                if (StringUtils.equalsIgnoreCase(musicProviderId, "AMAZON_MUSIC")
-                        || StringUtils.equalsIgnoreCase(musicProviderId, "TUNEIN")) {
+                if ("AMAZON_MUSIC".equalsIgnoreCase(musicProviderId) || "TUNEIN".equalsIgnoreCase(musicProviderId)) {
                     mediaState = connection.getMediaState(device);
                 }
             } catch (HttpException e) {
@@ -929,7 +922,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                 } else {
                     logger.info("getMediaState fails", e);
                 }
-            } catch (IOException | URISyntaxException e) {
+            } catch (IOException | URISyntaxException | InterruptedException e) {
                 logger.info("getMediaState fails", e);
             }
 
@@ -944,11 +937,14 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             String amazonMusicTrackId = "";
             String amazonMusicPlayListId = "";
             boolean amazonMusic = false;
-            if (mediaState != null && isPlaying && StringUtils.equals(mediaState.providerId, "CLOUD_PLAYER")
-                    && StringUtils.isNotEmpty(mediaState.contentId)) {
-                amazonMusicTrackId = mediaState.contentId;
-                lastKnownAmazonMusicId = amazonMusicTrackId;
-                amazonMusic = true;
+            if (mediaState != null) {
+                String contentId = mediaState.contentId;
+                if (isPlaying && "CLOUD_PLAYER".equals(mediaState.providerId) && contentId != null
+                        && !contentId.isEmpty()) {
+                    amazonMusicTrackId = contentId;
+                    lastKnownAmazonMusicId = amazonMusicTrackId;
+                    amazonMusic = true;
+                }
             }
 
             // handle bluetooth
@@ -963,34 +959,37 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                         if (paired == null) {
                             continue;
                         }
-                        if (paired.connected && paired.address != null) {
+                        String pairedAddress = paired.address;
+                        if (paired.connected && pairedAddress != null) {
                             bluetoothIsConnected = true;
-                            bluetoothMAC = paired.address;
+                            bluetoothMAC = pairedAddress;
                             bluetoothDeviceName = paired.friendlyName;
-                            if (StringUtils.isEmpty(bluetoothDeviceName)) {
-                                bluetoothDeviceName = paired.address;
+                            if (bluetoothDeviceName == null || bluetoothDeviceName.isEmpty()) {
+                                bluetoothDeviceName = pairedAddress;
                             }
                             break;
                         }
                     }
                 }
             }
-            if (StringUtils.isNotEmpty(bluetoothMAC)) {
+            if (!bluetoothMAC.isEmpty()) {
                 lastKnownBluetoothMAC = bluetoothMAC;
             }
 
             // handle radio
             boolean isRadio = false;
-            if (mediaState != null && StringUtils.isNotEmpty(mediaState.radioStationId)) {
-                lastKnownRadioStationId = mediaState.radioStationId;
-                if (StringUtils.equalsIgnoreCase(musicProviderId, "TUNEIN")) {
-                    isRadio = true;
-                }
-            }
             String radioStationId = "";
-            if (isRadio && mediaState != null && StringUtils.equals(mediaState.currentState, "PLAYING")
-                    && mediaState.radioStationId != null) {
-                radioStationId = mediaState.radioStationId;
+            if (mediaState != null) {
+                radioStationId = Objects.requireNonNullElse(mediaState.radioStationId, "");
+                if (!radioStationId.isEmpty()) {
+                    lastKnownRadioStationId = radioStationId;
+                    if ("TUNEIN".equalsIgnoreCase(musicProviderId)) {
+                        isRadio = true;
+                        if (!"PLAYING".equals(mediaState.currentState)) {
+                            radioStationId = "";
+                        }
+                    }
+                }
             }
 
             // handle title, subtitle, imageUrl
@@ -1021,13 +1020,13 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                     QueueEntry entry = queueEntries[0];
                     if (entry != null) {
                         if (isRadio) {
-                            if (StringUtils.isEmpty(imageUrl) && entry.imageURL != null) {
+                            if ((imageUrl == null || imageUrl.isEmpty()) && entry.imageURL != null) {
                                 imageUrl = entry.imageURL;
                             }
-                            if (StringUtils.isEmpty(subTitle1) && entry.radioStationSlogan != null) {
+                            if ((subTitle1 == null || subTitle1.isEmpty()) && entry.radioStationSlogan != null) {
                                 subTitle1 = entry.radioStationSlogan;
                             }
-                            if (StringUtils.isEmpty(subTitle2) && entry.radioStationLocation != null) {
+                            if ((subTitle2 == null || subTitle2.isEmpty()) && entry.radioStationLocation != null) {
                                 subTitle2 = entry.radioStationLocation;
                             }
                         }
@@ -1039,9 +1038,10 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             String providerDisplayName = "";
             if (provider != null) {
                 if (provider.providerDisplayName != null) {
-                    providerDisplayName = provider.providerDisplayName;
+                    providerDisplayName = Objects.requireNonNullElse(provider.providerDisplayName, providerDisplayName);
                 }
-                if (StringUtils.isNotEmpty(provider.providerName) && StringUtils.isEmpty(providerDisplayName)) {
+                String providerName = provider.providerName;
+                if (providerName != null && !providerName.isEmpty() && providerDisplayName.isEmpty()) {
                     providerDisplayName = provider.providerName;
                 }
             }
@@ -1153,7 +1153,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                 treble = equalizer.treble;
             }
             this.lastKnownEqualizer = equalizer;
-        } catch (IOException | URISyntaxException | HttpException | ConnectionException e) {
+        } catch (IOException | URISyntaxException | HttpException | ConnectionException | InterruptedException e) {
             logger.debug("Get equalizer failes", e);
             return;
         }
@@ -1204,20 +1204,22 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
             return;
         }
         Description description = pushActivity.parseDescription();
-        if (StringUtils.isEmpty(description.firstUtteranceId)
-                || StringUtils.startsWithIgnoreCase(description.firstUtteranceId, "TextClient:")) {
+        String firstUtteranceId = description.firstUtteranceId;
+        if (firstUtteranceId == null || firstUtteranceId.isEmpty()
+                || firstUtteranceId.toLowerCase().startsWith("textclient:")) {
             return;
         }
-        if (StringUtils.isEmpty(description.firstStreamId)) {
+        String firstStreamId = description.firstStreamId;
+        if (firstStreamId == null || firstStreamId.isEmpty()) {
             return;
         }
         String spokenText = description.summary;
-        if (spokenText != null && StringUtils.isNotEmpty(spokenText)) {
+        if (spokenText != null && !spokenText.isEmpty()) {
             // remove wake word
             String wakeWordPrefix = this.wakeWord;
             if (wakeWordPrefix != null) {
                 wakeWordPrefix += " ";
-                if (StringUtils.startsWithIgnoreCase(spokenText, wakeWordPrefix)) {
+                if (spokenText.toLowerCase().startsWith(wakeWordPrefix.toLowerCase())) {
                     spokenText = spokenText.substring(wakeWordPrefix.length());
                 }
             }
@@ -1234,12 +1236,10 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
         this.logger.debug("Handle push command {}", command);
         switch (command) {
             case "PUSH_VOLUME_CHANGE":
-                JsonCommandPayloadPushVolumeChange volumeChange = gson.fromJson(payload,
-                        JsonCommandPayloadPushVolumeChange.class);
+                JsonCommandPayloadPushVolumeChange volumeChange = Objects
+                        .requireNonNull(gson.fromJson(payload, JsonCommandPayloadPushVolumeChange.class));
                 Connection connection = this.findConnection();
-                @Nullable
                 Integer volumeSetting = volumeChange.volumeSetting;
-                @Nullable
                 Boolean muted = volumeChange.isMuted;
                 if (muted != null && muted) {
                     updateState(CHANNEL_VOLUME, new PercentType(0));
@@ -1274,14 +1274,15 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
         ZonedDateTime nextMusicAlarm = null;
         ZonedDateTime nextTimer = null;
         for (JsonNotificationResponse notification : notifications) {
-            if (StringUtils.equals(notification.deviceSerialNumber, device.serialNumber)) {
+            if (Objects.equals(notification.deviceSerialNumber, device.serialNumber)) {
                 // notification for this device
-                if (StringUtils.equals(notification.status, "ON")) {
+                if ("ON".equals(notification.status)) {
                     if ("Reminder".equals(notification.type)) {
                         String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
                         ZonedDateTime alarmTime = ZonedDateTime
                                 .parse(notification.originalDate + "T" + notification.originalTime + offset);
-                        if (StringUtils.isNotBlank(notification.recurringPattern) && alarmTime.isBefore(now)) {
+                        String recurringPattern = notification.recurringPattern;
+                        if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
                             continue; // Ignore recurring entry if alarm time is before now
                         }
                         if (nextReminder == null || alarmTime.isBefore(nextReminder)) {
@@ -1297,7 +1298,8 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                         String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
                         ZonedDateTime alarmTime = ZonedDateTime
                                 .parse(notification.originalDate + "T" + notification.originalTime + offset);
-                        if (StringUtils.isNotBlank(notification.recurringPattern) && alarmTime.isBefore(now)) {
+                        String recurringPattern = notification.recurringPattern;
+                        if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
                             continue; // Ignore recurring entry if alarm time is before now
                         }
                         if (nextAlarm == null || alarmTime.isBefore(nextAlarm)) {
@@ -1307,7 +1309,8 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler {
                         String offset = ZoneId.systemDefault().getRules().getOffset(Instant.now()).toString();
                         ZonedDateTime alarmTime = ZonedDateTime
                                 .parse(notification.originalDate + "T" + notification.originalTime + offset);
-                        if (StringUtils.isNotBlank(notification.recurringPattern) && alarmTime.isBefore(now)) {
+                        String recurringPattern = notification.recurringPattern;
+                        if (recurringPattern != null && !recurringPattern.isBlank() && alarmTime.isBefore(now)) {
                             continue; // Ignore recurring entry if alarm time is before now
                         }
                         if (nextMusicAlarm == null || alarmTime.isBefore(nextMusicAlarm)) {
index cbec9fcb9b06df4b4b6f646732e6c6cb22dd7046..bb948440532dee09276aaaae3711a3e1cd29f9bd 100644 (file)
@@ -14,12 +14,10 @@ package org.openhab.binding.amazonechocontrol.internal.handler;
 
 import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*;
 
-import java.io.IOException;
-import java.net.URISyntaxException;
+import java.util.Map;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
-import org.apache.commons.lang.StringUtils;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.amazonechocontrol.internal.Connection;
@@ -104,59 +102,54 @@ public class FlashBriefingProfileHandler extends BaseThingHandler {
         if (updateStateJob != null) {
             updateStateJob.cancel(false);
         }
-        try {
-            String channelId = channelUID.getId();
-            if (command instanceof RefreshType) {
-                waitForUpdate = 0;
+        String channelId = channelUID.getId();
+        if (command instanceof RefreshType) {
+            waitForUpdate = 0;
+        }
+        if (channelId.equals(CHANNEL_SAVE)) {
+            if (command.equals(OnOffType.ON)) {
+                saveCurrentProfile(accountHandler);
+                waitForUpdate = 500;
             }
-            if (channelId.equals(CHANNEL_SAVE)) {
-                if (command.equals(OnOffType.ON)) {
-                    saveCurrentProfile(accountHandler);
+        }
+        if (channelId.equals(CHANNEL_ACTIVE)) {
+            if (command.equals(OnOffType.ON)) {
+                String currentConfigurationJson = this.currentConfigurationJson;
+                if (!currentConfigurationJson.isEmpty()) {
+                    accountHandler.setEnabledFlashBriefingsJson(currentConfigurationJson);
+                    updateState(CHANNEL_ACTIVE, OnOffType.ON);
                     waitForUpdate = 500;
                 }
             }
-            if (channelId.equals(CHANNEL_ACTIVE)) {
-                if (command.equals(OnOffType.ON)) {
-                    String currentConfigurationJson = this.currentConfigurationJson;
-                    if (!currentConfigurationJson.isEmpty()) {
-                        accountHandler.setEnabledFlashBriefingsJson(currentConfigurationJson);
-                        updateState(CHANNEL_ACTIVE, OnOffType.ON);
-                        waitForUpdate = 500;
-                    }
-                }
-            }
-            if (channelId.equals(CHANNEL_PLAY_ON_DEVICE)) {
-                if (command instanceof StringType) {
-                    String deviceSerialOrName = ((StringType) command).toFullString();
-                    String currentConfigurationJson = this.currentConfigurationJson;
-                    if (!currentConfigurationJson.isEmpty()) {
-                        String old = accountHandler.getEnabledFlashBriefingsJson();
-                        accountHandler.setEnabledFlashBriefingsJson(currentConfigurationJson);
-                        Device device = accountHandler.findDeviceJsonBySerialOrName(deviceSerialOrName);
-                        if (device == null) {
-                            logger.warn("Device '{}' not found", deviceSerialOrName);
+        }
+        if (channelId.equals(CHANNEL_PLAY_ON_DEVICE)) {
+            if (command instanceof StringType) {
+                String deviceSerialOrName = command.toFullString();
+                String currentConfigurationJson = this.currentConfigurationJson;
+                if (!currentConfigurationJson.isEmpty()) {
+                    String old = accountHandler.getEnabledFlashBriefingsJson();
+                    accountHandler.setEnabledFlashBriefingsJson(currentConfigurationJson);
+                    Device device = accountHandler.findDeviceJsonBySerialOrName(deviceSerialOrName);
+                    if (device == null) {
+                        logger.warn("Device '{}' not found", deviceSerialOrName);
+                    } else {
+                        @Nullable
+                        Connection connection = accountHandler.findConnection();
+                        if (connection == null) {
+                            logger.warn("Connection for '{}' not found", accountHandler.getThing().getUID().getId());
                         } else {
-                            @Nullable
-                            Connection connection = accountHandler.findConnection();
-                            if (connection == null) {
-                                logger.warn("Connection for '{}' not found",
-                                        accountHandler.getThing().getUID().getId());
-                            } else {
-                                connection.executeSequenceCommand(device, "Alexa.FlashBriefing.Play", null);
-
-                                scheduler.schedule(() -> accountHandler.setEnabledFlashBriefingsJson(old), 1000,
-                                        TimeUnit.MILLISECONDS);
-
-                                updateState(CHANNEL_ACTIVE, OnOffType.ON);
-                            }
+                            connection.executeSequenceCommand(device, "Alexa.FlashBriefing.Play", Map.of());
+
+                            scheduler.schedule(() -> accountHandler.setEnabledFlashBriefingsJson(old), 1000,
+                                    TimeUnit.MILLISECONDS);
+
+                            updateState(CHANNEL_ACTIVE, OnOffType.ON);
                         }
-                        updatePlayOnDevice = true;
-                        waitForUpdate = 1000;
                     }
+                    updatePlayOnDevice = true;
+                    waitForUpdate = 1000;
                 }
             }
-        } catch (IOException | URISyntaxException e) {
-            logger.warn("Handle command failed", e);
         }
         if (waitForUpdate >= 0) {
             this.updateStateJob = scheduler.schedule(() -> accountHandler.updateFlashBriefingHandlers(), waitForUpdate,
@@ -189,7 +182,7 @@ public class FlashBriefingProfileHandler extends BaseThingHandler {
         } else {
             updateState(CHANNEL_ACTIVE, OnOffType.OFF);
         }
-        return StringUtils.equals(this.currentConfigurationJson, currentConfigurationJson);
+        return this.currentConfigurationJson.equals(currentConfigurationJson);
     }
 
     private String saveCurrentProfile(AccountHandler connection) {
index 967e6696c7a3f3f87628d34642cffeabf2f70c11..de0fdb5b4977c97b021dce7f3ceb7d2ff8a05959 100644 (file)
@@ -15,16 +15,7 @@ package org.openhab.binding.amazonechocontrol.internal.handler;
 import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.DEVICE_PROPERTY_ID;
 import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.SUPPORTED_INTERFACES;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.function.Supplier;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -32,7 +23,10 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.amazonechocontrol.internal.Connection;
 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability;
 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice;
+import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroupIdentifiers;
+import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroupIdentity;
 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroups.SmartHomeGroup;
+import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeTags;
 import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice;
 import org.openhab.binding.amazonechocontrol.internal.smarthome.Constants;
 import org.openhab.binding.amazonechocontrol.internal.smarthome.HandlerBase;
@@ -56,10 +50,7 @@ import org.openhab.core.types.StateDescription;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.gson.Gson;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
+import com.google.gson.*;
 
 /**
  * @author Lukas Knoeller - Initial contribution
@@ -176,6 +167,7 @@ public class SmartHomeDeviceHandler extends BaseThingHandler {
 
     public void updateChannelStates(List<SmartHomeBaseDevice> allDevices,
             Map<String, JsonArray> applianceIdToCapabilityStates) {
+        logger.trace("Updating {} with {}", allDevices, applianceIdToCapabilityStates);
         AccountHandler accountHandler = getAccountHandler();
         SmartHomeBaseDevice smartHomeBaseDevice = this.smartHomeBaseDevice;
         if (smartHomeBaseDevice == null) {
@@ -187,11 +179,11 @@ public class SmartHomeDeviceHandler extends BaseThingHandler {
         Map<String, List<JsonObject>> mapInterfaceToStates = new HashMap<>();
         SmartHomeDevice firstDevice = null;
         for (SmartHomeDevice shd : getSupportedSmartHomeDevices(smartHomeBaseDevice, allDevices)) {
-            JsonArray states = applianceIdToCapabilityStates.get(shd.applianceId);
             String applianceId = shd.applianceId;
             if (applianceId == null) {
                 continue;
             }
+            JsonArray states = applianceIdToCapabilityStates.get(applianceId);
             if (states != null) {
                 stateFound = true;
                 if (smartHomeBaseDevice.isGroup()) {
@@ -210,26 +202,28 @@ public class SmartHomeDeviceHandler extends BaseThingHandler {
             for (JsonElement stateElement : states) {
                 String stateJson = stateElement.getAsString();
                 if (stateJson.startsWith("{") && stateJson.endsWith("}")) {
-                    JsonObject state = gson.fromJson(stateJson, JsonObject.class);
-                    String interfaceName = state.get("namespace").getAsString();
-                    mapInterfaceToStates.computeIfAbsent(interfaceName, k -> new ArrayList<>()).add(state);
+                    JsonObject state = Objects.requireNonNull(gson.fromJson(stateJson, JsonObject.class));
+                    String interfaceName = Objects.requireNonNullElse(state.get("namespace"), JsonNull.INSTANCE)
+                            .getAsString();
+                    Objects.requireNonNull(mapInterfaceToStates.computeIfAbsent(interfaceName, k -> new ArrayList<>()))
+                            .add(state);
                 }
             }
         }
+
         for (HandlerBase handlerBase : handlers.values()) {
-            if (handlerBase == null) {
-                continue;
-            }
             UpdateChannelResult result = new UpdateChannelResult();
-
             for (String interfaceName : handlerBase.getSupportedInterface()) {
-                List<JsonObject> stateList = mapInterfaceToStates.getOrDefault(interfaceName, Collections.emptyList());
-                try {
-                    handlerBase.updateChannels(interfaceName, stateList, result);
-                } catch (Exception e) {
-                    // We catch all exceptions, otherwise all other things are not updated!
-                    logger.debug("Updating states failed", e);
-                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
+                List<JsonObject> stateList = mapInterfaceToStates.get(interfaceName);
+                if (stateList != null) {
+                    try {
+                        handlerBase.updateChannels(interfaceName, stateList, result);
+                    } catch (Exception e) {
+                        // We catch all exceptions, otherwise all other things are not updated!
+                        logger.debug("Updating states failed", e);
+                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                                e.getLocalizedMessage());
+                    }
                 }
             }
 
@@ -325,7 +319,8 @@ public class SmartHomeDeviceHandler extends BaseThingHandler {
             for (SmartHomeCapability capability : capabilities) {
                 String interfaceName = capability.interfaceName;
                 if (interfaceName != null) {
-                    result.computeIfAbsent(interfaceName, name -> new ArrayList<>()).add(capability);
+                    Objects.requireNonNull(result.computeIfAbsent(interfaceName, name -> new ArrayList<>()))
+                            .add(capability);
                 }
             }
         }
@@ -357,16 +352,20 @@ public class SmartHomeDeviceHandler extends BaseThingHandler {
             for (SmartHomeBaseDevice device : allDevices) {
                 if (device instanceof SmartHomeDevice) {
                     SmartHomeDevice shd = (SmartHomeDevice) device;
-                    if (shd.tags != null && shd.tags.tagNameToValueSetMap != null
-                            && shd.tags.tagNameToValueSetMap.groupIdentity != null
-                            && shg.applianceGroupIdentifier != null && shg.applianceGroupIdentifier.value != null
-                            && Arrays.asList(shd.tags.tagNameToValueSetMap.groupIdentity)
-                                    .contains(shg.applianceGroupIdentifier.value)) {
-                        SmartHomeCapability[] capabilities = shd.capabilities;
-                        if (capabilities != null) {
-                            if (Arrays.stream(capabilities).map(capability -> capability.interfaceName)
-                                    .anyMatch(SUPPORTED_INTERFACES::contains)) {
-                                result.add(shd);
+                    JsonSmartHomeTags.JsonSmartHomeTag tags = shd.tags;
+                    if (tags != null) {
+                        JsonSmartHomeGroupIdentity.SmartHomeGroupIdentity tagNameToValueSetMap = tags.tagNameToValueSetMap;
+                        JsonSmartHomeGroupIdentifiers.SmartHomeGroupIdentifier applianceGroupIdentifier = shg.applianceGroupIdentifier;
+                        if (tagNameToValueSetMap != null && tagNameToValueSetMap.groupIdentity != null
+                                && applianceGroupIdentifier != null && applianceGroupIdentifier.value != null
+                                && Arrays.asList(tagNameToValueSetMap.groupIdentity)
+                                        .contains(applianceGroupIdentifier.value)) {
+                            SmartHomeCapability[] capabilities = shd.capabilities;
+                            if (capabilities != null) {
+                                if (Arrays.stream(capabilities).map(capability -> capability.interfaceName)
+                                        .anyMatch(SUPPORTED_INTERFACES::contains)) {
+                                    result.add(shd);
+                                }
                             }
                         }
                     }
@@ -380,7 +379,7 @@ public class SmartHomeDeviceHandler extends BaseThingHandler {
             @Nullable Locale locale) {
         String channelId = channel.getUID().getId();
         for (HandlerBase handler : handlers.values()) {
-            if (handler != null && handler.hasChannel(channelId)) {
+            if (handler.hasChannel(channelId)) {
                 return handler.findStateDescription(channelId, originalStateDescription, locale);
             }
         }
index fef529fe76b3903ed77966b2801d34c27bd7d2ad..124c1af8ef68f2968304c5d7cd80eb24c928616a 100644 (file)
@@ -12,7 +12,8 @@
  */
 package org.openhab.binding.amazonechocontrol.internal.jsons;
 
-import org.apache.commons.lang.StringUtils;
+import java.util.Objects;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
@@ -35,7 +36,7 @@ public class JsonBluetoothStates {
             return null;
         }
         for (BluetoothState state : bluetoothStates) {
-            if (state != null && StringUtils.equals(state.deviceSerialNumber, device.serialNumber)) {
+            if (state != null && Objects.equals(state.deviceSerialNumber, device.serialNumber)) {
                 return state;
             }
         }
index ea6584b7bbccffbeacc84441d86fc2fba37456f0..6aa4a143fb1d8e1b4dae7aabc4410cd033ace1bf 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.amazonechocontrol.internal.jsons;
 
+import java.util.Arrays;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability;
@@ -24,7 +26,6 @@ import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeTags.Js
 @NonNullByDefault
 public class JsonSmartHomeDevices {
     public static class SmartHomeDevice implements SmartHomeBaseDevice {
-
         public @Nullable Integer updateIntervalInSeconds;
 
         @Override
@@ -52,11 +53,29 @@ public class JsonSmartHomeDevices {
         public @Nullable SmartHomeDevice @Nullable [] groupDevices;
         public @Nullable String connectedVia;
         public @Nullable DriverIdentity driverIdentity;
+
+        @Override
+        public String toString() {
+            return "SmartHomeDevice{" + "updateIntervalInSeconds=" + updateIntervalInSeconds + ", applianceId='"
+                    + applianceId + '\'' + ", manufacturerName='" + manufacturerName + '\'' + ", friendlyDescription='"
+                    + friendlyDescription + '\'' + ", modelName='" + modelName + '\'' + ", friendlyName='"
+                    + friendlyName + '\'' + ", reachability='" + reachability + '\'' + ", entityId='" + entityId + '\''
+                    + ", applianceNetworkState=" + applianceNetworkState + ", capabilities="
+                    + Arrays.toString(capabilities) + ", tags=" + tags + ", applianceTypes="
+                    + Arrays.toString(applianceTypes) + ", aliases=" + Arrays.toString(aliases) + ", groupDevices="
+                    + Arrays.toString(groupDevices) + ", connectedVia='" + connectedVia + '\'' + ", driverIdentity="
+                    + driverIdentity + '}';
+        }
     }
 
     public static class DriverIdentity {
         public @Nullable String namespace;
         public @Nullable String identifier;
+
+        @Override
+        public String toString() {
+            return "DriverIdentity{" + "namespace='" + namespace + '\'' + ", identifier='" + identifier + '\'' + '}';
+        }
     }
 
     public @Nullable SmartHomeDevice @Nullable [] smarthomeDevices;
index 4281534ee84dd9e29869a0baa5b2d44738f77f09..fac25424361b0e2d7f58a42e893204069ae8a389 100644 (file)
@@ -46,6 +46,12 @@ public class JsonSmartHomeGroups {
         public @Nullable Boolean isSpace;
         public @Nullable Boolean space;
         public @Nullable SmartHomeGroupIdentifier applianceGroupIdentifier;
+
+        @Override
+        public String toString() {
+            return "SmartHomeGroup{" + "applianceGroupName='" + applianceGroupName + '\'' + ", isSpace=" + isSpace
+                    + ", space=" + space + ", applianceGroupIdentifier=" + applianceGroupIdentifier + '}';
+        }
     }
 
     public @Nullable SmartHomeGroup @Nullable [] groups;
index 009aa174a6c91c20492d300356fc24259751f8de..e578bc8e1ccd036edeaa61b5204709c1e56c4a43 100644 (file)
@@ -47,7 +47,8 @@ public abstract class HandlerBase {
     public abstract void updateChannels(String interfaceName, List<JsonObject> stateList, UpdateChannelResult result);
 
     public abstract boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId,
-            SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException;
+            SmartHomeCapability[] capabilties, String channelId, Command command)
+            throws IOException, InterruptedException;
 
     public abstract @Nullable StateDescription findStateDescription(String channelId,
             StateDescription originalStateDescription, @Nullable Locale locale);
index 8c97e1a82c953a682601c30dc1c30ab86f780381..cc0e37f44aa1ce5476654f81fb514c9e163674b1 100644 (file)
@@ -91,7 +91,8 @@ public class HandlerBrightnessController extends HandlerBase {
 
     @Override
     public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId,
-            SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException {
+            SmartHomeCapability[] capabilties, String channelId, Command command)
+            throws IOException, InterruptedException {
         if (channelId.equals(BRIGHTNESS.channelId)) {
             if (containsCapabilityProperty(capabilties, BRIGHTNESS.propertyName)) {
                 if (command.equals(IncreaseDecreaseType.INCREASE)) {
index 4d1bc3f107d83823a8790748eb610ea7774de6d0..08b27a2168c9fbfa67e75bb4585431e5f047aefb 100644 (file)
@@ -19,7 +19,6 @@ import java.io.IOException;
 import java.util.List;
 import java.util.Locale;
 
-import org.apache.commons.lang.StringUtils;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants;
@@ -113,11 +112,10 @@ public class HandlerColorController extends HandlerBase {
                     }
                 }
             }
-            if (lastColorName == null) {
-                lastColorName = colorNameValue;
-            } else if (colorNameValue == null && lastColorName != null) {
+            if (colorNameValue == null && lastColorName != null) {
                 colorNameValue = lastColorName;
             }
+            lastColorName = colorNameValue;
             updateState(COLOR_PROPERTIES.channelId,
                     lastColorName == null ? UnDefType.UNDEF : new StringType(lastColorName));
         }
@@ -125,7 +123,8 @@ public class HandlerColorController extends HandlerBase {
 
     @Override
     public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId,
-            SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException {
+            SmartHomeCapability[] capabilties, String channelId, Command command)
+            throws IOException, InterruptedException {
         if (channelId.equals(COLOR.channelId)) {
             if (containsCapabilityProperty(capabilties, COLOR.propertyName)) {
                 if (command instanceof HSBType) {
@@ -134,15 +133,15 @@ public class HandlerColorController extends HandlerBase {
                     colorObject.addProperty("hue", color.getHue());
                     colorObject.addProperty("saturation", color.getSaturation().floatValue() / 100);
                     colorObject.addProperty("brightness", color.getBrightness().floatValue() / 100);
-                    connection.smartHomeCommand(entityId, "setColor", "color", colorObject);
+                    connection.smartHomeCommand(entityId, "setColor", "value", colorObject);
                 }
             }
         }
         if (channelId.equals(COLOR_PROPERTIES.channelId)) {
             if (containsCapabilityProperty(capabilties, COLOR.propertyName)) {
                 if (command instanceof StringType) {
-                    String colorName = ((StringType) command).toFullString();
-                    if (StringUtils.isNotEmpty(colorName)) {
+                    String colorName = command.toFullString();
+                    if (!colorName.isEmpty()) {
                         lastColorName = colorName;
                         connection.smartHomeCommand(entityId, "setColor", "colorName", colorName);
                         return true;
index 7df3ad56d744beb116f5fe2e0315bb70385d152c..0efb90324aecf689b1806a1bedb1f7ed30a79bab 100644 (file)
@@ -19,7 +19,6 @@ import java.io.IOException;
 import java.util.List;
 import java.util.Locale;
 
-import org.apache.commons.lang.StringUtils;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants;
@@ -121,7 +120,8 @@ public class HandlerColorTemperatureController extends HandlerBase {
 
     @Override
     public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId,
-            SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException {
+            SmartHomeCapability[] capabilties, String channelId, Command command)
+            throws IOException, InterruptedException {
         if (channelId.equals(COLOR_TEMPERATURE_IN_KELVIN.channelId)) {
             // WRITING TO THIS CHANNEL DOES CURRENTLY NOT WORK, BUT WE LEAVE THE CODE FOR FUTURE USE!
             if (containsCapabilityProperty(capabilties, COLOR_TEMPERATURE_IN_KELVIN.propertyName)) {
@@ -141,8 +141,8 @@ public class HandlerColorTemperatureController extends HandlerBase {
         if (channelId.equals(COLOR_TEMPERATURE_NAME.channelId)) {
             if (containsCapabilityProperty(capabilties, COLOR_TEMPERATURE_IN_KELVIN.propertyName)) {
                 if (command instanceof StringType) {
-                    String colorTemperatureName = ((StringType) command).toFullString();
-                    if (StringUtils.isNotEmpty(colorTemperatureName)) {
+                    String colorTemperatureName = command.toFullString();
+                    if (!colorTemperatureName.isEmpty()) {
                         lastColorName = colorTemperatureName;
                         connection.smartHomeCommand(entityId, "setColorTemperature", "colorTemperatureName",
                                 colorTemperatureName);
index 082f5c305979e32eea7c4be07c075f2d5ad3ded7..7f57ab65ab6b81c955e964ab3040f169ed5d7577 100644 (file)
@@ -91,7 +91,8 @@ public class HandlerPercentageController extends HandlerBase {
 
     @Override
     public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId,
-            SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException {
+            SmartHomeCapability[] capabilties, String channelId, Command command)
+            throws IOException, InterruptedException {
         if (channelId.equals(PERCENTAGE.channelId)) {
             if (containsCapabilityProperty(capabilties, PERCENTAGE.propertyName)) {
                 if (command.equals(IncreaseDecreaseType.INCREASE)) {
index 37969151c46787942f954c01232ec50b05bdbd94..a097695f5b142c0645c81213dc294d1be0881897 100644 (file)
@@ -29,6 +29,8 @@ import org.openhab.core.thing.type.ChannelTypeUID;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.StateDescription;
 import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.gson.JsonObject;
 
@@ -40,6 +42,8 @@ import com.google.gson.JsonObject;
  */
 @NonNullByDefault
 public class HandlerPowerController extends HandlerBase {
+    private final Logger logger = LoggerFactory.getLogger(HandlerPowerController.class);
+
     // Interface
     public static final String INTERFACE = "Alexa.PowerController";
 
@@ -67,6 +71,7 @@ public class HandlerPowerController extends HandlerBase {
 
     @Override
     public void updateChannels(String interfaceName, List<JsonObject> stateList, UpdateChannelResult result) {
+        logger.trace("{} received {}", this.smartHomeDeviceHandler.getId(), stateList);
         Boolean powerStateValue = null;
         for (JsonObject state : stateList) {
             if (POWER_STATE.propertyName.equals(state.get("name").getAsString())) {
@@ -74,19 +79,20 @@ public class HandlerPowerController extends HandlerBase {
                 // For groups take true if all true
                 if ("ON".equals(value)) {
                     powerStateValue = true;
-                } else if (powerStateValue == null) {
+                } else {
                     powerStateValue = false;
                 }
 
             }
         }
-        updateState(POWER_STATE.channelId,
-                powerStateValue == null ? UnDefType.UNDEF : (powerStateValue ? OnOffType.ON : OnOffType.OFF));
+        logger.trace("{} final state {}", this.smartHomeDeviceHandler.getId(), powerStateValue);
+        updateState(POWER_STATE.channelId, powerStateValue == null ? UnDefType.UNDEF : OnOffType.from(powerStateValue));
     }
 
     @Override
     public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId,
-            SmartHomeCapability[] capabilities, String channelId, Command command) throws IOException {
+            SmartHomeCapability[] capabilities, String channelId, Command command)
+            throws IOException, InterruptedException {
         if (channelId.equals(POWER_STATE.channelId)) {
             if (containsCapabilityProperty(capabilities, POWER_STATE.propertyName)) {
                 if (command.equals(OnOffType.ON)) {
index 719cefba31ce675e9ef94c5a73bb684286422d91..1341aeec176db0246e49cbb020498e3b02e64701 100644 (file)
@@ -92,7 +92,8 @@ public class HandlerPowerLevelController extends HandlerBase {
 
     @Override
     public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId,
-            SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException {
+            SmartHomeCapability[] capabilties, String channelId, Command command)
+            throws IOException, InterruptedException {
         if (channelId.equals(POWER_LEVEL.channelId)) {
             if (containsCapabilityProperty(capabilties, POWER_LEVEL.propertyName)) {
                 if (command.equals(IncreaseDecreaseType.INCREASE)) {
index 5ecdcbad21f6cb97dd253ab89436793e228fb452..38da19b47951777688882b6698918fa3c17d96a7 100644 (file)
@@ -19,7 +19,6 @@ import java.io.IOException;
 import java.util.List;
 import java.util.Locale;
 
-import org.apache.commons.lang.StringUtils;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants;
@@ -147,12 +146,13 @@ public class HandlerSecurityPanelController extends HandlerBase {
 
     @Override
     public boolean handleCommand(Connection connection, SmartHomeDevice shd, String entityId,
-            SmartHomeCapability[] capabilties, String channelId, Command command) throws IOException {
+            SmartHomeCapability[] capabilities, String channelId, Command command)
+            throws IOException, InterruptedException {
         if (channelId.equals(ARM_STATE.channelId)) {
-            if (containsCapabilityProperty(capabilties, ARM_STATE.propertyName)) {
+            if (containsCapabilityProperty(capabilities, ARM_STATE.propertyName)) {
                 if (command instanceof StringType) {
-                    String armStateValue = ((StringType) command).toFullString();
-                    if (StringUtils.isNotEmpty(armStateValue)) {
+                    String armStateValue = command.toFullString();
+                    if (!armStateValue.isEmpty()) {
                         connection.smartHomeCommand(entityId, "controlSecurityPanel", ARM_STATE.propertyName,
                                 armStateValue);
                         return true;
index 40dab399d7ae627585789779fd845c2da4bd52ed..866c20cd241c05d7cd8e119c3a2babb39e4ac504 100644 (file)
@@ -19,7 +19,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import org.apache.commons.lang.StringUtils;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability;
 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.DriverIdentity;
@@ -75,8 +74,9 @@ public class SmartHomeDeviceStateGroupUpdateCalculator {
             }
         }
         if (updateIntervalInSeconds == null) {
-            if ("openHAB".equalsIgnoreCase(shd.manufacturerName)
-                    || StringUtils.startsWithIgnoreCase(shd.manufacturerName, "ioBroker")) {
+            String manufacturerName = shd.manufacturerName;
+            if (manufacturerName != null && ("openHAB".equalsIgnoreCase(manufacturerName)
+                    || manufacturerName.toLowerCase().startsWith("iobroker"))) {
                 // OpenHAB or ioBroker skill
                 if (logger.isTraceEnabled()) {
                     updateIntervalInSeconds = UPDATE_INTERVAL_PRIVATE_SKILLS_IN_SECONDS_TRACE;