diff --git a/README.md b/README.md index 9d153ad..c79d455 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,10 @@ homebridge config for your Roku accessory: `"infoButtonOverride": "HOME"`. The list of possible keys can be found [here](https://github.com/bschlenk/node-roku-client/blob/master/lib/keys.ts). +### requestTimeout + +Wait for this value in milliseconds before considering the device unreachable. The default value is 1000 (1 second). + ## Migrating Major Versions ### 2.x.x -> 3.x.x @@ -116,6 +120,8 @@ overcome by sending 100 volume down requests before sending X amount of volume up requests. I didn't feel like implementing this for obvious reasons, but pull requests are welcome :) +Wake-on-LAN is supported, but your device must be connected via Ethernet. + ## TODO - Possibly fetch apps at homebridge start time or periodically so that the diff --git a/package-lock.json b/package-lock.json index dda1450..101d3c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8794,8 +8794,7 @@ "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "p-limit": { "version": "2.3.0", @@ -8815,6 +8814,14 @@ "p-limit": "^2.2.0" } }, + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "requires": { + "p-finally": "^1.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -10814,6 +10821,11 @@ "is-typed-array": "^1.1.3" } }, + "wol": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/wol/-/wol-1.0.7.tgz", + "integrity": "sha512-kg7ETY8g3V5+3GVhUfWCVjeXuCmfrX6xfw4cw4c88+MtoxkbFmcs9Y5yhT1wwOL8inogFUQZ8JMzH9OekaaawQ==" + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index ff6f076..a8d3269 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,9 @@ "dependencies": { "deepmerge": "^4.2.2", "lodash.map": "^4.6.0", - "roku-client": "^4.0.0" + "p-timeout": "^3.2.0", + "roku-client": "^4.0.0", + "wol": "^1.0.7" }, "devDependencies": { "@commitlint/cli": "^8.3.5", diff --git a/src/homebridge-roku.js b/src/homebridge-roku.js index b074bcf..4c4c388 100644 --- a/src/homebridge-roku.js +++ b/src/homebridge-roku.js @@ -1,6 +1,8 @@ 'use strict'; const { Client, keys } = require('roku-client'); +const pTimeout = require('p-timeout'); +const wol = require('wol'); const plugin = require('../package'); let hap; @@ -30,6 +32,7 @@ class RokuAccessory { this.volumeIncrement = config.volumeIncrement || DEFAULT_VOLUME_INCREMENT; this.volumeDecrement = config.volumeDecrement || this.volumeIncrement; + this.requestTimeout = config.requestTimeout || 1000; this.muted = false; @@ -100,29 +103,58 @@ class RokuAccessory { return accessoryInfo; } + doesSupportWakeOnLan() { + return this.info.supportsWakeOnWlan === 'true'; + } + setupTelevision() { const television = new Service.Television(this.name); television .getCharacteristic(Characteristic.Active) - .on('get', (callback) => { - this.roku - .info() - .then((info) => { - const value = - info.powerMode === 'PowerOn' - ? Characteristic.Active.ACTIVE - : Characteristic.Active.INACTIVE; - callback(null, value); - }) - .catch(callback); + .on('get', async (callback) => { + try { + const info = await pTimeout(this.roku.info(), this.requestTimeout); + + const value = + info.powerMode === 'PowerOn' + ? Characteristic.Active.ACTIVE + : Characteristic.Active.INACTIVE; + callback(null, value); + } catch (error) { + if ( + error.constructor === pTimeout.TimeoutError && + this.doesSupportWakeOnLan() + ) { + callback(null, Characteristic.Active.INACTIVE); + return; + } + + callback(error); + } }) - .on('set', (newValue, callback) => { + .on('set', async (newValue, callback) => { if (newValue === Characteristic.Active.ACTIVE) { - this.roku - .keypress('PowerOn') - .then(() => callback(null)) - .catch(callback); + try { + await pTimeout(this.roku.keypress('PowerOn'), this.requestTimeout); + + callback(null); + } catch (error) { + if ( + error.constructor === pTimeout.TimeoutError && + this.doesSupportWakeOnLan() + ) { + if (this.info.ethernetMac) { + // Send wake-on-lan packet + await wol.wake(this.info.ethernetMac); + + callback(null); + return; + } + } + + callback(error); + } } else { this.roku .keypress('PowerOff') @@ -133,18 +165,27 @@ class RokuAccessory { television .getCharacteristic(Characteristic.ActiveIdentifier) - .on('get', (callback) => { - this.roku - .active() - .then((app) => { - const index = - app !== null - ? this.inputs.findIndex((input) => input.id === app.id) - : -1; - const hapId = index + 1; - callback(null, hapId); - }) - .catch(callback); + .on('get', async (callback) => { + try { + const app = await pTimeout(this.roku.active(), this.requestTimeout); + + const index = + app !== null + ? this.inputs.findIndex((input) => input.id === app.id) + : -1; + const hapId = index + 1; + callback(null, hapId); + } catch (error) { + if ( + error.constructor === pTimeout.TimeoutError && + this.doesSupportWakeOnLan() + ) { + callback(null, 1); + return; + } + + callback(error); + } }) .on('set', (index, callback) => { const rokuId = this.inputs[index - 1].id;