From 1b588bc4fa60c7cb706ff95d811618955a5c9cec Mon Sep 17 00:00:00 2001 From: J-N-K Date: Wed, 25 Nov 2020 21:15:29 +0100 Subject: [PATCH] [amazonechocontrol] improvements and bug fixes (#9057) * 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 Co-authored-by: Tom Blum Co-authored-by: Connor Petty --- .../internal/AccountHandlerConfig.java | 4 +- .../internal/AccountServlet.java | 63 +- .../AmazonEchoControlHandlerFactory.java | 12 +- ...onEchoDynamicStateDescriptionProvider.java | 8 +- .../internal/BindingServlet.java | 7 +- .../internal/Connection.java | 936 +++++++++--------- .../internal/WebSocketConnection.java | 16 +- .../channelhandler/ChannelHandler.java | 2 +- .../ChannelHandlerSendMessage.java | 2 +- .../discovery/SmartHomeDevicesDiscovery.java | 15 +- .../internal/handler/AccountHandler.java | 49 +- .../internal/handler/EchoHandler.java | 197 ++-- .../handler/FlashBriefingProfileHandler.java | 85 +- .../handler/SmartHomeDeviceHandler.java | 83 +- .../internal/jsons/JsonBluetoothStates.java | 5 +- .../internal/jsons/JsonSmartHomeDevices.java | 21 +- .../internal/jsons/JsonSmartHomeGroups.java | 6 + .../internal/smarthome/HandlerBase.java | 3 +- .../HandlerBrightnessController.java | 3 +- .../smarthome/HandlerColorController.java | 15 +- .../HandlerColorTemperatureController.java | 8 +- .../HandlerPercentageController.java | 3 +- .../smarthome/HandlerPowerController.java | 14 +- .../HandlerPowerLevelController.java | 3 +- .../HandlerSecurityPanelController.java | 10 +- ...tHomeDeviceStateGroupUpdateCalculator.java | 6 +- 26 files changed, 805 insertions(+), 771 deletions(-) diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java index f5476e352..0a7129787 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountHandlerConfig.java @@ -13,10 +13,10 @@ 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 */ diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java index 0d902d36d..a865add09 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AccountServlet.java @@ -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 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, "" + StringEscapeUtils.escapeHtml(connection.getDeviceListJson()) + ""); } @@ -435,13 +451,13 @@ public class AccountServlet extends HttpServlet { StringBuilder html = new StringBuilder(); html.append("" + StringEscapeUtils.escapeHtml(BINDING_NAME + " - " + this.account.getThing().getLabel())); - if (StringUtils.isNotEmpty(title)) { + if (!title.isEmpty()) { html.append(" - "); html.append(StringEscapeUtils.escapeHtml(title)); } html.append(""); html.append("

" + 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 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(""); html.append(StringEscapeUtils.escapeHtml(displayName)); html.append(""); @@ -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(""); @@ -666,7 +685,7 @@ public class AccountServlet extends HttpServlet { return; } } - } catch (URISyntaxException | ConnectionException e) { + } catch (URISyntaxException | ConnectionException | InterruptedException e) { returnError(resp, e.getLocalizedMessage()); return; } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 26855346c..170c151c7 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -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> discoveryServiceRegistration = discoveryServiceRegistrations - .computeIfAbsent(bridgeHandler.getThing().getUID(), k -> new ArrayList<>()); + List> 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(), diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoDynamicStateDescriptionProvider.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoDynamicStateDescriptionProvider.java index 118cabb70..f4d36ae7a 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoDynamicStateDescriptionProvider.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoDynamicStateDescriptionProvider.java @@ -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 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)); } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java index c34595059..a5fee625b 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/BindingServlet.java @@ -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; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index ccc72288b..48bc57b91 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -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 announcements = new LinkedHashMap<>(); - private Map textToSpeeches = new LinkedHashMap<>(); - private Map volumes = new LinkedHashMap<>(); - private @Nullable ScheduledFuture announcementTimer; - private @Nullable ScheduledFuture textToSpeechTimer; - private @Nullable ScheduledFuture volumeTimer; + private Map announcements = Collections.synchronizedMap(new LinkedHashMap<>()); + private Map textToSpeeches = Collections.synchronizedMap(new LinkedHashMap<>()); + private Map volumes = Collections.synchronizedMap(new LinkedHashMap<>()); + private Map> devices = Collections.synchronizedMap(new LinkedHashMap<>()); - private final Gson gson; - private final Gson gsonWithNullSerialization; + private final Map> timers = new ConcurrentHashMap<>(); + private final Map locks = new ConcurrentHashMap<>(); - private Map singles = Collections.synchronizedMap(new LinkedHashMap<>()); - private Map 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 - * + *

* 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 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 customHeaders) throws IOException, URISyntaxException { + @Nullable Map 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 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> headerFields = connection.getHeaderFields(); - for (Map.Entry> header : headerFields.entrySet()) { + Map<@Nullable String, List> headerFields = connection.getHeaderFields(); + for (Map.Entry<@Nullable String, List> 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 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 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 getSmarthomeDeviceList() throws IOException, URISyntaxException { + public List 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 devices) { if (jsonNode instanceof Map) { @SuppressWarnings("rawtypes") - Map map = (Map) jsonNode; + Map 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 getDeviceList() throws IOException, URISyntaxException { + public List 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 getSmartHomeDeviceStatesJson(Set 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 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 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 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 iterator = announcements.values().iterator(); + while (iterator.hasNext()) { + Announcement announcement = iterator.next(); try { List 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 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; + + // 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(); } - 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 iterator = textToSpeeches.values().iterator(); - while (iterator.hasNext()) { - TextToSpeech textToSpeech = iterator.next(); - if (textToSpeech != null) { + 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 iterator = textToSpeeches.values().iterator(); + while (iterator.hasNext()) { + TextToSpeech textToSpeech = iterator.next(); try { List 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 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 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 iterator = volumes.values().iterator(); + while (iterator.hasNext()) { + Volume volume = iterator.next(); try { List 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 parameters, @NonNull Integer[] ttsVolumes, - @Nullable Integer @Nullable [] standardVolumes) throws IOException, URISyntaxException { + private void executeSequenceCommandWithVolume(List devices, @Nullable String command, + Map 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 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 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 parameters) throws IOException, URISyntaxException { + public void executeSequenceCommand(Device device, String command, Map parameters) { JsonObject nodeToExecute = createExecutionNode(device, command, parameters); - executeSequenceNode(new Device[] { device }, nodeToExecute); + executeSequenceNode(List.of(device), nodeToExecute); } - 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 executeSequenceNode(List 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 + " "; } - singleGroupTimer = scheduler.schedule(() -> executeSequenceNode(devices, nodeToExecute), 500, - TimeUnit.MILLISECONDS); + } + logger.debug("added {} device {}", queueObject.hashCode(), serialNumbers); + } + private void handleExecuteSequenceNode() { + Lock lock = locks.computeIfAbsent(TimerType.DEVICES, k -> new ReentrantLock()); + if (lock.tryLock()) { + try { + for (String serialNumber : devices.keySet()) { + LinkedBlockingQueue 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 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(); + } + } + } + + 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); - } else { - if (!groups.containsKey(devices[0])) { - groups.put(devices[0], new QueueObject()); - } - groups.get(devices[0]).queue.add(nodeToExecute); + List types = executionNodeObject.types; + long delay = 0; + if (types.contains("Alexa.DeviceControls.Volume")) { + delay += 2000; } + if (types.contains("Announcement")) { + delay += 3000; + } else { + 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); + + Map 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; + } + + makeRequest("POST", alexaServer + "/api/behaviors/preview", json, true, true, null, 3); + + Thread.sleep(delay); + } catch (IOException | URISyntaxException | InterruptedException e) { + logger.warn("execute sequence node fails with unexpected error", e); + } finally { + removeObjectFromQueueAfterExecutionCompletion(queueObject); } } - 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 types = executionNodeObject.types; - long delay = 0; - if (types.contains("Alexa.DeviceControls.Volume")) { - delay += 2000; - } - if (types.contains("Announcement")) { - delay += 3000; - } else { - delay += 2000; - } - 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); - - Map headers = new HashMap<>(); - headers.put("Routines-Version", "1.1.218665"); - - 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 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 parameters) { + private JsonObject createExecutionNode(@Nullable Device device, String command, Map 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); + operationPayload.addProperty("customerId", getCustomerId(device.deviceOwnerCustomerId)); } - 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)); - } + 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 getMusicProviders() { - String response; try { Map 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 devices = new ArrayList<>(); public String speak; @@ -2029,7 +2039,6 @@ public class Connection { } } - @NonNullByDefault private static class TextToSpeech { public List devices = new ArrayList<>(); public String text; @@ -2041,7 +2050,6 @@ public class Connection { } } - @NonNullByDefault private static class Volume { public List devices = new ArrayList<>(); public int volume; @@ -2052,22 +2060,12 @@ public class Connection { } } - @NonNullByDefault private static class QueueObject { - public LinkedBlockingQueue 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 devices = List.of(); + public JsonObject nodeToExecute = new JsonObject(); } - @NonNullByDefault private static class ExecutionNodeObject { public List types = new ArrayList<>(); @Nullable diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java index ad7670b67..afcbc7257 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/WebSocketConnection.java @@ -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 diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandler.java index 9118cc8f2..00779b7f5 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandler.java @@ -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; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerSendMessage.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerSendMessage.java index 09b3ab82b..03e672257 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerSendMessage.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/channelhandler/ChannelHandlerSendMessage.java @@ -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(); diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java index a3df33c8c..da0984b24 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/discovery/SmartHomeDevicesDiscovery.java @@ -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 diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java index f3c2497b9..09713617f 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java @@ -104,7 +104,6 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma private final Object synchronizeConnection = new Object(); private Map jsonSerialNumberDeviceMapping = new HashMap<>(); private Map jsonIdSmartHomeDeviceMapping = new HashMap<>(); - private Map 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 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); } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java index fb5223017..25c74419c 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java @@ -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)) { diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/FlashBriefingProfileHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/FlashBriefingProfileHandler.java index cbec9fcb9..bb9484405 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/FlashBriefingProfileHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/FlashBriefingProfileHandler.java @@ -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); + connection.executeSequenceCommand(device, "Alexa.FlashBriefing.Play", Map.of()); - scheduler.schedule(() -> accountHandler.setEnabledFlashBriefingsJson(old), 1000, - TimeUnit.MILLISECONDS); + scheduler.schedule(() -> accountHandler.setEnabledFlashBriefingsJson(old), 1000, + TimeUnit.MILLISECONDS); - updateState(CHANNEL_ACTIVE, OnOffType.ON); - } + 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) { diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java index 967e6696c..de0fdb5b4 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java @@ -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 allDevices, Map 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> 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 (HandlerBase handlerBase : handlers.values()) { + UpdateChannelResult result = new UpdateChannelResult(); for (String interfaceName : handlerBase.getSupportedInterface()) { - List 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 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); } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java index fef529fe7..124c1af8e 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonBluetoothStates.java @@ -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; } } diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevices.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevices.java index ea6584b7b..6aa4a143f 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevices.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeDevices.java @@ -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; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroups.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroups.java index 4281534ee..fac254243 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroups.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/jsons/JsonSmartHomeGroups.java @@ -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; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBase.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBase.java index 009aa174a..e578bc8e1 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBase.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBase.java @@ -47,7 +47,8 @@ public abstract class HandlerBase { public abstract void updateChannels(String interfaceName, List 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); diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java index 8c97e1a82..cc0e37f44 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerBrightnessController.java @@ -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)) { diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java index 4d1bc3f10..08b27a216 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java @@ -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; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java index 7df3ad56d..0efb90324 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerColorTemperatureController.java @@ -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); diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java index 082f5c305..7f57ab65a 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPercentageController.java @@ -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)) { diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java index 37969151c..a097695f5 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerController.java @@ -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 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)) { diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java index 719cefba3..1341aeec1 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerPowerLevelController.java @@ -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)) { diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java index 5ecdcbad2..38da19b47 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java @@ -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; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java index 40dab399d..866c20cd2 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/smarthome/SmartHomeDeviceStateGroupUpdateCalculator.java @@ -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;

NameValue