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!