From 8c043d524b62dff8737c77094e9210fe36ecb3c6 Mon Sep 17 00:00:00 2001 From: Grzegorz Date: Sun, 19 Jan 2025 10:06:25 +0100 Subject: [PATCH] release v3.3.0 --- CHANGELOG.md | 11 + README.md | 7 +- config.schema.json | 39 +- index.js | 44 +- package-lock.json | 18 +- package.json | 4 +- sample-config.json | 35 +- src/impulsegenerator.js | 26 +- src/localApi/xboxlocalapi.js | 1071 +++++++++++++++++----------------- src/mqtt.js | 6 +- src/restful.js | 6 +- src/webApi/xboxwebapi.js | 22 +- src/xboxdevice.js | 817 +++++++++++++------------- 13 files changed, 1080 insertions(+), 1026 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4063caa..9b5a7e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - After update to 2.x.x the plugin settings (xboxLiveId) need to be updated - After update to v3.0.0 RESTFull and MQTT config settings need to be updated +## [3.3.0] - (19.01.2025) + +## Changes + +- added possibility to disable/enable log success, info, warn, error +- refactor cnnect code +- bump dependencies +- config schema updated +- redme updated +- cleanup + ## [3.2.0] - (30.11.2024) ## Changes diff --git a/README.md b/README.md index 47cfd12..5a4d53a 100644 --- a/README.md +++ b/README.md @@ -106,9 +106,12 @@ Homebridge plugin for Microsoft game Consoles. Tested with Xbox One X/S and Xbox | `volumeControlName` | Here set Your own volume control name or leave empty. | | `volumeControl` | Here choice what a additional volume control mode You want to use (`0 - None/Disabled`, `1 - Lightbulb`, `2 - Fan`), not working yet. | | `infoButtonCommand` | Here select the function of `I` button in RC app. | -| `enableDebugMode` | If enabled, deep log will be present in homebridge console. | -| `disableLogInfo` | If enabled, disable log info, all values and state will not be displayed in Homebridge log console. | | `disableLogDeviceInfo` | If enabled, add ability to disable log device info by every connections device to the network. | +| `disableLogInfo` | If enabled, disable log info, all values and state will not be displayed in Homebridge log console. | +| `disableLogSuccess` | If enabled, disable logging device success. | +| `disableLogWarn` | If enabled, disable logging device warnings. | +| `disableLogError` | If enabled, disable logging device error. | +| `enableDebugMode` | If enabled, deep log will be present in homebridge console. | | `restFul` | This is RSTful server. | | `enable` | If enabled, RESTful server will start automatically and respond to any path request. | | `port` | Here set the listening `Port` for RESTful server. | diff --git a/config.schema.json b/config.schema.json index 4d984b1..786f80d 100644 --- a/config.schema.json +++ b/config.schema.json @@ -696,25 +696,41 @@ ], "required": false }, - "enableDebugMode": { - "title": "Debug", + "disableLogDeviceInfo": { + "title": "Disable Log Device Info", "type": "boolean", "default": false, - "description": "This enable debug mode.", + "description": "This disable logging device info by every connections device to the network.", "required": false }, "disableLogInfo": { "title": "Disable Log Info", "type": "boolean", "default": false, - "description": "This disable logging values and states on every it change.", "required": false }, - "disableLogDeviceInfo": { - "title": "Disable Log Device Info", + "disableLogSuccess": { + "title": "Disable Log Success", + "type": "boolean", + "default": false, + "required": false + }, + "disableLogWarn": { + "title": "Disable Log Warn", + "type": "boolean", + "default": false, + "required": false + }, + "disableLogError": { + "title": "Disable Log Error", + "type": "boolean", + "default": false, + "required": false + }, + "enableDebugMode": { + "title": "Enable Log Debug", "type": "boolean", "default": false, - "description": "This disable log device info by every connections device to the network.", "required": false }, "restFul": { @@ -983,11 +999,14 @@ }, { "key": "devices[]", - "title": "System", + "title": "Log", "items": [ - "devices[].enableDebugMode", + "devices[].disableLogDeviceInfo", "devices[].disableLogInfo", - "devices[].disableLogDeviceInfo" + "devices[].disableLogSuccess", + "devices[].disableLogWarn", + "devices[].disableLogError", + "devices[].enableDebugMode" ] }, { diff --git a/index.js b/index.js index 96b011c..0712190 100644 --- a/index.js +++ b/index.js @@ -33,9 +33,14 @@ class XboxPlatform { return; } - //debug config - const enableDebugMode = device.enableDebugMode; - const debug = enableDebugMode ? log.info(`Device: ${host} ${deviceName}, Did finish launching.`) : false; + //log config + const enableDebugMode = device.enableDebugMode || false; + const disableLogDeviceInfo = device.disableLogDeviceInfo || false; + const disableLogInfo = device.disableLogInfo || false; + const disableLogSuccess = device.disableLogSuccess || false; + const disableLogWarn = device.disableLogWarn || false; + const disableLogError = device.disableLogError || false; + const debug = !enableDebugMode ? false : log.info(`Device: ${host} ${deviceName}, did finish launching.`); const config = { ...device, xboxLiveId: 'removed', @@ -46,7 +51,7 @@ class XboxPlatform { passwd: 'removed' } }; - const debug1 = enableDebugMode ? log.info(`Device: ${host} ${deviceName}, Config: ${JSON.stringify(config, null, 2)}`) : false; + const debug1 = !enableDebugMode ? false : log.info(`Device: ${host} ${deviceName}, Config: ${JSON.stringify(config, null, 2)}.`); //check files exists, if not then create it const postFix = host.split('.').join(''); @@ -72,7 +77,7 @@ class XboxPlatform { } }); } catch (error) { - log.error(`Device: ${host} ${deviceName}, Prepare files error: ${error}`); + const emitLog = disableLogError ? false : log.error(`Device: ${host} ${deviceName}, Prepare files error: ${error}.`); return; } @@ -81,45 +86,46 @@ class XboxPlatform { const xboxDevice = new XboxDevice(api, device, authTokenFile, devInfoFile, inputsFile, inputsNamesFile, inputsTargetVisibilityFile); xboxDevice.on('publishAccessory', (accessory) => { api.publishExternalAccessories(PluginName, [accessory]); - log.success(`Device: ${host} ${deviceName}, Published as external accessory.`); + const emitLog = disableLogSuccess ? false : log.success(`Device: ${host} ${deviceName}, Published as external accessory.`); }) .on('devInfo', (devInfo) => { - log.info(devInfo); + const emitLog = disableLogDeviceInfo ? false : log.info(devInfo); }) - .on('success', (message) => { - log.success(`Device: ${host} ${deviceName}, ${message}`); + .on('success', (success) => { + const emitLog = disableLogSuccess ? false : log.success(`Device: ${host} ${deviceName}, ${success}.`); }) - .on('message', (message) => { - log.info(`Device: ${host} ${deviceName}, ${message}`); + .on('info', (info) => { + const emitLog = disableLogInfo ? false : log.info(`Device: ${host} ${deviceName}, ${info}.`); }) .on('debug', (debug) => { - log.info(`Device: ${host} ${deviceName}, debug: ${debug}`); + const emitLog = !enableDebugMode ? false : log.info(`Device: ${host} ${deviceName}, debug: ${debug}.`); }) .on('warn', (warn) => { - log.warn(`warn: ${host} ${deviceName}, ${warn}`); + const lemitLogog = disableLogWarn ? false : log.warn(`Device: ${host} ${deviceName}, ${warn}.`); }) .on('error', (error) => { - log.error(`Device: ${host} ${deviceName}, ${error}`); + const emitLog = disableLogError ? false : log.error(`Device: ${host} ${deviceName}, ${error}.`); }); //create impulse generator const impulseGenerator = new ImpulseGenerator(); impulseGenerator.on('start', async () => { try { - await xboxDevice.start(); - impulseGenerator.stop(); + const startDone = await xboxDevice.start(); + const stopImpulseGenerator = startDone ? impulseGenerator.stop() : false; } catch (error) { - const logError = disableLogConnectError ? false : log.error(`Device: ${host} ${deviceName}, ${error}, trying again.`); + const emitLog = disableLogError ? false : log.error(`Device: ${host} ${deviceName}, ${error}, trying again.`); }; }).on('state', (state) => { - const debug = enableDebugMode ? state ? log.info(`Device: ${host} ${deviceName}, Start impulse generator started.`) : log.info(`Device: ${host} ${deviceName}, Start impulse generator stopped.`) : false; + const emitLog = !enableDebugMode ? false : state ? log.info(`Device: ${host} ${deviceName}, Start impulse generator started.`) : log.info(`Device: ${host} ${deviceName}, Start impulse generator stopped.`); }); //start impulse generator impulseGenerator.start([{ name: 'start', sampling: 45000 }]); } catch (error) { - log.error(`Device: ${host} ${deviceName}, Did finish launching error: ${error}`); + throw new Error(`Device: ${host} ${deviceName}, Did finish launching error: ${error}.`); } + await new Promise(resolve => setTimeout(resolve, 500)); } }); } diff --git a/package-lock.json b/package-lock.json index b594071..268c7d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-xbox-tv", - "version": "3.2.2", + "version": "3.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "homebridge-xbox-tv", - "version": "3.2.2", + "version": "3.3.0", "license": "MIT", "dependencies": { "@homebridge/plugin-ui-utils": "^2.0.0", @@ -17,7 +17,7 @@ "hex-to-binary": "^1.0.1", "jsrsasign": "^11.1.0", "ping": "^0.4.4", - "uuid": "^11.0.3" + "uuid": "^11.0.5" }, "engines": { "homebridge": "^1.6.0 || ^2.0.0-beta.0", @@ -1337,9 +1337,9 @@ } }, "node_modules/uuid": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", - "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -2336,9 +2336,9 @@ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, "uuid": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", - "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==" + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==" }, "vary": { "version": "1.1.2", diff --git a/package.json b/package.json index 87cda62..4ecf559 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "displayName": "Xbox TV", "name": "homebridge-xbox-tv", - "version": "3.2.2", + "version": "3.3.0", "description": "Homebridge plugin to control Xbox game consoles.", "license": "MIT", "author": "grzegorz914", @@ -40,7 +40,7 @@ "elliptic": "^6.6.1", "hex-to-binary": "^1.0.1", "jsrsasign": "^11.1.0", - "uuid": "^11.0.3", + "uuid": "^11.0.5", "ping": "^0.4.4", "express": "^4.21.2", "axios": "^1.7.9" diff --git a/sample-config.json b/sample-config.json index 7d00f9f..52fb912 100644 --- a/sample-config.json +++ b/sample-config.json @@ -89,21 +89,28 @@ "volumeControlNamePrefix": false, "volumeControlName": "Volume", "volumeControl": 0, - "enableDebugMode": false, - "disableLogInfo": false, "disableLogDeviceInfo": false, - "enableRestFul": false, - "restFulPort": 3000, - "restFulDebug": false, - "enableMqtt": false, - "mqttDebug": false, - "mqttHost": "192.168.1.33", - "mqttPort": 1883, - "mqttClientId": "", - "mqttPrefix": "home/xbox", - "mqttAuth": false, - "mqttUser": "user", - "mqttPass": "password" + "disableLogInfo": false, + "disableLogSuccess": false, + "disableLogWarn": false, + "disableLogError": false, + "enableDebugMode": false, + "restFul": { + "enable": false, + "port": 3000, + "debug": false + }, + "mqtt": { + "enable": false, + "host": "192.168.1.33", + "port": 1883, + "clientId": "", + "prefix": "home/envoy", + "auth": false, + "user": "user", + "pass": "password", + "debug": false + } } ] } diff --git a/src/impulsegenerator.js b/src/impulsegenerator.js index fbed086..c35b46d 100644 --- a/src/impulsegenerator.js +++ b/src/impulsegenerator.js @@ -8,7 +8,8 @@ class ImpulseGenerator extends EventEmitter { async start(timers) { if (this.timersState) { - await this.stop(); + this.state(true); + return true; } this.timers = []; @@ -22,28 +23,27 @@ class ImpulseGenerator extends EventEmitter { }; //update state - this.timersState = true; - this.state(); - + this.state(true); return true; } async stop() { - if (this.timersState) { - this.timers.forEach(timer => clearInterval(timer)); + if (!this.timersState) { + this.state(false); + return true; } //update state + this.timers.forEach(timer => clearInterval(timer)); this.timers = []; - this.timersState = false; - this.state(); - - return true; + this.state(false); + return true } - state() { - this.emit('state', this.timersState); - return this.timersState; + state(state) { + this.timersState = state; + this.emit('state', state); } } export default ImpulseGenerator; + diff --git a/src/localApi/xboxlocalapi.js b/src/localApi/xboxlocalapi.js index affaf13..732496a 100644 --- a/src/localApi/xboxlocalapi.js +++ b/src/localApi/xboxlocalapi.js @@ -5,9 +5,10 @@ import { parse as UuIdParse, v4 as UuIdv4 } from 'uuid'; import EventEmitter from 'events'; import Ping from 'ping'; import SimplePacket from './simple.js'; -import MessagePacket from'./message.js'; +import MessagePacket from './message.js'; import SGCrypto from './sgcrypto.js'; import { LocalApi } from '../constants.js'; +import ImpulseGenerator from '../impulsegenerator.js'; class XboxLocalApi extends EventEmitter { constructor(config) { @@ -19,8 +20,8 @@ class XboxLocalApi extends EventEmitter { this.xboxLiveId = config.xboxLiveId; this.tokensFile = config.tokensFile; this.devInfoFile = config.devInfoFile; - this.infoLog = config.infoLog; - this.debugLog = config.debugLog; + this.disableLogInfo = config.disableLogInfo; + this.enableDebugMode = config.enableDebugMode; this.isConnected = false; this.isAuthorized = false; @@ -29,614 +30,614 @@ class XboxLocalApi extends EventEmitter { this.channels = []; this.channelRequestId = 0; this.mediaRequestId = 0; - }; - - //dgram socket - async connect() { - try { - const socket = Dgram.createSocket(this.udpType); - socket.on('error', (error) => { - this.emit('error', `Socket error: ${error}`); - socket.close(); - }).on('close', async () => { - const debug = this.debugLog ? this.emit('debug', 'Socket closed.') : false; - this.isConnected = false; - try { - await this.reconnect(); - } catch (error) { - this.emit('error', error) - }; - }).on('message', async (message, remote) => { - const debug = this.debugLog ? this.emit('debug', `Received message from: ${remote.address}:${remote.port}`) : false; - this.heartBeatStartTime = Date.now(); - - //get message type in hex - const messageTypeHex = message.slice(0, 2).toString('hex'); - const debug1 = this.debugLog ? this.emit('debug', `Received message type hex: ${messageTypeHex}`) : false; - - //check message type exist in types - const keysTypes = Object.keys(LocalApi.Messages.Types); - const keysTypesExist = keysTypes.includes(messageTypeHex); - - if (!keysTypesExist) { - const debug = this.debugLog ? this.emit('debug', `Received unknown message type: ${messageTypeHex}`) : false; - return; - }; - //get message type and request - const messageType = LocalApi.Messages.Types[messageTypeHex]; - const messageRequest = LocalApi.Messages.Requests[messageTypeHex]; - const debug2 = this.debugLog ? this.emit('debug', `Received message type: ${messageType}, request: ${messageRequest}`) : false; + //create impulse generator + this.impulseGenerator = new ImpulseGenerator(); + this.impulseGenerator.on('heartBeat', async () => { + try { + if (!this.isConnected) { + const debug = this.enableDebugMode ? this.emit('debug', `Plugin send heartbeat to console`) : false; + const state = await Ping.promise.probe(this.host, { timeout: 3 }); - let packeStructure; - switch (messageType) { - case 'simple': - packeStructure = new SimplePacket(messageRequest); - break; - case 'message': - packeStructure = new MessagePacket(messageRequest); - break; - }; + if (!state.alive || this.isConnected) { + return; + } - let packet = packeStructure.unpack(this.crypto, message); - const type = packet.type; - const debug3 = this.debugLog ? this.emit('debug', `Received type: ${type}`) : false; - const debug4 = this.debugLog ? this.emit('debug', `Received packet: ${JSON.stringify(packet, null, 2)}`) : false; + const debug1 = this.enableDebugMode ? this.emit('debug', `Plugin received heartbeat from console`) : false + const discoveryRequest = new SimplePacket('discoveryRequest'); + const message = discoveryRequest.pack(this.crypto); + await this.sendSocketMessage(message, 'discoveryRequest'); + } - if (messageType === 'message' && packet.targetParticipantId !== this.sourceParticipantId) { - const debug = this.debugLog ? this.emit('debug', `Participant Id: ${packet.targetParticipantId} !== ${this.sourceParticipantId}. Ignoring packet.`) : false; - return; + if (this.isConnected) { + const debug = this.enableDebugMode ? this.emit('debug', `Socket send heartbeat`) : false; + const elapse = (Date.now() - this.heartBeatStartTime) / 1000; + const debug1 = this.enableDebugMode && elapse >= 12 ? this.emit('debug', `Last message was: ${elapse} sec ago.`) : false; + const disconnect = elapse >= 12 ? await this.disconnect() : false; }; + } catch (error) { + this.emit('error', `Impulse generatotr error: ${error}, trying again`); + }; + }).on('state', (state) => { + const emit = state ? this.emit('success', `Heartbeat started`) : this.emit('warn', `Heartbeat stopped`); + }); + }; - switch (type) { - case 'json': - // Object to hold fragments - const fragments = {}; - const jsonMessage = JSON.parse(packet.payloadProtected.json); - const datagramId = jsonMessage.datagramId; - - // Check if JSON is fragmented - if (datagramId) { - const debug = this.debugLog ? this.emit('debug', `JSON message datagram Id: ${datagramId}`) : false; - - let partials = {}; - if (!fragments[datagramId]) { - fragments[datagramId] = { - getValue() { - const buffers = Object.values(partials).map(partial => Buffer.from(partial, 'base64')); - return Buffer.concat(buffers); - }, - isValid() { - try { - JSON.parse(this.getValue()); - return true; - } catch (error) { - this.emit('error', `Valid fragments error: ${error}`); - return false; - } - } - }; - } - partials[jsonMessage.fragmentOffset] = jsonMessage.fragmentData; - if (fragments[datagramId].isValid()) { - packet.payloadProtected.json = fragments[datagramId].getValue(); - const debug = this.debugLog ? this.emit('debug', `JSON fragments: ${fragments}`) : false; + async reconnect() { + await new Promise(resolve => setTimeout(resolve, 5000)); + try { + await this.connect(); + } catch (error) { + throw new Error(`Reconnect error: ${error.message || error}`); + } + }; - // Delete - delete fragments[datagramId]; - } - }; - break; - case 'discoveryResponse': - const deviceType = packet.clientType; - const deviceName = packet.name; - const certificate = packet.certificate; - const debug = this.debugLog ? this.emit('debug', `Discovered device: ${LocalApi.Console.Name[deviceType]}, name: ${deviceName}`) : false; + async readData(path) { + try { + const data = await fsPromises.readFile(path); + return data; + } catch (error) { + throw new Error(`Read data error: ${error.message || error}`); + } + } - try { - // Sign public key - const data = await this.crypto.getPublicKey(certificate); - const debug = this.debugLog ? this.emit('debug', `Signed public key: ${data.publicKey}, iv: ${data.iv}`) : false; + async saveData(path, data) { + try { + data = JSON.stringify(data, null, 2); + await fsPromises.writeFile(path, data); + const debug = this.enableDebugMode ? this.emit('debug', `Saved data: ${data}`) : false; + return true; + } catch (error) { + throw new Error(`Save data error: ${error.message || error}`); + }; + }; - // Connect request - if (!this.isConnected) { - try { - const connectRequest = new SimplePacket('connectRequest'); - connectRequest.set('uuid', Buffer.from(UuIdParse(UuIdv4()))); - connectRequest.set('publicKey', data.publicKey); - connectRequest.set('iv', data.iv); + async acknowledge(sequenceNumber) { + try { + const acknowledge = new MessagePacket('acknowledge'); + acknowledge.set('lowWatermark', sequenceNumber); + acknowledge.packet.processedList.value.push({ + id: sequenceNumber + }); + const sequenceNumber1 = await this.getSequenceNumber(); + const message = acknowledge.pack(this.crypto, sequenceNumber1, this.sourceParticipantId); + await this.sendSocketMessage(message, 'acknowledge'); + return true; + } catch (error) { + throw new Error(error); + }; + } - const token = await this.readData(this.tokensFile); - const parseTokenData = JSON.parse(token); - const tokenData = parseTokenData.xsts.Token.length > 0 ? parseTokenData : false; - if (tokenData) { - connectRequest.set('userHash', tokenData.xsts.DisplayClaims.xui[0].uhs, true); - connectRequest.set('jwt', tokenData.xsts.Token, true); - this.isAuthorized = true; - } - const debug = this.debugLog ? this.emit('debug', `Client connecting using: ${this.isAuthorized ? 'XSTS token' : 'Anonymous'}.`) : false; + async channelOpen(channelRequestId, service) { + const debug = this.enableDebugMode ? this.emit('debug', `Received channel Id: ${channelRequestId}, request open.`) : false; + this.channelRequestId = channelRequestId; - const message = connectRequest.pack(this.crypto); - await this.sendSocketMessage(message, 'connectRequest'); - } catch (error) { - this.emit('error', `Send connect request error: ${error}`) - }; - }; - } catch (error) { - this.emit('error', `Sign certificate error: ${error}`) - }; - break; - case 'connectResponse': - const connectionResult = packet.payloadProtected.connectResult; - const pairingState = packet.payloadProtected.pairingState; - const sourceParticipantId = packet.payloadProtected.participantId; - if (connectionResult !== 0) { - const errorTable = { - 0: 'Success.', - 1: 'Pending login. Reconnect to complete.', - 2: 'Unknown.', - 3: 'Anonymous connections disabled.', - 4: 'Device limit exceeded.', - 5: 'Remote connect is disabled on the console.', - 6: 'User authentication failed.', - 7: 'User Sign-In failed.', - 8: 'User Sign-In timeout.', - 9: 'User Sign-In required.' - }; - this.emit('error', `Connect error: ${errorTable[connectionResult]}`); - return; - } - this.isConnected = true; - this.sourceParticipantId = sourceParticipantId; - const debug1 = this.debugLog ? this.emit('debug', `Client connect state: ${this.isConnected ? 'Connected' : 'Not Connected'}.`) : false; - const debug2 = this.debugLog ? this.emit('debug', `Client pairing state: ${LocalApi.Console.PairingState[pairingState]}.`) : false; + try { + const channelStartRequest = new MessagePacket('channelStartRequest'); + channelStartRequest.set('channelRequestId', channelRequestId); + channelStartRequest.set('titleId', 0); + channelStartRequest.set('service', Buffer.from(service, 'hex')); + channelStartRequest.set('activityId', 0); + const sequenceNumber = await this.getSequenceNumber(); + const message = channelStartRequest.pack(this.crypto, sequenceNumber, this.sourceParticipantId); + await this.sendSocketMessage(message, 'channelStartRequest'); + return true; + } catch (error) { + throw new Error(error); + } + } - try { - const localJoin = new MessagePacket('localJoin'); - const sequenceNumber = await this.getSequenceNumber(); - const localJointMessage = localJoin.pack(this.crypto, sequenceNumber, sourceParticipantId); - await this.sendSocketMessage(localJointMessage, 'localJoin'); + async powerOn() { + if (this.isConnected) { + this.emit('warn', 'Console already On.'); + }; - // Open channels - try { - const channelNames = ['Input', 'TvRemote', 'Media']; - for (const channelName of channelNames) { - const channelRequestId = LocalApi.Channels.System[channelName].Id - const service = LocalApi.Channels.System[channelName].Uuid - //await this.channelOpen(channelRequestId, service); - }; - } catch (error) { - this.emit('error', `Channel open error: ${error}`) - }; - } catch (error) { - this.emit('error', `Send local join error: ${error}`) - }; - break; - case 'acknowledge': - const needAck = packet.flags.needAck; - if (!needAck) { - return; - }; + const info = this.disableLogInfo ? false : this.emit('info', 'Send power On.'); + try { + for (let i = 0; i < 15; i++) { + if (this.isConnected) { + return true; + } - try { - const sequenceNumber = packet.sequenceNumber; - const acknowledge = new MessagePacket('acknowledge'); - acknowledge.set('lowWatermark', sequenceNumber); - acknowledge.packet.processedList.value.push({ id: sequenceNumber }); - const sequenceNumber1 = await this.getSequenceNumber(); - const message = acknowledge.pack(this.crypto, sequenceNumber1, this.sourceParticipantId); - await this.sendSocketMessage(message, 'acknowledge'); - } catch (error) { - this.emit('error', `Send acknowledge error: ${error}`) - }; - break; - case 'consoleStatus': - if (!packet.payloadProtected) { - return; - }; + const powerOn = new SimplePacket('powerOn'); + powerOn.set('liveId', this.xboxLiveId); + const message = powerOn.pack(this.crypto); + await this.sendSocketMessage(message, 'powerOn'); - if (this.emitDevInfo) { - //connect to deice success - this.emit('success', `Connect Success.`) + await new Promise(resolve => setTimeout(resolve, 1000)); + return true; + } + this.emit('disconnected', 'Power On failed, please try again.'); + } catch (error) { + this.emit('disconnected', 'Power On error, please try again.'); + throw new Error(error); + }; + }; - const majorVersion = packet.payloadProtected.majorVersion; - const minorVersion = packet.payloadProtected.minorVersion; - const buildNumber = packet.payloadProtected.buildNumber; - const locale = packet.payloadProtected.locale; - const firmwareRevision = `${majorVersion}.${minorVersion}.${buildNumber}`; + async powerOff() { + if (!this.isConnected) { + this.emit('warn', 'Console already Off.'); + }; - const obj = { - manufacturer: 'Microsoft', - modelName: 'Xbox', - serialNumber: this.xboxLiveId, - firmwareRevision: firmwareRevision, - locale: locale - }; + const info = this.disableLogInfo ? false : this.emit('info', 'Send power Off.'); + try { + const powerOff = new MessagePacket('powerOff'); + powerOff.set('liveId', this.xboxLiveId); + const sequenceNumber = await this.getSequenceNumber(); + const message = powerOff.pack(this.crypto, sequenceNumber, this.sourceParticipantId); + await this.sendSocketMessage(message, 'powerOff'); - //save device info to the file - await this.saveData(this.devInfoFile, obj) + await new Promise(resolve => setTimeout(resolve, 3500)); + await this.disconnect(); + return true; + } catch (error) { + throw new Error(error); + }; - //emit device info - this.emit('deviceInfo', firmwareRevision, locale); - this.emitDevInfo = false; - }; + }; - //start external integration - this.emit('externalIntegrations'); + async recordGameDvr() { + if (!this.isConnected || !this.isAuthorized) { + this.emit('warn', `Send record game ignored, connection state: ${this.isConnected}, authorization state: ${this.isAuthorized}`); + }; - //prepare accessory - this.emit('prepareAccessory'); + const info = this.disableLogInfo ? false : this.emit('info', 'Send record game.'); + try { + const recordGameDvr = new MessagePacket('recordGameDvr'); + recordGameDvr.set('startTimeDelta', -60); + recordGameDvr.set('endTimeDelta', 0); + const sequenceNumber = await this.getSequenceNumber(); + const message = recordGameDvr.pack(this.crypto, sequenceNumber, this.sourceParticipantId); + await this.sendSocketMessage(message, 'recordGameDvr'); + return true; + } catch (error) { + throw new Error(error); + }; + }; - const appsCount = Array.isArray(packet.payloadProtected.activeTitles) ? packet.payloadProtected.activeTitles.length : 0; - if (appsCount > 0) { - const power = true; - const volume = 0; - const mute = power ? power : true; - const mediaState = 2; - const titleId = appsCount === 2 ? packet.payloadProtected.activeTitles[0].titleId : packet.payloadProtected.activeTitles[0].titleId; - const reference = appsCount === 2 ? packet.payloadProtected.activeTitles[0].aumId : packet.payloadProtected.activeTitles[0].aumId; + async sendButtonPress(channelName, command) { + if (!this.isConnected) { + this.emit('waarn', `Send command ignored, connection state: ${this.isConnected ? 'Not Connected' : 'Connected'}.`); + }; - this.emit('stateChanged', power, volume, mute, mediaState, titleId, reference); - const debug = this.debugLog ? this.emit('debug', `Status changed, app Id: ${titleId}, reference: ${reference}`) : false; + const channelRequestId = LocalApi.Channels.System[channelName].Id + const channelCommunicationId = this.channels[requestId].id; + const channelOpen = this.channels[requestId].open; - //emit restFul and mqtt - const obj = { - 'power': power, - 'titleId': titleId, - 'app': reference, - 'volume': volume, - 'mute': mute, - 'mediaState': mediaState, - }; - this.emit('restFul', 'state', obj); - this.emit('mqtt', 'State', obj); - }; + if (channelCommunicationId === -1 || !channelOpen) { + this.emit('warn', `Channel Id: ${channelCommunicationId}, state: ${channelOpen ? 'Open' : 'Closed'}, trying to open it.`); + }; - //acknowledge - try { - const acknowledge = packet.flags.needAck ? await this.acknowledge(packet.sequenceNumber) : false; - } catch (error) { - this.emit('error', `Acknowledge error: ${error}`) - }; - break; - case 'mediaState': + command = LocalApi.Channels.System[channelName][command]; + const debug = this.enableDebugMode ? this.emit('debug', `Channel communication Id: ${channelCommunicationId}, name: ${channelName} opened, send command; ${command}.`) : false; - break; - case 'mediaCommandResult': + switch (channelRequestId) { + case 0: + try { + const gamepad = new MessagePacket('gamepad'); + gamepad.set('timestamp', Buffer.from(`000${new Date().getTime().toString()}`, 'hex')); + gamepad.set('buttons', command); + const sequenceNumber = await this.getSequenceNumber(); + const message = gamepad.pack(this.crypto, sequenceNumber, this.sourceParticipantId, channelCommunicationId); + await this.sendSocketMessage(message, 'gamepad'); - break; - case 'channelStartResponse': - const requestIdMatch = this.channelRequestId === packet.payloadProtected.channelRequestId; - const channelState = requestIdMatch ? packet.payloadProtected.result === 0 : false; - const debug5 = this.debugLog ? this.emit('debug', `channelState: ${channelState}`) : false; - const channelCommunicationId = channelState && packet.payloadProtected.channelTargetId ? packet.payloadProtected.channelTargetId : -1; - const debug6 = this.debugLog ? this.emit('debug', `channelCommunicationId: ${packet.payloadProtected.channelTargetId}`) : false; - const channel = { - id: channelCommunicationId, - open: channelState + setTimeout(async () => { + const gamepadUnpress = new MessagePacket('gamepad'); + gamepadUnpress.set('timestamp', Buffer.from(`000${new Date().getTime().toString()}`, 'hex')); + gamepadUnpress.set('buttons', LocalApi.Channels.System.Input.Commands.unpress); + const sequenceNumber1 = await this.getSequenceNumber(); + const message = gamepadUnpress.pack(this.crypto, sequenceNumber1, this.sourceParticipantId, channelCommunicationId); + await this.sendSocketMessage(message, 'gamepadUnpress'); + }, 150) + } catch (error) { + this.emit('warn', `Send system input command error: ${error}`) + }; + break; + case 1: + try { + const sequenceNumber = await this.getSequenceNumber(); + const jsonRequest = JSON.stringify({ + msgid: `2ed6c0fd.${sequenceNumber}`, + request: LocalApi.Channels.System.TvRemote.MessageType.SendKey, + params: { + button_id: command, + device_id: null } - const pushChannel = channelCommunicationId !== -1 ? this.channels.push(channel) : false; - const debug3 = this.debugLog ? this.emit('debug', `Channel communication Id: ${channelCommunicationId}, state: ${channelState ? 'Open' : 'Closed'}.`) : false; - break; - case 'pairedIdentityStateChanged': - const pairingState1 = packet.payloadProtected.pairingState || 0; - const debug4 = this.debugLog ? this.emit('debug', `Client pairing state: ${LocalApi.Console.PairingState[pairingState1]}.`) : false; - break; + }); + const json = new MessagePacket('json'); + json.set('json', jsonRequest); + const sequenceNumber1 = await this.getSequenceNumber(); + const message = json.pack(this.crypto, sequenceNumber1, this.sourceParticipantId, channelCommunicationId); + this.sendSocketMessage(message, 'json'); + } catch (error) { + this.emit('warn', `Send tv remote command error: ${error}`) }; - }).on('listening', async () => { - //socket.setBroadcast(true); - const address = socket.address(); - const debug = this.debugLog ? this.emit('debug', `Server start listening: ${address.address}:${address.port}.`) : false; - this.socket = socket; - this.emitDevInfo = true; - - //ping console - setInterval(async () => { - if (this.isConnected) { - return; - } - - const state = await Ping.promise.probe(this.host, { timeout: 3 }); - const debug = this.debugLog ? this.emit('debug', `Ping console, state: ${state.alive ? 'Online' : 'Offline'}`) : false; - - if (!state.alive || this.isConnected) { - return; - } - - try { - const discoveryRequest = new SimplePacket('discoveryRequest'); - const message = discoveryRequest.pack(this.crypto); - await this.sendSocketMessage(message, 'discoveryRequest'); - } catch (error) { - this.emit('error', `Send discovery request error: ${error}`) - }; - }, 10000); - - //heart beat - setInterval(async () => { - if (!this.isConnected) { - return; - } + break; + case 2: + try { + let requestId = '0000000000000000'; + requestId = (`${requestId}${this.mediaRequestId}`).slice(-requestId.length); - const elapse = (Date.now() - this.heartBeatStartTime) / 1000; - const debug = this.debugLog && elapse >= 12 ? this.emit('debug', `Last message was: ${elapse} sec ago.`) : false; - const disconnect = elapse >= 12 ? await this.disconnect() : false; - }, 1000); + const mediaCommand = new MessagePacket('mediaCommand'); + mediaCommand.set('requestId', Buffer.from(requestId, 'hex')); + mediaCommand.set('titleId', 0); + mediaCommand.set('command', command); + const sequenceNumber = await this.getSequenceNumber(); + const message = mediaCommand.pack(this.crypto, sequenceNumber, this.sourceParticipantId, channelCommunicationId); + this.sendSocketMessage(message, 'mediaCommand'); + this.mediaRequestId++; + } catch (error) { + this.emit('warn', `Send system media command error: ${error}`) + }; + break; + } + return true; + }; - //Prepare accessory - await new Promise(resolve => setTimeout(resolve, 7500)); - //start external integration - const prepareExtInt = !this.isConnected ? this.emit('externalIntegration') : false; + async disconnect() { + const debug = this.enableDebugMode ? this.emit('debug', 'Disconnecting...') : false; - //prepare accessory - const prepareAccessory = !this.isConnected ? this.emit('prepareAccessory') : false; - }).bind(); + try { + const disconnect = new MessagePacket('disconnect'); + disconnect.set('reason', 2); + disconnect.set('errorCode', 0); + const sequenceNumber = await this.getSequenceNumber(); + const message = disconnect.pack(this.crypto, sequenceNumber, this.sourceParticipantId); + await this.sendSocketMessage(message, 'disconnect'); + this.isConnected = false; + this.sequenceNumber = 0; + this.sourceParticipantId = 0; + this.channels = []; + this.channelRequestId = 0; + this.mediaRequestId = 0; + await new Promise(resolve => setTimeout(resolve, 3000)); + this.emit('stateChanged', false, 0, true, 0, -1, -1); + this.emit('disconnected', 'Disconnected.'); return true; } catch (error) { - throw new Error(`Connect error: ${error.message || error}`); + throw new Error(error); }; }; - async reconnect() { - await new Promise(resolve => setTimeout(resolve, 5000)); - try { - await this.connect(); - } catch (error) { - throw new Error(`Reconnect error: ${error.message || error}`); - } + async getSequenceNumber() { + this.sequenceNumber++; + const debug = this.enableDebugMode ? this.emit('debug', `Sqquence number set to: ${this.sequenceNumber}`) : false; + return this.sequenceNumber; }; - async readData(path) { - try { - const data = await fsPromises.readFile(path); - return data; - } catch (error) { - throw new Error(`Read data error: ${error.message || error}`); - } - } + async sendSocketMessage(message, type) { + const offset = 0; + const length = message.byteLength; - async saveData(path, data) { try { - data = JSON.stringify(data, null, 2); - await fsPromises.writeFile(path, data); - const debug = this.debugLog ? this.emit('debug', `Saved data: ${data}`) : false; - return true; + this.socket.send(message, offset, length, 5050, this.host, (error, bytes) => { + if (error) { + throw new Error(error); + } + + const debug = this.enableDebugMode ? this.emit('debug', `Socket send: ${type}, ${bytes}B.`) : false; + return true; + }); } catch (error) { - throw new Error(`Save data error: ${error.message || error}`); + throw new Error(`Socket send error: ${error}`); }; }; - async acknowledge(sequenceNumber) { + //connect + async connect() { try { - const acknowledge = new MessagePacket('acknowledge'); - acknowledge.set('lowWatermark', sequenceNumber); - acknowledge.packet.processedList.value.push({ - id: sequenceNumber - }); - const sequenceNumber1 = await this.getSequenceNumber(); - const message = acknowledge.pack(this.crypto, sequenceNumber1, this.sourceParticipantId); - await this.sendSocketMessage(message, 'acknowledge'); - return true; - } catch (error) { - throw new Error(error); - }; - } + const socket = Dgram.createSocket(this.udpType); + socket.on('error', (error) => { + this.emit('error', `Socket error: ${error}`); + socket.close(); + }).on('close', async () => { + const debug = this.enableDebugMode ? this.emit('debug', 'Socket closed.') : false; + this.isConnected = false; + try { + await this.reconnect(); + } catch (error) { + this.emit('error', error) + }; + }).on('message', async (message, remote) => { + const debug = this.enableDebugMode ? this.emit('debug', `Received message from: ${remote.address}:${remote.port}`) : false; + this.heartBeatStartTime = Date.now(); - async channelOpen(channelRequestId, service) { - const debug = this.debugLog ? this.emit('debug', `Received channel Id: ${channelRequestId}, request open.`) : false; - this.channelRequestId = channelRequestId; + //get message type in hex + const messageTypeHex = message.slice(0, 2).toString('hex'); + const debug1 = this.enableDebugMode ? this.emit('debug', `Received message type hex: ${messageTypeHex}`) : false; - try { - const channelStartRequest = new MessagePacket('channelStartRequest'); - channelStartRequest.set('channelRequestId', channelRequestId); - channelStartRequest.set('titleId', 0); - channelStartRequest.set('service', Buffer.from(service, 'hex')); - channelStartRequest.set('activityId', 0); - const sequenceNumber = await this.getSequenceNumber(); - const message = channelStartRequest.pack(this.crypto, sequenceNumber, this.sourceParticipantId); - await this.sendSocketMessage(message, 'channelStartRequest'); - return true; - } catch (error) { - throw new Error(error); - } - } + //check message type exist in types + const keysTypes = Object.keys(LocalApi.Messages.Types); + const keysTypesExist = keysTypes.includes(messageTypeHex); - async powerOn() { - if (this.isConnected) { - this.emit('warn', 'Console already On.'); - }; + if (!keysTypesExist) { + const debug = this.enableDebugMode ? this.emit('debug', `Received unknown message type: ${messageTypeHex}`) : false; + return; + }; - const info = this.infoLog ? false : this.emit('message', 'Send power On.'); - try { - for (let i = 0; i < 15; i++) { - if (this.isConnected) { - return true; - } + //get message type and request + const messageType = LocalApi.Messages.Types[messageTypeHex]; + const messageRequest = LocalApi.Messages.Requests[messageTypeHex]; + const debug2 = this.enableDebugMode ? this.emit('debug', `Received message type: ${messageType}, request: ${messageRequest}`) : false; - const powerOn = new SimplePacket('powerOn'); - powerOn.set('liveId', this.xboxLiveId); - const message = powerOn.pack(this.crypto); - await this.sendSocketMessage(message, 'powerOn'); + let packeStructure; + switch (messageType) { + case 'simple': + packeStructure = new SimplePacket(messageRequest); + break; + case 'message': + packeStructure = new MessagePacket(messageRequest); + break; + }; - await new Promise(resolve => setTimeout(resolve, 1000)); - return true; - } - this.emit('disconnected', 'Power On failed, please try again.'); - } catch (error) { - this.emit('disconnected', 'Power On error, please try again.'); - throw new Error(error); - }; - }; + let packet = packeStructure.unpack(this.crypto, message); + const type = packet.type; + const debug3 = this.enableDebugMode ? this.emit('debug', `Received type: ${type}`) : false; + const debug4 = this.enableDebugMode ? this.emit('debug', `Received packet: ${JSON.stringify(packet, null, 2)}`) : false; - async powerOff() { - if (!this.isConnected) { - this.emit('warn', 'Console already Off.'); - }; + if (messageType === 'message' && packet.targetParticipantId !== this.sourceParticipantId) { + const debug = this.enableDebugMode ? this.emit('debug', `Participant Id: ${packet.targetParticipantId} !== ${this.sourceParticipantId}. Ignoring packet.`) : false; + return; + }; - const info = this.infoLog ? false : this.emit('message', 'Send power Off.'); - try { - const powerOff = new MessagePacket('powerOff'); - powerOff.set('liveId', this.xboxLiveId); - const sequenceNumber = await this.getSequenceNumber(); - const message = powerOff.pack(this.crypto, sequenceNumber, this.sourceParticipantId); - await this.sendSocketMessage(message, 'powerOff'); + switch (type) { + case 'json': + // Object to hold fragments + const fragments = {}; + const jsonMessage = JSON.parse(packet.payloadProtected.json); + const datagramId = jsonMessage.datagramId; + + // Check if JSON is fragmented + if (datagramId) { + const debug = this.enableDebugMode ? this.emit('debug', `JSON message datagram Id: ${datagramId}`) : false; + + let partials = {}; + if (!fragments[datagramId]) { + fragments[datagramId] = { + getValue() { + const buffers = Object.values(partials).map(partial => Buffer.from(partial, 'base64')); + return Buffer.concat(buffers); + }, + isValid() { + try { + JSON.parse(this.getValue()); + return true; + } catch (error) { + this.emit('error', `Valid fragments error: ${error}`); + return false; + } + } + }; + } + + partials[jsonMessage.fragmentOffset] = jsonMessage.fragmentData; + if (fragments[datagramId].isValid()) { + packet.payloadProtected.json = fragments[datagramId].getValue(); + const debug = this.enableDebugMode ? this.emit('debug', `JSON fragments: ${fragments}`) : false; + + // Delete + delete fragments[datagramId]; + } + }; + break; + case 'discoveryResponse': + const deviceType = packet.clientType; + const deviceName = packet.name; + const certificate = packet.certificate; + const debug = this.enableDebugMode ? this.emit('debug', `Discovered device: ${LocalApi.Console.Name[deviceType]}, name: ${deviceName}`) : false; + + try { + // Sign public key + const data = await this.crypto.getPublicKey(certificate); + const debug = this.enableDebugMode ? this.emit('debug', `Signed public key: ${data.publicKey}, iv: ${data.iv}`) : false; + + // Connect request + if (!this.isConnected) { + try { + const connectRequest = new SimplePacket('connectRequest'); + connectRequest.set('uuid', Buffer.from(UuIdParse(UuIdv4()))); + connectRequest.set('publicKey', data.publicKey); + connectRequest.set('iv', data.iv); + + const token = await this.readData(this.tokensFile); + const parseTokenData = JSON.parse(token); + const tokenData = parseTokenData.xsts.Token.length > 0 ? parseTokenData : false; + if (tokenData) { + connectRequest.set('userHash', tokenData.xsts.DisplayClaims.xui[0].uhs, true); + connectRequest.set('jwt', tokenData.xsts.Token, true); + this.isAuthorized = true; + } + const debug = this.enableDebugMode ? this.emit('debug', `Client connecting using: ${this.isAuthorized ? 'XSTS token' : 'Anonymous'}.`) : false; + + const message = connectRequest.pack(this.crypto); + await this.sendSocketMessage(message, 'connectRequest'); + } catch (error) { + this.emit('error', `Send connect request error: ${error}`) + }; + }; + } catch (error) { + this.emit('error', `Sign certificate error: ${error}`) + }; + break; + case 'connectResponse': + const connectionResult = packet.payloadProtected.connectResult; + const pairingState = packet.payloadProtected.pairingState; + const sourceParticipantId = packet.payloadProtected.participantId; + if (connectionResult !== 0) { + const errorTable = { + 0: 'Success.', + 1: 'Pending login. Reconnect to complete.', + 2: 'Unknown.', + 3: 'Anonymous connections disabled.', + 4: 'Device limit exceeded.', + 5: 'Remote connect is disabled on the console.', + 6: 'User authentication failed.', + 7: 'User Sign-In failed.', + 8: 'User Sign-In timeout.', + 9: 'User Sign-In required.' + }; + this.emit('error', `Connect error: ${errorTable[connectionResult]}`); + return; + } + this.isConnected = true; + this.sourceParticipantId = sourceParticipantId; + const debug1 = this.enableDebugMode ? this.emit('debug', `Client connect state: ${this.isConnected ? 'Connected' : 'Not Connected'}.`) : false; + const debug2 = this.enableDebugMode ? this.emit('debug', `Client pairing state: ${LocalApi.Console.PairingState[pairingState]}.`) : false; - await new Promise(resolve => setTimeout(resolve, 3500)); - await this.disconnect(); - return true; - } catch (error) { - throw new Error(error); - }; + try { + const localJoin = new MessagePacket('localJoin'); + const sequenceNumber = await this.getSequenceNumber(); + const localJointMessage = localJoin.pack(this.crypto, sequenceNumber, sourceParticipantId); + await this.sendSocketMessage(localJointMessage, 'localJoin'); - }; + // Open channels + try { + const channelNames = ['Input', 'TvRemote', 'Media']; + for (const channelName of channelNames) { + const channelRequestId = LocalApi.Channels.System[channelName].Id + const service = LocalApi.Channels.System[channelName].Uuid + //await this.channelOpen(channelRequestId, service); + }; + } catch (error) { + this.emit('error', `Channel open error: ${error}`) + }; + } catch (error) { + this.emit('error', `Send local join error: ${error}`) + }; + break; + case 'acknowledge': + const needAck = packet.flags.needAck; + if (!needAck) { + return; + }; - async recordGameDvr() { - if (!this.isConnected || !this.isAuthorized) { - this.emit('warn', `Send record game ignored, connection state: ${this.isConnected}, authorization state: ${this.isAuthorized}`); - }; + try { + const sequenceNumber = packet.sequenceNumber; + const acknowledge = new MessagePacket('acknowledge'); + acknowledge.set('lowWatermark', sequenceNumber); + acknowledge.packet.processedList.value.push({ id: sequenceNumber }); + const sequenceNumber1 = await this.getSequenceNumber(); + const message = acknowledge.pack(this.crypto, sequenceNumber1, this.sourceParticipantId); + await this.sendSocketMessage(message, 'acknowledge'); + } catch (error) { + this.emit('error', `Send acknowledge error: ${error}`) + }; + break; + case 'consoleStatus': + if (!packet.payloadProtected) { + return; + }; - const info = this.infoLog ? false : this.emit('message', 'Send record game.'); - try { - const recordGameDvr = new MessagePacket('recordGameDvr'); - recordGameDvr.set('startTimeDelta', -60); - recordGameDvr.set('endTimeDelta', 0); - const sequenceNumber = await this.getSequenceNumber(); - const message = recordGameDvr.pack(this.crypto, sequenceNumber, this.sourceParticipantId); - await this.sendSocketMessage(message, 'recordGameDvr'); - return true; - } catch (error) { - throw new Error(error); - }; - }; + if (this.emitDevInfo) { + //connect to deice success + this.emit('success', `Connect Success.`) - async sendButtonPress(channelName, command) { - if (!this.isConnected) { - this.emit('waarn', `Send command ignored, connection state: ${this.isConnected ? 'Not Connected' : 'Connected'}.`); - }; + const majorVersion = packet.payloadProtected.majorVersion; + const minorVersion = packet.payloadProtected.minorVersion; + const buildNumber = packet.payloadProtected.buildNumber; + const locale = packet.payloadProtected.locale; + const firmwareRevision = `${majorVersion}.${minorVersion}.${buildNumber}`; - const channelRequestId = LocalApi.Channels.System[channelName].Id - const channelCommunicationId = this.channels[requestId].id; - const channelOpen = this.channels[requestId].open; + const obj = { + manufacturer: 'Microsoft', + modelName: 'Xbox', + serialNumber: this.xboxLiveId, + firmwareRevision: firmwareRevision, + locale: locale + }; - if (channelCommunicationId === -1 || !channelOpen) { - this.emit('warn', `Channel Id: ${channelCommunicationId}, state: ${channelOpen ? 'Open' : 'Closed'}, trying to open it.`); - }; + //save device info to the file + await this.saveData(this.devInfoFile, obj) - command = LocalApi.Channels.System[channelName][command]; - const debug = this.debugLog ? this.emit('debug', `Channel communication Id: ${channelCommunicationId}, name: ${channelName} opened, send command; ${command}.`) : false; + //emit device info + this.emit('deviceInfo', firmwareRevision, locale); + this.emitDevInfo = false; + }; - switch (channelRequestId) { - case 0: - try { - const gamepad = new MessagePacket('gamepad'); - gamepad.set('timestamp', Buffer.from(`000${new Date().getTime().toString()}`, 'hex')); - gamepad.set('buttons', command); - const sequenceNumber = await this.getSequenceNumber(); - const message = gamepad.pack(this.crypto, sequenceNumber, this.sourceParticipantId, channelCommunicationId); - await this.sendSocketMessage(message, 'gamepad'); + const appsCount = Array.isArray(packet.payloadProtected.activeTitles) ? packet.payloadProtected.activeTitles.length : 0; + if (appsCount > 0) { + const power = true; + const volume = 0; + const mute = power ? power : true; + const mediaState = 2; + const titleId = appsCount === 2 ? packet.payloadProtected.activeTitles[0].titleId : packet.payloadProtected.activeTitles[0].titleId; + const reference = appsCount === 2 ? packet.payloadProtected.activeTitles[0].aumId : packet.payloadProtected.activeTitles[0].aumId; - setTimeout(async () => { - const gamepadUnpress = new MessagePacket('gamepad'); - gamepadUnpress.set('timestamp', Buffer.from(`000${new Date().getTime().toString()}`, 'hex')); - gamepadUnpress.set('buttons', LocalApi.Channels.System.Input.Commands.unpress); - const sequenceNumber1 = await this.getSequenceNumber(); - const message = gamepadUnpress.pack(this.crypto, sequenceNumber1, this.sourceParticipantId, channelCommunicationId); - await this.sendSocketMessage(message, 'gamepadUnpress'); - }, 150) - } catch (error) { - this.emit('warn', `Send system input command error: ${error}`) - }; - break; - case 1: - try { - const sequenceNumber = await this.getSequenceNumber(); - const jsonRequest = JSON.stringify({ - msgid: `2ed6c0fd.${sequenceNumber}`, - request: LocalApi.Channels.System.TvRemote.MessageType.SendKey, - params: { - button_id: command, - device_id: null - } - }); - const json = new MessagePacket('json'); - json.set('json', jsonRequest); - const sequenceNumber1 = await this.getSequenceNumber(); - const message = json.pack(this.crypto, sequenceNumber1, this.sourceParticipantId, channelCommunicationId); - this.sendSocketMessage(message, 'json'); - } catch (error) { - this.emit('warn', `Send tv remote command error: ${error}`) - }; - break; - case 2: - try { - let requestId = '0000000000000000'; - requestId = (`${requestId}${this.mediaRequestId}`).slice(-requestId.length); + this.emit('stateChanged', power, volume, mute, mediaState, titleId, reference); + const debug = this.enableDebugMode ? this.emit('debug', `Status changed, app Id: ${titleId}, reference: ${reference}`) : false; - const mediaCommand = new MessagePacket('mediaCommand'); - mediaCommand.set('requestId', Buffer.from(requestId, 'hex')); - mediaCommand.set('titleId', 0); - mediaCommand.set('command', command); - const sequenceNumber = await this.getSequenceNumber(); - const message = mediaCommand.pack(this.crypto, sequenceNumber, this.sourceParticipantId, channelCommunicationId); - this.sendSocketMessage(message, 'mediaCommand'); - this.mediaRequestId++; - } catch (error) { - this.emit('warn', `Send system media command error: ${error}`) - }; - break; - } - return true; - }; + //emit restFul and mqtt + const obj = { + 'power': power, + 'titleId': titleId, + 'app': reference, + 'volume': volume, + 'mute': mute, + 'mediaState': mediaState, + }; + this.emit('restFul', 'state', obj); + this.emit('mqtt', 'State', obj); + }; - async disconnect() { - const debug = this.debugLog ? this.emit('debug', 'Disconnecting...') : false; + //acknowledge + try { + const acknowledge = packet.flags.needAck ? await this.acknowledge(packet.sequenceNumber) : false; + } catch (error) { + this.emit('error', `Acknowledge error: ${error}`) + }; + break; + case 'mediaState': - try { - const disconnect = new MessagePacket('disconnect'); - disconnect.set('reason', 2); - disconnect.set('errorCode', 0); - const sequenceNumber = await this.getSequenceNumber(); - const message = disconnect.pack(this.crypto, sequenceNumber, this.sourceParticipantId); - await this.sendSocketMessage(message, 'disconnect'); + break; + case 'mediaCommandResult': - this.isConnected = false; - this.sequenceNumber = 0; - this.sourceParticipantId = 0; - this.channels = []; - this.channelRequestId = 0; - this.mediaRequestId = 0; - await new Promise(resolve => setTimeout(resolve, 3000)); - this.emit('stateChanged', false, 0, true, 0, -1, -1); - this.emit('disconnected', 'Disconnected.'); - return true; - } catch (error) { - throw new Error(error); - }; - }; + break; + case 'channelStartResponse': + const requestIdMatch = this.channelRequestId === packet.payloadProtected.channelRequestId; + const channelState = requestIdMatch ? packet.payloadProtected.result === 0 : false; + const debug5 = this.enableDebugMode ? this.emit('debug', `channelState: ${channelState}`) : false; + const channelCommunicationId = channelState && packet.payloadProtected.channelTargetId ? packet.payloadProtected.channelTargetId : -1; + const debug6 = this.enableDebugMode ? this.emit('debug', `channelCommunicationId: ${packet.payloadProtected.channelTargetId}`) : false; + const channel = { + id: channelCommunicationId, + open: channelState + } + const pushChannel = channelCommunicationId !== -1 ? this.channels.push(channel) : false; + const debug3 = this.enableDebugMode ? this.emit('debug', `Channel communication Id: ${channelCommunicationId}, state: ${channelState ? 'Open' : 'Closed'}.`) : false; + break; + case 'pairedIdentityStateChanged': + const pairingState1 = packet.payloadProtected.pairingState || 0; + const debug4 = this.enableDebugMode ? this.emit('debug', `Client pairing state: ${LocalApi.Console.PairingState[pairingState1]}.`) : false; + break; + }; + }).on('listening', async () => { + //socket.setBroadcast(true); + const address = socket.address(); + const debug = this.enableDebugMode ? this.emit('debug', `Server start listening: ${address.address}:${address.port}.`) : false; + this.socket = socket; + this.emitDevInfo = true; - async getSequenceNumber() { - this.sequenceNumber++; - const debug = this.debugLog ? this.emit('debug', `Sqquence number set to: ${this.sequenceNumber}`) : false; - return this.sequenceNumber; - }; + if (!this.isConnected) { + const debug = this.enableDebugMode ? this.emit('debug', `Plugin send heartbeat to console`) : false; + const state = await Ping.promise.probe(this.host, { timeout: 3 }); - async sendSocketMessage(message, type) { - const offset = 0; - const length = message.byteLength; + if (!state.alive || this.isConnected) { + return; + } - try { - this.socket.send(message, offset, length, 5050, this.host, (error, bytes) => { - if (error) { - throw new Error(error); + const debug1 = this.enableDebugMode ? this.emit('debug', `Plugin received heartbeat from console`) : false + const discoveryRequest = new SimplePacket('discoveryRequest'); + const message = discoveryRequest.pack(this.crypto); + await this.sendSocketMessage(message, 'discoveryRequest'); } + }).bind(); - const debug = this.debugLog ? this.emit('debug', `Socket send: ${type}, ${bytes}B.`) : false; - return true; - }); + await new Promise(resolve => setTimeout(resolve, 1500)); + return true; } catch (error) { - throw new Error(`Socket send error: ${error}`); + throw new Error(`Connect error: ${error.message || error}`); }; }; }; diff --git a/src/mqtt.js b/src/mqtt.js index 91955b4..e966f9c 100644 --- a/src/mqtt.js +++ b/src/mqtt.js @@ -32,11 +32,11 @@ class Mqtt extends EventEmitter { const value = Object.values(subscribedMessage)[0]; this.emit('set', key, value); } catch (error) { - this.emit('error', `MQTT Parse message error: ${error}`); + this.emit('warn', `MQTT Parse message error: ${error}`); }; }); } catch (error) { - this.emit('error', `MQTT Connect error: ${error}`); + this.emit('warn', `MQTT Connect error: ${error}`); }; }).on('publish', async (topic, message) => { try { @@ -45,7 +45,7 @@ class Mqtt extends EventEmitter { await this.mqttClient.publish(fullTopic, publishMessage); const emitDebug = config.debug ? this.emit('debug', `MQTT Publish topic: ${fullTopic}, message: ${publishMessage}`) : false; } catch (error) { - this.emit('error', `MQTT Publish error: ${error}`); + this.emit('warn', `MQTT Publish error: ${error}`); }; }); diff --git a/src/restful.js b/src/restful.js index f667780..8fb3723 100644 --- a/src/restful.js +++ b/src/restful.js @@ -42,7 +42,7 @@ class RestFul extends EventEmitter { this.emit('set', key, value); res.send('OK'); } catch (error) { - this.emit('error', `RESTFul Parse object error: ${error}`); + this.emit('warn', `RESTFul Parse object error: ${error}`); }; }); @@ -51,7 +51,7 @@ class RestFul extends EventEmitter { }); } catch (error) { - this.emit('error', `RESTful Connect error: ${error}`) + this.emit('warn', `RESTful Connect error: ${error}`) } }; @@ -79,7 +79,7 @@ class RestFul extends EventEmitter { this.restFulData.status = data; break; default: - this.emit('error', `RESTFul update unknown path: ${path}, data: ${data}`) + this.emit('warn', `RESTFul update unknown path: ${path}, data: ${data}`) break; }; const emitDebug = this.restFulDebug ? this.emit('debug', `RESTFul update path: ${path}, data: ${JSON.stringify(data, null, 2)}`) : false; diff --git a/src/webApi/xboxwebapi.js b/src/webApi/xboxwebapi.js index a29141a..f8bd43b 100644 --- a/src/webApi/xboxwebapi.js +++ b/src/webApi/xboxwebapi.js @@ -13,7 +13,7 @@ class XboxWebApi extends EventEmitter { this.webApiClientId = config.webApiClientId; this.webApiClientSecret = config.webApiClientSecret; this.inputsFile = config.inputsFile; - this.debugLog = config.debugLog; + this.enableDebugMode = config.enableDebugMode; //variables this.authorized = false; @@ -40,7 +40,7 @@ class XboxWebApi extends EventEmitter { async checkAuthorization() { try { const data = await this.authentication.checkAuthorization(); - const debug = this.debugLog ? this.emit('debug', `Authorization headers: ${JSON.stringify(data.headers, null, 2)}, tokens: ${JSON.stringify(data.tokens, null, 2)}`) : false; + const debug = this.enableDebugMode ? this.emit('debug', `Authorization headers: ${JSON.stringify(data.headers, null, 2)}, tokens: ${JSON.stringify(data.tokens, null, 2)}`) : false; const headers = { 'Authorization': data.headers, 'Accept-Language': 'en-US', @@ -70,7 +70,7 @@ class XboxWebApi extends EventEmitter { async xboxLiveData() { try { const rmEnabled = await this.consoleStatus(); - const debug1 = !rmEnabled ? this.emit('message', `Remote management not enabled, please check your console settings.`) : false; + const debug1 = !rmEnabled ? this.emit('info', `Remote management not enabled, please check your console settings.`) : false; //await this.consolesList(); await this.installedApps(); //await this.storageDevices(); @@ -86,7 +86,7 @@ class XboxWebApi extends EventEmitter { try { const url = `${WebApi.Url.Xccs}/consoles/${this.xboxLiveId}`; const getConsoleStatusData = await this.axiosInstance(url); - const debug = this.debugLog ? this.emit('debug', `Console status data: ${JSON.stringify(getConsoleStatusData.data, null, 2)}`) : false; + const debug = this.enableDebugMode ? this.emit('debug', `Console status data: ${JSON.stringify(getConsoleStatusData.data, null, 2)}`) : false; //get console status const consoleStatusData = getConsoleStatusData.data; @@ -121,7 +121,7 @@ class XboxWebApi extends EventEmitter { try { const url = `${WebApi.Url.Xccs}/lists/devices?queryCurrentDevice=false&includeStorageDevices=true`; const getConsolesListData = await this.axiosInstance(url); - const debug = this.debugLog ? this.emit('debug', `Consoles list data: ${getConsolesListData.data.result[0]}, ${getConsolesListData.data.result[0].storageDevices[0]}`) : false; + const debug = this.enableDebugMode ? this.emit('debug', `Consoles list data: ${getConsolesListData.data.result[0]}, ${getConsolesListData.data.result[0].storageDevices[0]}`) : false; //get consoles list this.consolesId = []; @@ -201,7 +201,7 @@ class XboxWebApi extends EventEmitter { try { const url = `${WebApi.Url.Xccs}/lists/installedApps?deviceId=${this.xboxLiveId}`; const getInstalledAppsData = await this.axiosInstance(url); - const debug = this.debugLog ? this.emit('debug', `Get installed apps data: ${JSON.stringify(getInstalledAppsData.data.result, null, 2)}`) : false; + const debug = this.enableDebugMode ? this.emit('debug', `Get installed apps data: ${JSON.stringify(getInstalledAppsData.data.result, null, 2)}`) : false; //get installed apps const appsArray = []; @@ -253,7 +253,7 @@ class XboxWebApi extends EventEmitter { try { const url = `${WebApi.Url.Xccs}/lists/storageDevices?deviceId=${this.xboxLiveId}`; const getStorageDevicesData = await this.axiosInstance(url); - const debug = this.debugLog ? this.emit('debug', `Get storage devices data: ${JSON.stringify(getStorageDevicesData.data, null, 2)}`) : false; + const debug = this.enableDebugMode ? this.emit('debug', `Get storage devices data: ${JSON.stringify(getStorageDevicesData.data, null, 2)}`) : false; //get console storages this.storageDeviceId = []; @@ -296,7 +296,7 @@ class XboxWebApi extends EventEmitter { try { const url = `https://profile.xboxlive.com/users/xuid(${this.tokens.xsts.DisplayClaims.xui[0].xid})/profile/settings?settings=GameDisplayName,GameDisplayPicRaw,Gamerscore,Gamertag`; const getUserProfileData = await this.axiosInstance(url); - const debug = this.debugLog ? this.emit('debug', `Get user profile data: ${JSON.stringify(getUserProfileData.data.profileUsers[0], null, 2)}, ${JSON.stringify(getUserProfileData.data.profileUsers[0].settings[0], null, 2)}`) : false + const debug = this.enableDebugMode ? this.emit('debug', `Get user profile data: ${JSON.stringify(getUserProfileData.data.profileUsers[0], null, 2)}, ${JSON.stringify(getUserProfileData.data.profileUsers[0].settings[0], null, 2)}`) : false //get user profiles this.userProfileId = []; @@ -339,7 +339,7 @@ class XboxWebApi extends EventEmitter { try { data = JSON.stringify(data, null, 2); await fsPromises.writeFile(path, data); - const debug = this.debugLog ? this.emit('debug', `Saved data: ${data}`) : false; + const debug = this.enableDebugMode ? this.emit('debug', `Saved data: ${data}`) : false; return true; } catch (error) { throw new Error(`Save data error: ${error.message || error}`); @@ -407,7 +407,7 @@ class XboxWebApi extends EventEmitter { "parameters": params, "linkedXboxId": this.xboxLiveId } - const debug = this.debugLog ? this.emit('debug', `send, type: ${commandType}, command: ${command}, params: ${params}.`) : false; + const debug = this.enableDebugMode ? this.emit('debug', `send, type: ${commandType}, command: ${command}, params: ${params}.`) : false; try { const stringifyPostParam = JSON.stringify(postParams); @@ -416,7 +416,7 @@ class XboxWebApi extends EventEmitter { headers: this.headers } const response = await axios.post(`${WebApi.Url.Xccs}/commands`, postParams, headers); - const debug1 = this.debugLog ? this.emit('debug', `send command, result: ${JSON.stringify(response.data, null, 2)}`) : false; + const debug1 = this.enableDebugMode ? this.emit('debug', `send command, result: ${JSON.stringify(response.data, null, 2)}`) : false; return true; } catch (error) { diff --git a/src/xboxdevice.js b/src/xboxdevice.js index 878a20f..47c700b 100644 --- a/src/xboxdevice.js +++ b/src/xboxdevice.js @@ -42,7 +42,6 @@ class XboxDevice extends EventEmitter { this.webApiClientSecret = device.webApiClientSecret; this.enableDebugMode = device.enableDebugMode || false; this.disableLogInfo = device.disableLogInfo || false; - this.disableLogDeviceInfo = device.disableLogDeviceInfo || false; this.infoButtonCommand = device.infoButtonCommand || 'nexus'; this.volumeControl = device.volumeControl || false; this.volumeControlNamePrefix = device.volumeControlNamePrefix || false; @@ -98,7 +97,7 @@ class XboxDevice extends EventEmitter { sensor.state = false; this.sensorsInputsConfigured.push(sensor); } else { - const log = sensorInputDisplayType === 0 ? false : this.emit('message', `Sensor Name: ${sensorInputName ? sensorInputName : 'Missing'}, Reference: ${sensorInputReference ? sensorInputReference : 'Missing'}.`); + const log = sensorInputDisplayType === 0 ? false : this.emit('info', `Sensor Name: ${sensorInputName ? sensorInputName : 'Missing'}, Reference: ${sensorInputReference ? sensorInputReference : 'Missing'}.`); }; } this.sensorsInputsConfiguredCount = this.sensorsInputsConfigured.length || 0; @@ -117,376 +116,17 @@ class XboxDevice extends EventEmitter { button.state = false; this.buttonsConfigured.push(button); } else { - const log = buttonDisplayType === 0 ? false : this.emit('message', `Button Name: ${buttonName ? buttonName : 'Missing'}, Command: ${buttonCommand ? buttonCommand : 'Missing'}, Reference: ${buttonReference ? buttonReference : 'Missing'}.`); + const log = buttonDisplayType === 0 ? false : this.emit('info', `Button Name: ${buttonName ? buttonName : 'Missing'}, Command: ${buttonCommand ? buttonCommand : 'Missing'}, Reference: ${buttonReference ? buttonReference : 'Missing'}.`); }; } this.buttonsConfiguredCount = this.buttonsConfigured.length || 0; } - async start() { - try { - //web api client - if (this.webApiControl) { - try { - this.xboxWebApi = new XboxWebApi({ - xboxLiveId: this.xboxLiveId, - webApiClientId: this.webApiClientId, - webApiClientSecret: this.webApiClientSecret, - tokensFile: this.authTokenFile, - inputsFile: this.inputsFile, - debugLog: this.enableDebugMode - }); - - this.xboxWebApi.on('consoleStatus', (consoleType) => { - if (this.informationService) { - this.informationService - .setCharacteristic(Characteristic.Model, consoleType) - }; - - //this.serialNumber = id; - this.modelName = consoleType; - //this.power = powerState; - //this.mediaState = playbackState; - }) - .on('powerOnError', (power) => { - if (this.televisionService) { - this.televisionService - .updateCharacteristic(Characteristic.Active, power) - }; - this.power = power; - }) - .on('success', (message) => { - this.emit('success', message); - }) - .on('message', (message) => { - this.emit('message', message); - }) - .on('debug', (debug) => { - this.emit('debug', debug); - }) - .on('warn', (warn) => { - this.emit('warn', warn); - }) - .on('error', (error) => { - this.emit('error', error); - }) - .on('restFul', (path, data) => { - const restFul = this.restFulConnected ? this.restFul1.update(path, data) : false; - }) - .on('mqtt', (topic, message) => { - const mqtt = this.mqttConnected ? this.mqtt1.emit('publish', topic, message) : false; - }); - - //check authorization - await this.xboxWebApi.checkAuthorization(); - - //start impulse generator - const timers = [{ name: 'checkAuthorization', sampling: 900000 }]; - await this.xboxWebApi.impulseGenerator.start(timers); - } catch (error) { - this.emit('error', error); - }; - }; - - //xbox local client - this.xboxLocalApi = new XboxLocalApi({ - host: this.host, - xboxLiveId: this.xboxLiveId, - tokensFile: this.authTokenFile, - devInfoFile: this.devInfoFile, - infoLog: this.disableLogInfo, - debugLog: this.enableDebugMode - }); - - this.xboxLocalApi.on('deviceInfo', (firmwareRevision, locale) => { - if (!this.disableLogDeviceInfo) { - this.emit('devInfo', `-------- ${this.name} --------'`); - this.emit('devInfo', `Manufacturer: Microsoft`); - this.emit('devInfo', `Model: ${this.modelName ?? 'Xbox'}`); - this.emit('devInfo', `Serialnr: ${this.xboxLiveId}`); - this.emit('devInfo', `Firmware: ${firmwareRevision}`); - this.emit('devInfo', `Locale: ${locale}`); - this.emit('devInfo', `----------------------------------`); - } - - if (this.informationService) { - this.informationService - .setCharacteristic(Characteristic.Manufacturer, 'Microsoft') - .setCharacteristic(Characteristic.FirmwareRevision, firmwareRevision); - }; - }) - .on('externalIntegrations', () => { - try { - //RESTFul server - const restFulEnabled = this.restFul.enable || false; - if (restFulEnabled) { - if (!this.restFulConnected) { - this.restFul1 = new RestFul({ - port: this.restFul.port || 3000, - debug: this.restFul.debug || false - }); - - this.restFul1.on('connected', (message) => { - this.restFulConnected = true; - this.emit('success', message); - }) - .on('set', async (key, value) => { - try { - await this.setOverExternalIntegration('RESTFul', key, value); - } catch (error) { - this.emit('warn', `RESTFul set error: ${error}`); - }; - }) - .on('debug', (debug) => { - this.emit('debug', debug); - }) - .on('error', (error) => { - this.emit('warn', error); - }); - } - } - - //mqtt client - const mqttEnabled = this.mqtt.enable || false; - if (mqttEnabled) { - if (!this.mqttConnected) { - this.mqtt1 = new Mqtt({ - host: this.mqtt.host, - port: this.mqtt.port || 1883, - clientId: this.mqtt.clientId || `xbox_${Math.random().toString(16).slice(3)}`, - prefix: `${this.mqtt.prefix}/${device.name}`, - user: this.mqtt.user, - passwd: this.mqtt.passwd, - debug: this.mqtt.debug || false - }); - - this.mqtt1.on('connected', (message) => { - this.mqttConnected = true; - this.emit('success', message); - }) - .on('subscribed', (message) => { - this.emit('success', message); - }) - .on('set', async (key, value) => { - try { - await this.setOverExternalIntegration('MQTT', key, value); - } catch (error) { - this.emit('warn', `MQTT set error: ${error}.`); - }; - }) - .on('debug', (debug) => { - this.emit('debug', debug); - }) - .on('error', (error) => { - this.emit('warn', error); - }); - }; - }; - } catch (error) { - this.emit('warn', `External integration start error: ${error.message || error}.`); - }; - }) - .on('prepareAccessory', async () => { - if (!this.startPrepareAccessory) { - return; - } - - try { - //read dev info from file - const savedInfo = await this.readData(this.devInfoFile); - this.savedInfo = savedInfo.toString().trim() !== '' ? JSON.parse(savedInfo) : {}; - const debug = !this.enableDebugMode ? false : this.emit('debug', `Read saved Info: ${JSON.stringify(this.savedInfo, null, 2)}`); - - //read inputs file - const savedInputs = await this.readData(this.inputsFile); - this.savedInputs = savedInputs.toString().trim() !== '' ? JSON.parse(savedInputs) : this.inputs; - const debug2 = !this.enableDebugMode ? false : this.emit('debug', `Read saved Inputs: ${JSON.stringify(this.savedInputs, null, 2)}`); - - //read inputs names from file - const savedInputsNames = await this.readData(this.inputsNamesFile); - this.savedInputsNames = savedInputsNames.toString().trim() !== '' ? JSON.parse(savedInputsNames) : {}; - const debug3 = !this.enableDebugMode ? false : this.emit('debug', `Read saved Inputs Names: ${JSON.stringify(this.savedInputsNames, null, 2)}`); - - //read inputs visibility from file - const savedInputsTargetVisibility = await this.readData(this.inputsTargetVisibilityFile); - this.savedInputsTargetVisibility = savedInputsTargetVisibility.toString().trim() !== '' ? JSON.parse(savedInputsTargetVisibility) : {}; - const debug4 = !this.enableDebugMode ? false : this.emit('debug', `Read saved Inputs Target Visibility: ${JSON.stringify(this.savedInputsTargetVisibility, null, 2)}`); - - //prepare accessory - const accessory = await this.prepareAccessory(); - this.emit('publishAccessory', accessory) - - //sort inputs list - const sortInputsDisplayOrder = this.televisionService ? await this.displayOrder() : false; - this.startPrepareAccessory = false; - } catch (error) { - throw new Error(` - Prepare accessory error: ${error.message || error}`); - }; - }) - .on('stateChanged', (power, volume, mute, mediaState, titleId, reference) => { - const input = this.inputsConfigured.find(input => input.reference === reference || input.titleId === titleId) ?? false; - const inputIdentifier = input ? input.identifier : this.inputIdentifier; - - //update characteristics - if (this.televisionService) { - this.televisionService - .updateCharacteristic(Characteristic.Active, power); - }; - - if (this.televisionService) { - this.televisionService - .updateCharacteristic(Characteristic.ActiveIdentifier, inputIdentifier); - }; - - if (this.speakerService) { - this.speakerService - .updateCharacteristic(Characteristic.Volume, volume) - .updateCharacteristic(Characteristic.Mute, mute); - if (this.volumeService) { - this.volumeService - .updateCharacteristic(Characteristic.Brightness, volume) - .updateCharacteristic(Characteristic.On, !mute); - }; - if (this.volumeServiceFan) { - this.volumeServiceFan - .updateCharacteristic(Characteristic.RotationSpeed, volume) - .updateCharacteristic(Characteristic.On, !mute); - }; - }; - - if (this.sensorPowerService) { - this.sensorPowerService - .updateCharacteristic(Characteristic.ContactSensorState, power); - } - - if (this.sensorInputService && reference !== this.reference) { - for (let i = 0; i < 2; i++) { - const state = power ? [true, false][i] : false; - this.sensorInputService - .updateCharacteristic(Characteristic.ContactSensorState, state); - this.sensorInputState = state; - } - } - - if (this.sensorScreenSaverService) { - const state = power ? (reference === 'Xbox.IdleScreen_8wekyb3d8bbwe!Xbox.IdleScreen.Application') : false; - this.sensorScreenSaverService - .updateCharacteristic(Characteristic.ContactSensorState, state); - this.sensorScreenSaverState = state; - } - - if (this.sensorsInputsServices) { - for (let i = 0; i < this.sensorsInputsConfiguredCount; i++) { - const sensorInput = this.sensorsInputsConfigured[i]; - const state = power ? sensorInput.reference === reference : false; - sensorInput.state = state; - const characteristicType = sensorInput.characteristicType; - this.sensorsInputsServices[i] - .updateCharacteristic(characteristicType, state); - } - } - - //buttons - if (this.buttonsServices) { - for (let i = 0; i < this.buttonsConfiguredCount; i++) { - const button = this.buttonsConfigured[i]; - const state = this.power ? button.reference === reference : false; - button.state = state; - this.buttonsServices[i] - .updateCharacteristic(Characteristic.On, state); - } - } - - this.inputIdentifier = inputIdentifier; - this.power = power; - this.reference = reference; - this.volume = volume; - this.mute = mute; - this.mediaState = mediaState; - - if (!this.disableLogInfo) { - const name = input ? input.name : reference; - const productId = input ? input.oneStoreProductId : reference; - this.emit('message', `Power: ${power ? 'ON' : 'OFF'}`); - this.emit('message', `Input Name: ${name}`); - this.emit('message', `Reference: ${reference}`); - this.emit('message', `Title Id: ${titleId}`); - this.emit('message', `Product Id: ${productId}`); - this.emit('message', `Volume: ${volume}%`); - this.emit('message', `Mute: ${mute ? 'ON' : 'OFF'}`); - this.emit('message', `Media State: ${['PLAY', 'PAUSE', 'STOPPED', 'LOADING', 'INTERRUPTED'][mediaState]}`); - }; - }) - .on('success', (message) => { - this.emit('success', message); - }) - .on('message', (message) => { - this.emit('message', message); - }) - .on('debug', (debug) => { - this.emit('debug', debug); - }) - .on('warn', (warn) => { - this.emit('warn', warn); - }) - .on('error', (error) => { - this.emit('error', error); - }) - .on('disconnected', (message) => { - this.emit('message', message); - }) - .on('restFul', (path, data) => { - const restFul = this.restFulConnected ? this.restFul1.update(path, data) : false; - }) - .on('mqtt', (topic, message) => { - const mqtt = this.mqttConnected ? this.mqtt1.emit('publish', topic, message) : false; - }); - - //connect to local api - await this.xboxLocalApi.connect(); - - return true; - } catch (error) { - throw new Error(`Start error: ${error.message || error}`); - }; - } - - - async displayOrder() { - try { - switch (this.inputsDisplayOrder) { - case 0: - this.inputsConfigured.sort((a, b) => a.identifier - b.identifier); - break; - case 1: - this.inputsConfigured.sort((a, b) => a.name.localeCompare(b.name)); - break; - case 2: - this.inputsConfigured.sort((a, b) => b.name.localeCompare(a.name)); - break; - case 3: - this.inputsConfigured.sort((a, b) => a.reference.localeCompare(b.reference)); - break; - case 4: - this.inputsConfigured.sort((a, b) => b.reference.localeCompare(a.reference)); - break; - } - const debug = !this.enableDebugMode ? false : this.emit('debug', `Inputs display order: ${JSON.stringify(this.inputsConfigured, null, 2)}`); - - const displayOrder = this.inputsConfigured.map(input => input.identifier); - this.televisionService.setCharacteristic(Characteristic.DisplayOrder, Encode(1, displayOrder).toString('base64')); - return true; - } catch (error) { - throw new Error(`Display order error: ${error.message || error}`); - }; - } - async saveData(path, data) { try { data = JSON.stringify(data, null, 2); await fsPromises.writeFile(path, data); - const debug = this.debugLog ? this.emit('debug', `Saved data: ${data}`) : false; + const debug = this.enableDebugMode ? this.emit('debug', `Saved data: ${data}`) : false; return true; } catch (error) { throw new Error(`Save data error: ${error.message || error}`); @@ -534,39 +174,172 @@ class XboxDevice extends EventEmitter { const payload = [{ 'oneStoreProductId': value }]; set = await this.xboxWebApi.send('Shell', 'ActivateApplicationWithOneStoreProductId', payload); break; - case 'Volume': - switch (value) { - case 'up': //Up - set = await this.xboxWebApi.send('Volume', 'Up'); - break; - case 'down': //Down - set = await this.xboxWebApi.send('Volume', 'Down'); - break; - } + case 'Volume': + switch (value) { + case 'up': //Up + set = await this.xboxWebApi.send('Volume', 'Up'); + break; + case 'down': //Down + set = await this.xboxWebApi.send('Volume', 'Down'); + break; + } + break; + case 'Mute': + switch (value) { + case true: //Mute + set = await this.xboxWebApi.send('Audio', 'Mute'); + break; + case false: //Unmute; + set = await this.xboxWebApi.send('Audio', 'Unmute'); + break; + } + break; + case 'RcControl': + set = await this.xboxWebApi.send('Shell', 'InjectKey', [{ 'keyType': value }]); + break; + default: + this.emit('warn', `${integration}, received key: ${key}, value: ${value}`); + break; + }; + return set; + } catch (error) { + throw new Error(`${integration} set key: ${key}, value: ${value}, error: ${error.message || error}`); + }; + } + + async externalIntegrations() { + try { + //RESTFul server + const restFulEnabled = this.restFul.enable || false; + if (restFulEnabled) { + this.restFul1 = new RestFul({ + port: this.restFul.port || 3000, + debug: this.restFul.debug || false + }); + + this.restFul1.on('connected', (message) => { + this.emit('success', message); + this.restFulConnected = true; + }) + .on('set', async (key, value) => { + try { + await this.setOverExternalIntegration('RESTFul', key, value); + } catch (error) { + this.emit('warn', `RESTFul set error: ${error}`); + }; + }) + .on('debug', (debug) => { + this.emit('debug', debug); + }) + .on('warn', (warn) => { + this.emit('warn', warn); + }) + .on('error', (error) => { + this.emit('error', error); + }); + } + + //mqtt client + const mqttEnabled = this.mqtt.enable || false; + if (mqttEnabled) { + this.mqtt1 = new Mqtt({ + host: this.mqtt.host, + port: this.mqtt.port || 1883, + clientId: this.mqtt.clientId || `lgwebos_${Math.random().toString(16).slice(3)}`, + prefix: `${this.mqtt.prefix}/${this.name}`, + user: this.mqtt.user, + passwd: this.mqtt.passwd, + debug: this.mqtt.debug || false + }); + + this.mqtt1.on('connected', (message) => { + this.emit('success', message); + this.mqttConnected = true; + }) + .on('subscribed', (message) => { + this.emit('success', message); + }) + .on('set', async (key, value) => { + try { + await this.setOverExternalIntegration('MQTT', key, value); + } catch (error) { + this.emit('warn', `MQTT set error: ${error}`); + }; + }) + .on('debug', (debug) => { + this.emit('debug', debug); + }) + .on('warn', (warn) => { + this.emit('warn', warn); + }) + .on('error', (error) => { + this.emit('error', error); + }); + }; + + return true; + } catch (error) { + this.emit('warn', `External integration start error: ${error}`); + }; + } + + async displayOrder() { + try { + switch (this.inputsDisplayOrder) { + case 0: + this.inputsConfigured.sort((a, b) => a.identifier - b.identifier); + break; + case 1: + this.inputsConfigured.sort((a, b) => a.name.localeCompare(b.name)); break; - case 'Mute': - switch (value) { - case true: //Mute - set = await this.xboxWebApi.send('Audio', 'Mute'); - break; - case false: //Unmute; - set = await this.xboxWebApi.send('Audio', 'Unmute'); - break; - } + case 2: + this.inputsConfigured.sort((a, b) => b.name.localeCompare(a.name)); break; - case 'RcControl': - set = await this.xboxWebApi.send('Shell', 'InjectKey', [{ 'keyType': value }]); + case 3: + this.inputsConfigured.sort((a, b) => a.reference.localeCompare(b.reference)); break; - default: - this.emit('warn', `${integration}, received key: ${key}, value: ${value}`); + case 4: + this.inputsConfigured.sort((a, b) => b.reference.localeCompare(a.reference)); break; - }; - return set; + } + const debug = !this.enableDebugMode ? false : this.emit('debug', `Inputs display order: ${JSON.stringify(this.inputsConfigured, null, 2)}`); + + const displayOrder = this.inputsConfigured.map(input => input.identifier); + this.televisionService.setCharacteristic(Characteristic.DisplayOrder, Encode(1, displayOrder).toString('base64')); + return true; } catch (error) { - throw new Error(`${integration} set key: ${key}, value: ${value}, error: ${error.message || error}`); + throw new Error(`Display order error: ${error.message || error}`); }; } + async prepareDataForAccessory() { + try { + //read dev info from file + const savedInfo = await this.readData(this.devInfoFile); + this.savedInfo = savedInfo.toString().trim() !== '' ? JSON.parse(savedInfo) : {}; + const debug = !this.enableDebugMode ? false : this.emit('debug', `Read saved Info: ${JSON.stringify(this.savedInfo, null, 2)}`); + + //read inputs file + const savedInputs = await this.readData(this.inputsFile); + this.savedInputs = savedInputs.toString().trim() !== '' ? JSON.parse(savedInputs) : this.inputs; + const debug2 = !this.enableDebugMode ? false : this.emit('debug', `Read saved Inputs: ${JSON.stringify(this.savedInputs, null, 2)}`); + + //read inputs names from file + const savedInputsNames = await this.readData(this.inputsNamesFile); + this.savedInputsNames = savedInputsNames.toString().trim() !== '' ? JSON.parse(savedInputsNames) : {}; + const debug3 = !this.enableDebugMode ? false : this.emit('debug', `Read saved Inputs Names: ${JSON.stringify(this.savedInputsNames, null, 2)}`); + + //read inputs visibility from file + const savedInputsTargetVisibility = await this.readData(this.inputsTargetVisibilityFile); + this.savedInputsTargetVisibility = savedInputsTargetVisibility.toString().trim() !== '' ? JSON.parse(savedInputsTargetVisibility) : {}; + const debug4 = !this.enableDebugMode ? false : this.emit('debug', `Read saved Inputs Target Visibility: ${JSON.stringify(this.savedInputsTargetVisibility, null, 2)}`); + + return true; + } catch (error) { + throw new Error(`Prepare data for accessory error: ${error}`); + } + } + //Prepare accessory async prepareAccessory() { try { @@ -632,7 +405,7 @@ class XboxDevice extends EventEmitter { } } - const logInfo = this.disableLogInfo ? false : this.emit('message', `set Power: ${state ? 'ON' : 'OFF'}`); + const logInfo = this.disableLogInfo ? false : this.emit('info', `set Power: ${state ? 'ON' : 'OFF'}`); await new Promise(resolve => setTimeout(resolve, 3000)); } catch (error) { this.emit('warn', `set Power, error: ${error}`); @@ -682,7 +455,7 @@ class XboxDevice extends EventEmitter { } await this.xboxWebApi.send(channelName, command, payload); - const logInfo = this.disableLogInfo ? false : this.emit('message', `set Input: ${inputName}, Reference: ${inputReference}, Product Id: ${inputOneStoreProductId}`); + const logInfo = this.disableLogInfo ? false : this.emit('info', `set Input: ${inputName}, Reference: ${inputReference}, Product Id: ${inputOneStoreProductId}`); break; } } catch (error) { @@ -752,7 +525,7 @@ class XboxDevice extends EventEmitter { }; await this.xboxWebApi.send(channelName, 'InjectKey', [{ 'keyType': command }]); - const logInfo = this.disableLogInfo ? false : this.emit('message', `Remote Key: ${command}`); + const logInfo = this.disableLogInfo ? false : this.emit('info', `Remote Key: ${command}`); } catch (error) { this.emit('warn', `set Remote Key error: ${JSON.stringify(error, null, 2)}`); }; @@ -776,7 +549,7 @@ class XboxDevice extends EventEmitter { try { const newMediaState = value; const setMediaState = this.power ? false : false; - const logInfo = this.disableLogInfo ? false : this.emit('message', `set Target Media: ${['PLAY', 'PAUSE', 'STOP', 'LOADING', 'INTERRUPTED'][value]}`); + const logInfo = this.disableLogInfo ? false : this.emit('info', `set Target Media: ${['PLAY', 'PAUSE', 'STOP', 'LOADING', 'INTERRUPTED'][value]}`); } catch (error) { this.emit('warn', `set Target Media error: ${error}`); }; @@ -799,7 +572,7 @@ class XboxDevice extends EventEmitter { }; await this.xboxWebApi.send(channelName, 'InjectKey', [{ 'keyType': command }]); - const logInfo = this.disableLogInfo ? false : this.emit('message', `set Power Mode Selection: ${powerModeSelection === 0 ? 'SHOW' : 'HIDE'}`); + const logInfo = this.disableLogInfo ? false : this.emit('info', `set Power Mode Selection: ${powerModeSelection === 0 ? 'SHOW' : 'HIDE'}`); } catch (error) { this.emit('warn', `set Power Mode Selection error: ${error}`); }; @@ -840,7 +613,7 @@ class XboxDevice extends EventEmitter { } await this.xboxWebApi.send(channelName, command); - const logInfo = this.disableLogInfo ? false : this.emit('message', `set Volume Selector: ${volumeSelector ? 'Down' : 'UP'}`); + const logInfo = this.disableLogInfo ? false : this.emit('info', `set Volume Selector: ${volumeSelector ? 'Down' : 'UP'}`); } catch (error) { this.emit('warn', `set Volume Selector error: ${error}`); }; @@ -852,7 +625,7 @@ class XboxDevice extends EventEmitter { return volume; }) .onSet(async (volume) => { - const logInfo = this.disableLogInfo ? false : this.emit('message', `set Volume: ${volume}`); + const logInfo = this.disableLogInfo ? false : this.emit('info', `set Volume: ${volume}`); }); this.speakerService.getCharacteristic(Characteristic.Mute) @@ -876,7 +649,7 @@ class XboxDevice extends EventEmitter { } await this.xboxWebApi.send(channelName, command); - const logInfo = this.disableLogInfo ? false : this.emit('message', `set Mute: ${state ? 'ON' : 'OFF'}`); + const logInfo = this.disableLogInfo ? false : this.emit('info', `set Mute: ${state ? 'ON' : 'OFF'}`); } catch (error) { this.emit('warn', `set Mute error: ${error}`); }; @@ -944,10 +717,6 @@ class XboxDevice extends EventEmitter { return input.name; }) .onSet(async (value) => { - if (value === this.savedInputsNames[inputReference]) { - return; - } - try { input.name = value; this.savedInputsNames[inputReference] = value; @@ -968,10 +737,6 @@ class XboxDevice extends EventEmitter { return input.visibility; }) .onSet(async (state) => { - if (state === this.savedInputsTargetVisibility[inputReference]) { - return; - } - try { input.visibility = state, this.savedInputsTargetVisibility[inputReference] = state; @@ -1189,7 +954,7 @@ class XboxDevice extends EventEmitter { const send5 = this.power && state ? await this.xboxWebApi.send('Shell', 'ShowGuideTab', [{ 'tabName': 'Guide' }]) : false; break; case 'Not set': case 'Web api disabled': - this.emit('message', `trying to launch App/Game with one store product id: ${buttonOneStoreProductId}.`); + this.emit('info', `trying to launch App/Game with one store product id: ${buttonOneStoreProductId}.`); break; default: const send6 = this.power && state ? await this.xboxWebApi.send('Shell', 'ActivateApplicationWithOneStoreProductId', [{ 'oneStoreProductId': buttonOneStoreProductId }]) : false; @@ -1206,10 +971,252 @@ class XboxDevice extends EventEmitter { accessory.addService(buttonService); } } + + //sort inputs list + const sortInputsDisplayOrder = this.televisionService ? await this.displayOrder() : false; + return accessory; } catch (error) { throw new Error(error) }; } + + //start + async start() { + try { + //web api client + if (this.webApiControl) { + try { + this.xboxWebApi = new XboxWebApi({ + xboxLiveId: this.xboxLiveId, + webApiClientId: this.webApiClientId, + webApiClientSecret: this.webApiClientSecret, + tokensFile: this.authTokenFile, + inputsFile: this.inputsFile, + enableDebugMode: this.enableDebugMode + }); + + this.xboxWebApi.on('consoleStatus', (consoleType) => { + if (this.informationService) { + this.informationService + .setCharacteristic(Characteristic.Model, consoleType) + }; + + //this.serialNumber = id; + this.modelName = consoleType; + //this.power = powerState; + //this.mediaState = playbackState; + }) + .on('powerOnError', (power) => { + if (this.televisionService) { + this.televisionService + .updateCharacteristic(Characteristic.Active, power) + }; + this.power = power; + }) + .on('success', (success) => { + this.emit('success', success); + }) + .on('info', (info) => { + this.emit('info', info); + }) + .on('debug', (debug) => { + this.emit('debug', debug); + }) + .on('warn', (warn) => { + this.emit('warn', warn); + }) + .on('error', (error) => { + this.emit('error', error); + }) + .on('restFul', (path, data) => { + const restFul = this.restFulConnected ? this.restFul1.update(path, data) : false; + }) + .on('mqtt', (topic, message) => { + const mqtt = this.mqttConnected ? this.mqtt1.emit('publish', topic, message) : false; + }); + + //check authorization + await this.xboxWebApi.checkAuthorization(); + + //start impulse generator + const timers = [{ name: 'checkAuthorization', sampling: 900000 }]; + await this.xboxWebApi.impulseGenerator.start(timers); + } catch (error) { + this.emit('error', error); + }; + }; + + //xbox local client + this.xboxLocalApi = new XboxLocalApi({ + host: this.host, + xboxLiveId: this.xboxLiveId, + tokensFile: this.authTokenFile, + devInfoFile: this.devInfoFile, + disableLogInfo: this.disableLogInfo, + enableDebugMode: this.enableDebugMode + }); + + this.xboxLocalApi.on('deviceInfo', (firmwareRevision, locale) => { + this.emit('devInfo', `-------- ${this.name} --------'`); + this.emit('devInfo', `Manufacturer: Microsoft`); + this.emit('devInfo', `Model: ${this.modelName ?? 'Xbox'}`); + this.emit('devInfo', `Serialnr: ${this.xboxLiveId}`); + this.emit('devInfo', `Firmware: ${firmwareRevision}`); + this.emit('devInfo', `Locale: ${locale}`); + this.emit('devInfo', `----------------------------------`); + + if (this.informationService) { + this.informationService + .setCharacteristic(Characteristic.Manufacturer, 'Microsoft') + .setCharacteristic(Characteristic.FirmwareRevision, firmwareRevision); + }; + }) + .on('stateChanged', (power, volume, mute, mediaState, titleId, reference) => { + const input = this.inputsConfigured.find(input => input.reference === reference || input.titleId === titleId) ?? false; + const inputIdentifier = input ? input.identifier : this.inputIdentifier; + + //update characteristics + if (this.televisionService) { + this.televisionService + .updateCharacteristic(Characteristic.Active, power); + }; + + if (this.televisionService) { + this.televisionService + .updateCharacteristic(Characteristic.ActiveIdentifier, inputIdentifier); + }; + + if (this.speakerService) { + this.speakerService + .updateCharacteristic(Characteristic.Volume, volume) + .updateCharacteristic(Characteristic.Mute, mute); + if (this.volumeService) { + this.volumeService + .updateCharacteristic(Characteristic.Brightness, volume) + .updateCharacteristic(Characteristic.On, !mute); + }; + if (this.volumeServiceFan) { + this.volumeServiceFan + .updateCharacteristic(Characteristic.RotationSpeed, volume) + .updateCharacteristic(Characteristic.On, !mute); + }; + }; + + if (this.sensorPowerService) { + this.sensorPowerService + .updateCharacteristic(Characteristic.ContactSensorState, power); + } + + if (this.sensorInputService && reference !== this.reference) { + for (let i = 0; i < 2; i++) { + const state = power ? [true, false][i] : false; + this.sensorInputService + .updateCharacteristic(Characteristic.ContactSensorState, state); + this.sensorInputState = state; + } + } + + if (this.sensorScreenSaverService) { + const state = power ? (reference === 'Xbox.IdleScreen_8wekyb3d8bbwe!Xbox.IdleScreen.Application') : false; + this.sensorScreenSaverService + .updateCharacteristic(Characteristic.ContactSensorState, state); + this.sensorScreenSaverState = state; + } + + if (this.sensorsInputsServices) { + for (let i = 0; i < this.sensorsInputsConfiguredCount; i++) { + const sensorInput = this.sensorsInputsConfigured[i]; + const state = power ? sensorInput.reference === reference : false; + sensorInput.state = state; + const characteristicType = sensorInput.characteristicType; + this.sensorsInputsServices[i] + .updateCharacteristic(characteristicType, state); + } + } + + //buttons + if (this.buttonsServices) { + for (let i = 0; i < this.buttonsConfiguredCount; i++) { + const button = this.buttonsConfigured[i]; + const state = this.power ? button.reference === reference : false; + button.state = state; + this.buttonsServices[i] + .updateCharacteristic(Characteristic.On, state); + } + } + + this.inputIdentifier = inputIdentifier; + this.power = power; + this.reference = reference; + this.volume = volume; + this.mute = mute; + this.mediaState = mediaState; + + if (!this.disableLogInfo) { + const name = input ? input.name : reference; + const productId = input ? input.oneStoreProductId : reference; + this.emit('info', `Power: ${power ? 'ON' : 'OFF'}`); + this.emit('info', `Input Name: ${name}`); + this.emit('info', `Reference: ${reference}`); + this.emit('info', `Title Id: ${titleId}`); + this.emit('info', `Product Id: ${productId}`); + this.emit('info', `Volume: ${volume}%`); + this.emit('info', `Mute: ${mute ? 'ON' : 'OFF'}`); + this.emit('info', `Media State: ${['PLAY', 'PAUSE', 'STOPPED', 'LOADING', 'INTERRUPTED'][mediaState]}`); + }; + }) + .on('success', (message) => { + this.emit('success', message); + }) + .on('info', (message) => { + this.emit('info', message); + }) + .on('debug', (debug) => { + this.emit('debug', debug); + }) + .on('warn', (warn) => { + this.emit('warn', warn); + }) + .on('error', (error) => { + this.emit('error', error); + }) + .on('disconnected', (message) => { + this.emit('info', message); + }) + .on('restFul', (path, data) => { + const restFul = this.restFulConnected ? this.restFul1.update(path, data) : false; + }) + .on('mqtt', (topic, message) => { + const mqtt = this.mqttConnected ? this.mqtt1.emit('publish', topic, message) : false; + }); + + //connect to local api + const connect = await this.xboxLocalApi.connect(); + if (!connect) { + return false; + } + + //start external integrations + const startExternalIntegrations = this.mqtt.enable ? await this.externalIntegrations() : false; + + //prepare data for accessory + await this.prepareDataForAccessory(); + + //prepare accessory + if (this.startPrepareAccessory) { + const accessory = await this.prepareAccessory(); + this.emit('publishAccessory', accessory); + this.startPrepareAccessory = false; + + //start impulse generator + await this.xboxLocalApi.impulseGenerator.start([{ name: 'heartBeat', sampling: 10000 }]); + } + + return true; + } catch (error) { + throw new Error(`Start error: ${error.message || error}`); + }; + } }; export default XboxDevice;