I created a basic HTML remote control to run on my Kindle Voyager eBook that triggers Hassio events via the RESTful integration API to control my Harmony Entertainment Hub.
Using a PI-4B device with Ubuntu Server, Dockers, and Hassio installed on it:
With a logitec Harmony Hub configured for my entertainment center:
And my Kindle Voyage eBook reader:
HTML Remote I destined for Kindle’s Web Browser:
Every button pressed triggers a “keyboard_remote_command_received”. Examle:
Event 8 fired 9:42 AM:
{
"event_type": "keyboard_remote_command_received",
"data": {
"key_code": 34,
"device_descriptor": "/dev/input/by-id/usb-flirc.tv_flirc-if01-event-kbd",
"device_name": "httpRemote"
},
"origin": "REMOTE",
"time_fired": "2019-11-25T17:42:54.714444+00:00",
"context": {
"id": "24f01d7c46684555a274f66990675af9",
"parent_id": null,
"user_id": "a92e7d11030d427a8462350d33be11cc"
}
}
HTML Code:
located in hassio file: hassio/homeassitant/www/httpRemotes.htm
url: http://192.168.0.140:8123/local/httpRemotes.htm
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Universal Http Remote</title>
<script language="javascript" type="text/javascript">
//##############################################
//### Use javascript's XMLHttpRequest Object
//### To Trigger HomeAssistant Events
//### Via the RESTful Integration API
//##############################################
function sendKeyCode(event) {
var xhttp = new XMLHttpRequest();
var keyElement = event.currentTarget;
var keyCode = keyElement.attributes['keyCode'].nodeValue;
var location = document.getElementById("location").value;
//alert("sendKeyCode: " + keyCode);
xhttp.onreadystatechange = function() {
if (this.readyState == 4) {
if(this.status == 200) {
//alert('<h2>Success, response: '+this.responseText+'</h2>');
} else {
alert('<h2>Failed, status: '+this.status+'</h2>');
}
keyElement.removeAttribute("sendingKeyCode");
}
};
xhttp.open("POST", "http://192.168.0.140:8123/api/events/keyboard_remote_command_received");
xhttp.setRequestHeader("Content-Type", "application/json");
xhttp.setRequestHeader("Authorization", "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiI4NzgzMWMwMGE0NGU0MGFkOWY3MThlNzhmNjg3MGJjZSIsImlhdCI6MTU3NDE4MTYxNywiZXhwIjoxODg5NTQxNjE3fQ.wzOT8BaZrN_Z38T_2RoJpT9sjtg5pelxIQaWJafUmgI");
xhttp.send('{"key_code": '+keyCode+', "device_descriptor": "'+location+'", "device_name": "IpRemote"}');
}
function run(message)
{
var buttons = document.getElementsByTagName("td");
for(button in buttons){
if(buttons[button].attributes == undefined) continue;
if(buttons[button].attributes.keyCode == undefined) continue;
buttons[button].addEventListener("click", function(event){
//alert("Hello World: ");
sendKeyCode(event);
});
};
}
//################
//### MAIN
//################
window.addEventListener("load", run, false);
</script>
<style>
body {
}
#power {
font-size: 120%;
display: table-cell;
vertical-align: middle;
}
table#buttons {
width: 1060px;
height: 1180px;
border-top: 40px solid #3366FF;
border-bottom: 40px solid #3366FF;
border-left: 40px solid #3366FF;
border-right: 40px solid #3366FF;
border-radius: 3em;
border-collapse: separate;
border-spacing: 20px 20px;"
//margin-left: 10%;
}
col {
span: 12;
}
select {
text-align-last: center;
font-size: 140%;
}
tr {
//height: 20px;
//padding-top: 10px;
}
td.line {
height: 10px;
background-color: #3366FF;
}
td.list {
height: 80px;
font-size: 160%;
color: #FFFFFF;
background-color: #3366FF;
}
td.button {
font-size: 160%;
width: 8.2%;
//margin: 30px;
border: 1px dashed black;
border-radius: 10em;
//background-color: yellow;
}
td.button:hover, td.button:focus, td.button:active {
background-color: grey;
}
td[keyCode]{
cursor: pointer;
}
td[sendingKeyCode]{
border: 10px solid black;
}
</style
</head>
<body>
<table id="buttons" class="buttons">
<colgroup>
<col class="button">
</colgroup>
<tbody>
<tr>
<td class="button" keyCode="60" colspan="2"><center><b>PWR</b></center></td>
<td class="list" colspan="8"><center><b>HTTP Remote Location</b></center>
<center><b><select id="location">
<option value="/dev/input/by-id/usb-flirc.tv_flirc-if01-event-kbd">Bedroom</option>
<option value="/dev/input/by-id/usb-flirc.tv_flirc-event-kbd">Living Room</option>
</select></b></center>
</td>
<td class="button" keyCode="27" colspan="2"><center><b>INPUT</b></center></td>
</tr>
<tr>
<td class="button" colspan="3"><center><b>|<<</b></center></td>
<td class="button" keyCode="57" colspan="3"><center><b>REC</b></center></td>
<td class="button" keyCode="58" colspan="3"><center><b>STOP</b></center></td>
<td class="button" colspan="3"><center><b>>>|</b></center></td>
</tr>
<tr>
<td class="button" keyCode="50" colspan="3"><center><b><<</b></center></td>
<td class="button" keyCode="51" colspan="3"><center><b><></b></center></td>
<td class="button" keyCode="52" colspan="3"><center><b>||</b></center></td>
<td class="button" keyCode="53" colspan="3"><center><b>>></b></center></td>
</tr>
<tr><td colspan="12" class="line"></td></tr>
<tr>
<td class="button" keyCode="40" colspan="4"><center><b>+</b></center></td>
<td class="button" keyCode="35" colspan="4"><center><b>LIST</b></center></td>
<td class="button" keyCode="44" colspan="4"><center><b>^</b></center></td>
</tr>
<tr>
<td class="button" colspan="4"><center><b>VOL</b></center></td>
<td class="button" keyCode="37" colspan="4"><center><b>GUIDE</b></center></td>
<td class="button" colspan="4"><center><b>CHAN</b></center></td>
</tr>
<tr>
<td class="button" keyCode="41" colspan="4"><center><b>-</b></center></td>
<td class="button" keyCode="36" colspan="4"><center><b>MENU</b></center></td>
<td class="button" keyCode="45" colspan="4"><center><b>V</b></center></td>
</tr>
<tr><td colspan="12" class="line"></td></tr>
<tr>
<td colspan="4"><center><b> </b></center></td>
<td class="button" keyCode="30" colspan="4"><center><b>^</b></center></td>
<td colspan="4"><center><b> </b></center></td>
</tr>
<tr>
<td colspan="2"><center><b> </b></center></td>
<td class="button" keyCode="32" colspan="2"><center><b><</b></center></td>
<td class="button" keyCode="34" colspan="4"><center><b>OK</b></center></td>
<td class="button" keyCode="33" colspan="2"><center><b>></b></center></td>
<td colspan="2"><center><b> </b></center></td>
</tr>
<tr>
<td keyCode="35" colspan="4"><center><b> </b></center></td>
<td class="button" keyCode="31" colspan="4"><center><b>V</b></center></td>
<td keyCode="35" colspan="4"><center><b> </b></center></td>
</tr>
<tr><td colspan="12" class="line"></td></tr>
<tr>
<td class="button" keyCode="38" colspan="3"><center><b>EXIT</b></center></td>
<td class="button" keyCode="46" colspan="3"><center><b><< CHAN</b></center></td>
<td class="button" keyCode="59" colspan="3"><center><b>DELETE</b></center></td>
<td class="button" keyCode="43" colspan="3"><center><b>MUTE</b></center></td>
</tr>
<tr>
<td class="button" keyCode="10" colspan="3"><center><b>0</b></center></td>
<td class="button" keyCode="1" colspan="3"><center><b>1</b></center></td>
<td class="button" keyCode="2" colspan="3"><center><b>2</b></center></td>
<td class="button" keyCode="3" colspan="3"><center><b>3</b></center></td>
</tr>
<tr>
<td class="button" keyCode="4" colspan="3"><center><b>4</b></center></td>
<td class="button" keyCode="5" colspan="3"><center><b>5</b></center></td>
<td class="button" keyCode="6" colspan="3"><center><b>6</b></center></td>
<td class="button" keyCode="7" colspan="3"><center><b>7</b></center></td>
</tr>
<tr>
<td class="button" keyCode="8" colspan="3"><center><b>8</b></center></td>
<td class="button" keyCode="9" colspan="3"><center><b>9</b></center></td>
<td class="button" keyCode="11" colspan="3"><center><b>.</b></center></td>
<td class="button" keyCode="12" colspan="3"><center><b>Enter</b></center></td>
</tr>
<tr>
<td class="button" keyCode="20" colspan="3"><center><b>YELLOW</b></center></td>
<td class="button" keyCode="21" colspan="3"><center><b>BLUE</b></center></td>
<td class="button" keyCode="22" colspan="3"><center><b>RED</b></center></td>
<td class="button" keyCode="23" colspan="3"><center><b>GREEN</b></center></td>
</tr>
</tbody>
</table>
</body>
</html>
To process the events triggered by my httpRemote, I reuse a script using the “Python Script” I wrote to handle my Flirc IR remote device events. How I control my Harmony Hub with an $8 remote
"automations.yaml" snippet":
- alias: Remote Key Pressed
id: '1571938694985'
description: ''
trigger:
- event_data: {}
event_type: keyboard_remote_command_received
platform: event
condition: []
action:
- service: python_script.control_hub
data_template:
key_code: '{{ trigger.event.data.key_code }}'
key_source: '{{ trigger.event.data.device_descriptor }}'
current_activity: "{{ state_attr('remote.hub_3911_master_bedroom', 'current_activity') }}"
log_level: 'warn'
Python script code for service: python_script.control_hub
'''
Get Harmony Hub Command
'''
########################
# Parameters
########################
entity = data.get('entity_id', "remote.hub_3911_master_bedroom")
activity = data.get('current_activity')
code = data.get('key_code')
source = data.get('key_source')
loglevel = data.get('log_level', 'warn')
indent=" "
logs = {'warn': logger.warn}
logger = logs[loglevel]
########################
# keyMap for Flirc 'keyboard_remote_command_received' event key_codes
########################
keyMap = {
### GROUP1 ###
"60": "tv|o",
"61": "bluray|o",
"62": "aux|o",
"63": "cable|o",
"64": "stream|o",
"65": "audio|o",
### GROUP2 ###
"20": "yel",
"21": "blu",
"22": "red",
"23": "grn",
"24": "app1",
"25": "app2",
"26": "app3",
"27": "input",
"28": "enter",
### GROUP3 ###
"30": "up",
"31": "down",
"32": "left",
"33": "right",
"34": "ok",
"35": "list",
"36": "menu",
"37": "guide",
"38": "back",
### GROUP4 ###
"40": "vol+",
"41": "vol-",
"43": "vol><",
"44": "chan+",
"45": "chan-",
"46": "chan<",
### GROUP5 ###
"50": "<<",
"51": "<>",
"52": "><",
"53": ">>",
"57": "rec",
"58": "stop",
"59": "eject",
### GROUP6 ###
"1" : "1",
"2" : "2",
"3" : "3",
"4" : "4",
"5" : "5",
"6" : "6",
"7" : "7",
"8" : "8",
"9" : "9",
"10": "0",
"11": ".",
"12": "enter"
}
########################
# hubCommands
########################
hubCommands = {
'Bedroom': {
'Watch DirecTV': {
"vol+" : {"device": "Sonos PLAYBAR", "command": "VolumeUp"},
"vol-" : {"device": "Sonos PLAYBAR", "command": "VolumeDown"},
"vol><": {"device": "Sonos PLAYBAR", "command": "Mute"},
"<<" : {"device": "DirecTV Genie", "command": "Rewind"},
"<>" : {"device": "DirecTV Genie", "command": "Play"},
"><" : {"device": "DirecTV Genie", "command": "Pause"},
">>" : {"device": "DirecTV Genie", "command": "FastForward"},
"chan+": {"device": "DirecTV Genie", "command": "ChannelUp"},
"chan-": {"device": "DirecTV Genie", "command": "ChannelDown"},
"chan<": {"device": "DirecTV Genie", "command": "ChannelPrev"},
"up" : {"device": "DirecTV Genie", "command": "DirectionUp"},
"down" : {"device": "DirecTV Genie", "command": "DirectionDown"},
"left" : {"device": "DirecTV Genie", "command": "DirectionLeft"},
"right": {"device": "DirecTV Genie", "command": "DirectionRight"},
"ok" : {"device": "DirecTV Genie", "command": "Select"},
"list" : {"device": "DirecTV Genie", "command": "List"},
"menu" : {"device": "DirecTV Genie", "command": "Menu"},
"guide": {"device": "DirecTV Genie", "command": "Guide"},
"back" : {"device": "DirecTV Genie", "command": "Exit"},
"rec" : {"device": "DirecTV Genie", "command": "Record"},
"stop" : {"device": "DirecTV Genie", "command": "Stop"},
"eject": {"device": "DirecTV Genie", "command": "Red"},
"." : {"device": "DirecTV Genie", "command": "-"},
"enter": {"device": "DirecTV Genie", "command": "enter"},
"num" : {"device": "DirecTV Genie", "command": "0"}
},
'Watch Fire TV': {
"vol+" : {"device": "Sonos PLAYBAR", "command": "VolumeUp"},
"vol-" : {"device": "Sonos PLAYBAR", "command": "VolumeDown"},
"mute" : {"device": "Sonos PLAYBAR", "command": "Mute"},
"<<" : {"device": "Amazon Fire TV Stick", "command": "Rewind"},
"<>" : {"device": "Amazon Fire TV Stick", "command": "Play"},
"><" : {"device": "Amazon Fire TV Stick", "command": "Pause"},
">>" : {"device": "Amazon Fire TV Stick", "command": "FastForward"},
"rec" : {"device": "Amazon Fire TV Stick", "command": "Record"},
"stop" : {"device": "Amazon Fire TV Stick", "command": "Stop"},
"eject": {"device": "Amazon Fire TV Stick", "command": "Red"},
"up" : {"device": "Amazon Fire TV Stick", "command": "DirectionUp"},
"down" : {"device": "Amazon Fire TV Stick", "command": "DirectionDown"},
"left" : {"device": "Amazon Fire TV Stick", "command": "DirectionLeft"},
"right": {"device": "Amazon Fire TV Stick", "command": "DirectionRight"},
"ok" : {"device": "Amazon Fire TV Stick", "command": "OK"},
"list" : {"device": "Amazon Fire TV Stick", "command": "Home"},
"menu" : {"device": "Amazon Fire TV Stick", "command": "Menu"},
"back" : {"device": "Amazon Fire TV Stick", "command": "Back"}
},
'Watch Antenna TV': {
"vol+" : {"device": "Sonos PLAYBAR", "command": "VolumeUp"},
"vol-" : {"device": "Sonos PLAYBAR", "command": "VolumeDown"},
"mute" : {"device": "Sonos PLAYBAR", "command": "Mute"},
"chan+": {"device": "Vizio TV", "command": "ChannelUp"},
"chan-": {"device": "Vizio TV", "command": "ChannelDown"},
"." : {"device": "Vizio TV", "command": "-"},
"num" : {"device": "Vizio TV", "command": "0"}
},
'PowerOff': {
"vol+" : {"device": "Sonos PLAYBAR", "command": "VolumeUp"},
"vol-" : {"device": "Sonos PLAYBAR", "command": "VolumeDown"},
"mute" : {"device": "Sonos PLAYBAR", "command": "Mute"}
}
},
'LivingRoom': {
'Watch DirecTV': {
"vol+" : {"device": "JBL Amp", "command": "VolumeUp"},
"vol-" : {"device": "JBL Amp", "command": "VolumeDown"},
"vol><": {"device": "JBL Amp", "command": "Mute"},
"<<" : {"device": "DirecTV Mini-Genie Client", "command": "Rewind"},
"<>" : {"device": "DirecTV Mini-Genie Client", "command": "Play"},
"><" : {"device": "DirecTV Mini-Genie Client", "command": "Pause"},
">>" : {"device": "DirecTV Mini-Genie Client", "command": "FastForward"},
"chan+": {"device": "DirecTV Mini-Genie Client", "command": "ChannelUp"},
"chan+": {"device": "DirecTV Mini-Genie Client", "command": "ChannelUp"},
"chan-": {"device": "DirecTV Mini-Genie Client", "command": "ChannelDown"},
"chan<": {"device": "DirecTV Mini-Genie Client", "command": "ChannelPrev"},
"up" : {"device": "DirecTV Mini-Genie Client", "command": "DirectionUp"},
"down" : {"device": "DirecTV Mini-Genie Client", "command": "DirectionDown"},
"left" : {"device": "DirecTV Mini-Genie Client", "command": "DirectionLeft"},
"right": {"device": "DirecTV Mini-Genie Client", "command": "DirectionRight"},
"ok" : {"device": "DirecTV Mini-Genie Client", "command": "Select"},
"list" : {"device": "DirecTV Mini-Genie Client", "command": "List"},
"menu" : {"device": "DirecTV Mini-Genie Client", "command": "Menu"},
"guide": {"device": "DirecTV Mini-Genie Client", "command": "Guide"},
"back" : {"device": "DirecTV Mini-Genie Client", "command": "Exit"},
"back" : {"device": "DirecTV Mini-Genie Client", "command": "Exit"},
"eject": {"device": "DirecTV Mini-Genie Client", "command": "Red"},
"." : {"device": "DirecTV Mini-Genie Client", "command": "-"},
"enter": {"device": "DirecTV Mini-Genie Client", "command": "enter"},
"num" : {"device": "DirecTV Mini-Genie Client", "command": "0"}
},
'Watch Fire TV': {
"vol+" : {"device": "JBL Amp", "command": "VolumeUp"},
"vol-" : {"device": "JBL Amp", "command": "VolumeDown"},
"mute" : {"device": "JBL Amp", "command": "Mute"},
"<<" : {"device": "Amazon Gen3 Fire TV", "command": "Rewind"},
"<>" : {"device": "Amazon Gen3 Fire TV", "command": "Play"},
"><" : {"device": "Amazon Gen3 Fire TV", "command": "Pause"},
">>" : {"device": "Amazon Gen3 Fire TV", "command": "FastForward"},
"rec" : {"device": "Amazon Gen3 Fire TV", "command": "Record"},
"stop" : {"device": "Amazon Gen3 Fire TV", "command": "Stop"},
"eject": {"device": "Amazon Gen3 Fire TV", "command": "Red"},
"up" : {"device": "Amazon Gen3 Fire TV", "command": "DirectionUp"},
"down" : {"device": "Amazon Gen3 Fire TV", "command": "DirectionDown"},
"left" : {"device": "Amazon Gen3 Fire TV", "command": "DirectionLeft"},
"right": {"device": "Amazon Gen3 Fire TV", "command": "DirectionRight"},
"ok" : {"device": "Amazon Gen3 Fire TV", "command": "OK"},
"list" : {"device": "Amazon Gen3 Fire TV", "command": "Home"},
"menu" : {"device": "Amazon Gen3 Fire TV", "command": "Menu"},
"back" : {"device": "Amazon Gen3 Fire TV", "command": "Back"}
},
'Watch HTPC': {
"vol+" : {"device": "JBL Amp", "command": "VolumeUp"},
"vol-" : {"device": "JBL Amp", "command": "VolumeDown"},
"mute" : {"device": "JBL Amp", "command": "Mute"},
"num" : {"device": "LG TV", "command": "0"},
"." : {"device": "LG TV", "command": "-"}
},
'PowerOff': {
"vol+" : {"device": "JBL Amp", "command": "VolumeUp"},
"vol-" : {"device": "JBL Amp", "command": "VolumeDown"},
"mute" : {"device": "JBL Amp", "command": "Mute"}
}
}
}
########################
# sendCommand
########################
def sendCommand(hass, logger, location, hub):
global activity, code, keyMap, hubCommands, indent
indent = ' '
logger(indent)
logger(" ## Send '{} TV Remote' Command to: '{}'".format(location, hub))
# Validate Data
if hub is None:
return logger(indent+"Abort: enityId is missing")
elif activity is None:
return logger(indent+"Abort: activityName is missing")
elif code is None:
return logger(indent+"Abort: keyCode is missing")
elif keyMap.get(code) is None:
return logger(indent+"Abort: keyMap is missing for remote code: {}".format(code))
# Determine service action and service data values
service_action = 'send_command'
service_data = {'entity_id' : hub}
if location == "Bedroom" and keyMap[code] == "tv|o":
logger(indent+"media_player called: 'volume_set'")
hass.services.call('media_player', 'volume_set', {'entity_id': 'media_player.master_bedroom', 'volume_level': 0.1}, False)
logger(indent+"media_player called: 'select_source'")
hass.services.call('media_player', 'select_source', {'entity_id': 'media_player.master_bedroom', 'source': 'Classic Rock Radio'}, False)
logger(indent+"media_player called: 'set_sleep_timer'")
hass.services.call('sonos', 'set_sleep_timer', {'entity_id': 'media_player.master_bedroom', 'sleep_time': 3600}, False)
service_action = 'turn_off'
elif keyMap[code] is "input":
# toggle activities
service_action = 'turn_on'
if activity == 'Watch DirecTV':
service_data.update({'activity': 'Watch Fire TV'})
elif activity == 'Watch Fire TV':
service_data.update({'activity': 'Watch Antenna TV'})
else:
service_data.update({'activity': 'Watch DirecTV'})
elif int(code) >= 1 and int(code) <= 10 and hubCommands[location].get(activity,{}).get('num') is not None:
# Numeric Key Pressed on supported device
service_action = 'send_command'
service_data.update(hubCommands[location][activity]['num'])
if int(code) != 10:
service_data['command'] = code
else:
service_data['command'] = '0'
elif hubCommands[location].get(activity,{}).get(keyMap[code]) is not None:
# Get device command
service_action = 'send_command'
service_data.update(hubCommands[location][activity][keyMap[code]])
else:
return logger(indent+"Abort: hubCommand is invalid for activity: '{}' or keyMap: '{}'".format(activity, keyMap[code]))
if service_action == 'send_command' and service_data['command'] == 'FastForward':
service_data.update({'hold_secs': 1})
logger(indent+"remote called: '{}'".format(service_action))
hass.services.call('remote', service_action, service_data, False)
logger(indent+"Service Called, service_data: {}".format(service_data))
if location == 'Bedroom' and service_action == 'turn_on':
# Stop Sleep timer and set initial volume
logger(indent+"media_player called: 'volume_set'")
hass.services.call('media_player', 'volume_set', {'entity_id': 'media_player.master_bedroom', 'volume_level': 0.25}, False)
return logger(indent+"Exit SendCommand")
########################
####### MAIN ###########
########################
logger("\n")
logger('#### Running Python_Script: "control_hub"')
logger(indent+"using input data:")
logger(indent+"entity: '{}'".format(entity))
logger(indent+"activity: '{}'".format(activity))
logger(indent+"key: '{}'".format(code))
logger(indent+"loglevel: '{}'".format(loglevel))
hass.bus.fire('control_hub_event', { "entity_id": entity, "state": "send_command", "key_code": code, "current_activity": activity })
if source == '/dev/input/by-id/usb-flirc.tv_flirc-event-kbd':
sendCommand(hass, logger, 'LivingRoom', 'remote.hub_3911_living_room')
elif source == '/dev/input/by-id/usb-flirc.tv_flirc-if01-event-kbd':
sendCommand(hass, logger, 'Bedroom', 'remote.hub_3911_master_bedroom')
else:
logger(indent+"Abort: Invalid key-source: '{}'".format(source))
hass.bus.fire('control_hub_event', { "entity_id": entity, "state": "command_sent", "key_code": code, "current_activity": activity })
Summary:
Overalll, it works quite well and because regular HTML is used, endless modifications can be made to suit your needs with some basic HTML tweaking.
It runs fairly slow on my 6 year old Kindle Voyage eBook. But I attribute that to the speed my old Kindle doesn’t have and think newer Kindles will be more responsive. The big advantage for me is that the battery works for weeks and has a great soft backlight for night time use and can be used in direct sunlight too.
Since my httpRemote only uses standard HTML this will run great on any smart device that implements the new “run as HTML app” in their browser.
I tested on my android tablet and the FireStick ‘Silk’ Browser and they both look great and the response is pretty speedy and quite useable.
-Regards
,