Roku (HFN) Plugin

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)
  • 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 or device.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");
}
1 Like

I’m getting the message “no public method for the specified argument exists” when I try to synchronize. This is for the 08-04 version as well as the 04-28 version.

Is there a line number associated with the error? What does the console log show?

Wow, how have I never noticed the log at the bottom of the designer. That’s going to make my journey so much easier. Here’s the output:

onConnect called...
  Host: 192.168.0.13
onSynchronizeDevices called...
  done syncing
onDisconnect called...
onConnect called...
  Host: 192.168.0.13
  building apps list...
  setting apps...
  set (12):
No public methods with the specified arguments were found.
onDisconnect called...

The console log is really helpful when trying to debug plugins. If you have the screen space (or a second monitor), it’s really nice to make it a floating window and pull it off to the side so it’s easier to watch while interacting with your plugin in the simulator.

With respect to the specific problem you’re having, the console log was exceedingly helpful in finding where the problem was. As for why it was a problem, I’m a little less clear, because the particular code hasn’t been changed in a while and I think it used to work. Perhaps @bill has some insights on this–javascript is not my strong suit, but the issue was that there was a line in onConnect() (2 of them actually, but you weren’t getting to the second one) that was doing:

console.log(device.SupportedInputSources)

And apparently javascript does not accept the array as-is as the input parameter to the logging function and also did not automatically convert it to a string. I normally run my plugins with the plugin logging stuff I created here (since it is very useful for debugging on the device itself), and since it prefixes every line with a common string, it was always doing the array->string conversion. Still, I was pretty sure I had tested the version posted as-is as well. Sorry about that. I’ll fix the posted plugin shortly–it’s a really simple fix–and hopefully you’ll be good to go!

Thanks so much for your help on this. It would synchronize correctly the first time but would error on subsequent times so I commented out lines 252 and 269:

console.log(" set (" + device.SupportedInputSources.length + "): " + device.SupportedInputSources);

console.log(" set (" + device.SupportedMediaCommands.length + "): " + device.SupportedMediaCommands);

and it works great! That keyboard is fantastic too! I love the way you coded the shift key.

1 Like

Glad you were able to get it (and the keyboard) working! Regarding having to comment out those lines, that was going to be my next suggestion, although it really shouldn’t be necessary–you don’t need to synchronize devices more than once (or at all, I don’t believe, with the new plugin import feature).

UPDATE: this was due to a bug in Home Remote that will be fixed in v3.6.0.

1 Like

I made a couple changes to your plugin.

The block of code that sets SupportedInputSources on the device needs to be called on every connect. Not just on initial connect when building the app list. Devices are completely reset on disconnect so that’s why it needs to be updated again.

I also added the PowerOff command. My Roku Streaming Stick+ supports this command. When it is off, the network is still active so you can still make API calls. It just turns the video output off so you’ll get a black screen.

Roku-HFN-2020-09-12.plugin (9.5 KB)

2 Likes

Thanks, Bill! I incorporated your changes and updated my initial post. I also went ahead and added several other commands that some Roku devices support, just in case you or others are interested in them.

One thing I didn’t add was for input select on a device like Roku TV. Interestingly, those are handled as key-presses, but I think in Home Remote they should be part of the MediaInputSource capability. It should be relatively straightforward to add those, but it does involve some additional logic and, since I can’t test it, I held off. If anyone is interested, let me know, and we can make it work.

Here I made another change. I’ve also added PowerOn & PowerToggle commands.

@amingle sent me a message the other day showing his use of the Power/PowerToggle command on a Roku TV. Just out of curiosity, I tested it on my Roku Streaming Stick+ & it worked on there too.

I was incorrect earlier when I said it was turning the video off. It’s actually turning the TV off. I assume it’s using HDMI CEC to incorporate these power controls.

Roku-HFN-2020-09-16-B.plugin (10.4 KB)

1 Like

Thanks again, Bill! I went ahead and also added in all of the other documented commands as well as adding the input commands. The input commands work through the InputSource attribute, just like the apps do, so, for example, you can InputSource = "InputHDMI1". Internally, it checks to see whether the input you’re switching is one of the predefined ones or an app and then sends the appropriate request.

I’m not able to test the input commands because I just have a regular Roku, but I’d appreciate it if someone could let me know if they work on a Roku TV.

As I was typing this up, it occurred to me that more work may still be needed because when reading InputSource, it will only show the current app, not the current TV input. I can’t really think of an easy fix for that problem. It would probably be better to have a second capability/attribute, or maybe even a second device, like a multi-zone receiver, one for the streamer portion and one for the TV portion, each having it’s own associated input… Without someone to at least assist in testing, however, I don’t want to go down that road blind (since I don’t have a Roku TV to test against). It’s an interesting problem, though.

1 Like

I have a Roku TV, so I can take a look, hopefully / maybe this weekend. I ended up taking your original Roku plugin and modified it to add in some things about current TV input / live TV channel / etc., so I’ll see what else I did that may help round it out for TVs. I integrated a “now playing” functionality that has to do different things based on whether it is playing an app or playing OTA TV. If I understand your post right, that’s getting at the heart of what you’re talking about here.

Thanks for all your work with this.

Thanks so much for testing it out–I hate not being able to test code before I publish it, but couldn’t be helped in this case. I’d also be very interested to see what other features you worked into your modified plugin and figure out a good way to incorporate them in so we can have a good one-stop-shop Roku plugin.

Overall, everything seems to work well. I have a couple of comments about your Power implementation and your Input implementation:

Power

These edits are part of the ‘if(mc)’ statement in Line 228. I added Wake-on-LAN functionality for a TV that is ‘deep sleeping’ (code borrowed from the WOL plugin floating around the forum).

if(mc) {
	if(mc !== 'Power') {
		//Regular commands
		console.log("  sending keypress command: " + mc);

		http.post("http://" + host + ":8060/keypress/" + mc);
	}	else {
		console.log("  sending power command");

		try {
			var keyResponse = http.post("http://" + host + ":8060/keypress/Power", "", { timeout: 1000 });
			console.log("  no WOL needed...powered on / off normally");
		}	catch (e) {
			console.log('  TV is most likely powered off in deep sleep...trying WOL');
			
			var mac = plugin.Settings["MACAddr"];;
			var ip = host;

			WOL.wake(mac);
			sleep(500);
			WOL.wake(mac, { address: ip });
			sleep(500);
			//WOL.wake(mac, { address: ip, port: parseInt(9) });
			WOL.wake(mac, { address: ip, port: 9 });
		};
	}
}

Input Source
Using keypress for those 6 inputs works ok, as long as someone did not rename the HDMI ports, for instance. What I would recommend is exclusively using apps to call up the correct input. Here is my list of apps:

AV receiver,Wall Jack,HDMI 3,Live TV,Prime Video,The Roku Channel,Pandora,PBS KIDS,ESPN,4K Spotlight,MLB,NBA,NASA TV,Roku Media Player,ABC,YouTube

AV receiver is my renamed HDMI 1, Wall Jack is my renamed HDMI 2, and Live TV is what the TV automatically calls Tuner. All of those can be called up using the ‘/launch/’ syntax, so I have found no need to use keypress for this.

Current TV channel
Your plugin does seem to be pulling the correct app in for InputSource. I take it one step further if the current app is Live TV to determine the current channel and use /query/tv-active-channel: Link to the Roku ECP docs.

Hope that helps.

@amingle Thank you so much for the feedback and testing! Couple of follow-up questions, just so I’m sure I understand things:

Power

Out of curiosity, will the TV always power on if you send it WOL? That is, is there any reason to ever actually try to send the Power command first? I implemented WOL functionality in my Sony BD Player plugin and just use the WOL message as the PowerOn action. I’m wondering if that would work for the Roku TV as well? It has the advantage of being simpler and faster (since you don’t have to wait for the initial Power command to timeout first).

Input Source

Are you saying that the apps list also includes all of the input names and that “launching” those works fine? If so, I will gladly pull all of that input code out. I just assumed from reading the docs that those were separately handled, but treating them all as apps is much cleaner.

Current TV channel

Have you already written the code to determine the active TV channel? If so, how are you exposing that information? That is, what attribute do you use to provide the name of the channel when the InputSource is “Live TV”? I’m hesitant to overwrite the InputSource itself for that situation. @bill I see that there is a TvChannel capability–would that be the appropriate place to put it?

Yes. You can use the TvChannel capability. When setting the attributes you’ll have to do it like this:

device["TvChannel.Name"] = "Channel Name";
device["TvChannel.Number"] = "Channel Number";

That was a good question that I had to test. But, surprisingly, no - it does not seem to work if it is in ‘Display Off’ mode (which is about 15 minutes before entering Deep Sleep). My guess is that it already considers the device powered on.

Exactly right. It works your way too, but I have left them all as apps.

Here is the code I use, adapting it to use the TvChannel capability Bill mentioned (which I had not previously been using):

var host = plugin.Settings["Host"];

var httpConfig = { responseType: 'xml', timeout: 1000 };
var npResponse = http.get("http://" + host + ":8060/query/active-app", httpConfig);
var npElement = npResponse.data;

var tvChannel_number = '';
var tvChannel_name = '';
var tvProgram_name = '';
var validSignal = true;
npElement.elements.forEach(function (node) {
	if(node.attributes) {
		//If an app is active
		
		//If Live TV, get the channel information
		if(node.attributes.id === 'tvinput.dtv') {
			var httpConfig = { responseType: 'xml', timeout: 1000 };
			var response = http.get("http://" + host + ":8060/query/tv-active-channel", httpConfig);
			var element = response.data;
			
			element.elements.forEach(function (channel) {
				channel.elements.forEach(function (item) {
					switch (item.name) {
						case 'number': 
							tvChannel_number = item.text;
							break;
						case 'name': 
							tvChannel_name = item.text;
							break;
						case 'program-title': 
							tvProgram_name = item.text;
							break;
						case 'signal-state':
						
							if(item.text === 'none') {
								validSignal = false;
							};
							break;
						default:
					};
				});
			});

			if(validSignal !== true) {
				tvProgram_name = '';
			};
		};
	}	else {
		//No app active - TV is likely powered off or display off, or on the Home screen
	};
});

device["TvChannel.Name"] = tvChannel_name;
device["TvChannel.Number"] = tvChannel_number;
//NOTE: currently nothing is being done with the TV program name (tvProgram_name)

Let me know what else I can help with!

Awesome, thanks! I am swamped this week, but will try to get these changes in later in the week or over the weekend.

Ahh don’t you hate it when real life gets in the way!

One addition that I just stumbled across tonight. When you are saving the appId and appName, they can come in with non-breaking spaces, which causes issues if you are later trying to compare with values typed in Home Remote that have a regular space. So, I modified the block of code to look like this:

//Need to replace nbsp with a regular space
var re = new RegExp(String.fromCharCode(160), "g");

apps = {};
appsElement.elements.forEach(function (node) {
	var appId = node.attributes.id.replace(re, " ");
	var appName = node.text.replace(re, " ");
	apps[appName] = appId;
});

Hopefully that makes sense. It was a weird issue, where I was trying to use HomeRemote to call a specific input, and it was not working. I ended up using charCodeAt to look at each character to see what was going on. Mystery solved.

Thanks, Bill! I saw you added TvChannel.ProgramTitle as well–very cool! I’m still doing some fine-tuning and testing before making another release of the plugin, but I had a quick question for you, @bill: in onConnect(), we store off the apps list into a plugin global variable (that was how it was done in your original plugin). I was thinking about it some more and, in theory, someone could add apps and then that list would be out of date until the remote was restarted. Is there any particular reason not to retrieve and regenerate the apps list every time onConnect() is called? Or is my understanding of how state is maintained incorrect?