alexa-remote2
Version:
Remote Control for amazon echo devices
1,046 lines (980 loc) • 166 kB
JavaScript
const https = require('https');
const querystring = require('querystring');
const os = require('os');
const extend = require('extend');
//const AlexaWsMqtt = require('./alexa-wsmqtt.js');
const AlexaHttp2Push = require('./alexa-http2push.js');
const { v1: uuidv1 } = require('uuid');
const zlib = require('zlib');
const fsPath = require('path');
const EventEmitter = require('events');
const officialUserAgent = 'AppleWebKit PitanguiBridge/2.2.595606.0-[HARDWARE=iPhone14_7][SOFTWARE=17.4.1][DEVICE=iPhone]';
function _00(val) {
let s = val.toString();
while (s.length < 2) s = `0${s}`;
return s;
}
const SmartHomeEndpointsGraphQlQuery = `query Endpoints {
endpoints {
items {
endpointId
id
friendlyName
displayCategories {
primary {
value
}
}
legacyIdentifiers {
dmsIdentifier {
deviceType {
type
value {
text
}
}
deviceSerialNumber {
type
value {
text
}
}
}
}
legacyAppliance {
applianceId
applianceTypes
endpointTypeId
friendlyName
friendlyDescription
manufacturerName
connectedVia
modelName
entityId
actions
mergedApplianceIds
capabilities
applianceNetworkState
version
isEnabled
customerDefinedDeviceType
customerPreference
alexaDeviceIdentifierList
aliases
driverIdentity
additionalApplianceDetails
isConsentRequired
applianceKey
appliancePairs
deduplicatedPairs
entityPairs
deduplicatedAliasesByEntityId
relations
}
serialNumber {
value {
text
}
}
enablement
model {
value {
text
}
}
manufacturer {
value {
text
}
}
features {
name
operations {
name
}
}
}
}
}
`;
class AlexaRemote extends EventEmitter {
constructor() {
super();
this.serialNumbers = {};
this.names = {};
this.friendlyNames = {};
this.lastAuthCheck = null;
this.cookie = null;
this.csrf = null;
this.cookieData = null;
this.authenticationDetails = null;
this.ownerCustomerId = null;
this.endpoints = null;
this.baseUrl = 'alexa.amazon.de';
this.authApiBearerToken = null;
this.authApiBearerExpiry = null;
this.activityCsrfToken = null;
this.activityCsrfTokenExpiry = null;
this.activityCsrfTokenReferer = null;
this.lastVolumes = {};
this.lastEqualizer = {};
this.lastPushedActivity = {};
this.activityUpdateQueue = [];
this.activityUpdateNotFoundCounter = 0;
this.activityUpdateTimeout = null;
this.activityUpdateRunning = false;
}
setCookie(_cookie) {
if (!_cookie) return;
if (typeof _cookie === 'string') {
this.cookie = _cookie;
}
else if (_cookie && _cookie.cookie && typeof _cookie.cookie === 'string') {
this.cookie = _cookie.cookie;
}
else if (_cookie && _cookie.localCookie && typeof _cookie.localCookie === 'string') {
this.cookie = _cookie.localCookie;
this._options.formerRegistrationData = this.cookieData = _cookie;
}
else if (_cookie && _cookie.cookie && typeof _cookie.cookie === 'object') {
return this.setCookie(_cookie.cookie);
}
if (!this.cookie || typeof this.cookie !== 'string') return;
const ar = this.cookie.match(/csrf=([^;]+)/);
if (ar && ar.length >= 2) {
this.csrf = ar[1];
}
if (!this.csrf) {
this.cookie = null;
return;
}
this._options.csrf = this.csrf;
this._options.cookie = this.cookie;
this.macDms = this._options.macDms = this._options.macDms || (this.cookieData && this.cookieData.macDms);
this.emit('cookie', this.cookie, this.csrf, this.macDms);
}
init(cookie, callback) {
if (typeof cookie === 'object') {
this._options = cookie;
if (!this._options.userAgent) {
const platform = os.platform();
if (platform === 'win32') {
this._options.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36';
}
/*else if (platform === 'darwin') {
this._options.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36';
}*/
else {
this._options.userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36';
}
}
if (this._options.apiUserAgentPostfix === undefined) {
this._options.apiUserAgentPostfix = `AlexaRemote/${require(fsPath.join(__dirname, 'package.json')).version}`;
}
this._options.amazonPage = this._options.amazonPage || 'amazon.de';
this.baseUrl = `alexa.${this._options.amazonPage}`;
cookie = this._options.cookie;
}
this._options.logger && this._options.logger(`Alexa-Remote: Use as User-Agent: ${this._options.userAgent}`);
this._options.logger && this._options.logger(`Alexa-Remote: Use as API User-Agent Postfix: ${this._options.apiUserAgentPostfix}`);
this._options.logger && this._options.logger(`Alexa-Remote: Use as Login-Amazon-URL: ${this._options.amazonPage}`);
if (this._options.alexaServiceHost) this.baseUrl = this._options.alexaServiceHost;
this._options.logger && this._options.logger(`Alexa-Remote: Use as Base-URL: ${this.baseUrl}`);
this._options.alexaServiceHost = this.baseUrl;
if (this._options.refreshCookieInterval !== undefined && this._options.cookieRefreshInterval === undefined) {
this._options.cookieRefreshInterval = this._options.refreshCookieInterval;
delete this._options.refreshCookieInterval;
}
if (this._options.cookieRefreshInterval !== 0) {
this._options.cookieRefreshInterval = this._options.cookieRefreshInterval || 4 * 24 * 60 * 60 * 1000; // Auto Refresh after 4 days
}
if (this._options.cookieRefreshInterval < 0 || this._options.cookieRefreshInterval > 2147483646) {
this._options.cookieRefreshInterval = 4 * 24 * 60 * 60 * 1000; // Auto Refresh after 4 days
}
const self = this;
function getCookie(callback) {
if (!self.cookie) {
self._options.logger && self._options.logger('Alexa-Remote: No cookie given, generate one');
self._options.cookieJustCreated = true;
self.generateCookie(self._options.email, self._options.password, function(err, res) {
if (!err && res) {
self.setCookie(res); // update
self.alexaCookie.stopProxyServer();
return callback (null);
}
callback(err);
});
}
else {
self._options.logger && self._options.logger('Alexa-Remote: cookie was provided');
if (self._options.formerRegistrationData) {
const tokensValidSince = Date.now() - self._options.formerRegistrationData.tokenDate;
if (tokensValidSince < 24 * 60 * 60 * 1000 && self._options.macDms && self._options.formerRegistrationData.dataVersion === 2) {
return callback(null);
}
self._options.logger && self._options.logger('Alexa-Remote: former registration data exist, try refresh');
self._options.logger && self._options.logger(JSON.stringify(self._options.formerRegistrationData));
self.refreshCookie(function(err, res) {
if (err || !res) {
self._options.logger && self._options.logger('Alexa-Remote: Error from refreshing cookies');
self.cookie = null;
return getCookie(callback); // error on refresh
}
self.setCookie(res); // update
return callback(null);
});
}
else {
callback(null);
}
}
}
this.setCookie(cookie); // set initial cookie
getCookie((err) => {
if (typeof callback === 'function') callback = callback.bind(this);
if (err) {
this._options.logger && this._options.logger('Alexa-Remote: Error from retrieving cookies');
return callback && callback(err);
}
if (!this.csrf) return callback && callback(new Error('no csrf found'));
this.checkAuthentication((authenticated, err) => {
if (err && authenticated === null) {
return callback && callback(new Error(`Error while checking Authentication: ${err}`));
}
this._options.logger && this._options.logger(`Alexa-Remote: Authentication checked: ${authenticated}`);
if ((!authenticated && !this._options.cookieJustCreated) || !this.macDms) {
this._options.logger && !this.macDms && this._options.logger('Alexa-Remote: JWT missing, forcing a refresh ...');
this._options.logger && this._options.logger('Alexa-Remote: Cookie was set, but authentication invalid');
delete this._options.cookie;
delete this._options.csrf;
delete this._options.localCookie;
delete this._options.macDms;
return this.init(this._options, callback);
}
this.lastAuthCheck = new Date().getTime();
if (this.cookieRefreshTimeout) {
clearTimeout(this.cookieRefreshTimeout);
this.cookieRefreshTimeout = null;
}
if (this._options.cookieRefreshInterval) {
this.cookieRefreshTimeout = setTimeout(() => {
this.cookieRefreshTimeout = null;
this._options.cookie = this.cookieData;
delete this._options.csrf;
this.init(this._options, callback);
}, this._options.cookieRefreshInterval);
}
this.getEndpoints((err, endpoints) => {
if (!err && endpoints && endpoints.websiteApiUrl) {
this.endpoints = endpoints;
this.baseUrl = this.endpoints.websiteApiUrl.replace(/^https?:\/\//, '');
this._options.logger && this._options.logger(`Alexa-Remote: Change Base URL for API calls to ${this.baseUrl}`);
} else {
this._options.logger && this._options.logger(`Alexa-Remote: Could not query endpoints: ${err}`);
}
this.prepare(() => {
if (this._options.useWsMqtt || this._options.usePushConnection) {
this.initPushConnection();
}
callback && callback();
});
});
});
});
}
prepare(callback) {
this.getAccount((err, result) => {
if (!err && result && Array.isArray(result)) {
result.forEach ((account) => {
if (!this.commsId) this.commsId = account.commsId;
//if (!this.directedId) this.directedId = account.directedId;
});
}
this.initDeviceState(() =>
this.initWakewords(() =>
this.initBluetoothState(() =>
this.initNotifications(callback)
)
)
);
});
return this;
}
getAuthenticationDetails() {
return this.authenticationDetails;
}
initNotifications(callback) {
if (!this._options.notifications) return callback && callback();
this.getNotifications((err, res) => {
if (err || !res || !res.notifications || !Array.isArray(res.notifications)) return callback && callback();
for (const serialNumber of Object.keys(this.serialNumbers)) {
this.serialNumbers[serialNumber].notifications = [];
}
res.notifications.forEach((noti) => {
const device = this.find(noti.deviceSerialNumber);
if (!device) {
//TODO: new stuff
return;
}
if (noti.alarmTime && !noti.originalTime && noti.originalDate && noti.type !== 'Timer' && !noti.rRuleData) {
const now = new Date(noti.alarmTime);
noti.originalTime = `${_00(now.getHours())}:${_00(now.getMinutes())}:${_00(now.getSeconds())}.000`;
}
noti.set = this.changeNotification.bind(this, noti);
noti.delete = this.deleteNotification.bind(this, noti);
noti.cancel = this.cancelNotification.bind(this, noti);
device.notifications.push(noti);
});
callback && callback();
});
}
initWakewords(callback) {
this.getWakeWords((err, wakeWords) => {
if (err || !wakeWords || !Array.isArray(wakeWords.wakeWords)) return callback && callback();
wakeWords.wakeWords.forEach((o) => {
const device = this.find(o.deviceSerialNumber);
if (!device) {
//TODO: new stuff
return;
}
if (typeof o.wakeWord === 'string') {
device.wakeWord = o.wakeWord.toLowerCase();
}
});
callback && callback();
});
}
initDeviceState(callback) {
this.getDevices((err, result) => {
if (!err && result && Array.isArray(result.devices)) {
this.getDevicePreferences((err, devicePrefs) => {
const devicePreferences = {};
if (!err && devicePrefs && devicePrefs.devicePreferences && Array.isArray(devicePrefs.devicePreferences)) {
devicePrefs.devicePreferences.forEach(pref => {
devicePreferences[pref.deviceSerialNumber] = pref;
});
}
const customerIds = {};
const joinedDevices = [];
result.devices.forEach((device) => {
joinedDevices.push(device);
if (device.appDeviceList && device.appDeviceList.length) {
device.appDeviceList.forEach(subDevice => {
const appDevice = Object.assign({}, device, subDevice);
appDevice.parentDeviceSerialNumber = device.serialNumber;
appDevice.appDeviceList = [];
joinedDevices.push(appDevice);
});
}
});
const processedSerialNumbers = [];
joinedDevices.forEach((device) => {
const existingDevice = this.find(device.serialNumber);
if (!existingDevice) {
this.serialNumbers[device.serialNumber] = device;
}
else {
if (device.parentDeviceSerialNumber && processedSerialNumbers.includes(device.serialNumber)) return;
device = extend(true, existingDevice, device);
}
processedSerialNumbers.push(device.serialNumber);
if (devicePreferences[device.serialNumber]) {
device.preferences = devicePreferences[device.serialNumber];
}
let name = device.accountName;
this.names[name] = device;
this.names[name.toLowerCase()] = device;
if (device.deviceTypeFriendlyName) {
name += ` (${device.deviceTypeFriendlyName})`;
this.names[name] = device;
this.names[name.toLowerCase()] = device;
}
//device._orig = JSON.parse(JSON.stringify(device));
device._name = name;
device.sendCommand = this.sendCommand.bind(this, device);
device.setTunein = this.setTunein.bind(this, device);
device.playAudible = this.playAudible.bind(this, device);
device.rename = this.renameDevice.bind(this, device);
device.setDoNotDisturb = this.setDoNotDisturb.bind(this, device);
device.delete = this.deleteDevice.bind(this, device);
device.getDevicePreferences = this.getDevicePreferences.bind(this, device);
device.setDevicePreferences = this.setDevicePreferences.bind(this, device);
device.getNotificationSounds = this.getNotificationSounds.bind(this, device);
device.setDevicePreferences = this.setDevicePreferences.bind(this, device);
device.getDeviceNotificationState = this.getDeviceNotificationState.bind(this, device);
device.setDeviceNotificationVolume = this.setDeviceNotificationVolume.bind(this, device);
device.setDeviceAscendingAlarmState = this.setDeviceAscendingAlarmState.bind(this, device);
device.getDeviceNotificationDefaultSound = this.getDeviceNotificationDefaultSound.bind(this, device);
device.setDeviceNotificationDefaultSound = this.setDeviceNotificationDefaultSound.bind(this, device);
if (device.deviceTypeFriendlyName) this.friendlyNames[device.deviceTypeFriendlyName] = device;
if (customerIds[device.deviceOwnerCustomerId] === undefined) customerIds[device.deviceOwnerCustomerId] = 0;
customerIds[device.deviceOwnerCustomerId] += 1;
device.isControllable = (
device.capabilities.includes('AUDIO_PLAYER') ||
device.capabilities.includes('AMAZON_MUSIC') ||
device.capabilities.includes('TUNE_IN')||
device.capabilities.includes('AUDIBLE') ||
device.deviceFamily === 'FIRE_TV'
);
device.hasMusicPlayer = (
device.capabilities.includes('AUDIO_PLAYER') ||
device.capabilities.includes('AMAZON_MUSIC') ||
device.deviceFamily === 'FIRE_TV'
);
device.isMultiroomDevice = (device.clusterMembers.length > 0);
device.isMultiroomMember = (device.parentClusters.length > 0);
});
//this.ownerCustomerId = Object.keys(customerIds)[0]; // this could end in the wrong one!
callback && callback();
});
}
else {
callback && callback();
}
});
}
initBluetoothState(callback) {
if (this._options.bluetooth) {
this.getBluetooth((err, res) => {
if (err || !res || !Array.isArray(res.bluetoothStates)) {
this._options.bluetooth = false;
return callback && callback ();
}
const self = this;
res.bluetoothStates.forEach((bt) => {
if (bt.pairedDeviceList && this.serialNumbers[bt.deviceSerialNumber]) {
this.serialNumbers[bt.deviceSerialNumber].bluetoothState = bt;
bt.pairedDeviceList.forEach((d) => {
bt[d.address] = d;
d.connect = function (on, cb) {
self[on ? 'connectBluetooth' : 'disconnectBluetooth'] (self.serialNumbers[bt.deviceSerialNumber], d.address, cb);
};
d.unpaire = function (val, cb) {
self.unpaireBluetooth (self.serialNumbers[bt.deviceSerialNumber], d.address, cb);
};
});
}
});
callback && callback();
});
} else {
callback && callback();
}
}
stopProxyServer(callback) {
if (!this.alexaCookie) {
return callback && callback();
}
this.alexaCookie.stopProxyServer(callback);
}
/** @deprecated */
isWsMqttConnected() {
return this.isPushConnected();
}
isPushConnected() {
return this.alexahttp2Push && this.alexahttp2Push.isConnected();
}
/**
* @deprecated
*/
initWsMqttConnection() {
return this.initPushConnection();
}
simulateActivity(deviceSerialNumber, destinationUserId) {
if (!this._options.autoQueryActivityOnTrigger) return;
if (this.activityUpdateTimeout && this.activityUpdateQueue.some(entry => entry.deviceSerialNumber === deviceSerialNumber && entry.destinationUserId === destinationUserId)) return;
this._options.logger && this._options.logger(`Alexa-Remote: Simulate activity for ${deviceSerialNumber} with destinationUserId ${destinationUserId} ... fetch in 3s`);
if (this.activityUpdateTimeout) {
clearTimeout(this.activityUpdateTimeout);
this.activityUpdateTimeout = null;
}
this.activityUpdateQueue.push({
deviceSerialNumber: deviceSerialNumber,
destinationUserId: destinationUserId,
activityTimestamp: Date.now()
});
this.activityUpdateTimeout = setTimeout(() => {
this.activityUpdateTimeout = null;
this.getPushedActivities();
}, 4000);
}
initPushConnection() {
if (this.alexahttp2Push) {
this.alexahttp2Push.removeAllListeners();
this.alexahttp2Push.disconnect();
this.alexahttp2Push = null;
}
if (!this.authApiBearerToken) {
this.updateApiBearerToken((err) => {
if (err) {
this._options.logger && this._options.logger('Alexa-Remote: Initializing WS-MQTT Push Connection failed because no Access-Token available!');
} else {
return this.initPushConnection();
}
});
return;
}
this.alexahttp2Push = new AlexaHttp2Push(this._options, (callback) => {
this._options.logger && this._options.logger('Alexa-Remote: Update access token ...');
this.updateApiBearerToken((err) => {
if (err) {
this._options.logger && this._options.logger('Alexa-Remote: Initializing WS-MQTT Push Connection failed because no Access-Token available!');
callback(null);
} else {
callback(this.authApiBearerToken);
}
});
});
if (!this.alexahttp2Push) return;
this._options.logger && this._options.logger('Alexa-Remote: Initialize WS-MQTT Push Connection');
this.alexahttp2Push.on('disconnect', (retries, msg) => {
this.emit('ws-disconnect', retries, msg);
});
this.alexahttp2Push.on('error', (error) => {
this.emit('ws-error', error);
});
this.alexahttp2Push.on('connect', () => {
this.emit('ws-connect');
});
this.alexahttp2Push.on('unknown', (incomingMsg) => {
this.emit('ws-unknown-message', incomingMsg);
});
this.alexahttp2Push.on('command', (command, payload) => {
this.emit('command', { 'command': command, 'payload': payload });
switch(command) {
case 'PUSH_DOPPLER_CONNECTION_CHANGE':
/*
{
'destinationUserId': 'A3NSX4MMJVG96V',
'dopplerId': {
'deviceSerialNumber': 'c6c113ab49ff498185aa1ee5eb50cd73',
'deviceType': 'A3H674413M2EKB'
},
'dopplerConnectionState': 'OFFLINE' / 'ONLINE'
}
*/
this.emit('ws-device-connection-change', {
destinationUserId: payload.destinationUserId,
deviceSerialNumber: payload.dopplerId.deviceSerialNumber,
deviceType: payload.dopplerId.deviceType,
connectionState: payload.dopplerConnectionState
});
return;
case 'PUSH_BLUETOOTH_STATE_CHANGE':
/*
{
'destinationUserId': 'A3NSX4MMJVG96V',
'dopplerId': {
'deviceSerialNumber': 'G090LF09643202VS',
'deviceType': 'A3S5BH2HU6VAYF'
},
'bluetoothEvent': 'DEVICE_DISCONNECTED',
'bluetoothEventPayload': null,
'bluetoothEventSuccess': false/true
}
*/
this.emit('ws-bluetooth-state-change', {
destinationUserId: payload.destinationUserId,
deviceSerialNumber: payload.dopplerId.deviceSerialNumber,
deviceType: payload.dopplerId.deviceType,
bluetoothEvent: payload.bluetoothEvent,
bluetoothEventPayload: payload.bluetoothEventPayload,
bluetoothEventSuccess: payload.bluetoothEventSuccess
});
return;
case 'PUSH_AUDIO_PLAYER_STATE':
/*
{
'destinationUserId': 'A3NSX4MMJVG96V',
'mediaReferenceId': '2868373f-058d-464c-aac4-12e12aa58883:2',
'dopplerId': {
'deviceSerialNumber': 'G090LF09643202VS',
'deviceType': 'A3S5BH2HU6VAYF'
},
'error': false,
'audioPlayerState': 'INTERRUPTED', / 'FINISHED' / 'PLAYING'
'errorMessage': null
}
*/
this.emit('ws-audio-player-state-change', {
destinationUserId: payload.destinationUserId,
deviceSerialNumber: payload.dopplerId.deviceSerialNumber,
deviceType: payload.dopplerId.deviceType,
mediaReferenceId: payload.mediaReferenceId,
audioPlayerState: payload.audioPlayerState, // 'INTERRUPTED', / 'FINISHED' / 'PLAYING'
error: payload.error,
errorMessage: payload.errorMessage
});
return;
case 'PUSH_MEDIA_QUEUE_CHANGE':
/*
{
'destinationUserId': 'A3NSX4MMJVG96V',
'changeType': 'NEW_QUEUE',
'playBackOrder': null,
'trackOrderChanged': false,
'loopMode': null,
'dopplerId': {
'deviceSerialNumber': 'G090LF09643202VS',
'deviceType': 'A3S5BH2HU6VAYF'
}
}
*/
this.emit('ws-media-queue-change', {
destinationUserId: payload.destinationUserId,
deviceSerialNumber: payload.dopplerId.deviceSerialNumber,
deviceType: payload.dopplerId.deviceType,
changeType: payload.changeType,
playBackOrder: payload.playBackOrder,
trackOrderChanged: payload.trackOrderChanged,
loopMode: payload.loopMode
});
return;
case 'PUSH_MEDIA_CHANGE':
/*
{
'destinationUserId': 'A3NT1OXG4QHVPX',
'mediaReferenceId': '71c4d721-0e94-4b3e-b912-e1f27fcebba1:1',
'dopplerId': {
'deviceSerialNumber': 'G000JN0573370K82',
'deviceType': 'A1NL4BVLQ4L3N3'
}
}
*/
this.emit('ws-media-change', {
destinationUserId: payload.destinationUserId,
deviceSerialNumber: payload.dopplerId.deviceSerialNumber,
deviceType: payload.dopplerId.deviceType,
mediaReferenceId: payload.mediaReferenceId
});
return;
case 'PUSH_MEDIA_PROGRESS_CHANGE':
/*
{
"destinationUserId": "A2Z2SH760RV43M",
"progress": {
"mediaProgress": 899459,
"mediaLength": 0
},
"dopplerId": {
"deviceSerialNumber": "G2A0V7048513067J",
"deviceType": "A18O6U1UQFJ0XK"
},
"mediaReferenceId": "c4a72dbe-ef6b-42b7-8104-0766aa32386f:1"
}
*/
this.emit('ws-media-progress-change', {
destinationUserId: payload.destinationUserId,
deviceSerialNumber: payload.dopplerId.deviceSerialNumber,
deviceType: payload.dopplerId.deviceType,
mediaReferenceId: payload.mediaReferenceId,
mediaProgress: payload.progress ? payload.progress.mediaProgress : null,
mediaLength: payload.progress ? payload.progress.mediaLength : null
});
return;
case 'PUSH_VOLUME_CHANGE':
/*
{
'destinationUserId': 'A3NSX4MMJVG96V',
'dopplerId': {
'deviceSerialNumber': 'c6c113ab49ff498185aa1ee5eb50cd73',
'deviceType': 'A3H674413M2EKB'
},
'isMuted': false,
'volumeSetting': 50
}
*/
if (
!this.lastVolumes[payload.dopplerId.deviceSerialNumber] ||
(
this.lastVolumes[payload.dopplerId.deviceSerialNumber].volumeSetting === payload.volumeSetting &&
this.lastVolumes[payload.dopplerId.deviceSerialNumber].isMuted === payload.isMuted
) ||
(
this.lastEqualizer[payload.dopplerId.deviceSerialNumber] &&
Math.abs(Date.now() - this.lastEqualizer[payload.dopplerId.deviceSerialNumber].updated) < 2000
)
) {
this.simulateActivity(payload.dopplerId.deviceSerialNumber, payload.destinationUserId);
}
this.lastVolumes[payload.dopplerId.deviceSerialNumber] = {
volumeSetting: payload.volumeSetting,
isMuted: payload.isMuted,
updated: Date.now()
};
this.emit('ws-volume-change', {
destinationUserId: payload.destinationUserId,
deviceSerialNumber: payload.dopplerId.deviceSerialNumber,
deviceType: payload.dopplerId.deviceType,
isMuted: payload.isMuted,
volume: payload.volumeSetting
});
return;
case 'PUSH_CONTENT_FOCUS_CHANGE':
/*
{
'destinationUserId': 'A3NSX4MMJVG96V',
'clientId': '{value=Dee-Domain-Music}',
'dopplerId': {
'deviceSerialNumber': 'G090LF09643202VS',
'deviceType': 'A3S5BH2HU6VAYF'
},
'deviceComponent': 'com.amazon.dee.device.capability.audioplayer.AudioPlayer'
}
*/
this.emit('ws-content-focus-change', {
destinationUserId: payload.destinationUserId,
deviceSerialNumber: payload.dopplerId.deviceSerialNumber,
deviceType: payload.dopplerId.deviceType,
deviceComponent: payload.deviceComponent
});
return;
case 'PUSH_EQUALIZER_STATE_CHANGE':
/*
{
'destinationUserId': 'A3NSX4MMJVG96V',
'bass': 0,
'treble': 0,
'dopplerId': {
'deviceSerialNumber': 'G090LA09751707NU',
'deviceType': 'A2M35JJZWCQOMZ'
},
'midrange': 0
}
*/
if (
!this.lastEqualizer[payload.dopplerId.deviceSerialNumber] ||
(
this.lastEqualizer[payload.dopplerId.deviceSerialNumber].bass === payload.bass &&
this.lastEqualizer[payload.dopplerId.deviceSerialNumber].treble === payload.treble &&
this.lastEqualizer[payload.dopplerId.deviceSerialNumber].midrange === payload.midrange
) ||
(
this.lastVolumes[payload.dopplerId.deviceSerialNumber] &&
Math.abs(Date.now() - this.lastVolumes[payload.dopplerId.deviceSerialNumber].updated) < 2000
)
) {
this.simulateActivity(payload.dopplerId.deviceSerialNumber, payload.destinationUserId);
}
this.lastEqualizer[payload.dopplerId.deviceSerialNumber] = {
bass: payload.bass,
treble: payload.treble,
midrange: payload.midrange,
updated: Date.now()
};
this.emit('ws-equilizer-state-change', {
destinationUserId: payload.destinationUserId,
deviceSerialNumber: payload.dopplerId.deviceSerialNumber,
deviceType: payload.dopplerId.deviceType,
bass: payload.bass,
treble: payload.treble,
midrange: payload.midrange
});
return;
case 'PUSH_NOTIFICATION_CHANGE':
/*
{
'destinationUserId': 'A3NSX4MMJVG96V',
'dopplerId': {
'deviceSerialNumber': 'G090LF09643202VS',
'deviceType': 'A3S5BH2HU6VAYF'
},
'eventType': 'UPDATE',
'notificationId': 'd676d954-3c34-3559-83ac-606754ff6ec1',
'notificationVersion': 2
}
*/
this.emit('ws-notification-change', {
destinationUserId: payload.destinationUserId,
deviceSerialNumber: payload.dopplerId.deviceSerialNumber,
deviceType: payload.dopplerId.deviceType,
eventType: payload.eventType,
notificationId: payload.notificationId,
notificationVersion: payload.notificationVersion
});
return;
case 'PUSH_ACTIVITY':
/*
{
'destinationUserId': 'A3NSX4MMJVG96V',
'key': {
'entryId': '1533932315288#A3S5BH2HU6VAYF#G090LF09643202VS',
'registeredUserId': 'A3NSX4MMJVG96V'
},
'timestamp': 1533932316865
}
{
'_disambiguationId': null,
'activityStatus': 'SUCCESS', // DISCARDED_NON_DEVICE_DIRECTED_INTENT // FAULT
'creationTimestamp': 1533932315288,
'description': '{\'summary\':\'spiel Mike Oldfield von meine bibliothek\',\'firstUtteranceId\':\'TextClient:1.0/2018/08/10/20/G090LF09643202VS/18:35::TNIH_2V.cb0c133b-3f90-4f7f-a052-3d105529f423LPM\',\'firstStreamId\':\'TextClient:1.0/2018/08/10/20/G090LF09643202VS/18:35::TNIH_2V.cb0c133b-3f90-4f7f-a052-3d105529f423LPM\'}',
'domainAttributes': '{\'disambiguated\':false,\'nBestList\':[{\'entryType\':\'PlayMusic\',\'mediaOwnerCustomerId\':\'A3NSX4MMJVG96V\',\'playQueuePrime\':false,\'marketplace\':\'A1PA6795UKMFR9\',\'imageURL\':\'https://album-art-storage-eu.s3.amazonaws.com/93fff3ba94e25a666e300facd1ede29bf84e6e17083dc7e60c6074a77de71a1e_256x256.jpg?response-content-type=image%2Fjpeg&x-amz-security-token=FQoGZXIvYXdzEP3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDInhZqxchOhE%2FCQ3bSKrAWGE9OKTrShkN7rSKEYzYXH486T6c%2Bmcbru4RGEGu9Sq%2BL%2FpG5o2EWsHnRULSM4cpreC1KG%2BIfzo8nuskQk8fklDgIyrK%2B%2B%2BFUm7rxmTKWBjavbKQxEtrnQATgo7%2FghmztEmXC5r742uvyUyAjZcZ4chCezxa%2Fkbr00QTv1HX18Hj5%2FK4cgItr5Kyv2bfmFTZ2Jlvr8IbAQn0X0my1XpGJyjUuW8IGIPhqiCQyi627fbBQ%3D%3D&AWSAccessKeyId=ASIAZZLLX6KM4MGDNROA&Expires=1533935916&Signature=OozE%2FmJbIVVvK2CRhpa2VJPYudE%3D\',\'artistName\':\'Mike Oldfield\',\'serviceName\':\'CLOUD_PLAYER\',\'isAllSongs\':true,\'isPrime\':false}]}',
'domainType': null,
'feedbackAttributes': null,
'id': 'A3NSX4MMJVG96V#1533932315288#A3S5BH2HU6VAYF#G090LF09643202VS',
'intentType': null,
'providerInfoDescription': null,
'registeredCustomerId': 'A3NSX4MMJVG96V',
'sourceActiveUsers': null,
'sourceDeviceIds': [{
'deviceAccountId': null,
'deviceType': 'A3S5BH2HU6VAYF',
'serialNumber': 'G090LF09643202VS'
}],
'utteranceId': 'TextClient:1.0/2018/08/10/20/G090LF09643202VS/18:35::TNIH_2V.cb0c133b-3f90-4f7f-a052-3d105529f423LPM',
'version': 1
}
*/
this.activityUpdateQueue.push(payload);
if (this.activityUpdateTimeout) {
clearTimeout(this.activityUpdateTimeout);
this.activityUpdateTimeout = null;
}
this.activityUpdateTimeout = setTimeout(() => {
this.activityUpdateTimeout = null;
this.getPushedActivities();
}, 200);
return;
case 'PUSH_TODO_CHANGE': // does not exist?
case 'PUSH_LIST_CHANGE': // does not exist?
case 'PUSH_LIST_ITEM_CHANGE':
/*
{
destinationUserId:'A12XXXXXWISGT',
listId:'YW16bjEuYWNjb3VudC5BRzJGWEpGWE5DRDZNVzNRSUdFM0xLWkZCWFhRLVRBU0s=',
eventName:'itemCreated',
version:1,
listItemId:'c6852978-bb79-44dc-b7e5-8f5e577432cf'
}
*/
this.emit('ws-todo-change', {
destinationUserId: payload.destinationUserId,
eventType: payload.eventName, // itemCreated, itemUpdated (including checked ToDo), itemDeleted
listId: payload.listId,
listItemVersion: payload.version,
listItemId: payload.listItemId
});
return;
case 'PUSH_MICROPHONE_STATE':
case 'PUSH_DELETE_DOPPLER_ACTIVITIES':
case 'PUSH_DEVICE_SETUP_STATE_CHANGE':
case 'NotifyNowPlayingUpdated': // TODO
case 'NotifyMediaSessionsUpdated':
return;
}
this.emit('ws-unknown-command', command, payload);
});
this.alexahttp2Push.connect();
}
getPushedActivities() {
if (!this._options.autoQueryActivityOnTrigger) return;
this._options.logger && this._options.logger(`Alexa-Remote: Get pushed activities ... ${this.activityUpdateQueue.length} entries in queue (already running: ${this.activityUpdateRunning})`);
if (this.activityUpdateRunning || !this.activityUpdateQueue.length) return;
this.activityUpdateRunning = true;
let earliestActionDate = Date.now();
this.activityUpdateQueue.forEach(entry => {
if (entry.activityTimestamp < earliestActionDate) earliestActionDate = entry.activityTimestamp;
});
this.getCustomerHistoryRecords({
maxRecordSize: this.activityUpdateQueue.length + 2,
filter: false,
forceRequest: true,
startTime: earliestActionDate - 60000,
}, (err, res) => {
this.activityUpdateRunning = false;
if (!res || (err && err.message.includes('no body'))) {
err = null;
res = [];
}
if (!err) {
res.reverse();
this._options.logger && this._options.logger(`Alexa-Remote: Activity data ${JSON.stringify(res)}`); // TODO REMOVE
let lastFoundQueueIndex = -1;
this.activityUpdateQueue.forEach((entry, queueIndex) => {
if (entry.key) { // deprecated
const found = res.findIndex(activity => activity.data.recordKey.endsWith(`#${entry.key.entryId}`) && activity.data.customerId === entry.key.registeredUserId);
if (found === -1) {
this._options.logger && this._options.logger(`Alexa-Remote: Activity for id ${entry.key.entryId} not found`);
} else {
lastFoundQueueIndex = queueIndex;
this.activityUpdateQueue.splice(0, lastFoundQueueIndex + 1);
const activity = res.splice(found, 1)[0];
this._options.logger && this._options.logger(`Alexa-Remote: Activity found entry ${found} for Activity ID ${entry.key.entryId}`);
activity.destinationUserId = entry.destinationUserId;
this.emit('ws-device-activity', activity);
}
} else {
const lastPushedActivity = this.lastPushedActivity[entry.deviceSerialNumber] || Date.now() - 30000;
const found = res.filter(activity => activity.data.recordKey.endsWith(`#${entry.deviceSerialNumber}`) && activity.data.customerId === entry.destinationUserId && activity.creationTimestamp >= entry.activityTimestamp - 10000 && activity.creationTimestamp > lastPushedActivity); // Only if current stuff is found
if (found.length === 0) {
this._options.logger && this._options.logger(`Alexa-Remote: Activity for device ${entry.deviceSerialNumber} not found`);
} else {
let foundSomething = false;
found.forEach((activity, index) => {
if (activity.data.utteranceType === 'WAKE_WORD_ONLY' && index === 0 && this.activityUpdateNotFoundCounter > 0 && found.length > 1) return;
this._options.logger && this._options.logger(`Alexa-Remote: Activity (ts=${activity.creationTimestamp}) found for device ${entry.deviceSerialNumber}`);
activity.destinationUserId = entry.destinationUserId;
this.emit('ws-device-activity', activity);
if (activity.data.utteranceType !== 'WAKE_WORD_ONLY') {
this.lastPushedActivity[entry.deviceSerialNumber] = activity.creationTimestamp;
foundSomething = true;
} else {
this._options.logger && this._options.logger(`Alexa-Remote: Only Wakeword activity for device ${entry.deviceSerialNumber} found. try again in 2,5s`);
if (!foundSomething) {
lastFoundQueueIndex = -2;
}
}
});
if (foundSomething) {
lastFoundQueueIndex = queueIndex;
this.activityUpdateQueue.splice(queueIndex, 1);
}
}
}
});
if (lastFoundQueueIndex < 0) {
this._options.logger && this._options.logger(`Alexa-Remote: No activities from stored ${this.activityUpdateQueue.length} entries found in queue (${this.activityUpdateNotFoundCounter})`);
this.activityUpdateNotFoundCounter++;
if (
(lastFoundQueueIndex === -1 && this.activityUpdateNotFoundCounter > 2) || // 2 tries without wakeword
(lastFoundQueueIndex === -2 && this.activityUpdateNotFoundCounter > 5) // 5 tries with wakeword
) {
this._options.logger && this._options.logger('Alexa-Remote: Reset expected activities');
this.activityUpdateQueue = [];
this.activityUpdateNotFoundCounter = 0;
}
}
else {
this.activityUpdateNotFoundCounter = 0;
this._options.logger && this._options.logger(`Alexa-Remote: ${this.activityUpdateQueue.length} entries left in activity queue`);
}
}
if (!err && this.activityUpdateQueue.length) {
this.activityUpdateTimeout = setTimeout(() => {
this.activityUpdateTimeout = null;
this.getPushedActivities();
}, 4000);
}
});
}
stop() {
if (this.cookieRefreshTimeout) {
clearTimeout(this.cookieRefreshTimeout);
this.cookieRefreshTimeout = null;
}
if (this.alexahttp2Push) {
this.alexahttp2Push.disconnect();
}
}
generateCookie(email, password, callback) {
if (!this.alexaCookie) this.alexaCookie = require('alexa-cookie2');
this.alexaCookie.generateAlexaCookie(email, password, this._options, callback);
}
refreshCookie(callback) {
if (!this.alexaCookie) this.alexaCookie = require('alexa-cookie2');
this.alexaCookie.refreshAlexaCookie(this._options, callback);
}
getAuthApiBearerToken(callback) {
if (!this.alexaCookie) this.alexaCookie = require('alexa-cookie2');
const deviceAppName = (this._options.formerRegistrationData && this._options.formerRegistrationData.deviceAppName) || this.alexaCookie.getDeviceAppName(); // Use the App Name from last cookie response or use default one
this.httpsGet(true, `https://api.${th