Roku Support

Configs Updated Oct 24, 2017 - Version 3.0

When I first started with OH a few months back I was confused to see that there was not a binding for the Roku products. After searching, it became apparent to me that the reason for this is the fact that it’s not actually needed. What I mean by that is that the existing bindings are already in place to build a robust connection to a Roku. After several months I have made some good progress and wanted to provide it back to the community. Below is an example of the rules/items/transforms/sitemaps that I have put together. This is now updated to support multiple Roku devices. There is no need to have duplicate files.

Some notes for the snip below:
*This is tested on OH 2.1.0 running on Ubuntu. The Roku tested against was a Roku2 running OS 7.7.0.
*Replace and with the serial number and IP address of the device. This allows for easy reuse.
*This requires the HTTP binding and the XPATH transform to be installed. Python is also required.
*There are errors when OH is started/restarted in openhab.log. There is a race condition that is happening when the items/rules/transforms are loaded. This clears up after a few minutes of stability.
*The CRON is an example that updates the items every minute. This showed no impact to the performance of the Roku while streaming a 1080P movie. Adjust to fit your needs.
*The apps I’ve shown are meant as examples and can be easily added/removed. See links below on how to get the app-id and icons.

The following links/commands will help get you started:

curl http://<IP ADDRESS>:8060/query/device-info - Gives you the current status of the device.  Use the XML info to create your XSLT transforms
curl http://<IP ADDRESS>:8060/query/apps - Shows all of the apps currently installed on the device.  This is where you can get your app-id used below.
curl http://<IP ADDRESS>:8060/query/active-app - Shows the current active app.  Also shows the screensaver if the system is idle.
curl -d '' http://<IP ADDRESS>:8060/keypress/home - Test keypress.  Replace "home" with the button you want to test.  Refer below for your options.
wget http://<IP ADDRESS>:8060/query/icon/ - Download the app icon from the Roku directly.  Put all icons in icons/classic/ 

Example of the files:

items/roku.items:
String ROKU_navigate <player>
String ROKU_keyboard
String ROKU_keyboard_capslock
Switch ROKU_devinfo
Switch ROKU_appinfo
String ROKU_ssdp

String ROKU<SERIALNUM>_status                     "[%s]"                       <player>
String ROKU<SERIALNUM>_url                        "URL [%s]"                   <settings>
String ROKU<SERIALNUM>_udn                        "UDN [%s]"                   <settings>
String ROKU<SERIALNUM>_serialnumber               "Serial Number [%s]"         <settings>
String ROKU<SERIALNUM>_deviceid                   "Device ID [%s]"             <settings>
String ROKU<SERIALNUM>_vendorname                 "Vendor Name [%s]"           <settings>
String ROKU<SERIALNUM>_modelname                  "Model Name [%s]"            <settings>
String ROKU<SERIALNUM>_modelnumber                "Model Number [%s]"          <settings>
String ROKU<SERIALNUM>_supportsethernet           "Supports Ethernet [%s]"     <settings>
String ROKU<SERIALNUM>_wifimac                    "Wifi MAC [%s]"              <settings>
String ROKU<SERIALNUM>_ethernetmac                "Ethernet MAC [%s]"          <settings>
String ROKU<SERIALNUM>_networktype                "Network Type [%s]"          <settings>
String ROKU<SERIALNUM>_userdevicename             "User Device Name [%s]"      <settings>
String ROKU<SERIALNUM>_softwareversion            "Software Version [%s]"      <settings>
String ROKU<SERIALNUM>_softwarebuild              "Software Build [%s]"        <settings>
String ROKU<SERIALNUM>_securedevice               "Secure Device [%s]"         <settings>
String ROKU<SERIALNUM>_powermode                  "Power Mode [%s]"            <settings>
String ROKU<SERIALNUM>_activeapp                  "Active App [%s]"            <settings>
String ROKU<SERIALNUM>_screensaver                "Screensaver [%s]"           <settings>

rules/roku.rules:
rule "ROKU DEVINFO"
when
    System started or
    Time cron "0 0 * * * ?" or
    Item ROKU_devinfo received command
then
        var String url = ""
        val String ssdp = executeCommandLine("/etc/openhab2/scripts/searchRokus.py", 20000)

        ROKU_ssdp.postUpdate(ssdp)

        val rokuAddrs = ssdp.split("\n")

        rokuAddrs.forEach[addr |
        val s = addr.split(" ")
        url = s.get(1)

        var String deviceinfo = sendHttpGetRequest(url + "query/device-info")

        postUpdate("ROKU" + s.get(0) + "_url",url)
        postUpdate("ROKU" + s.get(0) + "_udn",transform("XPATH","//udn",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_serialnumber",transform("XPATH","//serial-number",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_deviceid",transform("XPATH","//device-id",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_vendorname",transform("XPATH","//vendor-name",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_modelname",transform("XPATH","//model-name",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_modelnumber",transform("XPATH","//model-number",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_supportsethernet",transform("XPATH","//supports-ethernet",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_wifimac",transform("XPATH","//wifi-mac",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_ethernetmac",transform("XPATH","//ethernet-mac",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_networktype",transform("XPATH","//network-type",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_userdevicename",transform("XPATH","//user-device-name",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_softwareversion",transform("XPATH","//software-version",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_softwarebuild",transform("XPATH","//software-build",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_securedevice",transform("XPATH","//secure-device",deviceinfo))
        postUpdate("ROKU" + s.get(0) + "_powermode",transform("XPATH","//power-mode",deviceinfo))

        ROKU_appinfo.sendCommand(ON)

        ]

end

rule "ROKU APPINFO"
when
    Time cron "0 * * * * ?" or
    Item ROKU_appinfo received command
then
        val String ssdp = ROKU_ssdp.state.toString
        var String url = ""

        val rokuAddrs = ssdp.split("\n")

        rokuAddrs.forEach[addr |
        val s = addr.split(" ")
        url = s.get(1)

        var String activeapp = sendHttpGetRequest(url + "query/active-app")
        var String app = transform("XPATH","//app",activeapp)
        var String saver = transform("XPATH","//screensaver",activeapp)
        postUpdate("ROKU" + s.get(0) + "_activeapp",app)
        postUpdate("ROKU" + s.get(0) + "_screensaver",saver)
        if ( app == "Roku" && saver != "" ) {
                postUpdate("ROKU" + s.get(0) + "_status",saver)
        } else if ( app == "Roku" ) {
                postUpdate("ROKU" + s.get(0) + "_status","Home Screen")
        } else {
                postUpdate("ROKU" + s.get(0) + "_status",app)
        }

        ]

end

rule "ROKU Remote Control"
when
    Item ROKU_navigate received command
then
        var String command = ""

        if ( ( receivedCommand == "DEVINFO" ) || ( receivedCommand == "APPINFO" ) ) {
                sendCommand("ROKU_" + receivedCommand.toString.toLowerCase,"ON")
        } else if ( receivedCommand.toString.split("_").get(1).toString.isNumeric() ) {
                command = command + "launch/" + receivedCommand.toString.split("_").get(1)
        } else {
                command = command + "keypress/" + receivedCommand.toString.split("_").get(1).toString.toLowerCase
        }

        if ( receivedCommand != "APPINFO" && receivedCommand != "DEVINFO" ) {
                command = sendHttpGetRequest("http://127.0.0.1:8080/rest/items/ROKU" + receivedCommand.toString.split("_").get(0) + "_url/state") + command
                sendHttpPostRequest(command,5000)
                createTimer(now.plusSeconds(1), [| ROKU_appinfo.sendCommand(ON)])
        }

        ROKU_navigate.postUpdate("EMPTY")
end

rule "Roku Keyboard"
when
    Item ROKU_keyboard received command
then
        sendHttpPostRequest(sendHttpGetRequest("http://127.0.0.1:8080/rest/items/ROKU" + receivedCommand.toString.split("_").get(0) + "_url/state") + "keypress/Lit_" + receivedCommand.toString.split("_").get(1))
        ROKU_keyboard.postUpdate("EMPTY")
end

sitemaps/mobile.sitemap:
                        Frame label="Roku" icon="house" {
                                Text item=ROKU<SERIALNUM>_status label=""
                                Switch item=ROKU_navigate label="" mappings=['<SERIALNUM>_BACK'="Back",'<SERIALNUM>_HOME'="Home",'<SERIALNUM>_INFO'="Info"]
                                Switch item=ROKU_navigate label="" mappings=['NULL'="--",'<SERIALNUM>_UP'="▲",'NULL'="--"]
                                Switch item=ROKU_navigate label="" mappings=['<SERIALNUM>_LEFT'="◄",'<SERIALNUM>_SELECT'="◆",'<SERIALNUM>_RIGHT'="►"]
                                Switch item=ROKU_navigate label="" mappings=['NULL'="--",'<SERIALNUM>_DOWN'="▼",'NULL'="--"]
                                Switch item=ROKU_navigate label="" mappings=['<SERIALNUM>_REV'="◄◄",'<SERIALNUM>_PLAYPAUSE'="►||",'<SERIALNUM>_FWD'="►►"]
                                Switch item=ROKU_navigate label="" mappings=['<SERIALNUM>_INSTANTREPLAY'="Replay",'<SERIALNUM>_SEARCH'="Search"]
                                Switch item=ROKU_navigate label="" mappings=['<SERIALNUM>_BACKSPACE'="Backspace",'<SERIALNUM>_ENTER'="Enter"]
                                Text label="Roku Apps" icon="office" {
                                        Switch item=ROKU_navigate label="" icon="140474" mappings=['<SERIALNUM>_140474'="DirecTV Now"]
                                        Switch item=ROKU_navigate label="" icon="32828" mappings=['<SERIALNUM>_32828'="DisneyNow"]
                                        Switch item=ROKU_navigate label="" icon="12" mappings=['<SERIALNUM>_12'="Netflix"]
                                        Switch item=ROKU_navigate label="" icon="13842" mappings=['<SERIALNUM>_13842'="VUDU"]
                                        Switch item=ROKU_navigate label="" icon="186362" mappings=['<SERIALNUM>_186362'="Movies Anywhere"]
                                        Switch item=ROKU_navigate label="" icon="837" mappings=['<SERIALNUM>_837'="YouTube"]
                                        Switch item=ROKU_navigate label="" icon="13535" mappings=['<SERIALNUM>_13535'="PLEX"]
                                        Switch item=ROKU_navigate label="" icon="34376" mappings=['<SERIALNUM>_34376'="WatchESPN"]
                                        Switch item=ROKU_navigate label="" icon="13" mappings=['<SERIALNUM>_13'="Amazon Video"]
                                }
                                Text label="Roku Keyboard" icon="office" {
                                        Switch item=ROKU_keyboard_capslock label="Caps Lock" icon="" mappings=['ON'="ON",'OFF'="OFF"]
                                        Switch item=ROKU_keyboard label="" icon="" mappings=['<SERIALNUM>_A'="A",'<SERIALNUM>_B'="B",'<SERIALNUM>_C'="C",'<SERIALNUM>_D'="D",'<SERIALNUM>_E'="E",'<SERIALNUM>_F'="F"] visibility=[ROKU_keyboard_capslock==ON]
                                        Switch item=ROKU_keyboard label="" icon="" mappings=['<SERIALNUM>_G'="G",'<SERIALNUM>_H'="H",'<SERIALNUM>_I'="I",'<SERIALNUM>_J'="J",'<SERIALNUM>_K'="K",'<SERIALNUM>_L'="L"] visibility=[ROKU_keyboard_capslock==ON]
                                        Switch item=ROKU_keyboard label="" icon="" mappings=['<SERIALNUM>_M'="M",'<SERIALNUM>_N'="N",'<SERIALNUM>_O'="O",'<SERIALNUM>_P'="P",'<SERIALNUM>_Q'="Q",'<SERIALNUM>_R'="R"] visibility=[ROKU_keyboard_capslock==ON]
                                        Switch item=ROKU_keyboard label="" icon="" mappings=['<SERIALNUM>_S'="S",'<SERIALNUM>_T'="T",'<SERIALNUM>_U'="U",'<SERIALNUM>_V'="V",'<SERIALNUM>_W'="W",'<SERIALNUM>_X'="X"] visibility=[ROKU_keyboard_capslock==ON]
                                        Switch item=ROKU_keyboard label="" icon="" mappings=['<SERIALNUM>_Y'="Y",'<SERIALNUM>_Z'="Z",'<SERIALNUM>_!'="!",'<SERIALNUM>_@'="@",'<SERIALNUM>_#'="#",'<SERIALNUM>_$'="$"] visibility=[ROKU_keyboard_capslock==ON]
                                        Switch item=ROKU_keyboard label="" icon="" mappings=['<SERIALNUM>_%'="%",'<SERIALNUM>_^'="^",'<SERIALNUM>_&'="&",'<SERIALNUM>_*'="*",'<SERIALNUM>_('="(",'<SERIALNUM>_)'=")"] visibility=[ROKU_keyboard_capslock==ON]
                                        Switch item=ROKU_keyboard label="" icon="" mappings=['<SERIALNUM>_a'="a",'<SERIALNUM>_b'="b",'<SERIALNUM>_c'="c",'<SERIALNUM>_d'="d",'<SERIALNUM>_e'="e",'<SERIALNUM>_f'="f"] visibility=[ROKU_keyboard_capslock==OFF]
                                        Switch item=ROKU_keyboard label="" icon="" mappings=['<SERIALNUM>_g'="g",'<SERIALNUM>_h'="h",'<SERIALNUM>_i'="i",'<SERIALNUM>_j'="j",'<SERIALNUM>_k'="k",'<SERIALNUM>_l'="l"] visibility=[ROKU_keyboard_capslock==OFF]
                                        Switch item=ROKU_keyboard label="" icon="" mappings=['<SERIALNUM>_m'="m",'<SERIALNUM>_n'="n",'<SERIALNUM>_o'="o",'<SERIALNUM>_p'="p",'<SERIALNUM>_q'="q",'<SERIALNUM>_r'="r"] visibility=[ROKU_keyboard_capslock==OFF]
                                        Switch item=ROKU_keyboard label="" icon="" mappings=['<SERIALNUM>_s'="s",'<SERIALNUM>_t'="t",'<SERIALNUM>_u'="u",'<SERIALNUM>_v'="v",'<SERIALNUM>_w'="w",'<SERIALNUM>_x'="x"] visibility=[ROKU_keyboard_capslock==OFF]
                                        Switch item=ROKU_keyboard label="" icon="" mappings=['<SERIALNUM>_y'="y",'<SERIALNUM>_z'="z",'<SERIALNUM>_1'="1",'<SERIALNUM>_2'="2",'<SERIALNUM>_3'="3",'<SERIALNUM>_4'="4"] visibility=[ROKU_keyboard_capslock==OFF]
                                        Switch item=ROKU_keyboard label="" icon="" mappings=['<SERIALNUM>_5'="5",'<SERIALNUM>_6'="6",'<SERIALNUM>_7'="7",'<SERIALNUM>_8'="8",'<SERIALNUM>_9'="9",'<SERIALNUM>_0'="0"] visibility=[ROKU_keyboard_capslock==OFF]
                                        Switch item=ROKU_navigate label="" icon="" mappings=['<SERIALNUM>_BACKSPACE'="Backspace",'<SERIALNUM>_ENTER'="Enter"]
                                }
                                Text label="Device Info" icon="office" {
                                        Switch item=ROKU_navigate label="" mappings=['DEVINFO'="DEVINFO",'APPINFO'="APPINFO"]
                                        Text item=ROKU<SERIALNUM>_url
                                        Text item=ROKU<SERIALNUM>_udn
                                        Text item=ROKU<SERIALNUM>_serialnumber
                                        Text item=ROKU<SERIALNUM>_deviceid
                                        Text item=ROKU<SERIALNUM>_vendorname
                                        Text item=ROKU<SERIALNUM>_modelname
                                        Text item=ROKU<SERIALNUM>_modelnumber
                                        Text item=ROKU<SERIALNUM>_supportsethernet
                                        Text item=ROKU<SERIALNUM>_wifimac
                                        Text item=ROKU<SERIALNUM>_ethernetmac
                                        Text item=ROKU<SERIALNUM>_networktype
                                        Text item=ROKU<SERIALNUM>_userdevicename
                                        Text item=ROKU<SERIALNUM>_softwareversion
                                        Text item=ROKU<SERIALNUM>_softwarebuild
                                        Text item=ROKU<SERIALNUM>_securedevice
                                        Text item=ROKU<SERIALNUM>_powermode
                                        Text item=ROKU<SERIALNUM>_activeapp
                                        Text item=ROKU<SERIALNUM>_screensaver
                                }
                        }


scripts/searchRokus.py:
#!/usr/bin/python

import sys
import socket
import re

ssdpRequest = "M-SEARCH * HTTP/1.1\r\n" + \
        "HOST: 239.255.255.250:1900\r\n" + \
        "Man: \"ssdp:discover\"\r\n" + \
        "MX: 5\r\n" + \
        "ST: roku:ecp\r\n\r\n";
socket.setdefaulttimeout(10)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
sock.sendto(ssdpRequest, ("239.255.255.250", 1900))
while True:
    try:
        resp = sock.recv(1024)
        #print(resp)
        #print("Matches")
        matchObj = re.match(r'.*USN: uuid:roku:ecp:([\w\d]{12}).*LOCATION: (http://.*/).*', resp, re.S)
        print (matchObj.group(1) + " " + matchObj.group(2))
    except socket.timeout:
        break

Enjoy!

6 Likes

Indeed. However, I wouldn’t use that as the sole reason why a binding shouldn’t be built. There are a few good reasons why a Roku binding, particularly a 2.x version binding would be a good fit with OH. These include

I really like the example and, as the original author of the wiki article linked to above, I’m very appreciative. I do have some recommendations which could make the rules a bit easier to manage (I’m guessing at least a 50% reduction in the number of lines).

I do have some suggestions that might work to make the above a little more flexible. Note that some of these suggestions may be incompatible with each other.

  • Use the script I linked to linked to an Exec binding Item or executeCommandLine to get the IP addresses of the Rokus and save them to the appropriate Items. If you configure your DHCP or the Roku to use a static IP feel free to skip this step.

  • Use Design Pattern: Associated Items to locate and populate the correct Item to populate with the information pulled from the XSLT

  • Use XPATH instead of the XSLT transform. That will eliminate the need to maintain a separate xslt file for every piece of data and is much easier to deal with. The line for deviceid would be replaced with
    ROKU<SERIALNUM>_deviceid.postUpdate(transform("XPATH","//device-id",deviceinfo))
    I only recommend XSLT when you need to do some more transformation of the data beyond simply extracting the value of an element or attribute

  • Use the HTTP cache configuration in place of the cron triggered rule. This could also replace the System started rule so long as you are using restoreOnStartup for those Items

  • I’m a big fan of DRY. Consider using a lambda for the DEVINFO case and call that lambda from your System started Rule and your DEVINFO case in the navigation Rule.

  • A Thread::sleep(1000) is probably getting close to too long. Consider using a Timer

  • To minimize the amount of find and replace one has to do and another application of DRY, consider making only one call to sendHttpPostRequest in your rules and use the switch statements to build the URL.

    val url = "http://" + ROKU<SERIALNUM>_ip.state + ":8060/keypress/"
    var key = ""
    switch(receivedCommand){
        case "BACK": key = "back"
        ...
    }
    if(key == ""){
        switch(receivedCommand){
            case "DEVINFO": {
                ...
            }
           ...
    }
    else {
        sendHttpPostRequest(url + key)
    }
   ...
  • In addition, your keyboard rule could be reduced to the following two lines
    sendHttpPostRequest("http://"+ROKU<SERIALNUM>_ip.state + ":8060/keypress/Lit_" + receivedCommand)
    ROKU<SERIALNUM>_keyboard.postUpdate("EMPTY")

The Same approach could be used to drastically reduce the length of the navigation rule using something like:

    val url = "http://" + ROKU<SERIALNUM>_ip.state + ":8060/keypress/"
    var key = ""

    switch(receivedCommand){
        case "DEVINFO": {
            ...
        }
        case "APPINFO": {
            ...
        }
        case "DTVNOW": key = "140474"
        ... // all other apps
        default: key = receivedCommand.toString.toLower
    }
   ...
  • For those who have more than one Roku, use lambdas for the Rules, do not copy paste the rules. DRY.

  • To get to more of an OH 2.x type experience, one could write these rules to use the OH REST API to automatically generate the Items for you. Unfortunately, this will not help much with the Sitemap, but it would make the .items easier to deal with and potentially the Rules as well. Though ultimately I do think a new binding would be the better way to go, and I’m usually the one arguing against new bindings.

Don’t get me wrong, what you posted is great work! Thanks for the contributions to the community.

Greatly appreciate the input. I’m still learning my way around all of the different facets of OH and how to make it dance how I want it. I’ll go back and see if I can optimize the script down the line. I was more worried about functionality than efficiency for the first go. I’m sure I have plenty of scripts that can be tuned up down the line.

The next hurdle I was hoping to concur was in-app control. For example, I have a Vudu library to watch some movies. I want to be able to send a command that says “play movie XYZ” instead of just “start Vudu”. Same holds true with services like DirecTV Now. I want to be able to say “put on ESPN” instead of just starting the app. The button presses are not consistent or intuitive where I can create some kind of macro. Any thoughts?

I think it is going to depend on what sorts of APIs those apps may support. You will have to see what sorts of Deep Linking the individual apps support and pass the necessary information. Beyond that, I think you are stuck with some sort of macro implemented in a Rule which, of course, will work until the app decides to move things around on you.

I’ve updated the scripts above to a “version 2.0”. First, this is just where I’m at now, still more to go.

What I’ve changed:

  • XSLT replaced by XPATH to remove need for .xsl files.
  • Keyboard rule substantially reduced in length
  • Navigation rule substantially reduced in length and complexity
  • New Switch items created for DEVINFO and APPINFO. Rules adjusted to reduce duplication of code.

On my drawing board as to-do for “version 3.0”:

  • Dynamic detection of device IPs
  • Adjust calling of applications (e.g. launch/app-id) so that it can come straight from the sitemap. This makes it so that you only have to add the application to the sitemap and not have to mangle with the rule also.
  • Fix the duplication-of-files for each Roku issue

Other things:

  • I’m back and forth on HTTP cache. I can see the benefit here however we have to update the XML files so often that I don’t know if the effort is worth the return. Not saying no, just not going to put it at the top of the list.
  • I contemplated the Thread::sleep for a while. The intent of it is to give the Roku time to start a launched application so that it comes in when APPINFO is run. What I really need to do is reduce the number of times that this is fired off by excluding regular button presses from this. The only problem is that buttons like ENTER and SELECT can start apps which we would want to know about so I may have to make exclusions for those.
  • I still want to figure out how to script out certain actions inside of apps. I’ve looked at Deep Linking and unfortunately the apps that I want to do this on (primarily VUDU, DirecTV NOW, MA, Disney Now) don’t support it. On my TV, I can easily create a rule to IR blast the channel number for the channel I’m lookig for. I want to make it so I can hit a button on OH and it starts DTVNOW and (for example) tunes to ESPN or Disney.
  • I’m a big fan of the idea of using the OH REST API to manage the items file. I’m trying to figure out if I can build this where all of the items are created when OH starts. The process would be something like:
  1. OH Starts - Runs SSDP search for all Roku devices on the network.
  2. Create IP items for each identified Roku
  3. Run DEVINFO for each identified Roku. Create all necessary info bindings at time of first execution.
  4. Ditto above for APPINFO

As always, looking for input. Thanks!

Once you go through all the trouble of implementing all of the above in your plans for version 3.0 it really seems to me this may as well be a binding. then you can submit it and have it available as part of the distro or at least listed in the IoT Marketplace.

As long as you are using a polling rule then I don’t see the problem. It essentially does exactly the same thing as your APPINFO rule only you don’t have to code it yourself. The frequency of the polling is irrelevant.

I know what the purpose of the Thread::sleep is for. But the fact remains that long sleeps will cause problems down the road. When you sleep you tie up a rule’s runtime thread meaning there are fewer threads available to execute other rules. By using createTimer(now.plusSeconds(1), [| ROKU<SERIALNUM>_appinfo.sendCommand(ON)] you get the exact same behavior as your Thread::sleep but you don’t tie up a rule’s execution thread for that second. Even if it is run only infrequently any sleep longer than a few hundred milliseconds is too long.

So even when you reduce the number of times it is fired off, you still need to avoid the sleep.

Without deep linking, there really isn’t much you can do that will work permanently. What you can do is try to issue some blind commands to get to a known state on the screen (e.g. launch the app and send the ‘*’ key or something) and then just issue the sequence of remote keys necessary to get to the content you are after. If you can do a search for it so much the better.

This would be a pretty non-standard way to do an integration like this. Because Items are where the User develops their model for how their home automation works, it is usually a place that bindings and other third-party integrations like this use a hands-off attitude.

And again, I will suggest that if you are wanting to go that far you may as well implement this as a binding and have your binding automatically create Things based on discovery.

In a binding, all your DEVINFO and APPINFO stuff would become part of the binding. The binding would automatically discover the Rokus on your network and drop all the relevant Things into the Inbox. The user accepts those Things desired and links the appropriate channels to their Items.

This in particular troubles me. What if the Items already exist?

What if the Roku’s IP address lease is up and it changes IP address between OH restarts?

I don’t want to discourage you and your progress, but the more features you think about adding to this capability the more it really sounds like a new binding is the appropriate approach. Plus you will reach a far wider audience.

I’ve updated to version 3.0. I’ve made several major changes:

  • There is now no need to have multiple files. There is only a single items and rules file. This covers all devices.
  • Dynamic device discovery is now added (credit where credit is due to rlkoshak for the python and some of the rule syntax). This will find every Roku on the network. You will have to manually create the necessary items prior to discovery working. This will run every hour to find devices that may have changed IPs. This can be manually run at any time through the ROKU_devinfo switch.
  • There are now “global” items that cover all Roku devices. Those must exist for everything to work properly. Create new device specific items for each Roku on the network.
  • The rules file does not need to be modified when adding new devices or functions. The syntax is all in the sitemap. If you send _HOME for example, it will push the home button on the Roku matching that serial number. If you send _APPID for example, it will start the app matching that ID number on the Roku matching that serial number.
  • Thread::sleep has been replaced by a timer.
  • HTTP post for app start has been throwing errors due to HTTP timeout. I’ve increased the wait to 5 seconds.
  • The OH2 REST API is called to identify the stored URL for the ROKU in question. I want to research a better way to do this at some point. I feel like there should be a command like getItemByName(String) that would achieve the same goal.
  • To add a new Roku to the system, simply create items for the serial number and let auto discovery populate them.

As far as making this into a binding, given the reduction in complexity I’m not sure if we really have to do this. Plus, while it may sound backwards, I believe that making a true binding would prohibit beginner users from making changes as they may not understand the process of compiling the binding. I do however see the point of having it integrated into an easily installable package. I think this is probably a long term goal.

My next step is to try and convert the python script to Java so it can be simply in the rules file.

I’m trying to figure out if there is a way to dynamically create the items when a new Roku is detected. In theory, with a static set of global items, the system could create the items necessary based on a predefined style. Then the user would just need to make changes to the sitemap to have the device added. Not sure how to do this yet, just something I’m kicking around still.

Put the Items you want to search through into a Group (let’s call it gRokuURLs):

Group:String gRokuURLs

Then in your rule to get a handle on your Item:

val rokuUrlItem = gRokuURLs.members.findFirst[url | url.name == "ROKU" + receivedCommand.toString.split("_").get(0) + "_url"]

if(rokuUrlItem == null) // log error, fail fast with a return false;

val rokuURL = rokuUrlItem.state.toString

The way users are trained to use openHAB they would be much more comfortable dealing with Things and Channels than they will be deploying copy and pasted Rules and can creating Items in a .items file. This is already a problem with a lot of users and it will only become more so as PaperUI becomes more and more mature and the need to use the text configs become less and less important.

It will also future proof the work you have done as the Experimental Rules Engine matures and slowly starts to replace the current Rules DSL as the “default” rules engine. This is probably a year or more away but it is something to consider.

The point is that this binding would become part of the official OH. Or at a minimum they would be distributed on the IoT Marketplace. It would be listed in PaperUI under addons and installation would be just a matter of clicking on it and installing it. No one is asking beginner users to compile anything.

But there will be no need for beginner users to make changes. Your binding would essentially expose all the Roku API as Things and Channels. There will be no need for users to change anything since the access to the API would be complete. And there would be no need to users to really do anything except create Items for those Channels they want to use from the Inbox, like they would do for any other 2.x binding.

I just want to be clear to future readers. Rules are not written in Java. They have access to Java but they are not Java. They are written in a Domain Specific Language based on the Xtext language.

I looked into that. There is a reason it is a Python script. :wink:

You can create Items using the OH REST API. I would recommend against it but it is possible.

Alright… I conceed to making this into a formal binding. I’ll start to read up on the process as I’ve never tried that before. Can I pull the code from the rules in easily or is it a complete rewrite?

As far as the python/Java discussion, what was the lessons learned that brought you to using python?

Sadly it is going to be a complete rewrite. There is very little in common between rules are written and a binding would be written. You figured out how to do the hard part. You just have to figure out how to do it in Java in the construct of an OH binding. There is decent documentation in the docs and on the ESH docs. Look for the development tutorial.

It was just going to take a whole lot more code and using libraries that I was not familiar with. Java is a good language for a lot of things but if you want to hack together some let level networking scripts quickly Python is awesome.

At least this was a good learning experience then for the logical flow of how it should work. I used to be decent with Java, just haven’t done it in a few years. I’ll start to play with it. Given that the rule wasn’t really that complex I should be able to make the binding relatively easy.

1 Like

As planning is a good tenant of coding, here are my thoughts on how to develop the things/channels/items/etc:

  • Things should be based on the serial number primarily and rely on SSDP. There should be an optional IP element that can bypass the detection process for networks that block multicast traffic or for networks with multiple subnets. This flows with the OH2 PaperUI path as far as I can tell (Things would just show up in the Inbox).
  • Channels should be bound to the Roku REST API and not individual keypresses. For example, there should be a channel for “keypress” which is a String. Similarly there should be a channel for “launch” which could be a String or a Number. Ditto for the other 8 or so “commands” that the API has. I don’t see a reason to make a channel for things like the home button as they can be sent as a string.
  • While I would like to avoid it, I’m not sure how to otherwise pass the XML data back without creating a channel for each XML item that we can get back (e.g. serial number, current app, screen saver, etc). There are currently 5 XML files that can be pulled from a Roku (including RokuTV) that contain relevant data to the end users.
  • I will probably include a RAW channel for people to use for any number of uses (deep linking comes to mind). Effectively this will just send any data that is sent to the RAW channel as an HTTP Post to the Roku API. This also somewhat future proofs the binding as new API commands could be immediately implemented prior to the binding being updated.

My first step is to build a development OH instance so that I don’t break my working system while doing development. That will take some time to gather resources so please do not think that this will be something that I will commit to the repository for at least a few months. In the interim, I believe the rules/items/sitemap/python above can act as a viable control mechanism.

This sounds reasonable. Keep in mind my expertise is in using OH and helping others to use OH so my perspective is going to be focused on usability and consistency with existing bindings. Pay attention to the binding docs and other bindings for a developer’s perspective.

Theoretically, all bindings should be able to support auto discovered Things as well as manually defined Things in .things files. In practice, this is not always practical (see the zwave binding). In this case, I think supporting both will primarily be just a case of documenting how to write a Thing in the binding’s readme. I suspect that the Thing, whether automatically discovered or manually defined in a .things file will include an IP field though as you get into development you will learn more.

My essential point though is that there really is no practical difference between a Thing defined in a .things file and one automatically discovered as far as the binding is concerned.

This sounds reasonable as well. I can see arguments in both directions though.

For example, don’t expose the keypress APIs at all and instead make channels for the important buttons (e.g. Home, *, and OK buttons have a Switch Channel, Play/Pause/Stop/Reverse/Fast Forward links to a Player Channel, everything else linked to a String Channel that sends the text sent to it however appropriate).

From a developer’s perspective who is knowledgeable about the Roku API, just having one String Channel makes a lot of sense. But from a user’s perspective who does not necessarily know the Roku API, it is more intuitive to present a few more channels so from their perspective the channels map a little more closely to the Roku remote. You don’t have to have a channel per button on the remote, particularly given Player Item type, but IMHO just having a String Channel for all requires too much knowledge of the Roku API on the user’s part. This will also make the error handling a little easier on you and the users because they can’t mess up these important buttons.

I would consider representing the Roku apps as auto discovered Things with a Switch Channel to go to it and if you figure out any deep linking stuff separate channels for doing that.

From the user’s perspective, the closest you can come to making the channels look like what they are familiar with (i.e. the physical remote) the more intuitive it will be to use it.

Don’t worry too much about having too many channels. Just look at the number of channels on bindings like the Wunderground and new Nest 2.x bindings.

I don’t know the full XML that can come back but I see no problem with exposing the raw XML in a String Channel. However, if the XML coming back are listing things like what Roku apps are installed, why not handle that internally and convert the XML into Things in the inbox? Where it makes sense, convert the XML into something that the binding can do.

Given the user needs to input the serial number manually anyway, or it is auto discovered, I see no need to have a channel for the serial number. I absolutely think there should be a channel for current app. You want that to be highly visible and easy to access and use.

This sounds like a good idea and it might be a good thing to focus on first.

I wouldn’t expect it any sooner. And I agree, the link above is a great approach in the interum.

Just to post an update, I’ve been having some issues with SSDP on the new StreamingStick+. For no reason that I can find, they do not reply to SSDP requests in a timely manner reliably. Sometimes they reply in 2 seconds, sometimes its 20, and sometimes they don’t reply at all. I do not see this behavior on the “regular ones”. I’ve had to switch to a mechanism that I pre-specify the serial and IP into the ROKU_ssdp string above and then verify if the system is functioning via ping. It’s not as elegant and I’m not happy with it, but it seems to work fine when going to that.

1 Like

Hey I just wanted to say thank you for your great work on adding Roku support to openHAB! Also a friendly public service announcement about your searchRokus.py python script. There is currently a bug where if you have anything else on your network that responds to SSDP (Simple Service Discovery Protocol) like for example a Phillips Hue emulator, then it will also end up in the response list and cause a crash at the print for matchObj (since the regex does not find a match and matchObj is effectively null. Here is my workaround in the try block.

        if matchObj is not None:
            print (matchObj.group(1) + " " + matchObj.group(2))

Thanks again and great work!

Hi morph166955, great work so far on a great development and guide. Is there still talk on progressing this into a submitted/official binding?

So the answer is a loose yes. I’ve been trying to perfect the rules prior to writing the binding code and I’m finding some of the newer Rokus are a bit more flaky than the old one I had been doing my testing on. Mainly, the wifi versions vs the wired versions. The wifi versions seem to be inconsistent on replies. I believe there is some kind of low power mode or idling after the Roku has been untouched for a while. Things like SSDP become very inconsistent. This is causing my logic to think that a Roku has fallen off the network and not be queried. That being said, they respond to commands even though they aren’t sending their SSDP messages. Once I crack that nut, then it’s binding time.

From a practical perspective, how important is the SSDP to the binding. Clearly, it would be needed for the initial discovery of it but that can be handled by putting in the instructions to interact with the Roku before running the scan from the Inbox. It can also be used to determine whether the Roku is online or not, but we can probably live without that feature or use the arping feature of the Network binding for that.

So what if you just ignored the case where there isn’t an SSDP response for the given Roku. Should it wake up and get a new IP address from DHCP, well, it will respond with an SSDP at that point or at some point thereafter with the new IP address.

Since the Roku responds to commands even when not responding to SSDP I think it would be better to write the binding so it doesn’t wholly depend on it and focus on the functional parts. In short, I wouldn’t wait until you crack that nut, set it aside and do what does work and come back to it later. You can have a fully functional binding without it.

The DHCP issue is my concern here. Based on the logs from my wireless AP, they disconnect and reconnect frequently. I’ve already verified, signal is not an issue and all of them do it. Most systems will reply with the same IP address from the DHCP server if it’s been under 24 hours. However, most is not all. Also, for things like the streaming sticks, some Rokus are (optionally) powered by the USB port on the TV so there can be instances where a stick is actually offline because it is powered off. This could last longer than the 24 hours and cause an IP change. I suppose we could use SSDP just for initial discovery and then do some kind of other health check on the Roku.

Long story short, I wanted to get it solid before starting anything. To that end, with the exception of the SSDP problem, the rules based implementation has completely met my needs on this. I have 4 Rokus working perfectly. I’m still on the fence as to if this needs a formal binding given that the implementation here is effectively copy/paste. The real only reason here is to make it a little easier to click a button and have it work out of the box.

Perhaps combine the SSDP with a ping. Use SSDP to get the IP address but use a ping to determine if it is online/offline. Depending on how deep of a sleep it goes into it might need to use arping.

If the device goes offline, start up a periodic SSDP so you can catch it when the Roku comes back online and learn the new IP address.

Just document in the README that users should configure the Roku or the router to serve out a static IP address where possible for best results and then work on the SSDP problem as time permits later.

I am absolutely not on the fence. This should be a binding. See the iCloud binding for a similar “copy and paste” rule that became a new binding. The length and complexity of this rule will scare off a lot of users from even trying it and because this post is buried in the forums, the majority of users will never know that Roku support is even possible. It needs to be a binding.