-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathechosmart.js
More file actions
446 lines (392 loc) · 17.2 KB
/
echosmart.js
File metadata and controls
446 lines (392 loc) · 17.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
/// <reference path="typings/node/node.d.ts"/>
module.exports.settings = { 'serviceport' : 8281 };
var settings = module.exports.settings;
var server;
var fs = require('fs');
var http = require('http');
var qs = require('querystring');
var urlparser = require('url');
var request = require('request');
var dgram = require('dgram');
//var state = require('./state');
var ip = require('ip').address()
var port = settings.serviceport;
// magic values
var AUTH = "Bearer uuid" // change to use your oauth bearer UUID
var SMARTAPP = "https://graph.api.smartthings.com/api/smartapps/installations/uuid" // change to use your oauth endpoint
// Credit to Sagen here - the UPnP M-SEARCH response
// taken from https://github.com/sagen/hue-upnp
exports.addRoutes = function(server) {
};
// credit to https://github.com/armzilla/amazon-echo-ha-bridge - respond to M-SEARCH as WeMo, then use HUE description.xml
exports.enableDiscovery = function() {
var s = dgram.createSocket('udp4');
s.bind(1900, undefined, function() {
console.log('UPnP discovery started')
s.addMembership('239.255.255.250');
s.on('message', function(msg, rinfo) {
var msgString = msg.toString();
if (msgString.substr(0, 10) == 'M-SEARCH *') {
//console.log('M-SEARCH Message received')
//console.log(rinfo);
var response = "HTTP/1.1 200 OK\r\n\
CACHE-CONTROL: max-age=100\r\n\
EXT:\r\n\
LOCATION: http://" + ip + ":" + port + "/description.xml\r\n\
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1\r\n\
ST: urn:Belkin:device:**\r\n\
USN: uuid:Socket-1_0-221438K0100073::urn:Belkin:device:**\r\n\r\n"
var responseBuffer = new Buffer(response);
s.send(responseBuffer, 0, responseBuffer.length, rinfo.port, rinfo.address);
}
})
});
// this should be what Hue says, but it doesn't seem to work.
//ST: upnp:rootdevice\r\n\
//USN: uuid:2fa00080-d000-11e1-9b23-001f80007bbe::upnp:rootdevice\r\n\r\n";
var Hapi = require('hapi');
var server = new Hapi.Server();
server.connection({address:'0.0.0.0', port: 1902});
server.route( {
method: 'GET'
, path: '/description.xml'
, handler: function(request) {
request.reply(discoveryResponse)
.header('Content-Type', 'text/xml');
}});
server.start();
};
// return bridge info
var bridgejson = '{\
"name": "Philips hue",\
"zigbeechannel": 15,\
"mac": "00:17:88:00:00:00",\
"dhcp": true,\
"ipaddress": "BASEIP",\
"netmask": "255.255.255.0",\
"gateway": "10.0.0.1",\
"proxyaddress": "none",\
"proxyport": 0,\
"UTC": "2014-07-17T09:27:35",\
"localtime": "2014-07-17T11:27:35",\
"timezone": "Europe/Madrid",\
"whitelist": {\
"ffffffffe0341b1b376a2389376a2389": {\
"last use date": "2014-07-17T07:21:38",\
"create date": "2014-04-08T08:55:10",\
"name": "PhilipsHueAndroidApp#TCT ALCATEL ONE TOU"\
},\
"pAtwdCV8NZId25Gk": {\
"last use date": "2014-05-07T18:28:29",\
"create date": "2014-04-09T17:29:16",\
"name": "MyApplication"\
},\
"gDN3IaPYSYNPWa2H": {\
"last use date": "2014-05-07T09:15:21",\
"create date": "2014-05-07T09:14:38",\
"name": "iPhone Web 1"\
}\
},\
"swversion": "01012917",\
"apiversion": "1.3.0",\
"swupdate": {\
"updatestate": 0,\
"url": "",\
"text": "",\
"notify": false\
},\
"linkbutton": false,\
"portalservices": false,\
"portalconnection": "connected",\
"portalstate": {\
"signedon": true,\
"incoming": false,\
"outgoing": true,\
"communication": "disconnected"\
}\
}';
// return UPNP info (Wemo that thinks its a Hue)
var bridgexml='<root xmlns="urn:schemas-upnp-org:device-1-0">\
<specVersion>\
<major>1</major>\
<minor>0</minor>\
</specVersion>\
<URLBase>http://BASEIP:BASEPORT/</URLBase>\
<device>\
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>\
<friendlyName>Philips hue (BASEIP)</friendlyName>\
<manufacturer>Royal Philips Electronics</manufacturer>\
<manufacturerURL>http://www.philips.com</manufacturerURL>\
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>\
<modelName>Philips hue bridge 2012</modelName>\
<modelNumber>929000226503</modelNumber>\
<modelURL>http://www.meethue.com</modelURL>\
<serialNumber>01189998819991197253</serialNumber>\
<UDN>uuid:88f6698f-2c83-4393-bd03-cd54a9f8595</UDN>\
<serviceList>\
<service>\
<serviceType>(null)</serviceType>\
<serviceId>(null)</serviceId>\
<controlURL>(null)</controlURL>\
<eventSubURL>(null)</eventSubURL>\
<SCPDURL>(null)</SCPDURL>\
</service>\
</serviceList>\
<presentationURL>index.html</presentationURL>\
<iconList>\
<icon>\
<mimetype>image/png</mimetype>\
<height>48</height>\
<width>48</width>\
<depth>24</depth>\
<url>hue_logo_0.png</url>\
</icon>\
<icon>\
<mimetype>image/png</mimetype>\
<height>120</height>\
<width>120</width>\
<depth>24</depth>\
<url>hue_logo_3.png</url>\
</icon>\
</iconList>\
</device>\
</root>';
// my ST "lights" - just switches, YMMV - dynamically pulled from system
var lights;
function emptycb() {
console.log("lights fetched");
}
// get all light status on startup
fetchlights(emptycb);
makeserver();
exports.enableDiscovery();
// get map of light device id + name (order of lights will be HUE light #)
// assumes webapi installed/publshed and oath credentials valid above
function fetchlights(cb) {
// this query is looking just for switches, if you have other device types that
// echo can turn on/off/dim, you could get those here instead or in addition
var req = {"headers": {"Content-Type": "application/json", "Authorization": AUTH},
"uri": SMARTAPP + "/details/switch"};
var sendreq = {};
sendreq.headers = req.headers;
sendreq.uri = req.uri;
sendreq.method = "GET";
sendreq.followRedirect = true;
request(sendreq, function (err, res, body) {
try {
var jlights = JSON.parse(body);
if (jlights.hasOwnProperty("devices")) {
lights = jlights.devices;
//console.log(lights);
}
} catch (e) {
console.log(e);
console.log(body);
}
cb(); // call the callback once all "lights" are fetched
});
}
// set a light on/off/dim
function setLight(dev,staterec) {
var req = {"headers": {"Content-Type": "application/json", "Authorization": AUTH},
"uri": SMARTAPP + "/" + dev};
var sendreq = {};
var bodyStr = JSON.stringify({state: staterec}, undefined, 0);
sendreq.body = bodyStr;
sendreq.headers = req.headers;
sendreq.uri = req.uri;
sendreq.method = "PUT";
sendreq.followRedirect = true;
request(sendreq, function (err, res, body) {
//console.log("SUCCESS PUT: ", err);
});
}
function makeserver() {
// create the server - callback called when requests come in
var server = http.createServer(onRequest);
// we're done - except for the callback - give access to the server
module.exports.server = server;
// listen on the port
server.listen(settings.serviceport);
// nested callback function called when requests come in
function onRequest(request, response) {
var parsedreq = urlparser.parse(request.url, true);
var ipAddress = null;
var forwardedIpsStr = request.headers['x-forwarded-for'];
if (forwardedIpsStr) {
ipAddress = forwardedIpsStr[0];
}
if (!ipAddress) {
ipAddress = request.connection.remoteAddress;
}
//console.log(request.url + " from " + ipAddress);
var req = parsedreq.pathname.split("/");
var requestBody = '';
var action = req[1];
var a2 = req[2];
var a3 = req[3];
var a4 = req[4];
//console.log("Action is: " + action + " req len = " + req.length);
try {
if (request.method == "GET") {
// /api/<username>/config
if (action == "description.xml") {
// return config
response.writeHead(200, { 'Content-Type': 'application/xml' });
bridgexml = bridgexml.replace("BASEIP", require('ip').address()).replace("BASEPORT", settings.serviceport);
response.end(bridgexml);
} else
if ((action == "api") && (a3 == "config")) {
// return config
response.writeHead(200, { 'Content-Type': 'application/json' });
bridgejson = bridgejson.replace("BASEIP", require('ip').address()).replace("BASEPORT", settings.serviceport);
response.end(bridgejson);
}
if ((action == "api") && (a3 == "groups")) {
// assume groups/0
/*{
"action":
{
"on":false,
"bri":254,
"hue":14922,
"sat":144,
"xy":[0.4595,0.4105],
"ct":369,
"effect":"none",
"colormode":"ct"
},
"lights": ["1","2","3"],
"name": "Lightset 0"
}
*/
fetchlights(function () {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.write('{"action": { "on":false, "bri":254, "hue":14922, "sat":144, "xy":[0.4595,0.4105], "ct":369, "effect":"none", "colormod
e":"ct" }, "lights":[');
// all lights
var i;
for (i = 0; i < lights.length; i++) {
response.write('"' + (i + 1) + '"');
if (i < lights.length-1) response.write(",");
}
response.end('], "name": "Lightset 0" }');
});
}
if ((action == "api") && (req.length == 3)) {
fetchlights(function () {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.write('{"lights":{');
// all lights
var i;
for (i = 0; i < lights.length; i++) {
if (i != 0) response.write(',');
if (!lights[i].state.hasOwnProperty("level")) lights[i].state["level"]=99;
response.write('"' + (i + 1) + '": { "state": {"on": ' + ((lights[i].state.switch == "on") ? "true" : "false") + ',"bri":' + M
ath.floor(255 * lights[i].state.level / 100) + ',"hue": 13088,"sat": 212, "xy": [0.5128,0.4147],"ct": 467,\
"alert": "none","effect": "none","colormode": "xy", "reachable": true},"type": "Extended color light",\
"name": "'+ lights[i].label + '", "modelid": "LCT001","swversion": "66009461", "pointsymbol": {\
"1": "none", "2": "none", "3": "none","4": "none", "5": "none","6": "none","7": "none","8": "none" }}');
}
bridgejson = bridgejson.replace("BASEIP", require('ip').address()).replace("BASEPORT", settings.serviceport);
response.write(' }, "groups": {} , "schedules": {}, "config":');
response.write(bridgejson);
response.end("}");
});
} else if ((action == "api") && (a3 == "lights")) {
// return "fresh" lights - we fetch status on every request
fetchlights(function () {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.write("{")
if (req.length == 4) {
// all lights
var i;
for (i = 0; i < lights.length; i++) {
if (!lights[i].state.hasOwnProperty("level")) lights[i].state["level"]=99;
if (i != 0) response.write(',');
response.write('"' + (i + 1) + '": { "state": {"on": ' + ((lights[i].state.switch == "on") ? "true" : "false") + ',"bri":' + M
ath.floor(255 * lights[i].state.level / 100) + ',"hue": 13088,"sat": 212, "xy": [0.5128,0.4147],"ct": 467,\
"alert": "none","effect": "none","colormode": "xy", "reachable": true},"type": "Extended color light",\
"name": "'+ lights[i].label + '", "modelid": "LCT001","swversion": "66009461", "pointsymbol": {\
"1": "none", "2": "none", "3": "none","4": "none", "5": "none","6": "none","7": "none","8": "none" }}');
}
} else {
// one light
var i = parseInt(a4) - 1;
if (!lights[i].state.hasOwnProperty("level")) lights[i].state["level"]=99;
//console.log('"state": {"on": ' + ((lights[i].state.switch == "on") ? "true" : "false") + ',"bri":' + Math.floor(255 * lights[i].
//state.level / 100) + ',"hue": 13088,"sat": 212, "xy": [0.5128,0.4147],"ct": 467,\
// "alert": "none","effect": "none","colormode": "xy", "reachable": true},"type": "Extended color light",\
// "name": "'+ lights[i].label + '", "modelid": "LCT001","swversion": "66009461", "pointsymbol": {\
// "1": "none", "2": "none", "3": "none","4": "none", "5": "none","6": "none","7": "none","8": "none" }');
response.write('"state": {"on": ' + ((lights[i].state.switch == "on") ? "true" : "false") + ',"bri":' + Math.floor(255 * lights[i]
.state.level / 100) + ',"hue": 13088,"sat": 212, "xy": [0.5128,0.4147],"ct": 467,\
"alert": "none","effect": "none","colormode": "xy", "reachable": true},"type": "Extended color light",\
"name": "'+ lights[i].label + '", "modelid": "LCT001","swversion": "66009461", "pointsymbol": {\
"1": "none", "2": "none", "3": "none","4": "none", "5": "none","6": "none","7": "none","8": "none" }');
}
response.end("}");
});
}
} else if (request.method == "PUT") {
//console.log("got put request");
/*
http://<bridge ip address>/api/newdeveloper/lights/1/state
Body {"on":true, "sat":255, "bri":255,"hue":10000}
*/
request.on('data', function (data) {
requestBody += data;
if (requestBody.length > 1e7) {
response.writeHead(413, "Request Entity Too Large", { 'Content-Type': 'text/html' });
response.end('<html><head><title>413</title></head><body>413: Request Entity Too Large</body></html>');
}
});
request.on('end', processPut);
function processPut() {
//console.log(requestBody);
response.writeHead(200, { 'Content-Type': 'application/json' });
if ((action == "api") && (a3 == "lights") && (req.length == 6)) {
var i = parseInt(a4) - 1;
var state = {};
var rec = JSON.parse(requestBody);
if (rec.hasOwnProperty("on")) { state["switch"] = rec.on ? "on" : "off"; }
if (rec.hasOwnProperty("bri")) { state["level"] = Math.floor((100 * rec.bri) / 255); }
//console.log(state);
setLight(lights[i].id, state);
}
response.end("{}");
}
} else if (request.method == "POST") {
request.on('data', function (data) {
requestBody += data;
if (requestBody.length > 1e7) {
response.writeHead(413, "Request Entity Too Large", { 'Content-Type': 'text/html' });
response.end('<html><head><title>413</title></head><body>413: Request Entity Too Large</body></html>');
}
});
request.on('end', processPOST);
function processPOST() {
//console.log("got POST request");
// POST used to reguster user name, we just accept anything :)
// debugging
// console.log("rl: " + req.length + " Data: " + requestBody);
//console.log(" Data: " + requestBody);
var rec = JSON.parse(requestBody);
if ((action == "api") && (req.length == 2)) {
var user = rec.username;
// return user accepted
response.writeHead(200, { 'Content-Type': 'application/json' });
//[{"success":{"username": "1234567890"}}]
response.end(JSON.stringify([{ "success": { "username": user } }], undefined, 1));
}
}
}
}
catch (e) {
console.log(e);
console.log(e.stack);
response.writeHead(400, request.method + " not supported", { 'Content-Type': 'text/html' });
response.end('<html><head><title>400</title></head><body>400: OOPS!</body></html>');
}
}
}