Skip to content

Commit 85c9525

Browse files
committed
Adding Macro functionality to remote
Macros can now be defined in config.json. See example_configs/config.json for an example. Macros allow you to define N commands to be run from one HTTP endpoint. Macros currently appear as yellow buttons beneath the remote buttons. In my testing I found 100msec between commands to be sufficient.
1 parent 9eb5439 commit 85c9525

10 files changed

Lines changed: 174 additions & 51 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ node_modules/
33
.DS_Store
44
.project
55
.settings/
6-
.swp
6+
*.sw*

app.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ if (process.env.NODE_ENV == 'test' || process.env.NODE_ENV == 'development') {
5353
app.get('/', function(req, res) {
5454
res.send(JST['index'].render({
5555
remotes: lirc_node.remotes,
56+
macros: config.macros,
5657
repeaters: config.repeaters
5758
}));
5859
});
@@ -71,6 +72,21 @@ app.get('/remotes/:remote.json', function(req, res) {
7172
}
7273
});
7374

75+
// List all macros in JSON format
76+
app.get('/macros.json', function(req, res) {
77+
res.json(config.macros);
78+
});
79+
80+
// List all commands for :macro in JSON format
81+
app.get('/macros/:macro.json', function(req, res) {
82+
if (config.macros && config.macros[req.params.macro]) {
83+
res.json(config.macros[req.params.macro]);
84+
} else {
85+
res.send(404);
86+
}
87+
});
88+
89+
7490
// Send :remote/:command one time
7591
app.post('/remotes/:remote/:command', function(req, res) {
7692
lirc_node.irsend.send_once(req.params.remote, req.params.command, function() {});
@@ -92,6 +108,32 @@ app.post('/remotes/:remote/:command/send_stop', function(req, res) {
92108
res.send(200);
93109
});
94110

111+
// Execute a macro (a collection of commands to one or more remotes)
112+
app.post('/macros/:macro', function(req, res) {
113+
114+
// If the macro exists, execute each command in the macro with 100msec
115+
// delay between each command.
116+
if (config.macros && config.macros[req.params.macro]) {
117+
var i = 0;
118+
var interval = function() {
119+
if (config.macros[req.params.macro][i]) {
120+
var command = config.macros[req.params.macro][i];
121+
lirc_node.irsend.send_once(command[0], command[1], function() {});
122+
} else {
123+
clearInterval(interval);
124+
}
125+
126+
i += 1;
127+
};
128+
129+
setInterval(interval, 100);
130+
}
131+
132+
res.setHeader('Cache-Control', 'no-cache');
133+
res.send(200);
134+
});
135+
136+
95137
// Default port is 3000
96138
app.listen(3000);
97139
console.log("Open Source Universal Remote UI + API has started on port 3000.");

example_configs/config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,14 @@
44
"VolumeUp": true,
55
"VolumeDown": true
66
}
7+
},
8+
"macros": {
9+
"Xbox360": [
10+
{ "SonyTV": "Power" },
11+
{ "SonyTV": "Xbox360" },
12+
{ "Yamaha": "Power" },
13+
{ "Yamaha": "Xbox360" },
14+
{ "Xbox360": "Power" }
15+
]
716
}
817
}

static/css/app.less

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,12 @@ h1 {
9191
position: relative;
9292
}
9393

94-
.remotes-nav {
94+
.remotes-nav,
95+
.macros-nav {
9596
}
9697

97-
.remotes-nav li {
98+
.remotes-nav li,
99+
.macros-nav li {
98100
margin: 20px 0;
99101
}
100102

static/css/compiled/app.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ h1.is-remote {
6565
padding: 0;
6666
position: relative;
6767
}
68-
.remotes-nav li {
68+
.remotes-nav li,
69+
.macros-nav li {
6970
margin: 20px 0;
7071
}
7172
.back {

static/js/app/core.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,21 @@ $(function() {
5252
});
5353

5454
// If the user clicks, holds, and drags mouse outside of window - handle that too
55-
$(window).on('mouseup', function(evt) {
55+
$(window).on('mouseup touchend touchleave touchcancel', function(evt) {
5656
$('.command-repeater[data-active=true]').trigger('mouseup');
5757
});
5858

59+
// macro-link buttons send a command to execute the macro when clicked
60+
$('.macro-link').on('click', function(evt) {
61+
evt.preventDefault();
62+
$.ajax({
63+
type: "POST",
64+
url: $(this).attr('href'),
65+
success: function(data) {},
66+
error: function(xhr, type) {}
67+
});
68+
});
69+
5970
// Different visual behavior for touch devices
6071
if (OSUR.util.hasTouchEvents()) {
6172
$('body').addClass('has-touch');
@@ -80,6 +91,7 @@ $(function() {
8091
$('.back').on('click', function(evt) {
8192
$('.remote.active').removeClass('active');
8293
$('.remotes-nav').removeClass('hidden');
94+
$('.macros-nav').removeClass('hidden');
8395
$('.back').addClass('hidden');
8496
$('#title').html($('#title').attr('data-text'));
8597
$('#titlebar').removeClass('is-remote');
@@ -90,6 +102,7 @@ $(function() {
90102
evt.preventDefault();
91103
var href = $(this).attr('href');
92104
$('.remotes-nav').addClass('hidden');
105+
$('.macros-nav').addClass('hidden');
93106
$(href).addClass('active');
94107
$('.back').removeClass('hidden');
95108
$('#title').html($(this).html());

static/js/compiled/app.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

templates/index.swig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@
2121
{% endfor %}
2222
</ul>
2323

24+
<hr />
25+
26+
<ul class="macros-nav">
27+
{% for macro in macros %}
28+
{% set macroName = loop.key %}
29+
<li><button class="btn btn-wide btn-large btn-warning macro-link" href="/macros/{{ macroName }}">{{ macroName }}</button></li>
30+
{% endfor %}
31+
</ul>
32+
33+
2434
<div class="prev"></div>
2535

2636
<ul class="remotes">

test/fixtures/config.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,18 @@
44
"VolumeUp": true,
55
"VolumeDown": true
66
}
7+
},
8+
"macros": {
9+
"Play Xbox 360": [
10+
[ "SonyTV", "Power" ],
11+
[ "SonyTV", "Xbox360" ],
12+
[ "Yamaha", "Power" ],
13+
[ "Yamaha", "Xbox360" ],
14+
[ "Xbox360", "Power" ]
15+
],
16+
"Listen to Music": [
17+
[ "Yamaha", "Power" ],
18+
[ "Yamaha", "AirPlay" ]
19+
]
720
}
821
}

test/lirc_web.js

Lines changed: 78 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,60 @@ var app = require('../app'),
22
assert = require('assert'),
33
request = require('supertest'),
44
sinon = require('sinon');
5-
jsdom = require("jsdom");
5+
jsdom = require("jsdom");
66

77

88
describe('lirc_web', function() {
99

1010
describe('routes', function() {
1111

12+
// Root route
1213
it('should have an index route "/"', function(done) {
1314
assert(request(app).get('/').expect(200, done));
1415
});
1516

16-
it('should have a POST route for sending a command', function(done) {
17+
18+
// JSON API
19+
it('should have GET route for JSON list of macros', function(done) {
20+
assert(request(app).get('/macros.json').expect(200, done));
21+
});
22+
23+
it('should have GET route for JSON list of remotes', function(done) {
24+
assert(request(app).get('/remotes.json').expect(200, done));
25+
});
26+
27+
it('should have GET route for JSON list of commands for remote', function(done) {
28+
assert(request(app).get('/remotes/Xbox360.json').expect(200, done));
29+
});
30+
31+
it('should have GET route for JSON list of commands for macro', function(done) {
32+
assert(request(app).get('/macros/Play%20Xbox%20360.json').expect(200, done));
33+
});
34+
35+
it('should return 404 for unknown remote', function(done) {
36+
assert(request(app).get('/remotes/DOES_NOT_EXIST.json').expect(404, done));
37+
});
38+
39+
40+
// Sending commands
41+
it('should have POST route for sending a command', function(done) {
1742
assert(request(app).post('/remotes/tv/power').expect(200, done));
1843
});
1944

20-
it('should have a POST route to start repeatedly sending a command', function(done) {
45+
it('should have POST route to start repeatedly sending a command', function(done) {
2146
assert(request(app).post('/remotes/tv/volumeup/send_start').expect(200, done));
2247
});
2348

24-
it('should have a POST route to stop repeatedly sending a command', function(done) {
49+
it('should have POST route to stop repeatedly sending a command', function(done) {
2550
assert(request(app).post('/remotes/tv/volumeup/send_stop').expect(200, done));
2651
});
52+
53+
54+
// Sending macros
55+
it('should have POST route for sending a macro', function(done) {
56+
assert(request(app).post('/macros/xbox_360').expect(200, done));
57+
});
58+
2759
});
2860

2961
describe('index action', function() {
@@ -32,52 +64,53 @@ describe('lirc_web', function() {
3264
assert(request(app).get('/').expect('Content-Type', /html/, done));
3365
});
3466

35-
it('should return an HTML document in which all button elements of class command-link have an href of the form /remotes/:remote/:command', function(done) {
36-
request(app)
37-
.get('/')
38-
.end(function(err, res) {
39-
assert.equal(err, null);
40-
jsdom.env(res.text, ["http://code.jquery.com/jquery.js"], function (errors, window) {
41-
var $ = window.$;
42-
$("button.command-link").each(function(idx, elem) {
43-
var s = $(elem).attr('href').split("/");
44-
assert.equal(4, s.length);
45-
assert.equal("", s[0]);
46-
assert.equal("remotes", s[1]);
47-
});
48-
done();
49-
});
50-
});
51-
});
67+
it('should return an HTML document in which all button elements of class command-link have an href of the form /remotes/:remote/:command', function(done) {
68+
request(app)
69+
.get('/')
70+
.end(function(err, res) {
71+
assert.equal(err, null);
72+
// TODO: Remove external dependency so offline development and testing is possible
73+
jsdom.env(res.text, ["http://code.jquery.com/jquery.js"], function (errors, window) {
74+
var $ = window.$;
75+
$("button.command-link").each(function(idx, elem) {
76+
var s = $(elem).attr('href').split("/");
77+
assert.equal(4, s.length);
78+
assert.equal("", s[0]);
79+
assert.equal("remotes", s[1]);
80+
});
81+
done();
82+
});
83+
});
84+
});
5285
});
5386

5487
describe('json api', function() {
55-
it('should return a list of all remotes (and commands) when /remotes.json is accessed', function(done) {
56-
request(app)
57-
.get('/remotes.json')
58-
.end(function(err, res) {
59-
assert.equal(res.status, 200);
60-
done();
61-
});
62-
});
63-
64-
it('should return a list of all commands for a remote when /remotes/:remote.json is accessed', function(done) {
65-
request(app)
66-
.get('/remotes/Microsoft_Xbox360.json')
67-
.end(function(err, res) {
68-
assert.equal(res.status, 200);
69-
done();
70-
});
71-
});
88+
it('should return a list of all remotes (and commands) when /remotes.json is accessed', function(done) {
89+
request(app)
90+
.get('/remotes.json')
91+
.end(function(err, res) {
92+
assert.equal(res.status, 200);
93+
done();
94+
});
95+
});
7296

73-
it('should return a 404 for an unknown remote', function(done) {
97+
it('should return a list of all commands for a remote when /remotes/:remote.json is accessed', function(done) {
7498
request(app)
75-
.get('/remotes/DOES_NOT_EXIST.json')
76-
.end(function(err, res) {
77-
assert.equal(res.status, 404);
78-
done();
79-
});
80-
});
99+
.get('/remotes/Xbox360.json')
100+
.end(function(err, res) {
101+
assert.equal(res.status, 200);
102+
done();
103+
});
104+
});
105+
106+
it('should return a 404 for an unknown remote', function(done) {
107+
request(app)
108+
.get('/remotes/DOES_NOT_EXIST.json')
109+
.end(function(err, res) {
110+
assert.equal(res.status, 404);
111+
done();
112+
});
113+
});
81114
});
82115

83116
});

0 commit comments

Comments
 (0)