All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.smarthomej.binding.amazonechocontrol.internal.Connection Maven / Gradle / Ivy

There is a newer version: 4.2.0
Show newest version
/**
 * Copyright (c) 2010-2021 Contributors to the openHAB project
 * Copyright (c) 2021 Contributors to the SmartHome/J project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.smarthomej.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;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
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;
import java.util.stream.StreamSupport;
import java.util.zip.GZIPInputStream;

import javax.net.ssl.HttpsURLConnection;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonActivities;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonActivities.Activity;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonAnnouncementContent;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonAnnouncementTarget;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonAscendingAlarm.AscendingAlarmModel;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonAutomation;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonAutomation.Payload;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonBootstrapResult;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonBootstrapResult.Authentication;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonDeviceNotificationState.DeviceNotificationState;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonDevices;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonEnabledFeeds;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonEqualizer;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonExchangeTokenResponse;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonExchangeTokenResponse.Cookie;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonFeed;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonMediaState;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonMusicProvider;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonNetworkDetails;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonNotificationRequest;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonNotificationResponse;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonNotificationSound;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonNotificationSounds;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonNotificationsResponse;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonPlaySearchPhraseOperationPayload;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonPlayValidationResult;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonPlayerState;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonPlaylists;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonRegisterAppRequest;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.Bearer;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.DeviceInfo;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.Extensions;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.Response;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.Success;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonRegisterAppResponse.Tokens;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonRenewTokenResponse;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroups.SmartHomeGroup;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonStartRoutineRequest;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonUsersMeResponse;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonWakeWords;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonWakeWords.WakeWord;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonWebSiteCookie;
import org.smarthomej.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice;

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;

/**
 * The {@link Connection} is responsible for the connection to the amazon server
 * and handling of the commands
 *
 * @author Michael Geramb - Initial contribution
 */
@NonNullByDefault
public class Connection {
    private static final String THING_THREADPOOL_NAME = "thingHandler";
    private static final long EXPIRES_IN = 432000; // five days
    private static final Pattern CHARSET_PATTERN = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)");
    private static final String DEVICE_TYPE = "A2IVLV5VM2W81";

    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;
    private String frc;
    private String serial;
    private String deviceId;

    private @Nullable String refreshToken;
    private @Nullable Date loginTime;
    private @Nullable Date verifyTime;
    private long renewTime = 0;
    private @Nullable String deviceName;
    private @Nullable String accountCustomerId;
    private @Nullable String customerName;

    private Map announcements = Collections.synchronizedMap(new LinkedHashMap<>());
    private Map textToSpeeches = Collections.synchronizedMap(new LinkedHashMap<>());
    private Map textCommands = Collections.synchronizedMap(new LinkedHashMap<>());

    private Map volumes = Collections.synchronizedMap(new LinkedHashMap<>());
    private Map> devices = Collections.synchronizedMap(new LinkedHashMap<>());

    private final Map> timers = new ConcurrentHashMap<>();
    private final Map locks = new ConcurrentHashMap<>();

    private enum TimerType {
        ANNOUNCEMENT,
        TTS,
        VOLUME,
        DEVICES,
        TEXT_COMMAND
    }

    public Connection(@Nullable Connection oldConnection, Gson gson) {
        this.gson = gson;
        String frc = null;
        String serial = null;
        String deviceId = null;
        if (oldConnection != null) {
            deviceId = oldConnection.getDeviceId();
            frc = oldConnection.getFrc();
            serial = oldConnection.getSerial();
        }
        if (frc != null) {
            this.frc = frc;
        } else {
            // generate frc
            byte[] frcBinary = new byte[313];
            rand.nextBytes(frcBinary);
            this.frc = Base64.getEncoder().encodeToString(frcBinary);
        }
        if (serial != null) {
            this.serial = serial;
        } else {
            // generate serial
            byte[] serialBinary = new byte[16];
            rand.nextBytes(serialBinary);
            this.serial = HexUtils.bytesToHex(serialBinary);
        }
        if (deviceId != null) {
            this.deviceId = deviceId;
        } else {
            this.deviceId = generateDeviceId();
        }

        // build user agent
        this.userAgent = "AmazonWebView/Amazon Alexa/2.2.223830.0/iOS/11.4.1/iPhone";
        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 */ private String generateDeviceId() { byte[] bytes = new byte[16]; rand.nextBytes(bytes); String hexStr = HexUtils.bytesToHex(bytes).toUpperCase() + "#" + DEVICE_TYPE; return HexUtils.bytesToHex(hexStr.getBytes()); } /** * Check if deviceId is valid (consisting of hex(hex(16 random bytes)) + "#" + DEVICE_TYPE) * * @param deviceId the deviceId * @return true if valid, false if invalid */ private boolean checkDeviceIdIsValid(@Nullable String deviceId) { if (deviceId != null && deviceId.matches("^[0-9a-fA-F]{92}$")) { String hexString = new String(HexUtils.hexToBytes(deviceId)); if (hexString.matches("^[0-9A-F]{32}#" + DEVICE_TYPE + "$")) { return true; } } return false; } private void setAmazonSite(@Nullable String amazonSite) { String correctedAmazonSite = amazonSite != null ? amazonSite : "amazon.com"; if (correctedAmazonSite.toLowerCase().startsWith("http://")) { correctedAmazonSite = correctedAmazonSite.substring(7); } if (correctedAmazonSite.toLowerCase().startsWith("https://")) { correctedAmazonSite = correctedAmazonSite.substring(8); } if (correctedAmazonSite.toLowerCase().startsWith("www.")) { correctedAmazonSite = correctedAmazonSite.substring(4); } if (correctedAmazonSite.toLowerCase().startsWith("alexa.")) { correctedAmazonSite = correctedAmazonSite.substring(6); } this.amazonSite = correctedAmazonSite; alexaServer = "https://alexa." + this.amazonSite; } public @Nullable Date tryGetLoginTime() { return loginTime; } public @Nullable Date tryGetVerifyTime() { return verifyTime; } public String getFrc() { return frc; } public String getSerial() { return serial; } public String getDeviceId() { return deviceId; } public String getAmazonSite() { return amazonSite; } public String getAlexaServer() { return alexaServer; } public String getDeviceName() { String deviceName = this.deviceName; if (deviceName == null) { return "Unknown"; } return deviceName; } public String getCustomerId() { String customerId = this.accountCustomerId; if (customerId == null) { return "Unknown"; } return customerId; } public String getCustomerName() { String customerName = this.customerName; if (customerName == null) { return "Unknown"; } return customerName; } public boolean isSequenceNodeQueueRunning() { return devices.values().stream().anyMatch( (queueObjects) -> (queueObjects.stream().anyMatch(queueObject -> queueObject.future != null))); } public String serializeLoginData() { Date loginTime = this.loginTime; if (refreshToken == null || loginTime == null) { return ""; } StringBuilder builder = new StringBuilder(); builder.append("7\n"); // version builder.append(frc); builder.append("\n"); builder.append(serial); builder.append("\n"); builder.append(deviceId); builder.append("\n"); builder.append(refreshToken); builder.append("\n"); builder.append(amazonSite); builder.append("\n"); builder.append(deviceName); builder.append("\n"); builder.append(accountCustomerId); builder.append("\n"); builder.append(loginTime.getTime()); builder.append("\n"); List cookies = cookieManager.getCookieStore().getCookies(); builder.append(cookies.size()); builder.append("\n"); for (HttpCookie cookie : cookies) { writeValue(builder, cookie.getName()); writeValue(builder, cookie.getValue()); writeValue(builder, cookie.getComment()); writeValue(builder, cookie.getCommentURL()); writeValue(builder, cookie.getDomain()); writeValue(builder, cookie.getMaxAge()); writeValue(builder, cookie.getPath()); writeValue(builder, cookie.getPortlist()); writeValue(builder, cookie.getVersion()); writeValue(builder, cookie.getSecure()); writeValue(builder, cookie.getDiscard()); } return builder.toString(); } private void writeValue(StringBuilder builder, @Nullable Object value) { if (value == null) { builder.append('0'); } else { builder.append('1'); builder.append("\n"); builder.append(value.toString()); } builder.append("\n"); } private String readValue(Scanner scanner) { if (scanner.nextLine().equals("1")) { String result = scanner.nextLine(); if (result != null) { return result; } } return ""; } public boolean tryRestoreLogin(@Nullable String data, @Nullable String overloadedDomain) { Date loginTime = tryRestoreSessionData(data, overloadedDomain); if (loginTime != null) { try { if (verifyLogin()) { this.loginTime = loginTime; return true; } } catch (IOException e) { return false; } catch (URISyntaxException | InterruptedException e) { } } return false; } private @Nullable Date tryRestoreSessionData(@Nullable String data, @Nullable String overloadedDomain) { // verify store data if (data == null || data.isEmpty()) { return null; } Scanner scanner = new Scanner(data); String version = scanner.nextLine(); // check if serialize version is supported if (!"5".equals(version) && !"6".equals(version) && !"7".equals(version)) { scanner.close(); return null; } int intVersion = Integer.parseInt(version); frc = scanner.nextLine(); serial = scanner.nextLine(); deviceId = scanner.nextLine(); // Recreate session and cookies refreshToken = scanner.nextLine(); String domain = scanner.nextLine(); if (overloadedDomain != null) { domain = overloadedDomain; } setAmazonSite(domain); deviceName = scanner.nextLine(); if (intVersion > 5) { String accountCustomerId = scanner.nextLine(); // Note: version 5 have wrong customer id serialized. // Only use it, if it at least version 6 of serialization if (intVersion > 6) { if (!"null".equals(accountCustomerId)) { this.accountCustomerId = accountCustomerId; } } } Date loginTime = new Date(Long.parseLong(scanner.nextLine())); CookieStore cookieStore = cookieManager.getCookieStore(); cookieStore.removeAll(); Integer numberOfCookies = Integer.parseInt(scanner.nextLine()); for (Integer i = 0; i < numberOfCookies; i++) { String name = readValue(scanner); String value = readValue(scanner); HttpCookie clientCookie = new HttpCookie(name, value); clientCookie.setComment(readValue(scanner)); clientCookie.setCommentURL(readValue(scanner)); clientCookie.setDomain(readValue(scanner)); clientCookie.setMaxAge(Long.parseLong(readValue(scanner))); clientCookie.setPath(readValue(scanner)); clientCookie.setPortlist(readValue(scanner)); clientCookie.setVersion(Integer.parseInt(readValue(scanner))); clientCookie.setSecure(Boolean.parseBoolean(readValue(scanner))); clientCookie.setDiscard(Boolean.parseBoolean(readValue(scanner))); cookieStore.add(null, clientCookie); } scanner.close(); try { checkRenewSession(); String accountCustomerId = this.accountCustomerId; if (accountCustomerId == null || accountCustomerId.isEmpty()) { List devices = this.getDeviceList(); accountCustomerId = devices.stream().filter(device -> serial.equals(device.serialNumber)).findAny() .map(device -> device.deviceOwnerCustomerId).orElse(null); if (accountCustomerId == null || accountCustomerId.isEmpty()) { 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 | InterruptedException | ConnectionException e) { logger.debug("Getting account customer Id failed", e); } return loginTime; } 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 && contentType.toLowerCase().startsWith("application/json")) { try { String bootstrapResultJson = convertStream(connection); JsonBootstrapResult result = parseJson(bootstrapResultJson, JsonBootstrapResult.class); Authentication authentication = result.authentication; if (authentication != null && authentication.authenticated) { this.customerName = authentication.customerName; if (this.accountCustomerId == null) { this.accountCustomerId = authentication.customerId; } return authentication; } } catch (JsonSyntaxException | IllegalStateException e) { logger.info("No valid json received", e); return null; } } return null; } public String convertStream(HttpsURLConnection connection) throws IOException { InputStream input = connection.getInputStream(); if (input == null) { return ""; } InputStream readerStream; if ("gzip".equalsIgnoreCase(connection.getContentEncoding())) { readerStream = new GZIPInputStream(connection.getInputStream()); } else { readerStream = input; } String contentType = connection.getContentType(); String charSet = null; if (contentType != null) { Matcher m = CHARSET_PATTERN.matcher(contentType); if (m.find()) { charSet = m.group(1).trim().toUpperCase(); } } Scanner inputScanner = charSet == null || charSet.isEmpty() ? new Scanner(readerStream, StandardCharsets.UTF_8.name()) : new Scanner(readerStream, charSet); Scanner scannerWithoutDelimiter = inputScanner.useDelimiter("\\A"); String result = scannerWithoutDelimiter.hasNext() ? scannerWithoutDelimiter.next() : null; inputScanner.close(); scannerWithoutDelimiter.close(); input.close(); if (result == null) { result = ""; } return result; } 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, InterruptedException { HttpsURLConnection connection = makeRequest(verb, url, postData, json, true, customHeaders, 3); String result = convertStream(connection); logger.trace("Result of {} {}:{}", verb, url, result); return result; } public HttpsURLConnection makeRequest(String verb, String url, @Nullable String postData, boolean json, boolean autoredirect, @Nullable Map customHeaders, int badRequestRepeats) throws IOException, URISyntaxException, InterruptedException { String currentUrl = url; int redirectCounter = 0; int retryCounter = 0; // loop for handling redirect and bad request, using automatic redirect is not // possible, because all response headers must be catched while (true) { int code; HttpsURLConnection connection = null; try { logger.debug("Make request to {}", url); connection = (HttpsURLConnection) new URL(currentUrl).openConnection(); connection.setRequestMethod(verb); connection.setRequestProperty("Accept-Language", "en-US"); if (customHeaders == null || !customHeaders.containsKey("User-Agent")) { connection.setRequestProperty("User-Agent", userAgent); } connection.setRequestProperty("Accept-Encoding", "gzip"); connection.setRequestProperty("DNT", "1"); connection.setRequestProperty("Upgrade-Insecure-Requests", "1"); if (customHeaders != null) { for (String key : customHeaders.keySet()) { String value = customHeaders.get(key); if (value != null && !value.isEmpty()) { connection.setRequestProperty(key, value); } } } connection.setInstanceFollowRedirects(false); // add cookies URI uri = connection.getURL().toURI(); if (customHeaders == null || !customHeaders.containsKey("Cookie")) { StringBuilder cookieHeaderBuilder = new StringBuilder(); for (HttpCookie cookie : cookieManager.getCookieStore().get(uri)) { if (cookieHeaderBuilder.length() > 0) { cookieHeaderBuilder.append(";"); } cookieHeaderBuilder.append(cookie.getName()); cookieHeaderBuilder.append("="); cookieHeaderBuilder.append(cookie.getValue()); if (cookie.getName().equals("csrf")) { connection.setRequestProperty("csrf", cookie.getValue()); } } if (cookieHeaderBuilder.length() > 0) { String cookies = cookieHeaderBuilder.toString(); connection.setRequestProperty("Cookie", cookies); } } if (postData != null) { logger.debug("{}: {}", verb, postData); // post data byte[] postDataBytes = postData.getBytes(StandardCharsets.UTF_8); int postDataLength = postDataBytes.length; connection.setFixedLengthStreamingMode(postDataLength); if (json) { connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); } else { connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); } connection.setRequestProperty("Content-Length", Integer.toString(postDataLength)); if ("POST".equals(verb)) { connection.setRequestProperty("Expect", "100-continue"); } connection.setDoOutput(true); OutputStream outputStream = connection.getOutputStream(); outputStream.write(postDataBytes); outputStream.close(); } // handle result code = connection.getResponseCode(); String location = null; // handle response headers 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 ("Set-Cookie".equalsIgnoreCase(key)) { // store cookie for (String cookieHeader : header.getValue()) { if (!cookieHeader.isEmpty()) { List cookies = HttpCookie.parse(cookieHeader); for (HttpCookie cookie : cookies) { cookieManager.getCookieStore().add(uri, cookie); } } } } if ("Location".equalsIgnoreCase(key)) { // get redirect location location = header.getValue().get(0); if (!location.isEmpty()) { location = uri.resolve(location).toString(); // check for https if (location.toLowerCase().startsWith("http://")) { // always use https location = "https://" + location.substring(7); logger.debug("Redirect corrected to {}", location); } } } } } if (code == 200) { logger.debug("Call to {} succeeded", url); return connection; } else if (code == 302 && location != null) { logger.debug("Redirected to {}", location); redirectCounter++; if (redirectCounter > 30) { throw new ConnectionException("Too many redirects"); } currentUrl = location; if (autoredirect) { continue; // repeat with new location } return connection; } else { logger.debug("Retry call to {}", url); retryCounter++; if (retryCounter > badRequestRepeats) { throw new HttpException(code, verb + " url '" + url + "' failed: " + connection.getResponseMessage()); } 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(); } logger.warn("Request to url '{}' fails with unknown error", url, e); throw e; } catch (Exception e) { if (connection != null) { connection.disconnect(); } throw e; } } } public String registerConnectionAsApp(String oAutRedirectUrl) throws ConnectionException, IOException, URISyntaxException, InterruptedException { URI oAutRedirectUri = new URI(oAutRedirectUrl); Map queryParameters = new LinkedHashMap<>(); String query = oAutRedirectUri.getQuery(); String[] pairs = query.split("&"); for (String pair : pairs) { int idx = pair.indexOf("="); queryParameters.put(URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name()), URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name())); } String accessToken = queryParameters.get("openid.oa2.access_token"); Map cookieMap = new HashMap<>(); List webSiteCookies = new ArrayList<>(); for (HttpCookie cookie : getSessionCookies("https://www.amazon.com")) { cookieMap.put(cookie.getName(), cookie.getValue()); webSiteCookies.add(new JsonWebSiteCookie(cookie.getName(), cookie.getValue())); } JsonRegisterAppRequest registerAppRequest = new JsonRegisterAppRequest(serial, accessToken, frc, webSiteCookies); String registerAppRequestJson = gson.toJson(registerAppRequest); HashMap registerHeaders = new HashMap<>(); registerHeaders.put("x-amzn-identity-auth-domain", "api.amazon.com"); String registerAppResultJson = makeRequestAndReturnString("POST", "https://api.amazon.com/auth/register", registerAppRequestJson, true, registerHeaders); JsonRegisterAppResponse registerAppResponse = parseJson(registerAppResultJson, JsonRegisterAppResponse.class); Response response = registerAppResponse.response; if (response == null) { throw new ConnectionException("Error: No response received from register application"); } Success success = response.success; if (success == null) { throw new ConnectionException("Error: No success received from register application"); } Tokens tokens = success.tokens; if (tokens == null) { throw new ConnectionException("Error: No tokens received from register application"); } Bearer bearer = tokens.bearer; if (bearer == null) { throw new ConnectionException("Error: No bearer received from register application"); } String refreshToken = bearer.refreshToken; this.refreshToken = refreshToken; if (refreshToken == null || refreshToken.isEmpty()) { throw new ConnectionException("Error: No refresh token received"); } try { exchangeToken(); // Check which is the owner domain String usersMeResponseJson = makeRequestAndReturnString("GET", "https://alexa.amazon.com/api/users/me?platform=ios&version=2.2.223830.0", null, false, null); JsonUsersMeResponse usersMeResponse = parseJson(usersMeResponseJson, JsonUsersMeResponse.class); URI uri = new URI(usersMeResponse.marketPlaceDomainName); String host = uri.getHost(); // Switch to owner domain setAmazonSite(host); exchangeToken(); tryGetBootstrap(); } catch (Exception e) { logout(); throw e; } String deviceName = null; Extensions extensions = success.extensions; if (extensions != null) { DeviceInfo deviceInfo = extensions.deviceInfo; if (deviceInfo != null) { deviceName = deviceInfo.deviceName; } } if (deviceName == null) { deviceName = "Unknown"; } this.deviceName = deviceName; return deviceName; } private void exchangeToken() throws IOException, URISyntaxException, InterruptedException { this.renewTime = 0; String cookiesJson = "{\"cookies\":{\"." + getAmazonSite() + "\":[]}}"; String cookiesBase64 = Base64.getEncoder().encodeToString(cookiesJson.getBytes()); String exchangePostData = "di.os.name=iOS&app_version=2.2.223830.0&domain=." + getAmazonSite() + "&source_token=" + URLEncoder.encode(this.refreshToken, "UTF8") + "&requested_token_type=auth_cookies&source_token_type=refresh_token&di.hw.version=iPhone&di.sdk.version=6.10.0&cookies=" + cookiesBase64 + "&app_name=Amazon%20Alexa&di.os.version=11.4.1"; HashMap exchangeTokenHeader = new HashMap<>(); exchangeTokenHeader.put("Cookie", ""); String exchangeTokenJson = makeRequestAndReturnString("POST", "https://www." + getAmazonSite() + "/ap/exchangetoken", exchangePostData, false, exchangeTokenHeader); JsonExchangeTokenResponse exchangeTokenResponse = Objects .requireNonNull(gson.fromJson(exchangeTokenJson, JsonExchangeTokenResponse.class)); org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonExchangeTokenResponse.Response response = exchangeTokenResponse.response; if (response != null) { org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonExchangeTokenResponse.Tokens tokens = response.tokens; if (tokens != null) { Map cookiesMap = tokens.cookies; if (cookiesMap != null) { for (String domain : cookiesMap.keySet()) { Cookie[] cookies = cookiesMap.get(domain); 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); } } } } } } } if (!verifyLogin()) { throw new ConnectionException("Verify login failed after token exchange"); } this.renewTime = (long) (System.currentTimeMillis() + Connection.EXPIRES_IN * 1000d / 0.8d); // start renew at } 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()) + "&package_name=com.amazon.echo&di.hw.version=iPhone&platform=iOS&requested_token_type=access_token&source_token_type=refresh_token&di.os.name=iOS&di.os.version=11.4.1¤t_version=6.10.0"; String renewTokenResponseJson = makeRequestAndReturnString("POST", "https://api.amazon.com/auth/token", renewTokenPostData, false, null); parseJson(renewTokenResponseJson, JsonRenewTokenResponse.class); exchangeToken(); return true; } return false; } public boolean getIsLoggedIn() { return loginTime != null; } public String getLoginPage() throws IOException, URISyntaxException, InterruptedException { // clear session data logout(); logger.debug("Start Login to {}", alexaServer); if (!checkDeviceIdIsValid(deviceId)) { deviceId = generateDeviceId(); logger.debug("Generating new device id (old device id had invalid format)."); } String mapMdJson = "{\"device_user_dictionary\":[],\"device_registration_data\":{\"software_version\":\"1\"},\"app_identifier\":{\"app_version\":\"2.2.223830\",\"bundle_id\":\"com.amazon.echo\"}}"; String mapMdCookie = Base64.getEncoder().encodeToString(mapMdJson.getBytes()); cookieManager.getCookieStore().add(new URI("https://www.amazon.com"), new HttpCookie("map-md", mapMdCookie)); cookieManager.getCookieStore().add(new URI("https://www.amazon.com"), new HttpCookie("frc", frc)); Map customHeaders = new HashMap<>(); customHeaders.put("authority", "www.amazon.com"); String loginFormHtml = makeRequestAndReturnString("GET", "https://www.amazon.com" + "/ap/signin?openid.return_to=https://www.amazon.com/ap/maplanding&openid.assoc_handle=amzn_dp_project_dee_ios&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&pageId=amzn_dp_project_dee_ios&accountStatusPolicy=P1&openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns.oa2=http://www.amazon.com/ap/ext/oauth/2&openid.oa2.client_id=device:" + deviceId + "&openid.ns.pape=http://specs.openid.net/extensions/pape/1.0&openid.oa2.response_type=token&openid.ns=http://specs.openid.net/auth/2.0&openid.pape.max_auth_age=0&openid.oa2.scope=device_auth_access", null, false, customHeaders); logger.debug("Received login form {}", loginFormHtml); return loginFormHtml; } public boolean verifyLogin() throws IOException, URISyntaxException, InterruptedException { if (this.refreshToken == null) { return false; } Authentication authentication = tryGetBootstrap(); if (authentication != null && authentication.authenticated) { verifyTime = new Date(); if (loginTime == null) { loginTime = verifyTime; } return true; } return false; } public List getSessionCookies() { try { return cookieManager.getCookieStore().get(new URI(alexaServer)); } catch (URISyntaxException e) { return new ArrayList<>(); } } public List getSessionCookies(String server) { try { return cookieManager.getCookieStore().get(new URI(server)); } catch (URISyntaxException e) { return new ArrayList<>(); } } // current value in compute can be null 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 refreshToken = null; loginTime = null; verifyTime = null; deviceName = null; replaceTimer(TimerType.ANNOUNCEMENT, null); announcements.clear(); replaceTimer(TimerType.TTS, null); textToSpeeches.clear(); replaceTimer(TimerType.VOLUME, null); volumes.clear(); replaceTimer(TimerType.DEVICES, null); textCommands.clear(); replaceTimer(TimerType.TTS, null); devices.values().forEach((queueObjects) -> { queueObjects.forEach((queueObject) -> { Future future = queueObject.future; if (future != null) { future.cancel(true); queueObject.future = null; } }); }); } // parser private T parseJson(String json, Class type) throws JsonSyntaxException, IllegalStateException { try { // gson.fromJson is always non-null if json is non-null return Objects.requireNonNull(gson.fromJson(json, type)); } catch (JsonParseException | IllegalStateException e) { logger.warn("Parsing json failed: {}", json, e); throw e; } } // commands and states public List getWakeWords() { String json; try { json = makeRequestAndReturnString(alexaServer + "/api/wake-word?cached=true"); JsonWakeWords wakeWords = parseJson(json, JsonWakeWords.class); return Objects.requireNonNullElse(wakeWords.wakeWords, List.of()); } catch (IOException | URISyntaxException | InterruptedException e) { logger.info("getting wakewords failed", e); } return List.of(); } public List getSmarthomeDeviceList() throws IOException, URISyntaxException, InterruptedException { try { String json = makeRequestAndReturnString(alexaServer + "/api/phoenix"); logger.debug("getSmartHomeDevices result: {}", json); JsonNetworkDetails networkDetails = parseJson(json, JsonNetworkDetails.class); Object jsonObject = gson.fromJson(networkDetails.networkDetail, Object.class); List result = new ArrayList<>(); searchSmartHomeDevicesRecursive(jsonObject, result); return result; } catch (Exception e) { logger.warn("getSmartHomeDevices fails: {}", e.getMessage()); throw e; } } private void searchSmartHomeDevicesRecursive(@Nullable Object jsonNode, List devices) { if (jsonNode instanceof Map) { @SuppressWarnings("rawtypes") 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); SmartHomeDevice shd = parseJson(element.toString(), SmartHomeDevice.class); devices.add(shd); } else if (map.containsKey("applianceGroupName")) { JsonElement element = gson.toJsonTree(jsonNode); SmartHomeGroup shg = parseJson(element.toString(), SmartHomeGroup.class); devices.add(shg); } else { map.values().forEach(value -> searchSmartHomeDevicesRecursive(value, devices)); } } } public List getDeviceList() throws IOException, URISyntaxException, InterruptedException { JsonDevices devices = Objects.requireNonNull(parseJson(getDeviceListJson(), JsonDevices.class)); logger.trace("Devices {}", devices.devices); // @Nullable because of a limitation of the null-checker, we filter null-serialNumbers before Set<@Nullable String> serialNumbers = ConcurrentHashMap.newKeySet(); return devices.devices.stream().filter(d -> d.serialNumber != null && serialNumbers.add(d.serialNumber)) .collect(Collectors.toList()); } public String getDeviceListJson() throws IOException, URISyntaxException, InterruptedException { String json = makeRequestAndReturnString(alexaServer + "/api/devices-v2/device?cached=false"); return json; } public Map getSmartHomeDeviceStatesJson(Set devices) throws IOException, URISyntaxException, InterruptedException { JsonObject requestObject = new JsonObject(); JsonArray stateRequests = new JsonArray(); Map mergedApplianceMap = new HashMap<>(); for (SmartHomeBaseDevice device : devices) { String applianceId = device.findId(); if (applianceId != null) { JsonObject stateRequest; if (device instanceof SmartHomeDevice && ((SmartHomeDevice) device).mergedApplianceIds != null) { List mergedApplianceIds = Objects .requireNonNullElse(((SmartHomeDevice) device).mergedApplianceIds, List.of()); for (String idToMerge : mergedApplianceIds) { mergedApplianceMap.put(idToMerge, applianceId); stateRequest = new JsonObject(); stateRequest.addProperty("entityId", idToMerge); stateRequest.addProperty("entityType", "APPLIANCE"); stateRequests.add(stateRequest); } } else { stateRequest = new JsonObject(); stateRequest.addProperty("entityId", applianceId); stateRequest.addProperty("entityType", "APPLIANCE"); stateRequests.add(stateRequest); } } } requestObject.add("stateRequests", stateRequests); String requestBody = requestObject.toString(); String json = makeRequestAndReturnString("POST", alexaServer + "/api/phoenix/state", requestBody, true, null); logger.trace("Requested {} and received {}", requestBody, json); JsonObject responseObject = Objects.requireNonNull(gson.fromJson(json, JsonObject.class)); JsonArray deviceStates = (JsonArray) responseObject.get("deviceStates"); Map result = new HashMap<>(); for (JsonElement deviceState : deviceStates) { JsonObject deviceStateObject = deviceState.getAsJsonObject(); JsonObject entity = deviceStateObject.get("entity").getAsJsonObject(); String applianceId = entity.get("entityId").getAsString(); JsonElement capabilityState = deviceStateObject.get("capabilityStates"); if (capabilityState != null && capabilityState.isJsonArray()) { String realApplianceId = mergedApplianceMap.get(applianceId); if (realApplianceId != null) { var capabilityArray = result.get(realApplianceId); if (capabilityArray != null) { capabilityArray.addAll(capabilityState.getAsJsonArray()); result.put(realApplianceId, capabilityArray); } else { result.put(realApplianceId, capabilityState.getAsJsonArray()); } } else { result.put(applianceId, capabilityState.getAsJsonArray()); } } } return result; } 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, InterruptedException { String json = makeRequestAndReturnString(alexaServer + "/api/media/state?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType); JsonMediaState mediaState = parseJson(json, JsonMediaState.class); return mediaState; } public List getActivities(int number, @Nullable Long startTime) { try { String json = makeRequestAndReturnString(alexaServer + "/api/activities?startTime=" + (startTime != null ? startTime : "") + "&size=" + number + "&offset=1"); JsonActivities activities = parseJson(json, JsonActivities.class); return Objects.requireNonNullElse(activities.activities, List.of()); } catch (IOException | URISyntaxException | InterruptedException e) { logger.info("getting activities failed", e); } return List.of(); } public @Nullable JsonBluetoothStates getBluetoothConnectionStates() { String json; try { json = makeRequestAndReturnString(alexaServer + "/api/bluetooth?cached=true"); } catch (IOException | URISyntaxException | InterruptedException e) { logger.debug("failed to get bluetooth state: {}", e.getMessage()); return new JsonBluetoothStates(); } JsonBluetoothStates bluetoothStates = parseJson(json, JsonBluetoothStates.class); return bluetoothStates; } 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, 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, InterruptedException { smartHomeCommand(entityId, action, Map.of()); } public void smartHomeCommand(String entityId, String action, Map values) throws IOException, InterruptedException { String url = alexaServer + "/api/phoenix/state"; JsonObject json = new JsonObject(); JsonArray controlRequests = new JsonArray(); JsonObject controlRequest = new JsonObject(); controlRequest.addProperty("entityId", entityId); controlRequest.addProperty("entityType", "APPLIANCE"); JsonObject parameters = new JsonObject(); parameters.addProperty("action", action); if (!values.isEmpty()) { values.forEach((property, value) -> { if (value instanceof QuantityType) { JsonObject propertyObj = new JsonObject(); propertyObj.addProperty("value", Double.toString(((QuantityType) value).doubleValue())); propertyObj.addProperty("scale", ((QuantityType) value).getUnit().equals(SIUnits.CELSIUS) ? "celsius" : "fahrenheit"); parameters.add(property, propertyObj); } else if (value instanceof Boolean) { parameters.addProperty(property, (boolean) value); } else if (value instanceof String) { parameters.addProperty(property, (String) value); } else if (value instanceof StringType) { JsonObject propertyObj = new JsonObject(); propertyObj.addProperty("value", value.toString()); parameters.add(property, propertyObj); } else if (value instanceof Number) { parameters.addProperty(property, (Number) value); } else if (value instanceof Character) { parameters.addProperty(property, (Character) value); } else if (value instanceof JsonElement) { parameters.add(property, (JsonElement) value); } }); } controlRequest.add("parameters", parameters); controlRequests.add(controlRequest); json.add("controlRequests", controlRequests); String requestBody = json.toString(); try { String resultBody = makeRequestAndReturnString("PUT", url, requestBody, true, null); logger.trace("Request '{}' resulted in '{}", requestBody, resultBody); JsonObject result = parseJson(resultBody, JsonObject.class); JsonElement errors = result.get("errors"); if (errors != null && errors.isJsonArray()) { JsonArray errorList = errors.getAsJsonArray(); if (errorList.size() > 0) { logger.warn("Smart home device command failed. The request '{}' resulted in error(s): {}", requestBody, StreamSupport.stream(errorList.spliterator(), false).map(JsonElement::toString) .collect(Collectors.joining(" / "))); } } } catch (URISyntaxException e) { logger.warn("URL '{}' has invalid format for request '{}': {}", url, requestBody, e.getMessage()); } } 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 + "\",\"softwareVersion\":\"" + device.softwareVersion + "\",\"volumeLevel\":" + volume + "}"; makeRequest("PUT", url, command, true, true, null, 0); } 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 + "\",\"deviceAccountId\":null}"; makeRequest("PUT", url, command, true, true, null, 0); } public List getDeviceNotificationStates() { try { String json = makeRequestAndReturnString(alexaServer + "/api/device-notification-state"); JsonDeviceNotificationState result = parseJson(json, JsonDeviceNotificationState.class); return Objects.requireNonNullElse(result.deviceNotificationStates, List.of()); } catch (IOException | URISyntaxException | InterruptedException e) { logger.info("Error getting device notification states", e); } return List.of(); } public List getAscendingAlarm() { String json; try { json = makeRequestAndReturnString(alexaServer + "/api/ascending-alarm"); JsonAscendingAlarm result = parseJson(json, JsonAscendingAlarm.class); return Objects.requireNonNullElse(result.ascendingAlarmModelList, List.of()); } catch (IOException | URISyntaxException | InterruptedException e) { logger.info("Error getting device notification states", e); } return List.of(); } public void bluetooth(Device device, @Nullable String address) throws IOException, URISyntaxException, InterruptedException { if (address == null || address.isEmpty()) { // disconnect makeRequest("POST", alexaServer + "/api/bluetooth/disconnect-sink/" + device.deviceType + "/" + device.serialNumber, "", true, true, null, 0); } else { makeRequest("POST", alexaServer + "/api/bluetooth/pair-sink/" + device.deviceType + "/" + device.serialNumber, "{\"bluetoothDeviceAddress\":\"" + address + "\"}", true, true, null, 0); } } 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 { makeRequest("POST", alexaServer + "/api/tunein/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&guideId=" + stationId + "&contentType=station&callSign=&mediaOwnerCustomerId=" + getCustomerId(device.deviceOwnerCustomerId), "", true, true, null, 0); } } public void playAmazonMusicTrack(Device device, @Nullable String trackId) throws IOException, URISyntaxException, InterruptedException { if (trackId == null || trackId.isEmpty()) { command(device, "{\"type\":\"PauseCommand\"}"); } else { String command = "{\"trackId\":\"" + trackId + "\",\"playQueuePrime\":true}"; makeRequest("POST", alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + getCustomerId(device.deviceOwnerCustomerId) + "&shuffle=false", command, true, true, null, 0); } } public void playAmazonMusicPlayList(Device device, @Nullable String playListId) throws IOException, URISyntaxException, InterruptedException { if (playListId == null || playListId.isEmpty()) { command(device, "{\"type\":\"PauseCommand\"}"); } else { String command = "{\"playlistId\":\"" + playListId + "\",\"playQueuePrime\":true}"; makeRequest("POST", alexaServer + "/api/cloudplayer/queue-and-play?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&mediaOwnerCustomerId=" + getCustomerId(device.deviceOwnerCustomerId) + "&shuffle=false", command, true, true, null, 0); } } public void announcement(Device device, String speak, String bodyText, @Nullable String title, @Nullable Integer ttsVolume, @Nullable Integer standardVolume) { String plainSpeak = speak.replaceAll("<.+?>", " ").replaceAll("\\s+", " ").trim(); String plainBody = bodyText.replaceAll("<.+?>", " ").replaceAll("\\s+", " ").trim(); if (plainSpeak.isEmpty() && plainBody.isEmpty()) { // if there is neither a bodytext nor (except tags) a speaktext, we have nothing to announce return; } // we lock announcements until we have finished adding this one Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.ANNOUNCEMENT, k -> new ReentrantLock())); lock.lock(); try { AnnouncementWrapper announcement = Objects.requireNonNull(announcements.computeIfAbsent( Objects.hash(speak, plainBody, title), k -> new AnnouncementWrapper(speak, plainBody, 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(); } } private void sendAnnouncement() { // we lock new announcements until we have dispatched everything Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.ANNOUNCEMENT, k -> new ReentrantLock())); lock.lock(); try { Iterator iterator = announcements.values().iterator(); while (iterator.hasNext()) { AnnouncementWrapper announcement = iterator.next(); try { List devices = announcement.devices; if (!devices.isEmpty()) { JsonAnnouncementContent content = new JsonAnnouncementContent(announcement); Map parameters = new HashMap<>(); parameters.put("expireAfter", "PT5S"); parameters.put("content", new JsonAnnouncementContent[] { content }); parameters.put("target", new JsonAnnouncementTarget(devices)); String customerId = getCustomerId(devices.get(0).deviceOwnerCustomerId); if (customerId != null) { parameters.put("customerId", customerId); } executeSequenceCommandWithVolume(devices, "AlexaAnnouncement", parameters, announcement.ttsVolumes, announcement.standardVolumes); } } catch (Exception e) { logger.warn("send announcement fails with unexpected error", e); } iterator.remove(); } } finally { // the timer is done anyway immediately after we unlock timers.remove(TimerType.ANNOUNCEMENT); lock.unlock(); } } public void textToSpeech(Device device, String text, @Nullable Integer ttsVolume, @Nullable Integer standardVolume) { if (text.replaceAll("<.+?>", "").replaceAll("\\s+", " ").trim().isEmpty()) { return; } // we lock TTS until we have finished adding this one Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TTS, k -> new ReentrantLock())); lock.lock(); try { TextToSpeech textToSpeech = Objects .requireNonNull(textToSpeeches.computeIfAbsent(Objects.hash(text), k -> new TextToSpeech(text))); textToSpeech.devices.add(device); textToSpeech.ttsVolumes.add(ttsVolume); textToSpeech.standardVolumes.add(standardVolume); // schedule a TTS only if it has not been scheduled before timers.computeIfAbsent(TimerType.TTS, k -> scheduler.schedule(this::sendTextToSpeech, 500, TimeUnit.MILLISECONDS)); } finally { lock.unlock(); } } private void sendTextToSpeech() { // we lock new TTS until we have dispatched everything Lock lock = Objects.requireNonNull(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.isEmpty()) { String text = textToSpeech.text; Map parameters = Map.of("textToSpeak", text); executeSequenceCommandWithVolume(devices, "Alexa.Speak", parameters, textToSpeech.ttsVolumes, textToSpeech.standardVolumes); } } catch (Exception e) { logger.warn("send textToSpeech fails with unexpected error", e); } iterator.remove(); } } finally { // the timer is done anyway immediately after we unlock timers.remove(TimerType.TTS); lock.unlock(); } } public void textCommand(Device device, String text, @Nullable Integer ttsVolume, @Nullable Integer standardVolume) { if (text.replaceAll("<.+?>", "").replaceAll("\\s+", " ").trim().isEmpty()) { return; } // we lock TextCommands until we have finished adding this one Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TEXT_COMMAND, k -> new ReentrantLock())); lock.lock(); try { TextCommand textCommand = Objects .requireNonNull(textCommands.computeIfAbsent(Objects.hash(text), k -> new TextCommand(text))); textCommand.devices.add(device); textCommand.ttsVolumes.add(ttsVolume); textCommand.standardVolumes.add(standardVolume); // schedule a TextCommand only if it has not been scheduled before timers.computeIfAbsent(TimerType.TEXT_COMMAND, k -> scheduler.schedule(this::sendTextCommand, 500, TimeUnit.MILLISECONDS)); } finally { lock.unlock(); } } private synchronized void sendTextCommand() { // we lock new TTS until we have dispatched everything Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TEXT_COMMAND, k -> new ReentrantLock())); lock.lock(); try { Iterator iterator = textCommands.values().iterator(); while (iterator.hasNext()) { TextCommand textCommand = iterator.next(); try { List devices = textCommand.devices; if (!devices.isEmpty()) { String text = textCommand.text; Map parameters = Map.of("text", text); executeSequenceCommandWithVolume(devices, "Alexa.TextCommand", parameters, textCommand.ttsVolumes, textCommand.standardVolumes); } } catch (Exception e) { logger.warn("send textCommand fails with unexpected error", e); } iterator.remove(); } } finally { // the timer is done anyway immediately after we unlock timers.remove(TimerType.TEXT_COMMAND); lock.unlock(); } } public void volume(Device device, int vol) { // we lock volume until we have finished adding this one Lock lock = Objects.requireNonNull(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(); } } private void sendVolume() { // we lock new volume until we have dispatched everything Lock lock = Objects.requireNonNull(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.isEmpty()) { executeSequenceCommandWithVolume(devices, null, Map.of(), volume.volumes, List.of()); } } catch (Exception e) { logger.warn("send volume fails with unexpected error", e); } iterator.remove(); } } finally { // the timer is done anyway immediately after we unlock timers.remove(TimerType.VOLUME); lock.unlock(); } } private void executeSequenceCommandWithVolume(List devices, @Nullable String command, Map parameters, List<@Nullable Integer> ttsVolumes, List<@Nullable Integer> standardVolumes) { JsonArray serialNodesToExecute = new JsonArray(); 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.isEmpty()) { JsonArray commandNodesToExecute = new JsonArray(); if ("Alexa.Speak".equals(command) || "Alexa.TextCommand".equals(command)) { for (Device device : devices) { commandNodesToExecute.add(createExecutionNode(device, command, parameters)); } } else { commandNodesToExecute.add(createExecutionNode(devices.get(0), command, parameters)); } if (commandNodesToExecute.size() > 0) { JsonObject parallelNodesToExecute = new JsonObject(); parallelNodesToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode"); parallelNodesToExecute.add("nodesToExecute", commandNodesToExecute); serialNodesToExecute.add(parallelNodesToExecute); } } 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 && !"AlexaAnnouncement".equals(command)) { JsonObject parallelNodesToExecute = new JsonObject(); parallelNodesToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode"); parallelNodesToExecute.add("nodesToExecute", standardVolumeNodesToExecute); serialNodesToExecute.add(parallelNodesToExecute); } if (serialNodesToExecute.size() > 0) { executeSequenceNodes(devices, serialNodesToExecute, false); if (standardVolumeNodesToExecute.size() > 0 && "AlexaAnnouncement".equals(command)) { executeSequenceNodes(devices, standardVolumeNodesToExecute, true); } } } // 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(Device device, String command, Map parameters) { JsonObject nodeToExecute = createExecutionNode(device, command, parameters); executeSequenceNode(List.of(device), nodeToExecute); } 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 + " "; } } logger.debug("added {} device {}", queueObject.hashCode(), serialNumbers); } private void handleExecuteSequenceNode() { Lock lock = Objects.requireNonNull(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 = null; if (tmpQueueObject != null) { 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; } 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 = 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 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(List devices, JsonArray nodesToExecute, boolean parallel) { JsonObject serialNode = new JsonObject(); if (parallel) { serialNode.addProperty("@type", "com.amazon.alexa.behaviors.model.ParallelNode"); } else { serialNode.addProperty("@type", "com.amazon.alexa.behaviors.model.SerialNode"); } serialNode.add("nodesToExecute", nodesToExecute); executeSequenceNode(devices, serialNode); } 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", getCustomerId(device.deviceOwnerCustomerId)); } for (String key : parameters.keySet()) { Object value = parameters.get(key); if (value instanceof String) { operationPayload.addProperty(key, (String) value); } else if (value instanceof Number) { operationPayload.addProperty(key, (Number) value); } else if (value instanceof Boolean) { operationPayload.addProperty(key, (Boolean) value); } else if (value instanceof Character) { operationPayload.addProperty(key, (Character) value); } else { operationPayload.add(key, gson.toJsonTree(value)); } } JsonObject nodeToExecute = new JsonObject(); nodeToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"); nodeToExecute.addProperty("type", command); if ("Alexa.TextCommand".equals(command)) { nodeToExecute.addProperty("skillId", "amzn1.ask.1p.tellalexa"); } nodeToExecute.add("operationPayload", operationPayload); return nodeToExecute; } @Nullable private ExecutionNodeObject getExecutionNodeObject(JsonObject nodeToExecute) { ExecutionNodeObject executionNodeObject = new ExecutionNodeObject(); if (nodeToExecute.has("nodesToExecute")) { JsonArray serialNodesToExecute = nodeToExecute.getAsJsonArray("nodesToExecute"); if (serialNodesToExecute != null && serialNodesToExecute.size() > 0) { for (int i = 0; i < serialNodesToExecute.size(); i++) { JsonObject serialNodesToExecuteJsonObject = serialNodesToExecute.get(i).getAsJsonObject(); if (serialNodesToExecuteJsonObject.has("nodesToExecute")) { JsonArray parallelNodesToExecute = serialNodesToExecuteJsonObject .getAsJsonArray("nodesToExecute"); if (parallelNodesToExecute != null && parallelNodesToExecute.size() > 0) { JsonObject parallelNodesToExecuteJsonObject = parallelNodesToExecute.get(0) .getAsJsonObject(); if (processNodesToExecuteJsonObject(executionNodeObject, parallelNodesToExecuteJsonObject)) { break; } } } else { if (processNodesToExecuteJsonObject(executionNodeObject, serialNodesToExecuteJsonObject)) { break; } } } } } return executionNodeObject; } 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("text")) { executionNodeObject.text = operationPayload.get("text").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 : routines) { if (routine != null) { if (routine.sequence != null) { List triggers = Objects.requireNonNullElse(routine.triggers, List.of()); for (JsonAutomation.Trigger trigger : triggers) { Payload payload = trigger.payload; if (payload == null) { continue; } String payloadUtterance = payload.utterance; if (payloadUtterance != null && payloadUtterance.equalsIgnoreCase(utterance)) { found = routine; deviceLocale = payload.locale; break; } } } } } if (found != null) { String sequenceJson = gson.toJson(found.sequence); JsonStartRoutineRequest request = new JsonStartRoutineRequest(); request.behaviorId = found.automationId; // replace tokens // "deviceType":"ALEXA_CURRENT_DEVICE_TYPE" String deviceType = "\"deviceType\":\"ALEXA_CURRENT_DEVICE_TYPE\""; String newDeviceType = "\"deviceType\":\"" + device.deviceType + "\""; sequenceJson = sequenceJson.replace(deviceType.subSequence(0, deviceType.length()), newDeviceType.subSequence(0, newDeviceType.length())); // "deviceSerialNumber":"ALEXA_CURRENT_DSN" String deviceSerial = "\"deviceSerialNumber\":\"ALEXA_CURRENT_DSN\""; String newDeviceSerial = "\"deviceSerialNumber\":\"" + device.serialNumber + "\""; sequenceJson = sequenceJson.replace(deviceSerial.subSequence(0, deviceSerial.length()), newDeviceSerial.subSequence(0, newDeviceSerial.length())); // "customerId": "ALEXA_CUSTOMER_ID" String customerId = "\"customerId\":\"ALEXA_CUSTOMER_ID\""; String newCustomerId = "\"customerId\":\"" + getCustomerId(device.deviceOwnerCustomerId) + "\""; sequenceJson = sequenceJson.replace(customerId.subSequence(0, customerId.length()), newCustomerId.subSequence(0, newCustomerId.length())); // "locale": "ALEXA_CURRENT_LOCALE" String locale = "\"locale\":\"ALEXA_CURRENT_LOCALE\""; String newlocale = deviceLocale != null && !deviceLocale.isEmpty() ? "\"locale\":\"" + deviceLocale + "\"" : "\"locale\":null"; sequenceJson = sequenceJson.replace(locale.subSequence(0, locale.length()), newlocale.subSequence(0, newlocale.length())); request.sequenceJson = sequenceJson; String requestJson = gson.toJson(request); makeRequest("POST", alexaServer + "/api/behaviors/preview", requestJson, true, true, null, 3); } else { logger.warn("Routine {} not found", utterance); } } public @Nullable JsonAutomation @Nullable [] getRoutines() throws IOException, URISyntaxException, InterruptedException { String json = makeRequestAndReturnString(alexaServer + "/api/behaviors/v2/automations?limit=2000"); JsonAutomation[] result = parseJson(json, JsonAutomation[].class); return result; } public List getEnabledFlashBriefings() throws IOException, URISyntaxException, InterruptedException { String json = makeRequestAndReturnString(alexaServer + "/api/content-skills/enabled-feeds"); JsonEnabledFeeds result = parseJson(json, JsonEnabledFeeds.class); return Objects.requireNonNullElse(result.enabledFeeds, List.of()); } public void setEnabledFlashBriefings(List 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 List getNotificationSounds(Device device) throws IOException, URISyntaxException, InterruptedException { String json = makeRequestAndReturnString( alexaServer + "/api/notification/sounds?deviceSerialNumber=" + device.serialNumber + "&deviceType=" + device.deviceType + "&softwareVersion=" + device.softwareVersion); JsonNotificationSounds result = parseJson(json, JsonNotificationSounds.class); return Objects.requireNonNullElse(result.notificationSounds, List.of()); } public List notifications() throws IOException, URISyntaxException, InterruptedException { String response = makeRequestAndReturnString(alexaServer + "/api/notifications"); JsonNotificationsResponse result = parseJson(response, JsonNotificationsResponse.class); return Objects.requireNonNullElse(result.notifications, List.of()); } public @Nullable JsonNotificationResponse notification(Device device, String type, @Nullable String label, @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 // the past (compared with the server time) long alarmTime = alarm.getTime(); JsonNotificationRequest request = new JsonNotificationRequest(); request.type = type; request.deviceSerialNumber = device.serialNumber; request.deviceType = device.deviceType; request.createdDate = createdDate; request.alarmTime = alarmTime; request.reminderLabel = label; request.sound = sound; request.originalDate = new SimpleDateFormat("yyyy-MM-dd").format(alarm); request.originalTime = new SimpleDateFormat("HH:mm:ss.SSSS").format(alarm); request.type = type; request.id = "create" + type; String data = gsonWithNullSerialization.toJson(request); String response = makeRequestAndReturnString("PUT", alexaServer + "/api/notifications/createReminder", data, true, null); JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); return result; } 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, InterruptedException { String response = makeRequestAndReturnString("GET", alexaServer + "/api/notifications/" + notification.id, null, true, null); JsonNotificationResponse result = parseJson(response, JsonNotificationResponse.class); return result; } public List getMusicProviders() { try { Map headers = new HashMap<>(); headers.put("Routines-Version", "1.1.218665"); String response = makeRequestAndReturnString("GET", alexaServer + "/api/behaviors/entities?skillId=amzn1.ask.1p.music", null, true, headers); if (!response.isEmpty()) { JsonMusicProvider[] musicProviders = parseJson(response, JsonMusicProvider[].class); return Arrays.asList(musicProviders); } } catch (IOException | URISyntaxException | InterruptedException e) { logger.warn("getMusicProviders fails: {}", e.getMessage()); } return List.of(); } public void playMusicVoiceCommand(Device device, String providerId, String voiceCommand) throws IOException, URISyntaxException, InterruptedException { JsonPlaySearchPhraseOperationPayload payload = new JsonPlaySearchPhraseOperationPayload(); payload.customerId = getCustomerId(device.deviceOwnerCustomerId); payload.locale = "ALEXA_CURRENT_LOCALE"; payload.musicProviderId = providerId; payload.searchPhrase = voiceCommand; String playloadString = gson.toJson(payload); JsonObject postValidationJson = new JsonObject(); postValidationJson.addProperty("type", "Alexa.Music.PlaySearchPhrase"); postValidationJson.addProperty("operationPayload", playloadString); String postDataValidate = postValidationJson.toString(); String validateResultJson = makeRequestAndReturnString("POST", alexaServer + "/api/behaviors/operation/validate", postDataValidate, true, null); if (!validateResultJson.isEmpty()) { JsonPlayValidationResult validationResult = parseJson(validateResultJson, JsonPlayValidationResult.class); JsonPlaySearchPhraseOperationPayload validatedOperationPayload = validationResult.operationPayload; if (validatedOperationPayload != null) { payload.sanitizedSearchPhrase = validatedOperationPayload.sanitizedSearchPhrase; payload.searchPhrase = validatedOperationPayload.searchPhrase; } } payload.locale = null; payload.deviceSerialNumber = device.serialNumber; payload.deviceType = device.deviceType; JsonObject sequenceJson = new JsonObject(); sequenceJson.addProperty("@type", "com.amazon.alexa.behaviors.model.Sequence"); JsonObject startNodeJson = new JsonObject(); startNodeJson.addProperty("@type", "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"); startNodeJson.addProperty("type", "Alexa.Music.PlaySearchPhrase"); startNodeJson.add("operationPayload", gson.toJsonTree(payload)); sequenceJson.add("startNode", startNodeJson); JsonStartRoutineRequest startRoutineRequest = new JsonStartRoutineRequest(); startRoutineRequest.sequenceJson = sequenceJson.toString(); startRoutineRequest.status = null; String postData = gson.toJson(startRoutineRequest); makeRequest("POST", alexaServer + "/api/behaviors/preview", postData, true, true, null, 3); } 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, InterruptedException { String postData = gson.toJson(settings); makeRequest("POST", alexaServer + "/api/equalizer/" + device.serialNumber + "/" + device.deviceType, postData, true, true, null, 0); } public static class AnnouncementWrapper { public List devices = new ArrayList<>(); public String speak; public String bodyText; public @Nullable String title; public List<@Nullable Integer> ttsVolumes = new ArrayList<>(); public List<@Nullable Integer> standardVolumes = new ArrayList<>(); public AnnouncementWrapper(String speak, String bodyText, @Nullable String title) { this.speak = speak; this.bodyText = bodyText; this.title = title; } } private static class TextToSpeech { public List devices = new ArrayList<>(); public String text; public List<@Nullable Integer> ttsVolumes = new ArrayList<>(); public List<@Nullable Integer> standardVolumes = new ArrayList<>(); public TextToSpeech(String text) { this.text = text; } } private static class TextCommand { public List devices = new ArrayList<>(); public String text; public List<@Nullable Integer> ttsVolumes = new ArrayList<>(); public List<@Nullable Integer> standardVolumes = new ArrayList<>(); public TextCommand(String text) { this.text = text; } } private static class Volume { public List devices = new ArrayList<>(); public int volume; public List<@Nullable Integer> volumes = new ArrayList<>(); public Volume(int volume) { this.volume = volume; } } private static class QueueObject { public @Nullable Future future; public List devices = List.of(); public JsonObject nodeToExecute = new JsonObject(); } private static class ExecutionNodeObject { public List types = new ArrayList<>(); @Nullable public String text; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy