org.smarthomej.binding.amazonechocontrol.internal.Connection Maven / Gradle / Ivy
/**
* 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;
}
}