From a38447a7518eca285d3355bcb0dc11b432070b9b Mon Sep 17 00:00:00 2001 From: Max Isom Date: Mon, 7 Sep 2020 15:49:15 -0400 Subject: [PATCH 1/5] feat: basic WoL implementation --- package-lock.json | 16 ++++++- package.json | 4 +- src/homebridge-roku.js | 96 ++++++++++++++++++++++++++++++------------ 3 files changed, 85 insertions(+), 31 deletions(-) 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..d30702c 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; @@ -100,29 +102,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(), 1000); + + 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'), 1000); + + callback(null); + } catch (error) { + if ( + error.constructor === pTimeout.TimeoutError && + this.doesSupportWakeOnLan() + ) { + if (this.info.ethernetMac && 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 +164,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(), 1000); + + 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; From bb1fbed6516f1a84a6083d8db2374d820ff840c7 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Mon, 7 Sep 2020 16:54:48 -0400 Subject: [PATCH 2/5] chore: update documentation --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 9d153ad..1575ba2 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 From 5bb8eb926097f746ba1c340274eda7417e2ddabc Mon Sep 17 00:00:00 2001 From: Max Isom Date: Wed, 9 Sep 2020 09:21:28 -0400 Subject: [PATCH 3/5] fix: use requestTimeout parameter for pTimeout --- src/homebridge-roku.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/homebridge-roku.js b/src/homebridge-roku.js index d30702c..9a587b1 100644 --- a/src/homebridge-roku.js +++ b/src/homebridge-roku.js @@ -32,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; @@ -113,7 +114,7 @@ class RokuAccessory { .getCharacteristic(Characteristic.Active) .on('get', async (callback) => { try { - const info = await pTimeout(this.roku.info(), 1000); + const info = await pTimeout(this.roku.info(), this.requestTimeout); const value = info.powerMode === 'PowerOn' @@ -135,7 +136,7 @@ class RokuAccessory { .on('set', async (newValue, callback) => { if (newValue === Characteristic.Active.ACTIVE) { try { - await pTimeout(this.roku.keypress('PowerOn'), 1000); + await pTimeout(this.roku.keypress('PowerOn'), this.requestTimeout); callback(null); } catch (error) { @@ -166,7 +167,7 @@ class RokuAccessory { .getCharacteristic(Characteristic.ActiveIdentifier) .on('get', async (callback) => { try { - const app = await pTimeout(this.roku.active(), 1000); + const app = await pTimeout(this.roku.active(), this.requestTimeout); const index = app !== null From b33324284103f1824e01caed865b5adfb2f42c12 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Wed, 9 Sep 2020 09:25:49 -0400 Subject: [PATCH 4/5] fix: remove duplicate check --- src/homebridge-roku.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/homebridge-roku.js b/src/homebridge-roku.js index 9a587b1..4c4c388 100644 --- a/src/homebridge-roku.js +++ b/src/homebridge-roku.js @@ -144,7 +144,7 @@ class RokuAccessory { error.constructor === pTimeout.TimeoutError && this.doesSupportWakeOnLan() ) { - if (this.info.ethernetMac && this.info.ethernetMac !== '') { + if (this.info.ethernetMac) { // Send wake-on-lan packet await wol.wake(this.info.ethernetMac); From 9ea294ace09a6acf07e53b2858113d11eb31a519 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Wed, 9 Sep 2020 09:31:41 -0400 Subject: [PATCH 5/5] fix: add note to README about WoL --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1575ba2..c79d455 100644 --- a/README.md +++ b/README.md @@ -120,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