UNPKG

alexa-remote2

Version:

Remote Control for amazon echo devices

1,046 lines (980 loc) 166 kB
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