This is a plugin for Roku that is based on @bill’s Roku plugin originally posted April 5, 2020 in the old forum. The main changes are:
- various code cleanups/stylistic changes
- support for keyboard commands per the Roku documentation (including using some concepts from the MCE Controller Plugin I wrote; there is a sample keyboard control provided at the bottom of the first post for that plugin)
- support for querying the active app via
device.InputSource
, which completes support for the Media Input Source capability (note that screensaver state is not indicated, although that could be added if desired) - support for more media commands not supported by all Roku devices, such as commands used by Roku TV
- support for WOL and Live TV channel/program identification (thanks to @amingle), so added new Settings “MAC”, “UseIPforWOL”, and “WOLPort” and new Capability “TvChannel”
- active app is now only polled if something on the page is subscribed to
device.InputSource
ordevice.TvChannel.Name/Number/ProgramTitle
and changed polling interval to be 1 second so it is more responsive if something actually wants the info
The Roku’s API is documented here:
The plugin has 4 settings that can be configured: Host
, MAC
, UseIPforWOL
, and WOLPort
. Host
should be set to the Roku’s IP address. The Roku will not actually respond to commands sent to its hostname, but only it’s IP address, for whatever reason. For example, commands to “http://roku.mynetwork.com/” will be ignored whereas commands to “http://192.168.1.1/” will work, even though “roku.mynetwork.com” resolves to “192.168.1.1”. MAC
, if set, will be used for sending WOL for PowerOn
/PowerToggle
(note that (1) PowerToggle
is not “smart” because does not check current power state, (2) the Roku command it uses is undocumented, and (3) would recommend avoiding it if possible). UseIPforWOL
, if it is set to anything other than an empty string, will also send the Host
and, if specified, WOLPort
in the WOL message.
Last updated: 2020-10-01
Roku-HFN-2020-10-01.plugin (15.8 KB)
plugin.Name = "Roku";
plugin.OnChangeRequest = onChangeRequest;
plugin.OnConnect = onConnect;
plugin.OnDisconnect = onDisconnect;
plugin.OnPoll = onPoll;
plugin.OnSynchronizeDevices = onSynchronizeDevices;
plugin.PollingInterval = 1000;
plugin.DefaultSettings = {
"Host" : "192.168.1.1", // this needs to be an IP address and not a hostname or the Roku won't respond
"MAC" : "", // if set, WOL will be sent for PowerOn/PowerToggle
"UseIPforWOL" : "", // set this to anything other than empty string if need to send IP (and port) for WOL; host must be IP address and not name in this case
"WOLPort" : "", // standard is 9; if left empty, the port will not be sent
};
var VERSION = "2020-10-01";
/*
This is based on Bill Venhaus's Roku_Plugin posted on April 5, 2020,
available at:
https://groups.google.com/d/msg/thehomeremote/t1_lODTwmqA/7LCheBHSAAAJ
Modifications made include:
- various code cleanups/stylistic changes
- support for keyboard commands per the Roku documentation (including using
some concepts from the MCE Controller Plugin I wrote)
- support for querying the active app, which completes support for the Media
Input Source capability (except does not indicate whether the screensaver
is on, although that could be added)
- support for more media commands not supported by all Roku devices, such as
commands used by Roku TV
- support for WOL and Live TV channel/program identification (thanks to
@amingle), so added new Settings "MAC", "UseIPforWOL", and "WOLPort" and
new Capability "TvChannel"
- active app is now only polled if something on the page is subscribed to
InputSource or TvChannel.Name/Number/ProgramTitle and changed polling
interval to be 1 second so it is more responsive if something actually
wants the info
*/
var http = new HTTPClient();
var apps = null;
// This is used to track the up/down state of the shift key.
var shiftKeyState = false;
var mediaCommandMappings = {
"DirectionUp" : "Up",
"DirectionLeft" : "Left",
"DirectionDown" : "Down",
"DirectionRight" : "Right",
"Select" : "Select",
"Back" : "Back",
"Info" : "Info",
"Home" : "Home",
"Play" : "Play",
"Pause" : "Pause",
"Rewind" : "Rev",
"FastForward" : "Fwd",
"InstantReplay" : "InstantReplay",
"Search" : "Search",
"PowerOff" : "PowerOff", // these are only supported by some devices, such as Roku TV
"PowerOn" : "PowerOn",
"PowerToggle" : "Power",
"VolumeUp" : "VolumeUp",
"VolumeDown" : "VolumeDown",
"Mute" : "VolumeMute",
"ChannelUp" : "ChannelUp",
"ChannelDown" : "ChannelDown",
"FindRemote" : "FindRemote",
"Enter" : "Select", // technically, Roku supports an Enter command different than
"LetterEnter" : "Select", // Select, but testing showed Select was more intuitive for this
"Number1" : "Lit_1",
"Number2" : "Lit_2",
"Number3" : "Lit_3",
"Number4" : "Lit_4",
"Number5" : "Lit_5",
"Number6" : "Lit_6",
"Number7" : "Lit_7",
"Number8" : "Lit_8",
"Number9" : "Lit_9",
"Number0" : "Lit_0",
"LetterEscape" : "Back",
"LetterBackspace" : "Backspace",
};
var mediaCommandShiftMappings = {
"LetterShift" : "shift",
"LetterShiftOn" : "shift",
"LetterShiftOff" : "shift",
};
var mediaCommandKeyMappings = {
"LetterA" : [ "Lit_a", "Lit_A" ],
"LetterB" : [ "Lit_b", "Lit_B" ],
"LetterC" : [ "Lit_c", "Lit_C" ],
"LetterD" : [ "Lit_d", "Lit_D" ],
"LetterE" : [ "Lit_e", "Lit_E" ],
"LetterF" : [ "Lit_f", "Lit_F" ],
"LetterG" : [ "Lit_g", "Lit_G" ],
"LetterH" : [ "Lit_h", "Lit_H" ],
"LetterI" : [ "Lit_i", "Lit_I" ],
"LetterJ" : [ "Lit_j", "Lit_J" ],
"LetterK" : [ "Lit_k", "Lit_K" ],
"LetterL" : [ "Lit_l", "Lit_L" ],
"LetterM" : [ "Lit_m", "Lit_M" ],
"LetterN" : [ "Lit_n", "Lit_N" ],
"LetterO" : [ "Lit_o", "Lit_O" ],
"LetterP" : [ "Lit_p", "Lit_P" ],
"LetterQ" : [ "Lit_q", "Lit_Q" ],
"LetterR" : [ "Lit_r", "Lit_R" ],
"LetterS" : [ "Lit_s", "Lit_S" ],
"LetterT" : [ "Lit_t", "Lit_T" ],
"LetterU" : [ "Lit_u", "Lit_U" ],
"LetterV" : [ "Lit_v", "Lit_V" ],
"LetterW" : [ "Lit_w", "Lit_W" ],
"LetterX" : [ "Lit_x", "Lit_X" ],
"LetterY" : [ "Lit_y", "Lit_Y" ],
"LetterZ" : [ "Lit_z", "Lit_Z" ],
"Letter1" : [ "Lit_1", "Lit_!" ],
"Letter2" : [ "Lit_2", "Lit_%40" /* @ */ ],
"Letter3" : [ "Lit_3", "Lit_%23" /* # */ ],
"Letter4" : [ "Lit_4", "Lit_%24" /* $ */ ],
"Letter5" : [ "Lit_5", "Lit_%" ],
"Letter6" : [ "Lit_6", "Lit_^" ],
"Letter7" : [ "Lit_7", "Lit_%26" /* & */ ],
"Letter8" : [ "Lit_8", "Lit_*" ],
"Letter9" : [ "Lit_9", "Lit_(" ],
"Letter0" : [ "Lit_0", "Lit_)" ],
"LetterTab" : [ "Lit_%09" /* <tab> */, "Lit_%09" /* <tab> */ ],
"LetterSpace" : [ "Lit_%20" /* <space> */, "Lit_%20" /* <space> */ ],
"LetterBackQuote" : [ "Lit_`", "Lit_~" ],
"LetterHyphen" : [ "Lit_-", "Lit__" ],
"LetterEqual" : [ "Lit_%3d" /* = */, "Lit_%2b" /* + */ ],
"LetterLeftSquareBracket" : [ "Lit_[", "Lit_{" ],
"LetterRightSquareBracket" : [ "Lit_]", "Lit_}" ],
"LetterBackslash" : [ "Lit_%5c" /* \ */, "Lit_|" ],
"LetterSemicolon" : [ "Lit_%3b" /* ; */, "Lit_%3a" /* : */ ],
"LetterSingleQuote" : [ "Lit_\'", "Lit_\"" ],
"LetterComma" : [ "Lit_%2c" /* , */, "Lit_<" ],
"LetterPeriod" : [ "Lit_.", "Lit_>" ],
"LetterSlash" : [ "Lit_%2f" /* / */, "Lit_%3f" /* ? */ ],
};
// helper function to get the current active app and, if applicable, TV channel information
function rokuGetActiveApp() {
var host = plugin.Settings["Host"];
var device = plugin.Devices[plugin.Name];
if(device) {
var activeResponse = http.get("http://" + host + ":8060/query/active-app", { responseType: "xml" });
var activeElement = activeResponse.data;
activeElement.elements.forEach(function (node) {
// find the app node (usually the only one, but there may also be a screensaver)
if(node.name == "app") {
device.InputSource = node.text;
}
// look to see if it's Live TV, and if so, extract channel information
if(node.attributes && node.attributes.id == "tvinput.dtv") {
// it is Live TV, so get the channel information
var channelNumber = "";
var channelName = "";
var programTitle = "";
var validSignal = true;
var channelResponse = http.get("http://" + host + ":8060/query/tv-active-channel", { responseType: "xml" });
var channelElement = channelResponse.data;
channelElement.elements.forEach(function (channel) {
channel.elements.forEach(function (item) {
switch (item.name) {
case "number":
channelNumber = item.text;
break;
case "name":
channelName = item.text;
break;
case "program-title":
programTitle = item.text;
break;
case "signal-state":
validSignal = false;
break;
default:
// not a value we care about, do nothing
break;
}
});
});
// if we don't have a valid signal, indicate that in the prorgam title
if(!validSignal) {
programTitle = "NO SIGNAL"
}
// update channel information
device["TvChannel.Name"] = channelName;
device["TvChannel.Number"] = channelNumber;
device["TvChannel.ProgramTitle"] = programTitle;
}
// done with that node
});
}
// all done
}
function onChangeRequest(device, attribute, value) {
console.log("onChangeRequest called...");
var host = plugin.Settings["Host"];
var mac = plugin.Settings["MAC"];
var useipforwol = (plugin.Settings["UseIPforWOL"] == "") ? false : true;
var wolport = plugin.Settings["WOLPort"];
switch(attribute) {
case "MediaCommand":
var mc = "";
if(mediaCommandMappings.hasOwnProperty(value)) {
console.log(" standard HR command identified: " + value);
// standard commands have no special processing needed
mc = mediaCommandMappings[value];
} else if(mediaCommandShiftMappings.hasOwnProperty(value)) {
console.log(" shift command identified: " + value);
// because apparently we don't have endsWith()...
if(value.slice(-2) == "On") {
shiftKeyState = true;
} else if(value.slice(-3) == "Off") {
shiftKeyState = false;
} else {
// toggle shift state
shiftKeyState = !shiftKeyState;
}
console.log(" shift state is now " + shiftKeyState);
} else if(mediaCommandKeyMappings.hasOwnProperty(value)) {
console.log(" key command identified: " + value);
// get the correct key value based on the current shift state
if(shiftKeyState) {
mc = mediaCommandKeyMappings[value][1];
} else {
mc = mediaCommandKeyMappings[value][0];
}
} else {
// unknown command
console.log("ERROR: unsupported command: " + value);
}
if(mac != "" && (value == "PowerOn" || value == "PowerToggle")) {
// we may need to send WOL instead, try power command first and
// then, if it times out, we'll send WOL instead
try {
var keyResponse = http.post("http://" + host + ":8060/keypress/" + mc, "", { timeout: 1000 });
console.log(" no WOL needed, power command sent successfully");
} catch (e) {
console.log(" power command timed out, Roku device is most likely powered off in deep sleep");
if(mac) {
console.log(" MAC: " + mac);
console.log(" use IP for WOL?: " + useipforwol);
console.log(" Host: " + host);
console.log(" WOLPort: " + wolport);
if(useipforwol) {
var wolOpts = {};
wolOpts["address"] = host;
if(wolport != "") {
wolOpts["port"] = parseInt(wolport);
}
console.log(" sending WOL to: " + mac + " with " + wolOpts);
WOL.wake(mac, wolOpts);
} else {
console.log(" sending WOL to: " + mac);
WOL.wake(mac);
}
} else {
// no MAC address configured, can't send WOL, so nothing else to do
}
}
} else if(mc) {
// send the command, if there is one
console.log(" sending keypress command: " + mc);
http.post("http://" + host + ":8060/keypress/" + mc);
}
break;
case "InputSource":
http.post("http://" + host + ":8060/launch/" + apps[value]);
break;
default:
console.log("ERROR: unsupported command type: " + attribute);
break;
}
}
function onConnect() {
console.log("onConnect called...");
var host = plugin.Settings["Host"];
console.log(" Host: " + host);
var device = plugin.Devices[plugin.Name];
if(device) {
// get available apps
console.log(" building apps list...");
var appsResponse = http.get("http://" + host + ":8060/query/apps", { responseType: "xml" });
var appsElement = appsResponse.data;
var re = new RegExp(String.fromCharCode(160), "g"); // for cleaning out non-breaking spaces
apps = {};
appsElement.elements.forEach(function (node) {
var appId = node.attributes.id.replace(re, " ");
var appName = node.text.replace(re, " ");
apps[appName] = appId;
});
// set the app list
console.log(" setting apps...");
device.SupportedInputSources = Object.keys(apps);
console.log(" set (" + device.SupportedInputSources.length + "): " + device.SupportedInputSources);
// get the active app
console.log(" getting active app...");
rokuGetActiveApp();
// set the media commands
console.log(" building supported media commands...");
var mediaCommands = [];
// build all of our actual MediaCommands
mediaCommands = mediaCommands.concat(Object.keys(mediaCommandMappings));
mediaCommands = mediaCommands.concat(Object.keys(mediaCommandShiftMappings));
mediaCommands = mediaCommands.concat(Object.keys(mediaCommandKeyMappings));
console.log(" setting supported media commands...");
device.SupportedMediaCommands = mediaCommands;
console.log(" set (" + device.SupportedMediaCommands.length + "): " + device.SupportedMediaCommands);
}
}
function onDisconnect() {
console.log("onDisconnect called...");
}
function onPoll() {
var device = plugin.Devices[plugin.Name];
if(device && (device.HasSubscribers("InputSource")
|| device.HasSubscribers("TvChannel.Name")
|| device.HasSubscribers("TvChannel.Number")
|| device.HasSubscribers("TvChannel.ProgramTitle")
)) {
console.log("polling for active app...");
// refresh the active app
rokuGetActiveApp();
}
}
function onSynchronizeDevices() {
console.log("onSynchronizeDevices called...");
var device = new Device();
device.Id = plugin.Name;
device.DisplayName = "Roku";
device.Icon = "Remote";
device.Capabilities = [ "MediaControl", "MediaInputSource", "TvChannel" ];
device.Attributes = [ ];
plugin.Devices[plugin.Name] = device;
console.log(" done syncing");
}