diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b613e0..e1e867a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### NOTE!!! ## After update to 2.x.x the plugin settings (xboxLiveId) need to be updated. -## [2.2.4] - (xx.04.2022) +## [2.3.0] - (24.08.2022) ## Changed -- fixed MQTT device info -- refactor send debug and info log +- fix MQTT device info +- refactor debug and info log - refactor send mqtt message - bump dependencies +- code cleanup +- added Xbox Guide as default input +- fix [#137](https://github.com/grzegorz914/homebridge-xbox-tv/issues/137) ## [2.2.2] - (09.03.2022) ## Changed diff --git a/README.md b/README.md index 2dcb6aa..41b7ad5 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Homebridge plugin for Microsoft game Consoles. Tested with Xbox One X/S and Xbox ## Troubleshooting * If for some reason the device is not displayed in HomeKit app try this procedure: - * Go to `./homebridge/persist` or `/var/lib/homebridge/persist` for RPI. + * Go to `./homebridge/persist` macOS or `/var/lib/homebridge/persist` for RPI. * Remove `AccessoryInfo.xxx` file which contain Your device data: `{"displayName":"Xbox"}`. * Next remove `IdentifierCashe.xxx` file with same name as `AccessoryInfo.xxx`. * Restart Homebridge and try add it to the HomeKit app again. @@ -60,24 +60,18 @@ Homebridge plugin for Microsoft game Consoles. Tested with Xbox One X/S and Xbox ## Configuration Console * [Device must have Instant-on power mode enabled](https://support.xbox.com/help/hardware-network/power/learn-about-power-modes) * Profile & System > Settings > General > Power mode & startup -* Console need to allow connect from any 3rd app. *Allow Connections from any device* should be enabled. + * Console need to allow connect from any 3rd app. *Allow Connections from any device* should be enabled. * Profile & System > Settings > Devices & Connections > Remote features > Xbox app preferences. ## Authorization Manager * First of all please use built in Authorization Manager. -* Start new authorization need remove old token first, to clear token use Authorization Manager GUI. -* Make sure Your web browser do not block pop-up window, if Yes allow pop-up window for this app. -* If for some reason you cannot use Authorization Manager, please use Authorization Manual Mode (removed ab v2.0.13). + * Start new authorization need remove old token first, to clear token use Authorization Manager GUI. + * Make sure Your web browser do not block pop-up window, if Yes allow pop-up window for this app.
-### Authorization Manual Mode (removed ab v2.0.13) -* After enable `webApiControl` option, restart the plugin and go to Homebridge console log. -* Follow the instructions in the console log. -* Start new authorization need remove old token first, go to *./homebridge/xboxTv/* and remove token file. - ### Configuration * Run this plugin as a child bridge (Highly Recommended). * Install and use [Homebridge Config UI X](https://github.com/oznu/homebridge-config-ui-x/wiki) to configure this plugin (Highly Recommended). @@ -108,7 +102,7 @@ Homebridge plugin for Microsoft game Consoles. Tested with Xbox One X/S and Xbox | `filterApps` | If enabled, Apps will be hidden and not displayed in the inputs list, only available if `webApiControl` enabled. | | `filterSystemApps` | If enabled, System Apps (Accessory, Microsoft Store, Television) will be hidden and not displayed in the inputs list, only available if `webApiControl` enabled. | | `filterDlc` | If enabled, Dlc will be hidden and not displayed in the inputs list, only available if `webApiControl` enabled. | -| `inputs.name` | Here set *Input Name* which You want expose to the *Homebridge/HomeKit*, `Screensaver`, `Television`, `TV Settings`, `Dashboard`, `Accessory`, `Settings`, `Network Troubleshooter`, `Microsoft Store` are created by default. | +| `inputs.name` | Here set *Input Name* which You want expose to the *Homebridge/HomeKit*, `Screensaver`, `Television`, `TV Settings`, `Dashboard`, `Accessory`, `Settings`, `Network Troubleshooter`, `Microsoft Store`, `Xbox Guide` are created by default. | | `inputs.reference` | Required to identify current running app. | | `inputs.oneStoreProductId` | Required to switch apps. | | `inputs.type` | Here select from available types. | @@ -203,13 +197,13 @@ Homebridge plugin for Microsoft game Consoles. Tested with Xbox One X/S and Xbox ``` ### Adding to HomeKit -Each accessory needs to be manually paired. -1. Open the Home app on your device. -2. Tap the Home tab, then tap . -3. Tap *Add Accessory*, and select *I Don't Have a Code, Cannot Scan* or *More options*. -4. Select Your accessory and press add anyway. -5. Enter the PIN or scan the QR code, this can be found in Homebridge UI or Homebridge logs. -6. Complete the accessory setup. +* Each accessory needs to be manually paired. + * Open the Home app on your device. + * Tap the Home tab, then tap . + * Tap *Add Accessory*, and select *I Don't Have a Code, Cannot Scan* or *More options*. + * Select Your accessory and press add anyway. + * Enter the PIN or scan the QR code, this can be found in Homebridge UI or Homebridge logs. + * Complete the accessory setup. ## Limitations * That maximum Services for 1 accessory is 100. If Services > 100, accessory stop responding. diff --git a/homebridge-ui/server.js b/homebridge-ui/server.js index 5aedb1e..b973f40 100644 --- a/homebridge-ui/server.js +++ b/homebridge-ui/server.js @@ -1,3 +1,5 @@ +"use strict"; + const { HomebridgePluginUiServer, RequestError diff --git a/index.js b/index.js index 77f291b..8eeab8a 100644 --- a/index.js +++ b/index.js @@ -5,138 +5,13 @@ const fs = require('fs'); const fsPromises = fs.promises; const XboxWebApi = require('xbox-webapi'); -const Smartglass = require('./src/smartglass.js'); -const mqttClient = require('./src/mqtt.js'); +const XboxLocalApi = require('./src/xboxlocalapi.js'); +const Mqtt = require('./src/mqtt.js'); +const CONSTANS = require('./src/constans.json'); const PLUGIN_NAME = 'homebridge-xbox-tv'; const PLATFORM_NAME = 'XboxTv'; -const CONSOLES_NAME = { - 'XboxSeriesX': 'Xbox Series X', - 'XboxSeriesS': 'Xbox Series S', - 'XboxOne': 'Xbox One', - 'XboxOneS': 'Xbox One S', - 'XboxOneX': 'Xbox One X' -}; -const CONSOLE_POWER_STATE = { - 'Off': 0, - 'On': 1, - 'ConnectedStandby': 2, - 'SystemUpdate': 3, - 'Unknown': 4 -}; -const CONSOLE_PLAYBACK_STATE = { - 'Stopped': 0, - 'Playing': 1, - 'Paused': 2, - 'Unknown': 3 -}; -const DEFAULT_INPUTS = [{ - 'name': 'Screensaver', - 'titleId': '851275400', - 'reference': 'Xbox.IdleScreen_8wekyb3d8bbwe!Xbox.IdleScreen.Application', - 'oneStoreProductId': 'Screensaver', - 'type': 'HOME_SCREEN', - 'contentType': 'Dashboard' - }, - { - 'name': 'Dashboard', - 'titleId': '750323071', - 'reference': 'Xbox.Dashboard_8wekyb3d8bbwe!Xbox.Dashboard.Application', - 'oneStoreProductId': 'Dashboard', - 'type': 'HOME_SCREEN', - 'contentType': 'Dashboard' - }, - { - 'name': 'Settings', - 'titleId': '1837352387', - 'reference': 'Microsoft.Xbox.Settings_8wekyb3d8bbwe!Xbox.Settings.Application', - 'oneStoreProductId': 'Settings', - 'type': 'HOME_SCREEN', - 'contentType': 'Dashboard' - }, - { - 'name': 'Television', - 'titleId': '371594669', - 'reference': 'Microsoft.Xbox.LiveTV_8wekyb3d8bbwe!Microsoft.Xbox.LiveTV.Application', - 'oneStoreProductId': 'Television', - 'type': 'HDMI', - 'contentType': 'systemApp' - }, - { - 'name': 'Settings TV', - 'titleId': '2019308066', - 'reference': 'Microsoft.Xbox.TvSettings_8wekyb3d8bbwe!Microsoft.Xbox.TvSettings.Application', - 'oneStoreProductId': 'SettingsTv', - 'type': 'HOME_SCREEN', - 'contentType': 'Dashboard' - }, - { - 'name': 'Accessory', - 'titleId': '758407307', - 'reference': 'Microsoft.XboxDevices_8wekyb3d8bbwe!App', - 'oneStoreProductId': 'Accessory', - 'type': 'HOME_SCREEN', - 'contentType': 'systemApp' - }, - { - 'name': 'Network Troubleshooter', - 'titleId': '1614319806', - 'reference': 'Xbox.NetworkTroubleshooter_8wekyb3d8bbwe!Xbox.NetworkTroubleshooter.Application', - 'oneStoreProductId': 'NetworkTroubleshooter', - 'type': 'HOME_SCREEN', - 'contentType': 'systemApp' - }, - { - 'name': 'Microsoft Store', - 'titleId': '1864271209', - 'reference': 'Microsoft.storify_8wekyb3d8bbwe!App', - 'oneStoreProductId': 'MicrosoftStore', - 'type': 'HOME_SCREEN', - 'contentType': 'systemApp' - } -]; - -const SYSTEM_MEDIA_COMMANDS = { - play: 2, - pause: 4, - playpause: 8, - stop: 16, - record: 32, - nextTrack: 64, - prevTrack: 128, - fastForward: 256, - rewind: 512, - channelUp: 1024, - channelDown: 2048, - back: 4096, - view: 8192, - menu: 16384, - seek: 32786 -}; - -const SYSTEM_INPUTS_COMMANDS = { - nexus: 2, - view1: 4, - menu1: 8, - a: 16, - b: 32, - x: 64, - y: 128, - up: 256, - down: 512, - left: 1024, - right: 2048 -}; - -const TV_REMOTE_COMMANDS = { - volUp: 'btn.vol_up', - volDown: 'btn.vol_down', - volMute: 'btn.vol_mute' -}; - -const INPUT_SOURCE_TYPES = ['OTHER', 'HOME_SCREEN', 'TUNER', 'HDMI', 'COMPOSITE_VIDEO', 'S_VIDEO', 'COMPONENT_VIDEO', 'DVI', 'AIRPLAY', 'USB', 'APPLICATION']; - let Accessory, Characteristic, Service, Categories, UUID; module.exports = (api) => { @@ -145,30 +20,30 @@ module.exports = (api) => { Service = api.hap.Service; Categories = api.hap.Categories; UUID = api.hap.uuid; - api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, xboxTvPlatform, true); + api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, XBOXPLATFORM, true); }; - -class xboxTvPlatform { +class XBOXPLATFORM { constructor(log, config, api) { // only load if configured if (!config || !Array.isArray(config.devices)) { - log('No configuration found for %s', PLUGIN_NAME); + log(`No configuration found for ${PLUGIN_NAME}`); return; } this.log = log; this.api = api; - this.devices = config.devices || []; + this.devicesConfig = config.devices; this.accessories = []; this.api.on('didFinishLaunching', () => { this.log.debug('didFinishLaunching'); - for (let i = 0; i < this.devices.length; i++) { - const device = this.devices[i]; - if (!device.name || !device.host || !device.xboxLiveId) { + const devicesCount = this.devicesConfig.length; + for (let i = 0; i < devicesCount; i++) { + const deviceConfig = this.devicesConfig[i]; + if (!deviceConfig.name || !deviceConfig.host || !deviceConfig.xboxLiveId) { this.log.warn('Device Name, Host or Xbox Live ID Missing'); } else { - new xboxTvDevice(this.log, device, this.api); + new XBOXDEVICE(this.log, this.api, deviceConfig); } } }); @@ -185,11 +60,11 @@ class xboxTvPlatform { } } -class xboxTvDevice { - constructor(log, config, api) { +class XBOXDEVICE { + constructor(log, api, config) { this.log = log; - this.config = config; this.api = api; + this.config = config; //device configuration this.name = config.name || 'Game console'; @@ -224,18 +99,19 @@ class xboxTvDevice { //add configured inputs to the default inputs const inputsArr = new Array(); - const defaultInputsCount = DEFAULT_INPUTS.length; + const defaultInputsCount = CONSTANS.DefaultInputs.length; for (let i = 0; i < defaultInputsCount; i++) { - inputsArr.push(DEFAULT_INPUTS[i]); + inputsArr.push(CONSTANS.DefaultInputs[i]); } const inputsCount = this.inputs.length; for (let j = 0; j < inputsCount; j++) { + const input = this.inputs[j]; const obj = { - 'name': this.inputs[j].name, - 'titleId': this.inputs[j].titleId, - 'reference': this.inputs[j].reference, - 'oneStoreProductId': this.inputs[j].oneStoreProductId, - 'type': this.inputs[j].type, + 'name': input.name, + 'titleId': input.titleId, + 'reference': input.reference, + 'oneStoreProductId': input.oneStoreProductId, + 'type': input.type, 'contentType': 'Game' } inputsArr.push(obj); @@ -264,9 +140,6 @@ class xboxTvDevice { this.mediaState = 0; this.inputIdentifier = 0; - this.pictureMode = 0; - this.brightness = 0; - this.prefDir = path.join(api.user.storagePath(), 'xboxTv'); this.authTokenFile = `${this.prefDir}/authToken_${this.host.split('.').join('')}`; this.devInfoFile = `${this.prefDir}/devInfo_${this.host.split('.').join('')}`; @@ -302,7 +175,7 @@ class xboxTvDevice { } //mqtt client - this.mqttClient = new mqttClient({ + this.mqtt = new Mqtt({ enabled: this.enableMqtt, host: this.mqttHost, port: this.mqttPort, @@ -314,7 +187,7 @@ class xboxTvDevice { debug: this.mqttDebug }); - this.mqttClient.on('connected', (message) => { + this.mqtt.on('connected', (message) => { this.log('Device: %s %s, %s', this.host, this.name, message); }) .on('error', (error) => { @@ -346,7 +219,7 @@ class xboxTvDevice { }; //xbox client - this.xbox = new Smartglass({ + this.xboxLocalApi = new XboxLocalApi({ host: this.host, xboxLiveId: this.xboxLiveId, userToken: this.userToken, @@ -356,7 +229,7 @@ class xboxTvDevice { mqttEnabled: this.enableMqtt }); - this.xbox.on('connected', (message) => { + this.xboxLocalApi.on('connected', (message) => { this.log('Device: %s %s, %s', this.host, this.name, message) }) .on('error', (error) => { @@ -368,7 +241,7 @@ class xboxTvDevice { .on('message', (message) => { this.log('Device: %s %s, %s', this.host, this.name, message); }) - .on('deviceInfo', async (firmwareRevision) => { + .on('deviceInfo', async (firmwareRevision, locale) => { if (!this.disableLogDeviceInfo) { this.log('-------- %s --------', this.name); this.log('Manufacturer: %s', this.manufacturer); @@ -378,32 +251,29 @@ class xboxTvDevice { this.log('----------------------------------'); } - const obj = { - 'manufacturer': this.manufacturer, - 'modelName': this.modelName, - 'serialNumber': this.serialNumber, - 'firmwareRevision': firmwareRevision - }; - const devInfo = JSON.stringify(obj, null, 2); try { + const obj = { + 'manufacturer': this.manufacturer, + 'modelName': this.modelName, + 'serialNumber': this.serialNumber, + 'firmwareRevision': firmwareRevision + }; + const devInfo = JSON.stringify(obj, null, 2); const writeDevInfo = await fsPromises.writeFile(this.devInfoFile, devInfo); - const debug = this.enableDebugMode ? this.log('Device: %s %s, debug writeDevInfo: %s', this.host, this.name, devInfo) : false; + const debug = this.enableDebugMode ? this.log('Device: %s %s, debug saved device info: %s', this.host, this.name, devInfo) : false; } catch (error) { - this.log.error('Device: %s %s, get Device Info error: %s', this.host, this.name, error); + this.log.error('Device: %s %s, device info error: %s', this.host, this.name, error); }; - this.devInfo = devInfo; this.firmwareRevision = firmwareRevision; }) .on('stateChanged', (power, titleId, inputReference, volume, mute, mediaState) => { - - const powerState = power; const inputIdentifier = this.inputsReference.indexOf(inputReference) >= 0 ? this.inputsReference.indexOf(inputReference) : this.inputsTitleId.indexOf(titleId) >= 0 ? this.inputsTitleId.indexOf(titleId) : this.inputIdentifier; //update characteristics if (this.televisionService) { this.televisionService - .updateCharacteristic(Characteristic.Active, powerState) + .updateCharacteristic(Characteristic.Active, power) .updateCharacteristic(Characteristic.ActiveIdentifier, inputIdentifier); }; @@ -423,15 +293,15 @@ class xboxTvDevice { }; }; - this.powerState = powerState; + this.powerState = power; this.volume = volume; this.muteState = mute; this.mediaState = mediaState; this.inputIdentifier = inputIdentifier; - this.mqttClient.send('Info', this.devInfo); + this.mqtt.send('Info', this.devInfo); }) .on('mqtt', (topic, message) => { - this.mqttClient.send(topic, message); + this.mqtt.send(topic, message); }) .on('disconnected', (message) => { this.log('Device: %s %s, %s', this.host, this.name, message); @@ -494,17 +364,18 @@ class xboxTvDevice { const consolesListCount = consolesListData.length; for (let i = 0; i < consolesListCount; i++) { - const id = consolesListData[i].id; - const name = consolesListData[i].name; - const locale = consolesListData[i].locale; - const region = consolesListData[i].region; - const consoleType = consolesListData[i].consoleType; - const powerState = CONSOLE_POWER_STATE[consolesListData[i].powerState]; // 0 - Off, 1 - On, 2 - ConnectedStandby, 3 - SystemUpdate - const digitalAssistantRemoteControlEnabled = (consolesListData[i].digitalAssistantRemoteControlEnabled == true); - const remoteManagementEnabled = (consolesListData[i].remoteManagementEnabled == true); - const consoleStreamingEnabled = (consolesListData[i].consoleStreamingEnabled == true); - const wirelessWarning = (consolesListData[i].wirelessWarning == true); - const outOfHomeWarning = (consolesListData[i].outOfHomeWarning == true); + const console = consolesListData[i]; + const id = console.id; + const name = console.name; + const locale = console.locale; + const region = console.region; + const consoleType = console.consoleType; + const powerState = CONSTANS.ConsolePowerState[console.powerState]; // 0 - Off, 1 - On, 2 - ConnectedStandby, 3 - SystemUpdate + const digitalAssistantRemoteControlEnabled = (console.digitalAssistantRemoteControlEnabled == true); + const remoteManagementEnabled = (console.remoteManagementEnabled == true); + const consoleStreamingEnabled = (console.consoleStreamingEnabled == true); + const wirelessWarning = (console.wirelessWarning == true); + const outOfHomeWarning = (console.outOfHomeWarning == true); this.consolesId.push(id); this.consolesName.push(name); @@ -518,14 +389,15 @@ class xboxTvDevice { this.consolesWirelessWarning.push(wirelessWarning); this.consolesOutOfHomeWarning.push(outOfHomeWarning); - const consolesStorageDevicesCount = consolesListData[i].storageDevices.length; + const consolesStorageDevicesCount = console.storageDevices.length; for (let j = 0; j < consolesStorageDevicesCount; j++) { - const storageDeviceId = consolesListData[i].storageDevices[j].storageDeviceId; - const storageDeviceName = consolesListData[i].storageDevices[j].storageDeviceName; - const isDefault = (consolesListData[i].storageDevices[j].isDefault == true); - const freeSpaceBytes = consolesListData[i].storageDevices[j].freeSpaceBytes; - const totalSpaceBytes = consolesListData[i].storageDevices[j].totalSpaceBytes; - const isGen9Compatible = consolesListData[i].storageDevices[j].isGen9Compatible; + const consoleStorageDevice = console.storageDevices[j]; + const storageDeviceId = consoleStorageDevice.storageDeviceId; + const storageDeviceName = consoleStorageDevice.storageDeviceName; + const isDefault = (consoleStorageDevice.isDefault == true); + const freeSpaceBytes = consoleStorageDevice.freeSpaceBytes; + const totalSpaceBytes = consoleStorageDevice.totalSpaceBytes; + const isGen9Compatible = consoleStorageDevice.isGen9Compatible; this.consolesStorageDeviceId.push(storageDeviceId); this.consolesStorageDeviceName.push(storageDeviceName); @@ -560,18 +432,20 @@ class xboxTvDevice { const profileUsersCount = userProfileData.length; for (let i = 0; i < profileUsersCount; i++) { - const id = userProfileData[i].id; - const hostId = userProfileData[i].hostId; - const isSponsoredUser = userProfileData[i].isSponsoredUser; + const userProfile = userProfileData[i]; + const id = userProfile.id; + const hostId = userProfile.hostId; + const isSponsoredUser = userProfile.isSponsoredUser; this.userProfileId.push(id); this.userProfileHostId.push(hostId); this.userProfileIsSponsoredUser.push(isSponsoredUser); - const profileUsersSettingsCount = userProfileData[i].settings.length; + const profileUsersSettingsCount = userProfile.settings.length; for (let j = 0; j < profileUsersSettingsCount; j++) { - const id = userProfileData[i].settings[j].id; - const value = userProfileData[i].settings[j].value; + const userProfileSettings = userProfileData[i].settings[j]; + const id = userProfileSettings.id; + const value = userProfileSettings.value; this.userProfileSettingsId.push(id); this.userProfileSettingsValue.push(value); @@ -593,31 +467,32 @@ class xboxTvDevice { const debug = this.enableDebugMode ? this.log('Device: %s %s, debug getInstalledAppsData: %s', this.host, this.name, getInstalledAppsData.result) : false; const inputsArr = new Array(); - const defaultInputsCount = DEFAULT_INPUTS.length; + const defaultInputsCount = CONSTANS.DefaultInputs.length; for (let i = 0; i < defaultInputsCount; i++) { - inputsArr.push(DEFAULT_INPUTS[i]); + inputsArr.push(CONSTANS.DefaultInputs[i]); }; //get installed inputs/apps from web const inputsData = getInstalledAppsData.result; const inputsCount = inputsData.length; for (let i = 0; i < inputsCount; i++) { - const oneStoreProductId = inputsData[i].oneStoreProductId; - const titleId = inputsData[i].titleId; - const aumid = inputsData[i].aumid; - const lastActiveTime = inputsData[i].lastActiveTime; - const isGame = (inputsData[i].isGame == true); - const name = inputsData[i].name; - const contentType = inputsData[i].contentType; - const instanceId = inputsData[i].instanceId; - const storageDeviceId = inputsData[i].storageDeviceId; - const uniqueId = inputsData[i].uniqueId; - const legacyProductId = inputsData[i].legacyProductId; - const version = inputsData[i].version; - const sizeInBytes = inputsData[i].sizeInBytes; - const installTime = inputsData[i].installTime; - const updateTime = inputsData[i].updateTime; - const parentId = inputsData[i].parentId; + const input = inputsData[i]; + const oneStoreProductId = input.oneStoreProductId; + const titleId = input.titleId; + const aumid = input.aumid; + const lastActiveTime = input.lastActiveTime; + const isGame = (input.isGame == true); + const name = input.name; + const contentType = input.contentType; + const instanceId = input.instanceId; + const storageDeviceId = input.storageDeviceId; + const uniqueId = input.uniqueId; + const legacyProductId = input.legacyProductId; + const version = input.version; + const sizeInBytes = input.sizeInBytes; + const installTime = input.installTime; + const updateTime = input.updateTime; + const parentId = input.parentId; const type = 'APPLICATION'; const inputsObj = { @@ -661,12 +536,13 @@ class xboxTvDevice { const storageDevicesCount = storageDeviceData.length; for (let i = 0; i < storageDevicesCount; i++) { - const storageDeviceId = storageDeviceData[i].storageDeviceId; - const storageDeviceName = storageDeviceData[i].storageDeviceName; - const isDefault = (storageDeviceData[i].isDefault == true); - const freeSpaceBytes = storageDeviceData[i].freeSpaceBytes; - const totalSpaceBytes = storageDeviceData[i].totalSpaceBytes; - const isGen9Compatible = storageDeviceData[i].isGen9Compatible; + const storageDevice = storageDeviceData[i]; + const storageDeviceId = storageDevice.storageDeviceId; + const storageDeviceName = storageDevice.storageDeviceName; + const isDefault = (storageDevice.isDefault == true); + const freeSpaceBytes = storageDevice.freeSpaceBytes; + const totalSpaceBytes = storageDevice.totalSpaceBytes; + const isGen9Compatible = storageDevice.isGen9Compatible; this.storageDeviceId.push(storageDeviceId); this.storageDeviceName.push(storageDeviceName); @@ -688,16 +564,16 @@ class xboxTvDevice { this.log.debug('Device: %s %s, requesting device info from Web API.', this.host, this.name); try { const getConsoleStatusData = await this.xboxWebApi.getProvider('smartglass').getConsoleStatus(this.xboxLiveId); - const debug = this.enableDebugMode ? this.log('Device: %s %s, debug getConsoleStatusData, result: %s', this.host, this.name, getConsoleStatusData) : false; + const debug = this.enableDebugMode ? this.log('Device: %s %s, debug getConsoleStatusData, status: %s result: %s', this.host, this.name, getConsoleStatusData.status, getConsoleStatusData) : false; const consoleStatusData = getConsoleStatusData; const id = consoleStatusData.id; const name = consoleStatusData.name; const locale = consoleStatusData.locale; const region = consoleStatusData.region; - const consoleType = CONSOLES_NAME[consoleStatusData.consoleType]; - const powerState = (CONSOLE_POWER_STATE[consoleStatusData.powerState] == 1); // 0 - Off, 1 - On, 2 - InStandby, 3 - SystemUpdate - const playbackState = (CONSOLE_PLAYBACK_STATE[consoleStatusData.playbackState] == 1); // 0 - Stopped, 1 - Playng, 2 - Paused + const consoleType = CONSTANS.ConsoleName[consoleStatusData.consoleType]; + const powerState = (CONSTANS.ConsolePowerState[consoleStatusData.powerState] == 1); // 0 - Off, 1 - On, 2 - InStandby, 3 - SystemUpdate + const playbackState = (CONSTANS.ConsolePlaybackState[consoleStatusData.playbackState] == 1); // 0 - Stopped, 1 - Playng, 2 - Paused const loginState = consoleStatusData.loginState; const focusAppAumid = consoleStatusData.focusAppAumid; const isTvConfigured = (consoleStatusData.isTvConfigured == true); @@ -720,37 +596,35 @@ class xboxTvDevice { //Prepare accessory async prepareAccessory() { this.log.debug('prepareAccessory'); + + //accessory const accessoryName = this.name; const accessoryUUID = UUID.generate(this.xboxLiveId); const accessoryCategory = Categories.TV_SET_TOP_BOX; const accessory = new Accessory(accessoryName, accessoryUUID, accessoryCategory); accessory.context.device = this.config.device; - //Prepare information service - this.log.debug('prepareInformationService'); try { const readDevInfo = await fsPromises.readFile(this.devInfoFile); const devInfo = JSON.parse(readDevInfo); - const debug = this.enableDebugMode ? this.log('Device: %s %s, debug devInfo: %s', this.host, accessoryName, devInfo) : false; + const debug = this.enableDebugMode ? this.log('Device: %s %s, debug devInfo: %s', this.host, this.name, devInfo) : false; - const manufacturer = devInfo.manufacturer; - const modelName = devInfo.modelName; - const serialNumber = devInfo.serialNumber; - const firmwareRevision = devInfo.firmwareRevision; + const manufacturer = devInfo.manufacturer || 'Undefined'; + const modelName = devInfo.modelName || 'Undefined'; + const serialNumber = devInfo.serialNumber || 'Undefined'; + const firmwareRevision = devInfo.firmwareRevision || 'Undefined'; - accessory.removeService(accessory.getService(Service.AccessoryInformation)); - const informationService = new Service.AccessoryInformation(accessoryName); - informationService + //Pinformation service + this.log.debug('prepareInformationService'); + accessory.getService(Service.AccessoryInformation) .setCharacteristic(Characteristic.Manufacturer, manufacturer) .setCharacteristic(Characteristic.Model, modelName) .setCharacteristic(Characteristic.SerialNumber, serialNumber) .setCharacteristic(Characteristic.FirmwareRevision, firmwareRevision); - accessory.addService(informationService); } catch (error) { - this.log.error('Device: %s %s, prepareInformationService error: %s', this.host, accessoryName, error); + this.log.error('Device: %s %s, read devInfo error: %s', this.host, this.name, error); }; - //Prepare television service this.log.debug('prepareTelevisionService'); this.televisionService = new Service.Television(`${accessoryName} Television`, 'Television'); @@ -766,7 +640,7 @@ class xboxTvDevice { .onSet(async (state) => { try { //const setPower = this.webApiEnabled ? (!this.powerState && state) ? await this.xboxWebApi.getProvider('smartglass').powerOn(this.xboxLiveId) : (this.powerState && !state) ? this.xboxWebApi.getProvider('smartglass').powerOff(this.xboxLiveId) : false : false; - const setPower = (!this.powerState && state) ? await this.xbox.powerOn() : (this.powerState && !state) ? await this.xbox.powerOff() : false; + const setPower = (!this.powerState && state) ? await this.xboxLocalApi.powerOn() : (this.powerState && !state) ? await this.xboxLocalApi.powerOff() : false; this.powerState = (this.powerState != state) ? state : this.powerState; const logInfo = this.disableLogInfo ? false : this.log('Device: %s %s, set Power successful, %s', this.host, accessoryName, state ? 'ON' : 'OFF'); } catch (error) { @@ -787,7 +661,7 @@ class xboxTvDevice { const inputName = this.inputsName[inputIdentifier]; const inputReference = this.inputsReference[inputIdentifier]; const inputOneStoreProductId = this.inputsOneStoreProductId[inputIdentifier]; - const setDashboard = (inputOneStoreProductId === 'Dashboard' || inputOneStoreProductId === 'Settings' || inputOneStoreProductId === 'SettingsTv' || inputOneStoreProductId === 'Accessory' || inputOneStoreProductId === 'Screensaver' || inputOneStoreProductId === 'NetworkTroubleshooter'); + const setDashboard = (inputOneStoreProductId === 'Dashboard' || inputOneStoreProductId === 'Settings' || inputOneStoreProductId === 'SettingsTv' || inputOneStoreProductId === 'Accessory' || inputOneStoreProductId === 'Screensaver' || inputOneStoreProductId === 'NetworkTroubleshooter' || inputOneStoreProductId === 'XboxGuide'); const setTelevision = (inputOneStoreProductId === 'Television'); const setApp = ((inputOneStoreProductId != undefined && inputOneStoreProductId != '0') && !setDashboard && !setTelevision); try { @@ -857,7 +731,7 @@ class xboxTvDevice { break; }; try { - const sendCommand = this.powerState ? this.webApiEnabled ? await this.xboxWebApi.getProvider('smartglass').sendButtonPress(this.xboxLiveId, command) : await this.xbox.sendCommand(channelName, command) : false; + const sendCommand = this.powerState ? this.webApiEnabled ? await this.xboxWebApi.getProvider('smartglass').sendButtonPress(this.xboxLiveId, command) : await this.xboxLocalApi.sendCommand(channelName, command) : false; const logInfo = this.disableLogInfo ? false : this.log('Device: %s %s, Remote Key command successful: %s', this.host, accessoryName, command); } catch (error) { this.log.error('Device: %s %s, set Remote Key command error: %s', this.host, accessoryName, error); @@ -902,7 +776,7 @@ class xboxTvDevice { }; try { const channelName = 'systemInput'; - const setPowerModeSelection = this.powerState ? this.webApiEnabled ? await this.xboxWebApi.getProvider('smartglass').sendButtonPress(this.xboxLiveId, command) : await this.xbox.sendCommand(channelName, command) : false; + const setPowerModeSelection = this.powerState ? this.webApiEnabled ? await this.xboxWebApi.getProvider('smartglass').sendButtonPress(this.xboxLiveId, command) : await this.xboxLocalApi.sendCommand(channelName, command) : false; const logInfo = this.disableLogInfo ? false : this.log('Device: %s %s, set Power Mode Selection command successful: %s', this.host, accessoryName, command); } catch (error) { this.log.error('Device: %s %s, set Power Mode Selection command error: %s', this.host, accessoryName, error); @@ -1031,12 +905,13 @@ class xboxTvDevice { const inputsArr = new Array(); const allInputsCount = allInputs.length; for (let i = 0; i < allInputsCount; i++) { - const contentType = allInputs[i].contentType; + const input = allInputs[i]; + const contentType = input.contentType; const filterGames = this.filterGames ? (contentType != 'Game') : true; const filterApps = this.filterApps ? (contentType != 'App') : true; const filterSystemApps = this.filterSystemApps ? (contentType != 'systemApp') : true; const filterDlc = this.filterDlc ? (contentType != 'Dlc') : true; - const push = (this.getInputsFromDevice) ? (filterGames && filterApps && filterSystemApps && filterDlc) ? inputsArr.push(allInputs[i]) : false : inputsArr.push(allInputs[i]); + const push = (this.getInputsFromDevice) ? (filterGames && filterApps && filterSystemApps && filterDlc) ? inputsArr.push(input) : false : inputsArr.push(input); } //check available inputs and possible inputs count (max 93) @@ -1044,21 +919,23 @@ class xboxTvDevice { const inputsCount = inputs.length; const maxInputsCount = (inputsCount < 93) ? inputsCount : 93; for (let j = 0; j < maxInputsCount; j++) { + //get input + const input = inputs[j]; //get title Id - const inputTitleId = (inputs[j].titleId != undefined) ? inputs[j].titleId : undefined; + const inputTitleId = (input.titleId != undefined) ? input.titleId : undefined; //get input reference - const inputReference = (inputs[j].reference != undefined) ? inputs[j].reference : undefined; + const inputReference = (input.reference != undefined) ? input.reference : undefined; //get input oneStoreProductId - const inputOneStoreProductId = (inputs[j].oneStoreProductId != undefined) ? inputs[j].oneStoreProductId : undefined; + const inputOneStoreProductId = (input.oneStoreProductId != undefined) ? input.oneStoreProductId : undefined; //get input name - const inputName = (savedInputsNames[inputTitleId] != undefined) ? savedInputsNames[inputTitleId] : (savedInputsNames[inputReference] != undefined) ? savedInputsNames[inputReference] : (savedInputsNames[inputOneStoreProductId] != undefined) ? savedInputsNames[inputOneStoreProductId] : inputs[j].name; + const inputName = (savedInputsNames[inputTitleId] != undefined) ? savedInputsNames[inputTitleId] : (savedInputsNames[inputReference] != undefined) ? savedInputsNames[inputReference] : (savedInputsNames[inputOneStoreProductId] != undefined) ? savedInputsNames[inputOneStoreProductId] : input.name; //get input type - const inputType = (inputs[j].type != undefined) ? INPUT_SOURCE_TYPES.indexOf(inputs[j].type) : 10; + const inputType = (input.type != undefined) ? CONSTANS.InputSourceTypes.indexOf(input.type) : 10; //get input configured const isConfigured = 1; @@ -1082,9 +959,9 @@ class xboxTvDevice { const nameIdentifier = (inputTitleId != undefined) ? inputTitleId : (inputReference != undefined) ? inputReference : (inputOneStoreProductId != undefined) ? inputOneStoreProductId : false; let newName = savedInputsNames; newName[nameIdentifier] = name; - const newCustomName = JSON.stringify(newName); + const newCustomName = JSON.stringify(newName, null, 2); try { - const writeNewCustomName = (nameIdentifier != false) ? await fsPromises.writeFile(this.inputsNamesFile, newCustomName) : false; + const writeNewCustomName = nameIdentifier ? await fsPromises.writeFile(this.inputsNamesFile, newCustomName) : false; const logInfo = this.disableLogInfo ? false : this.log('Device: %s %s, saved new Input name successful, name: %s, product Id: %s', this.host, accessoryName, newCustomName, inputOneStoreProductId); } catch (error) { this.log.error('Device: %s %s, saved new Input Name error: %s', this.host, accessoryName, error); @@ -1097,9 +974,9 @@ class xboxTvDevice { const targetVisibilityIdentifier = (inputTitleId != undefined) ? inputTitleId : (inputReference != undefined) ? inputReference : (inputOneStoreProductId != undefined) ? inputOneStoreProductId : false; let newState = savedTargetVisibility; newState[targetVisibilityIdentifier] = state; - const newTargetVisibility = JSON.stringify(newState); + const newTargetVisibility = JSON.stringify(newState, null, 2); try { - const writeNewTargetVisibility = (targetVisibilityIdentifier != false) ? await fsPromises.writeFile(this.inputsTargetVisibilityFile, newTargetVisibility) : false; + const writeNewTargetVisibility = targetVisibilityIdentifier ? await fsPromises.writeFile(this.inputsTargetVisibilityFile, newTargetVisibility) : false; const logInfo = this.disableLogInfo ? false : this.log('Device: %s %s, saved new Target Visibility successful, input: %s, state: %s', this.host, accessoryName, inputName, state ? 'HIDEN' : 'SHOWN'); inputService.setCharacteristic(Characteristic.CurrentVisibilityState, state); } catch (error) { @@ -1140,15 +1017,15 @@ class xboxTvDevice { let buttonMode = 0; let channelName = ''; let command = ''; - if (buttonCommand in SYSTEM_MEDIA_COMMANDS) { + if (buttonCommand in CONSTANS.SystemMediaCommands) { buttonMode = 0; channelName = 'systemMedia'; command = buttonCommand; - } else if (buttonCommand in SYSTEM_INPUTS_COMMANDS) { + } else if (buttonCommand in CONSTANS.SystemInputCommands) { buttonMode = 1; channelName = 'systemInput'; command = buttonCommand; - } else if (buttonCommand in TV_REMOTE_COMMANDS) { + } else if (buttonCommand in CONSTANS.TvRemoteCommands) { buttonMode = 2; channelName = 'tvRemote'; } else if (buttonCommand === 'recordGameDvr') { @@ -1172,27 +1049,26 @@ class xboxTvDevice { return state; }) .onSet(async (state) => { - const setDashboard = (buttonOneStoreProductId === 'Dashboard' || buttonOneStoreProductId === 'Settings' || buttonOneStoreProductId === 'SettingsTv' || buttonOneStoreProductId === 'Accessory' || buttonOneStoreProductId === 'Screensaver' || buttonOneStoreProductId === 'NetworkTroubleshooter'); + const setDashboard = (buttonOneStoreProductId === 'Dashboard' || buttonOneStoreProductId === 'Settings' || buttonOneStoreProductId === 'SettingsTv' || buttonOneStoreProductId === 'Accessory' || buttonOneStoreProductId === 'Screensaver' || buttonOneStoreProductId === 'NetworkTroubleshooter' || buttonOneStoreProductId === 'XboxGuide'); const setTelevision = (buttonOneStoreProductId === 'Television'); const setApp = ((buttonOneStoreProductId != undefined && buttonOneStoreProductId != '0') && !setDashboard && !setTelevision); try { const setCommand = (this.powerState && state && this.webApiEnabled && buttonMode <= 2) ? await this.xboxWebApi.getProvider('smartglass').sendButtonPress(this.xboxLiveId, command) : false - const recordGameDvr = (this.powerState && state && buttonMode == 3) ? await this.xbox.recordGameDvr() : false; + const recordGameDvr = (this.powerState && state && buttonMode == 3) ? await this.xboxLocalApi.recordGameDvr() : false; const rebootConsole = (this.powerState && state && this.webApiEnabled && buttonMode == 4) ? await this.xboxWebApi.getProvider('smartglass').reboot(this.xboxLiveId) : false; const setAppInput = (this.powerState && state && this.webApiEnabled && buttonMode == 5) ? setApp ? await this.xboxWebApi.getProvider('smartglass').launchApp(this.xboxLiveId, buttonOneStoreProductId) : setDashboard ? await this.xboxWebApi.getProvider('smartglass').launchDashboard(this.xboxLiveId) : setTelevision ? await this.xboxWebApi.getProvider('smartglass').launchOneGuide(this.xboxLiveId) : false : false; const logInfo = this.disableLogInfo ? false : this.log('Device: %s %s, set button successful, name: %s, command: %s', this.host, accessoryName, buttonName, buttonCommand); + buttonService.updateCharacteristic(Characteristic.On, false); } catch (error) { this.log.error('Device: %s %s, set button error, name: %s, error: %s', this.host, accessoryName, buttonName, error); - }; - setTimeout(() => { buttonService.updateCharacteristic(Characteristic.On, false); - }, 200); + }; }); accessory.addService(buttonService); } } - const debug3 = this.enableDebugMode ? this.log('Device: %s %s, publishExternalAccessory.', this.host, accessoryName) : false; this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]); + const debug3 = this.enableDebugMode ? this.log(`Device: ${ this.host} ${accessoryName}, published as external accessory.`) : false; } }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a9a5333..8bf6117 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "homebridge-xbox-tv", - "version": "2.2.4-beta.23", + "version": "2.3.0-beta.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "homebridge-xbox-tv", - "version": "2.2.4-beta.23", + "version": "2.3.0-beta.0", "license": "MIT", "dependencies": { "@homebridge/plugin-ui-utils": ">=0.0.19", "async-mqtt": "^2.6.2", "elliptic": "^6.5.4", "hex-to-binary": ">=1.0.1", - "jsrsasign": "^10.5.25", + "jsrsasign": "^10.5.26", "uuid": ">=8.3.2", "uuid-parse": ">=1.1.0", "xbox-webapi": "^1.3.0" @@ -1208,9 +1208,9 @@ } }, "node_modules/jsrsasign": { - "version": "10.5.25", - "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.5.25.tgz", - "integrity": "sha512-N7zxHaCwYvFlXsybq4p4RxRwn4AbEq3cEiyjbCrWmwA7g8aS4LTKDJ9AJmsXxwtYesYx0imJ+ITtkyyxLCgeIg==", + "version": "10.5.26", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.5.26.tgz", + "integrity": "sha512-TjEu1yPdI+8whpe6CA/6XNb7U1sm9+PUItOUfSThOLvx7JCfYHIfuvZK2Egz2DWUKioafn98LPuk+geLGckxMg==", "funding": { "url": "https://github.com/kjur/jsrsasign#donations" } @@ -2960,9 +2960,9 @@ } }, "jsrsasign": { - "version": "10.5.25", - "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.5.25.tgz", - "integrity": "sha512-N7zxHaCwYvFlXsybq4p4RxRwn4AbEq3cEiyjbCrWmwA7g8aS4LTKDJ9AJmsXxwtYesYx0imJ+ITtkyyxLCgeIg==" + "version": "10.5.26", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.5.26.tgz", + "integrity": "sha512-TjEu1yPdI+8whpe6CA/6XNb7U1sm9+PUItOUfSThOLvx7JCfYHIfuvZK2Egz2DWUKioafn98LPuk+geLGckxMg==" }, "leven": { "version": "2.1.0", diff --git a/package.json b/package.json index 9f70833..b07e52a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "displayName": "Xbox TV", "name": "homebridge-xbox-tv", - "version": "2.2.4-beta.23", + "version": "2.3.0", "description": "Homebridge plugin (https://github.com/homebridge/homebridge) to control Xbox game consoles.", "license": "MIT", "author": "grzegorz914", @@ -37,10 +37,11 @@ "async-mqtt": "^2.6.2", "elliptic": "^6.5.4", "hex-to-binary": ">=1.0.1", - "jsrsasign": "^10.5.25", + "jsrsasign": "^10.5.27", "uuid": ">=8.3.2", "uuid-parse": ">=1.1.0", - "xbox-webapi": "^1.3.0" + "xbox-webapi": "^1.4.0", + "ping": "^0.4.2" }, "keywords": [ "homebridge", diff --git a/src/constans.json b/src/constans.json new file mode 100644 index 0000000..4577e27 --- /dev/null +++ b/src/constans.json @@ -0,0 +1,174 @@ +{ + "ConsoleName": { + "XboxSeriesX": "Xbox Series X", + "XboxSeriesS": "Xbox Series S", + "XboxOne": "Xbox One", + "XboxOneS": "Xbox One S", + "XboxOneX": "Xbox One X" + }, + "ConsolePowerState": { + "Off": 0, + "On": 1, + "ConnectedStandby": 2, + "SystemUpdate": 3, + "Unknown": 4 + }, + "ConsolePlaybackState": { + "Stopped": 0, + "Playing": 1, + "Paused": 2, + "Unknown": 3 + }, + "DefaultInputs": [{ + "name": "Screensaver", + "titleId": "851275400", + "reference": "Xbox.IdleScreen_8wekyb3d8bbwe!Xbox.IdleScreen.Application", + "oneStoreProductId": "Screensaver", + "type": "HOME_SCREEN", + "contentType": "Dashboard" + }, + { + "name": "Dashboard", + "titleId": "750323071", + "reference": "Xbox.Dashboard_8wekyb3d8bbwe!Xbox.Dashboard.Application", + "oneStoreProductId": "Dashboard", + "type": "HOME_SCREEN", + "contentType": "Dashboard" + }, + { + "name": "Settings", + "titleId": "1837352387", + "reference": "Microsoft.Xbox.Settings_8wekyb3d8bbwe!Xbox.Settings.Application", + "oneStoreProductId": "Settings", + "type": "HOME_SCREEN", + "contentType": "Dashboard" + }, + { + "name": "Television", + "titleId": "371594669", + "reference": "Microsoft.Xbox.LiveTV_8wekyb3d8bbwe!Microsoft.Xbox.LiveTV.Application", + "oneStoreProductId": "Television", + "type": "HDMI", + "contentType": "systemApp" + }, + { + "name": "Settings TV", + "titleId": "2019308066", + "reference": "Microsoft.Xbox.TvSettings_8wekyb3d8bbwe!Microsoft.Xbox.TvSettings.Application", + "oneStoreProductId": "SettingsTv", + "type": "HOME_SCREEN", + "contentType": "Dashboard" + }, + { + "name": "Accessory", + "titleId": "758407307", + "reference": "Microsoft.XboxDevices_8wekyb3d8bbwe!App", + "oneStoreProductId": "Accessory", + "type": "HOME_SCREEN", + "contentType": "systemApp" + }, + { + "name": "Network Troubleshooter", + "titleId": "1614319806", + "reference": "Xbox.NetworkTroubleshooter_8wekyb3d8bbwe!Xbox.NetworkTroubleshooter.Application", + "oneStoreProductId": "NetworkTroubleshooter", + "type": "HOME_SCREEN", + "contentType": "systemApp" + }, + { + "name": "Microsoft Store", + "titleId": "1864271209", + "reference": "Microsoft.storify_8wekyb3d8bbwe!App", + "oneStoreProductId": "MicrosoftStore", + "type": "HOME_SCREEN", + "contentType": "systemApp" + }, + { + "name": "Xbox Guide", + "titleId": "1052052983", + "reference": "Xbox.Guide_8wekyb3d8bbwe!Xbox.Guide.Application", + "oneStoreProductId": "XboxGuide", + "type": "HOME_SCREEN", + "contentType": "systemApp" + } + ], + "InputSourceTypes": ["OTHER", "HOME_SCREEN", "TUNER", "HDMI", "COMPOSITE_VIDEO", "S_VIDEO", "COMPONENT_VIDEO", "DVI", "AIRPLAY", "USB", "APPLICATION"], + "SystemMediaCommands": { + "play": 2, + "pause": 4, + "playpause": 8, + "stop": 16, + "record": 32, + "nextTrack": 64, + "prevTrack": 128, + "fastForward": 256, + "rewind": 512, + "channelUp": 1024, + "channelDown": 2048, + "back": 4096, + "view": 8192, + "menu": 16384, + "seek": 32786 + }, + "SystemInputCommands": { + "unpress": 0, + "nexus": 2, + "view1": 4, + "menu1": 8, + "a": 16, + "b": 32, + "x": 64, + "y": 128, + "up": 256, + "down": 512, + "left": 1024, + "right": 2048 + }, + "TvRemoteCommands": { + "volUp": "btn.vol_up", + "volDown": "btn.vol_down", + "volMute": "btn.vol_mute" + }, + "ChannelIds": { + "systemMedia": 0, + "systemInput": 1, + "tvRemote": 2, + "sysConfig": 3 + }, + "ChannelUuids": { + "systemMedia": "48a9ca24eb6d4e128c43d57469edd3cd", + "systemInput": "fa20b8ca66fb46e0adb60b978a59d35f", + "tvRemote": "d451e3b360bb4c71b3dbf994b1aca3a7", + "sysConfig": "d451e3b360bb4c71b3dbf994b1aca3a7" + }, + "ChannelNames": ["systemMedia", "systemInput", "tvRemote", "sysConfig"], + "ConfigNames": ["GetConfiguration", "GetHeadendInfo", "GetLiveTVInfo", "GetTunerLineups", "GetAppChannelLineups"], + "PlaybackStatus": { + "0": "Closed", + "1": "Changing", + "2": "Stopped", + "3": "Playing", + "4": "Paused" + }, + "MediaTypes": { + "0": "No Media", + "1": "Music", + "2": "Video", + "3": "Image", + "4": "Conversation", + "5": "Game" + }, + "SoundStatus": { + "0": "Muted", + "1": "Low", + "2": "Full" + }, + "Types": { + "d00d": "message", + "cc00": "simple.connectRequest", + "cc01": "simple.connectResponse", + "dd00": "simple.discoveryRequest", + "dd01": "simple.discoveryResponse", + "dd02": "simple.powerOn" + } +} \ No newline at end of file diff --git a/src/mqtt.js b/src/mqtt.js index 9dd1f51..591d180 100644 --- a/src/mqtt.js +++ b/src/mqtt.js @@ -1,5 +1,6 @@ "use strict"; -const MQTT = require("async-mqtt"); + +const Mqtt = require("async-mqtt"); const EventEmitter = require('events'); class MQTTCLIENT extends EventEmitter { @@ -26,7 +27,7 @@ class MQTTCLIENT extends EventEmitter { password: this.mqttPasswd } const url = `mqtt://${this.mqttHost}:${this.mqttPort}`; - this.mqttClient = await MQTT.connectAsync(url, options); + this.mqttClient = await Mqtt.connectAsync(url, options); this.isConnected = true; this.emit('connected', 'MQTT Connected.'); } catch (error) { diff --git a/src/packet/message.js b/src/packet/message.js index 7ace31d..fc93a7c 100644 --- a/src/packet/message.js +++ b/src/packet/message.js @@ -1,28 +1,8 @@ -const PacketStructure = require('./structure'); -const hexToBin = require('hex-to-binary'); - -const playbackStatus = { - 0: 'Closed', - 1: 'Changing', - 2: 'Stopped', - 3: 'Playing', - 4: 'Paused' -}; +"use strict"; -const mediaTypes = { - 0: 'No Media', - 1: 'Music', - 2: 'Video', - 3: 'Image', - 4: 'Conversation', - 5: 'Game' -}; - -const soundStatus = { - 0: 'Muted', - 1: 'Low', - 2: 'Full' -}; +const PacketStructure = require('./structure'); +const HexToBin = require('hex-to-binary'); +const CONSTANS = require('../constans.json'); class MESSAGE { constructor(type, packetData = false) { @@ -254,10 +234,10 @@ class MESSAGE { titleId: Type.uInt32('0'), aumId: Type.sgString(), assetId: Type.sgString(), - mediaType: Type.mapper(mediaTypes, Type.uInt16('0')), - soundLevel: Type.mapper(soundStatus, Type.uInt16('0')), + mediaType: Type.mapper(CONSTANS.MediaTypes, Type.uInt16('0')), + soundLevel: Type.mapper(CONSTANS.SoundStatus, Type.uInt16('0')), enabledCommands: Type.uInt32('0'), - playbackStatus: Type.mapper(playbackStatus, Type.uInt16('0')), + playbackStatus: Type.mapper(CONSTANS.PlaybackStatus, Type.uInt16('0')), rate: Type.uInt32('0'), position: Type.bytes(8, ''), enabmediaStart: Type.bytes(8, ''), @@ -346,7 +326,7 @@ class MESSAGE { }; readFlags(flags) { - flags = hexToBin(flags.toString('hex')); + flags = HexToBin(flags.toString('hex')); const needAck = (flags.slice(2, 3) == 1) ? true : false; const isFragment = (flags.slice(3, 4) == 1) ? true : false; const type = this.getMsgType(parseInt(flags.slice(4, 16), 2)); diff --git a/src/packet/packer.js b/src/packet/packer.js index 07028c4..c60e3e3 100644 --- a/src/packet/packer.js +++ b/src/packet/packer.js @@ -1,22 +1,16 @@ +"use strict"; + const SimplePacket = require('./simple'); const MessagePacket = require('./message'); - -const Types = { - d00d: 'message', - cc00: 'simple.connectRequest', - cc01: 'simple.connectResponse', - dd00: 'simple.discoveryRequest', - dd01: 'simple.discoveryResponse', - dd02: 'simple.powerOn', -}; +const CONSTANS = require('../constans.json'); class PACKER { constructor(type) { const packetType = type.slice(0, 2).toString('hex'); this.packetStructure = ''; - if (packetType in Types) { + if (packetType in CONSTANS.Types) { const packetValue = type; - type = Types[packetType]; + type = CONSTANS.Types[packetType]; this.packetStructure = this.loadPacketStructure(type, packetValue); } else { this.packetStructure = this.loadPacketStructure(type); diff --git a/src/packet/simple.js b/src/packet/simple.js index 09642e9..0d9fa9b 100644 --- a/src/packet/simple.js +++ b/src/packet/simple.js @@ -1,3 +1,5 @@ +"use strict"; + const PacketStructure = require('./structure'); class SIMPLE { diff --git a/src/packet/structure.js b/src/packet/structure.js index 8e828b8..b74f00c 100644 --- a/src/packet/structure.js +++ b/src/packet/structure.js @@ -1,6 +1,10 @@ +"use strict"; + class STRUCTURE { constructor(packet) { - this.packet = (packet == undefined) ? Buffer.from('') : packet; + packet = (packet == undefined) ? Buffer.from('') : packet; + this.packet = packet; + this.totalLength = packet.length; this.offset = 0; }; @@ -19,7 +23,7 @@ class STRUCTURE { readSGString() { const dataLength = this.readUInt16(); const data = this.packet.slice(this.offset, this.offset + dataLength); - this.offset = this.offset + 1 + dataLength; + this.offset = (this.offset + 1 + dataLength); return data; }; @@ -33,12 +37,11 @@ class STRUCTURE { let data = ''; if (count == false) { - const totalLength = this.packet.length; data = this.packet.slice(this.offset); - this.offset = totalLength; + this.offset = this.totalLength; } else { data = this.packet.slice(this.offset, this.offset + count); - this.offset = this.offset + count; + this.offset = (this.offset + count); }; return data; }; @@ -52,7 +55,7 @@ class STRUCTURE { readUInt8() { const data = this.packet.readUInt8BE(this.offset); - this.offset = this.offset + 1; + this.offset = (this.offset + 1); return data; }; @@ -65,7 +68,7 @@ class STRUCTURE { readUInt16() { const data = this.packet.readUInt16BE(this.offset); - this.offset = this.offset + 2; + this.offset = (this.offset + 2); return data; }; @@ -78,7 +81,7 @@ class STRUCTURE { readUInt32() { const data = this.packet.readUInt32BE(this.offset); - this.offset = this.offset + 4; + this.offset = (this.offset + 4); return data; }; @@ -91,7 +94,7 @@ class STRUCTURE { readInt32() { const data = this.packet.readInt32BE(this.offset); - this.offset = this.offset + 4; + this.offset = (this.offset + 4); return data; }; @@ -105,11 +108,10 @@ class STRUCTURE { }; add(data) { - const packet = Buffer.concat([ + this.packet = Buffer.concat([ this.packet, data ]); - this.packet = packet; }; }; module.exports = STRUCTURE; \ No newline at end of file diff --git a/src/sgcrypto.js b/src/sgcrypto.js index 1cdf3ae..5dcd701 100644 --- a/src/sgcrypto.js +++ b/src/sgcrypto.js @@ -1,4 +1,6 @@ -const crypto = require('crypto'); +"use strict"; + +const Crypto = require('crypto'); const EC = require('elliptic').ec; class SGCRYPTO { @@ -42,7 +44,7 @@ class SGCRYPTO { }; signPublicKey(publicKey) { - const sha512 = crypto.createHash("sha512"); + const sha512 = Crypto.createHash("sha512"); // Generate keys const key1 = this.ec.genKeyPair(); @@ -103,7 +105,7 @@ class SGCRYPTO { key = this.getEncryptionKey(); }; - let cipher = crypto.createCipheriv('aes-128-cbc', key, iv); + const cipher = Crypto.createCipheriv('aes-128-cbc', key, iv); cipher.setAutoPadding(false); let encryptedPayload = cipher.update(data, 'binary', 'binary'); @@ -123,7 +125,7 @@ class SGCRYPTO { iv = Buffer.from('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'); }; - let cipher = crypto.createDecipheriv('aes-128-cbc', key, iv); + const cipher = Crypto.createDecipheriv('aes-128-cbc', key, iv); cipher.setAutoPadding(false); let decryptedPayload = cipher.update(data, 'binary', 'binary'); @@ -133,7 +135,7 @@ class SGCRYPTO { }; sign(data) { - let hashHmac = crypto.createHmac('sha256', this.getHashKey()); + const hashHmac = Crypto.createHmac('sha256', this.getHashKey()); hashHmac.update(data, 'binary', 'binary'); const protectedPayloadHash = hashHmac.digest('binary'); const protectedPayloadHashBuffer = Buffer.from(protectedPayloadHash, 'binary'); diff --git a/src/smartglass.js b/src/xboxlocalapi.js similarity index 54% rename from src/smartglass.js rename to src/xboxlocalapi.js index 781ff08..57665ca 100644 --- a/src/smartglass.js +++ b/src/xboxlocalapi.js @@ -1,67 +1,17 @@ -const dgram = require('dgram'); -const uuidParse = require('uuid-parse'); -const uuid = require('uuid'); +"use strict"; + +const Dgram = require('dgram'); +const UuIdParse = require('uuid-parse'); +const UuId = require('uuid'); const EOL = require('os').EOL; -const jsrsasign = require('jsrsasign'); +const JsRsaSign = require('jsrsasign'); const EventEmitter = require('events'); const Packer = require('./packet/packer'); const SGCrypto = require('./sgcrypto'); -const { clearTimeout } = require('timers'); - -const systemMediaCommands = { - play: 2, - pause: 4, - playpause: 8, - stop: 16, - record: 32, - nextTrack: 64, - prevTrack: 128, - fastForward: 256, - rewind: 512, - channelUp: 1024, - channelDown: 2048, - back: 4096, - view: 8192, - menu: 16384, - seek: 32786 -}; - -const systemInputCommands = { - unpress: 0, - nexus: 2, - view1: 4, - menu1: 8, - a: 16, - b: 32, - x: 64, - y: 128, - up: 256, - down: 512, - left: 1024, - right: 2048 -}; - -const tvRemoteCommands = { - volUp: 'btn.vol_up', - volDown: 'btn.vol_down', - volMute: 'btn.vol_mute' -}; -const channelIds = { - systemMedia: 0, - systemInput: 1, - tvRemote: 2, - // sysConfig: 3 -}; -const channelUuids = { - systemMedia: '48a9ca24eb6d4e128c43d57469edd3cd', - systemInput: 'fa20b8ca66fb46e0adb60b978a59d35f', - tvRemote: 'd451e3b360bb4c71b3dbf994b1aca3a7', - //sysConfig: 'd451e3b360bb4c71b3dbf994b1aca3a7' -}; -const channelNames = ['systemMedia', 'systemInput', 'tvRemote', 'sysConfig']; -const configNames = ['GetConfiguration', 'GetHeadendInfo', 'GetLiveTVInfo', 'GetTunerLineups', 'GetAppChannelLineups']; +const Ping = require('ping'); +const CONSTANS = require('./constans.json'); -class SMARTGLASS extends EventEmitter { +class XBOXLOCALAPI extends EventEmitter { constructor(config) { super(); @@ -96,11 +46,124 @@ class SMARTGLASS extends EventEmitter { this.channelTargetId = null; this.channelRequestId = null; this.message = {}; - this.sendBlock = false; + + this.power = false; + this.volume = 0; + this.mute = true; + this.titleId = ''; + this.reference = ''; + this.mediaState = 0; + this.emitDevInfo = true; + + //dgram socket + this.socket = new Dgram.createSocket('udp4'); + this.socket.on('error', (error) => { + this.emit('error', `Socket error: ${error}`); + }) + .on('message', (message, remote) => { + const debug = this.debugLog ? this.emit('debug', `Received message from: ${remote.address}:${remote.port}`) : false; + + message = new Packer(message); + if (message.structure == false) { + return; + }; + this.response = message.unpack(this); + + if (this.response.packetDecoded.type != 'd00d') { + this.function = this.response.name; + } else { + if (this.response.packetDecoded.targetParticipantId != this.participantId) { + const debug1 = this.debugLog ? this.emit('debug', 'Participant id does not match. Ignoring packet.') : false; + return; + }; + this.function = message.structure.packetDecoded.name; + }; + + if (this.function == 'json') { + const jsonMessage = JSON.parse(this.response.packetDecoded.protectedPayload.json) + + // Check if JSON is fragmented + if (jsonMessage.datagramId != undefined) { + const debug = this.debugLog ? this.emit('debug', `Json message is fragmented: ${jsonMessage.datagramId}`) : false; + if (this.fragments[jsonMessage.datagramId] == undefined) { + // Prepare buffer for JSON + this.fragments[jsonMessage.datagramId] = { + + getValue() { + let buffer = Buffer.from(''); + for (let partial in this.partials) { + buffer = Buffer.concat([ + buffer, + Buffer.from(this.partials[partial]) + ]) + }; + const bufferDecoded = Buffer(buffer.toString(), 'base64'); + return bufferDecoded; + }, + isValid() { + const json = this.getValue(); + let isValid = false; + try { + JSON.parse(json.toString()); + isValid = true; + } catch (error) { + isValid = false; + this.emit('error', `Valid packet error: ${error}`); + }; + return isValid; + }, + partials: {} + }; + }; + + this.fragments[jsonMessage.datagramId].partials[jsonMessage.fragmentOffset] = jsonMessage.fragmentData; + if (this.fragments[jsonMessage.datagramId].isValid()) { + const debug = this.debugLog ? this.emit('debug', 'Json completed fragmented packet.') : false; + this.response.packetDecoded.protectedPayload.json = this.fragments[jsonMessage.datagramId].getValue().toString(); + this.fragments[jsonMessage.datagramId] = undefined; + }; + this.function = 'jsonFragment'; + }; + }; + + if (this.function == 'status') { + const decodedMessage = JSON.stringify(this.response.packetDecoded.protectedPayload); + if (this.message === decodedMessage) { + const debug = this.debugLog ? this.emit('debug', 'Received unchanged status message.') : false; + return; + }; + this.message = decodedMessage; + }; + + const debug1 = this.debugLog ? this.emit('debug', `Received event type: ${this.function}`) : false; + this.emit(this.function, this.response); + }) + .on('listening', () => { + const address = this.socket.address(); + const debug = this.debugLog ? this.emit('debug', `Server start listening: ${address.address}:${address.port}.`) : false; + + setInterval(async () => { + if (!this.isConnected) { + const state = await Ping.promise.probe(this.host, { + timeout: 3 + }); + const debug = this.debugLog ? this.emit('debug', `Ping state: ${JSON.stringify(state, null, 2)}`) : false; + if (state.alive) { + const discoveryPacket = new Packer('simple.discoveryRequest'); + const message = discoveryPacket.pack(); + await this.sendSocketMessage(message); + } + }; + }, 5000); + }) + .on('close', () => { + const debug = this.debugLog ? this.emit('debug', 'Socket closed.') : false; + }) + .bind(); //EventEmmiter - this.on('discoveryResponse', (message) => { - clearInterval(this.boot); + this.on('discoveryResponse', async (message) => { + clearInterval(this.setPowerOn); const decodedMessage = message.packetDecoded; if (decodedMessage != undefined && !this.isConnected) { @@ -113,10 +176,10 @@ class SMARTGLASS extends EventEmitter { const pem = `-----BEGIN CERTIFICATE-----${EOL}${certyficate}-----END CERTIFICATE-----`; // Set uuid - const uuid4 = Buffer.from(uuidParse.parse(uuid.v4())); + const uuid4 = Buffer.from(UuIdParse.parse(UuId.v4())); // Create public key - const ecKey = jsrsasign.X509.getPublicKeyFromCertPEM(pem); + const ecKey = JsRsaSign.X509.getPublicKeyFromCertPEM(pem); const debug1 = this.debugLog ? this.emit('debug', `Signing public key: ${ecKey.pubKeyHex}`) : false; // Load crypto data @@ -138,11 +201,16 @@ class SMARTGLASS extends EventEmitter { this.isAuthorized = false; const debug = this.debugLog ? this.emit('debug', 'Connecting using anonymous login.') : false; } - const message = connectRequest.pack(this); - this.sendSocketMessage(message); + + try { + const message = connectRequest.pack(this); + await this.sendSocketMessage(message); + } catch (error) { + this.emit('error', `Send connect request error: ${error}`) + }; }; }) - .on('connectResponse', (message) => { + .on('connectResponse', async (message) => { const connectionResult = message.packetDecoded.protectedPayload.connectResult; const participantId = message.packetDecoded.protectedPayload.participantId; this.participantId = participantId; @@ -150,10 +218,15 @@ class SMARTGLASS extends EventEmitter { if (connectionResult == 0) { const debug = this.debugLog ? this.emit('debug', 'Connect response received.') : false; - - const localJoin = new Packer('message.localJoin'); - const message = localJoin.pack(this); - this.sendSocketMessage(message); + this.isConnected = true; + + try { + const localJoin = new Packer('message.localJoin'); + const message = localJoin.pack(this); + await this.sendSocketMessage(message); + } catch (error) { + this.emit('error', `Send local join error: ${error}`) + }; } else { const errorTable = { 0: 'Success.', @@ -167,21 +240,25 @@ class SMARTGLASS extends EventEmitter { 8: 'Sign-in timeout.', 9: 'Sign-in required.' }; - this.isConnected = false; this.emit('error', `Connect error: ${errorTable[connectionResult]}`); }; }) - .on('acknowledge', () => { + .on('acknowledge', async () => { const debug = this.debugLog ? this.emit('debug', 'Packet needs to be acknowledged, send acknowledge.') : false; - const acknowledge = new Packer('message.acknowledge'); - acknowledge.set('lowWatermark', this.requestNum); - acknowledge.structure.structure.processedList.value.push({ - id: this.requestNum - }); - const message = acknowledge.pack(this); - this.sendSocketMessage(message); + try { + const acknowledge = new Packer('message.acknowledge'); + acknowledge.set('lowWatermark', this.requestNum); + acknowledge.structure.structure.processedList.value.push({ + id: this.requestNum + }); + const message = acknowledge.pack(this); + await this.sendSocketMessage(message); + } catch (error) { + this.emit('error', `Send acknowledge error: ${error}`) + }; + clearTimeout(this.closeConnection); if (this.isConnected) { this.closeConnection = setTimeout(() => { const debug = this.debugLog ? this.emit('debug', `Last message was: 12 seconds ago, send disconnect.`) : false; @@ -190,44 +267,41 @@ class SMARTGLASS extends EventEmitter { }; }) .on('status', (message) => { - if (message.packetDecoded.protectedPayload.apps[0] != undefined) { + if (message.packetDecoded.protectedPayload != undefined) { const decodedMessage = message.packetDecoded.protectedPayload; - const debug = this.debugLog ? this.emit('debug', `Status: ${JSON.stringify(decodedMessage)}`) : false; - - if (!this.isConnected) { - clearInterval(this.discovery); - const debug = this.debugLog ? this.emit('debug', 'Stop discovery.') : false; - - this.isConnected = true; - this.emit('connected', 'Connected.'); + const debug = this.debugLog ? this.emit('debug', `Status message: ${JSON.stringify(decodedMessage)}`) : false; + if (this.emitDevInfo) { const majorVersion = decodedMessage.majorVersion; const minorVersion = decodedMessage.minorVersion; - const buildNumber = decodedMessage.buildNumber + const buildNumber = decodedMessage.buildNumber; + const locale = decodedMessage.locale; const firmwareRevision = `${majorVersion}.${minorVersion}.${buildNumber}`; - this.emit('deviceInfo', firmwareRevision); + this.emit('connected', 'Connected.'); + this.emit('deviceInfo', firmwareRevision, locale); + this.emitDevInfo = false; }; - const appsArray = new Array(); - const appsCount = decodedMessage.apps.length; - for (let i = 0; i < appsCount; i++) { - const titleId = decodedMessage.apps[i].titleId; - const reference = decodedMessage.apps[i].aumId; - const app = { - titleId: titleId, - reference: reference - }; - appsArray.push(app); - const debug = this.debugLog ? this.emit('debug', `Status changed, app Id: ${titleId}, reference: ${reference}`) : false; - } - const power = this.isConnected; + const appsCount = Array.isArray(decodedMessage.apps) ? decodedMessage.apps.length : 0; + const power = true; const volume = 0; const mute = power ? power : true; - const titleId = appsArray[appsCount - 1].titleId; - const inputReference = appsArray[appsCount - 1].reference; const mediaState = 0; - this.emit('stateChanged', power, titleId, inputReference, volume, mute, mediaState); - const mqtt1 = this.mqttEnabled ? this.emit('mqtt', 'State', JSON.stringify(decodedMessage, null, 2)) : false; + const titleId = (appsCount == 1) ? decodedMessage.apps[0].titleId : (appsCount == 2) ? decodedMessage.apps[1].titleId : this.titleId; + const inputReference = (appsCount == 1) ? decodedMessage.apps[0].reference : (appsCount == 2) ? decodedMessage.apps[1].reference : this.reference; + + if (this.power != power || this.titleId != titleId || this.inputReference != inputReference || this.volume != volume || this.mute != mute || this.mediaState != mediaState) { + this.power = power; + this.titleId = titleId; + this.reference = inputReference; + this.volume = volume; + this.mute = mute; + this.mediaState = mediaState; + + this.emit('stateChanged', power, titleId, inputReference, volume, mute, mediaState); + const debug = this.debugLog ? this.emit('debug', `Status changed, app Id: ${titleId}, reference: ${inputReference}`) : false; + const mqtt1 = this.mqttEnabled ? this.emit('mqtt', 'State', JSON.stringify(decodedMessage, null, 2)) : false; + }; }; }).on('channelResponse', (message) => { if (message.packetDecoded.protectedPayload.result == 0) { @@ -241,93 +315,110 @@ class SMARTGLASS extends EventEmitter { }; }; }) - .on('sendCommand', (command) => { + .on('sendCommand', async (command) => { const debug = this.debugLog ? this.emit('debug', `Channel send command for name: ${channelNames[this.channelRequestId]}, request id: ${this.channelRequestId}, command: ${command}`) : false; if (this.channelRequestId == 0) { - if (command in systemMediaCommands) { - - let mediaRequestId = 0; - let requestId = '0000000000000000'; - const requestIdLength = requestId.length; - requestId = (requestId + mediaRequestId++).slice(-requestIdLength); - - const mediaCommand = new Packer('message.mediaCommand'); - mediaCommand.set('requestId', Buffer.from(requestId, 'hex')); - mediaCommand.set('titleId', 0); - mediaCommand.set('command', systemMediaCommands[command]); - mediaCommand.setChannel(this.channelTargetId); - const message = mediaCommand.pack(this); - const debug = this.debugLog ? this.emit('debug', `System media send command: ${command}`) : false; - this.sendSocketMessage(message); + if (command in CONSTANS.systemMediaCommands) { + try { + let mediaRequestId = 0; + let requestId = '0000000000000000'; + const requestIdLength = requestId.length; + requestId = (requestId + mediaRequestId++).slice(-requestIdLength); + + const mediaCommand = new Packer('message.mediaCommand'); + mediaCommand.set('requestId', Buffer.from(requestId, 'hex')); + mediaCommand.set('titleId', 0); + mediaCommand.set('command', CONSTANS.systemMediaCommands[command]); + mediaCommand.setChannel(this.channelTargetId); + const message = mediaCommand.pack(this); + const debug = this.debugLog ? this.emit('debug', `System media send command: ${command}`) : false; + await this.sendSocketMessage(message); + } catch (error) { + this.emit('error', `Send media command error: ${error}`) + }; } else { const debug = this.debugLog ? this.emit('debug', `Unknown media input command: ${command}`) : false; }; }; if (this.channelRequestId == 1) { - if (command in systemInputCommands) { - const timeStampPress = new Date().getTime(); - const gamepadPress = new Packer('message.gamepad'); - gamepadPress.set('timestamp', Buffer.from(`000${timeStampPress.toString()}`, 'hex')); - gamepadPress.set('buttons', systemInputCommands[command]); - gamepadPress.setChannel(this.channelTargetId); - const message = gamepadPress.pack(this); - const debug = this.debugLog ? this.emit('debug', `System input send press, command: ${command}`) : false; - this.sendSocketMessage(message); - - setTimeout(() => { - const timeStampUnpress = new Date().getTime(); - const gamepadUnpress = new Packer('message.gamepad'); - gamepadUnpress.set('timestamp', Buffer.from(`000${timeStampUnpress.toString()}`, 'hex')); - gamepadUnpress.set('buttons', systemInputCommands['unpress']); - gamepadUnpress.setChannel(this.channelTargetId); - const message = gamepadUnpress.pack(this); - const debug = this.debugLog ? this.emit('debug', `System input send unpress, command: unpress`) : false; - this.sendSocketMessage(message); - }, 150); + if (command in CONSTANS.systemInputCommands) { + try { + const timeStampPress = new Date().getTime(); + const gamepadPress = new Packer('message.gamepad'); + gamepadPress.set('timestamp', Buffer.from(`000${timeStampPress.toString()}`, 'hex')); + gamepadPress.set('buttons', CONSTANS.systemInputCommands[command]); + gamepadPress.setChannel(this.channelTargetId); + const message = gamepadPress.pack(this); + const debug = this.debugLog ? this.emit('debug', `System input send press, command: ${command}`) : false; + await this.sendSocketMessage(message); + + try { + const timeStampUnpress = new Date().getTime(); + const gamepadUnpress = new Packer('message.gamepad'); + gamepadUnpress.set('timestamp', Buffer.from(`000${timeStampUnpress.toString()}`, 'hex')); + gamepadUnpress.set('buttons', CONSTANS.systemInputCommands['unpress']); + gamepadUnpress.setChannel(this.channelTargetId); + const message = gamepadUnpress.pack(this); + const debug = this.debugLog ? this.emit('debug', `System input send unpress, command: unpress`) : false; + await this.sendSocketMessage(message); + } catch (error) { + this.emit('error', `Send system input command unpress error: ${error}`) + }; + } catch (error) { + this.emit('error', `Send system input command press error: ${error}`) + }; } else { const debug = this.debugLog ? this.emit('debug', `Unknown system input command: ${command}`) : false; }; }; if (this.channelRequestId == 2) { - if (command in tvRemoteCommands) { - let messageNum = 0; - const jsonRequest = { - msgid: `2ed6c0fd.${messageNum++}`, - request: 'SendKey', - params: { - button_id: tvRemoteCommands[command], - device_id: null - } + if (command in CONSTANS.tvRemoteCommands) { + try { + let messageNum = 0; + const jsonRequest = { + msgid: `2ed6c0fd.${messageNum++}`, + request: 'SendKey', + params: { + button_id: CONSTANS.tvRemoteCommands[command], + device_id: null + } + }; + const json = new Packer('message.json'); + json.set('json', JSON.stringify(jsonRequest)); + json.setChannel(this.channelTargetId); + const message = json.pack(this); + const debug = this.debugLog ? this.emit('debug', `TV remote send command: ${command}`) : false; + await this.sendSocketMessage(message); + } catch (error) { + this.emit('error', `Send tv remote command error: ${error}`) }; - const json = new Packer('message.json'); - json.set('json', JSON.stringify(jsonRequest)); - json.setChannel(this.channelTargetId); - const message = json.pack(this); - const debug = this.debugLog ? this.emit('debug', `TV remote send command: ${command}`) : false; - this.sendSocketMessage(message); } else { const debug = this.debugLog ? this.emit('debug', `Unknown tv remote command: ${command}`) : false; }; }; if (this.channelRequestId == 3) { - const configNamesCount = configNames.length; + const configNamesCount = CONSTANS.configNames.length; for (let i = 0; i < configNamesCount; i++) { - const configName = configNames[i]; - const jsonRequest = { - msgid: `2ed6c0fd.${i}`, - request: configName, - params: null + try { + const configName = CONSTANS.configNames[i]; + const jsonRequest = { + msgid: `2ed6c0fd.${i}`, + request: configName, + params: null + }; + const json = new Packer('message.json'); + json.set('json', JSON.stringify(jsonRequest)); + json.setChannel(this.channelTargetId); + const message = json.pack(this); + const debug = this.debugLog ? this.emit('debug', `System config send: ${configName}`) : false; + await this.sendSocketMessage(message); + } catch (error) { + this.emit('error', `Send json error: ${error}`) }; - const json = new Packer('message.json'); - json.set('json', JSON.stringify(jsonRequest)); - json.setChannel(this.channelTargetId); - const message = json.pack(this); - const debug = this.debugLog ? this.emit('debug', `System config send: ${configName}`) : false; - this.sendSocketMessage(message); }; }; }) @@ -361,276 +452,189 @@ class SMARTGLASS extends EventEmitter { .on('jsonFragment', (message) => { const debug = this.debugLog ? this.emit('debug', `Json fragment: ${message}`) : false; }); - - this.connect(); - }; - - connect() { - this.socket = new dgram.createSocket('udp4'); - this.socket.on('error', (error) => { - this.emit('error', `Socket error: ${error}`); - this.socket.close(); - }) - .on('message', (message, remote) => { - const debug = this.debugLog ? this.emit('debug', `Received message from: ${remote.address}:${remote.port}`) : false; - - message = new Packer(message); - if (message.structure == false) { - return; - }; - this.response = message.unpack(this); - - if (this.response.packetDecoded.type != 'd00d') { - this.function = this.response.name; - } else { - if (this.response.packetDecoded.targetParticipantId != this.participantId) { - const debug1 = this.debugLog ? this.emit('debug', 'Participant id does not match. Ignoring packet.') : false; - return; - }; - this.function = message.structure.packetDecoded.name; - }; - - if (this.function == 'json') { - const jsonMessage = JSON.parse(this.response.packetDecoded.protectedPayload.json) - - // Check if JSON is fragmented - if (jsonMessage.datagramId != undefined) { - const debug = this.debugLog ? this.emit('debug', `Json message is fragmented: ${jsonMessage.datagramId}`) : false; - if (this.fragments[jsonMessage.datagramId] == undefined) { - // Prepare buffer for JSON - this.fragments[jsonMessage.datagramId] = { - - getValue() { - let buffer = Buffer.from(''); - for (let partial in this.partials) { - buffer = Buffer.concat([ - buffer, - Buffer.from(this.partials[partial]) - ]) - }; - const bufferDecoded = Buffer(buffer.toString(), 'base64'); - return bufferDecoded; - }, - isValid() { - const json = this.getValue(); - let isValid = false; - try { - JSON.parse(json.toString()); - isValid = true; - } catch (error) { - isValid = false; - this.emit('error', `Valid packet error: ${error}`); - }; - return isValid; - }, - partials: {} - }; - }; - - this.fragments[jsonMessage.datagramId].partials[jsonMessage.fragmentOffset] = jsonMessage.fragmentData; - if (this.fragments[jsonMessage.datagramId].isValid()) { - const debug = this.debugLog ? this.emit('debug', 'Json completed fragmented packet.') : false; - this.response.packetDecoded.protectedPayload.json = this.fragments[jsonMessage.datagramId].getValue().toString(); - this.fragments[jsonMessage.datagramId] = undefined; - }; - this.function = 'jsonFragment'; - }; - }; - - clearTimeout(this.closeConnection); - - if (this.function == 'status') { - const decodedMessage = JSON.stringify(this.response.packetDecoded.protectedPayload); - if (this.message === decodedMessage) { - const debug = this.debugLog ? this.emit('debug', 'Received unchanged status message.') : false; - return; - }; - this.message = decodedMessage; - }; - - const debug1 = this.debugLog ? this.emit('debug', `Received event type: ${this.function}`) : false; - this.emit(this.function, this.response); - }) - .on('listening', () => { - const address = this.socket.address(); - const debug = this.debugLog ? this.emit('debug', `Server start listening: ${address.address}:${address.port}.`) : false; - - // Start discovery - this.startDiscovery(); - }) - .on('close', () => { - const debug = this.debugLog ? this.emit('debug', 'Socket closed.') : false; - clearInterval(this.discovery); - clearTimeout(this.closeConnection); - - this.isConnected = false; - this.requestNum = 0; - this.channelTargetId = null; - this.channelRequestId = null; - this.emit('stateChanged', false, 0, 0, 0, true, 0); - this.emit('disconnected', 'Disconnected.'); - - //reconnect - this.reconnect(); - }) - .bind(); - }; - - reconnect() { - this.connect(); - }; - - startDiscovery() { - const debug = this.debugLog ? this.emit('debug', 'Start discovery.') : false; - this.discovery = setInterval(() => { - if (!this.isConnected) { - const discoveryPacket = new Packer('simple.discoveryRequest'); - const message = discoveryPacket.pack(); - this.sendSocketMessage(message); - }; - }, 5000); - }; - - getRequestNum() { - this.requestNum++; - const debug = this.debugLog ? this.emit('debug', `Request number set to: ${this.requestNum}`) : false; - }; - - sendSocketMessage(message) { - if (!this.sendBlock) { - this.sendBlock = true; - const messageLength = message.length; - this.socket.send(message, 0, messageLength, 5050, this.host, (error, bytes) => { - if (error) { - this.emit('error', `Socket send message error: ${error}`); - this.sendBlock = false; - }; - const debug = this.debugLog ? this.emit('debug', `Socket send ${bytes} bytes.`) : false; - this.sendBlock = false; - }); - } }; powerOn() { return new Promise((resolve, reject) => { if (!this.isConnected) { const info = this.infoLog ? false : this.emit('message', 'Send power On.'); - const powerOnStartTime = (new Date().getTime()) / 1000; - - this.boot = setInterval(() => { - const powerOn = new Packer('simple.powerOn'); - powerOn.set('liveId', this.xboxLiveId); - const message = powerOn.pack(); - this.sendSocketMessage(message); - - const lastPowerOnTime = (Math.trunc(((new Date().getTime()) / 1000) - powerOnStartTime)); - if (lastPowerOnTime > 15) { - clearInterval(this.boot) - this.emit('stateChanged', false, 0, 0, 0, true, 0); - this.emit('disconnected', 'Power On failed, please try again.'); + this.setPowerOn = setInterval(() => { + try { + this.sendPowerOn(); + } catch (error) { + reject({ + status: 'Send power On error.', + error: error + }); }; - }, 500); + }, 600); setTimeout(() => { resolve(true); }, 3500); + + setTimeout(() => { + if (!this.isConnected) { + clearInterval(this.setPowerOn); + this.emit('stateChanged', false, 0, 0, 0, true, 0); + this.emit('disconnected', 'Power On failed, please try again.'); + } + }, 15000); } else { reject({ - status: 'error', - error: 'Already connected.' + status: 'Console already On.' }); }; }); }; - recordGameDvr() { - return new Promise((resolve, reject) => { - if (this.isConnected && this.isAuthorized) { - const info = this.infoLog ? false : this.emit('message', 'Send record game.'); + async sendPowerOn() { + try { + const powerOn = new Packer('simple.powerOn'); + powerOn.set('liveId', this.xboxLiveId); + const message = powerOn.pack(); + await this.sendSocketMessage(message); + } catch (error) { + this.emit('error', `Send power On error: ${error}`) + }; + }; - const recordGameDvr = new Packer('message.recordGameDvr'); - recordGameDvr.set('startTimeDelta', -60); - recordGameDvr.set('endTimeDelta', 0); - const message = recordGameDvr.pack(this); - this.sendSocketMessage(message); - resolve(true); + powerOff() { + return new Promise(async (resolve, reject) => { + if (this.isConnected) { + const info = this.infoLog ? false : this.emit('message', 'Send power Off.'); + try { + const powerOff = new Packer('message.powerOff'); + powerOff.set('liveId', this.xboxLiveId); + const message = powerOff.pack(this); + await this.sendSocketMessage(message); + + setTimeout(() => { + this.disconnect(); + resolve(true); + }, 3500); + } catch (error) { + reject({ + status: 'Send power Off error.', + error: error + }); + }; } else { - const debug = this.debugLog ? this.emit('debug', 'Not connected or not authorized, send record game ignored. ') : false; reject({ - status: 'error', - error: `Connection state: ${this.isConnected}, authorization state: ${this.isAuthorized}` + status: 'Console already Off.' }); }; }); }; - powerOff() { - return new Promise((resolve, reject) => { - if (this.isConnected) { - const info = this.infoLog ? false : this.emit('message', 'Send power Off.'); - - const powerOff = new Packer('message.powerOff'); - powerOff.set('liveId', this.xboxLiveId); - const message = powerOff.pack(this); - this.sendSocketMessage(message); - - setTimeout(() => { - this.disconnect(); + recordGameDvr() { + return new Promise(async (resolve, reject) => { + if (this.isConnected && this.isAuthorized) { + const info = this.infoLog ? false : this.emit('message', 'Send record game.'); + try { + const recordGameDvr = new Packer('message.recordGameDvr'); + recordGameDvr.set('startTimeDelta', -60); + recordGameDvr.set('endTimeDelta', 0); + const message = recordGameDvr.pack(this); + await this.sendSocketMessage(message); resolve(true); - }, 3500); + } catch (error) { + reject({ + status: 'Send record game error.', + error: error + }); + }; } else { reject({ - status: 'error', - error: 'Already disconnected.' + status: `Send record game ignored, connection state: ${this.isConnected}, authorization state: ${this.isAuthorized}` }); }; }); }; sendCommand(channelName, command) { - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { if (this.isConnected) { const debug = this.debugLog ? this.emit('debug', 'Send command.') : false; + if (CONSTANS.channelIds[channelName] != this.channelRequestId) { + try { + const channelRequest = new Packer('message.channelRequest'); + channelRequest.set('channelRequestId', CONSTANS.channelIds[channelName]); + channelRequest.set('titleId', 0); + channelRequest.set('service', Buffer.from(CONSTANS.channelUuids[channelName], 'hex')); + channelRequest.set('activityId', 0); + const message = channelRequest.pack(this); + const debug1 = this.debugLog ? this.emit('debug', `Send channel request name: ${channelName}, id: ${CONSTANS.channelIds[channelName]}`) : false; + await this.sendSocketMessage(message); - if (channelIds[channelName] != this.channelRequestId) { - const channelRequest = new Packer('message.channelRequest'); - channelRequest.set('channelRequestId', channelIds[channelName]); - channelRequest.set('titleId', 0); - channelRequest.set('service', Buffer.from(channelUuids[channelName], 'hex')); - channelRequest.set('activityId', 0); - const message = channelRequest.pack(this); - const debug = this.debugLog ? this.emit('debug', `Send channel request name: ${channelName}, id: ${channelIds[channelName]}`) : false; - this.sendSocketMessage(message); - - setTimeout(() => { - this.emit('sendCommand', command) - }, 500); + setTimeout(() => { + this.emit('sendCommand', command) + }, 500); + resolve(true); + } catch (error) { + reject({ + status: `Send command: ${command} error.`, + error: error + }); + } } else { - this.emit('sendCommand', command) + reject({ + status: `Send command: ${command} ignored, channel request id duplicated.` + }); } - resolve(true); } else { reject({ - status: 'error', - error: 'Not connected, send command ignored.' + status: `Console not connected, send command: ${command} ignored.` }); }; }); }; - disconnect() { + async disconnect() { const debug = this.debugLog ? this.emit('debug', 'Disconnecting...') : false; + clearTimeout(this.closeConnection); - const disconnect = new Packer('message.disconnect'); - disconnect.set('reason', 4); - disconnect.set('errorCode', 0); - const message = disconnect.pack(this); - this.sendSocketMessage(message); + try { + const disconnect = new Packer('message.disconnect'); + disconnect.set('reason', 4); + disconnect.set('errorCode', 0); + const message = disconnect.pack(this); + await this.sendSocketMessage(message); + this.emit('disconnected', 'Disconnected.'); - //colose socket - setTimeout(() => { - this.socket.close(); - }, 5000); + setTimeout(() => { + this.isConnected = false; + this.requestNum = 0; + this.channelTargetId = null; + this.channelRequestId = null; + this.power = false; + this.emitDevInfo = true; + this.emit('stateChanged', false, 0, 0, 0, true, 0); + }, 3000); + } catch (error) { + this.emit('error', `Send disconnect error: ${error}`) + }; + }; + + getRequestNum() { + this.requestNum++; + const debug = this.debugLog ? this.emit('debug', `Request number set to: ${this.requestNum}`) : false; + }; + + sendSocketMessage(message) { + return new Promise((resolve, reject) => { + const offset = 0; + const length = message.byteLength; + + this.socket.send(message, offset, length, 5050, this.host, (error, bytes) => { + if (error == null) { + const debug = this.debugLog ? this.emit('debug', `Socket send ${bytes} bytes.`) : false; + resolve(true); + } else { + reject(error); + }; + }); + }); }; }; -module.exports = SMARTGLASS; \ No newline at end of file +module.exports = XBOXLOCALAPI; \ No newline at end of file