209 lines
5.0 KiB
JavaScript
209 lines
5.0 KiB
JavaScript
"use strict";
|
|
|
|
var Q = require("q"),
|
|
dgram = require('dgram'),
|
|
EventEmitter = require('events').EventEmitter,
|
|
util = require("util");
|
|
|
|
var socketCleanUp = new SocketCleanUp();
|
|
|
|
/**
|
|
* Locates possible Philips Hue Bridges on the network.
|
|
* @param timeout The maximum time to wait for responses, or if none provided will default to 5 seconds
|
|
* @return {Function|promise|promise|exports.promise}
|
|
*/
|
|
function locateBridges(timeout) {
|
|
var deferred = Q.defer(),
|
|
search = new SSDPSearch(timeout),
|
|
results = [];
|
|
|
|
search.on("response", function(value) {
|
|
results.push(value);
|
|
});
|
|
search.search();
|
|
|
|
// Give up after the timeout and process whatever results we have
|
|
setTimeout(function() {
|
|
_close(search);
|
|
deferred.resolve(_filterResults(results));
|
|
}, timeout || 5000);
|
|
|
|
return deferred.promise;
|
|
}
|
|
module.exports.locateBridges = locateBridges;
|
|
|
|
function SSDPSearch(timeout) {
|
|
var self = this;
|
|
|
|
self.socket = dgram.createSocket('udp4');
|
|
|
|
self.socket.on('error', function (err) {
|
|
console.log('############### Got an error!');
|
|
console.trace(err);
|
|
});
|
|
|
|
self.socket.on('message', function onMessage(msg, rinfo) {
|
|
var msgStrings = msg.toString().split("\r\n");
|
|
|
|
// HTTP/#.# ### Response
|
|
if (msgStrings[0].match(/HTTP\/(\d{1})\.(\d{1}) (\d+) (.*)/)) {
|
|
self.emit('response', _parseSearchResponse(msgStrings.slice(1)));
|
|
}
|
|
});
|
|
|
|
socketCleanUp.add(self);
|
|
socketCleanUp.registerExitListener();
|
|
}
|
|
SSDPSearch.prototype.__proto__ = EventEmitter.prototype;
|
|
|
|
SSDPSearch.prototype.search = function search() {
|
|
var ip = "239.255.255.250",
|
|
port = 1900;
|
|
|
|
var pkt = new Buffer(_buildSearchPacket(
|
|
{
|
|
"HOST": ip + ":" + port,
|
|
"MAN": "ssdp:discover",
|
|
"MX": 10,
|
|
// "ST": "SsdpSearch:all"
|
|
"ST": "urn:schemas-upnp-org:device:Basic:1"
|
|
}
|
|
));
|
|
this.socket.send(pkt, 0, pkt.length, port, ip);
|
|
};
|
|
|
|
function _close(target) {
|
|
if (!target.closed) {
|
|
target.closed = true;
|
|
target.socket.close();
|
|
}
|
|
socketCleanUp.remove(target);
|
|
}
|
|
|
|
function _buildSearchPacket(vars) {
|
|
var packet = "M-SEARCH * HTTP/1.1\r\n";
|
|
Object.keys(vars).forEach(function (n) {
|
|
packet += n + ": " + vars[n] + "\r\n";
|
|
});
|
|
return packet + "\r\n";
|
|
}
|
|
|
|
function _parseSearchResponse(lines) {
|
|
var line,
|
|
separatorIndex,
|
|
key,
|
|
value,
|
|
result = {},
|
|
idx,
|
|
len;
|
|
|
|
for (idx = 0, len = lines.length; idx < lines.length; idx++) {
|
|
line = lines[idx];
|
|
separatorIndex = line.indexOf(":");
|
|
if (separatorIndex > 0 && separatorIndex < line.length) {
|
|
key = line.substring(0, separatorIndex).toLowerCase();
|
|
value = line.substring(separatorIndex + 1, line.length).trim();
|
|
result[key] = value;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function _filterResults(resultObjects) {
|
|
var uniqueValues = {};
|
|
|
|
resultObjects.forEach(function(result) {
|
|
if (! uniqueValues[result.location]) {
|
|
uniqueValues[result.location] = result;
|
|
} else {
|
|
uniqueValues[result.location] = _combineResults(uniqueValues[result.location], result);
|
|
}
|
|
});
|
|
return uniqueValues;
|
|
}
|
|
|
|
function _combineResults(objectOne, objectTwo) {
|
|
var result = {},
|
|
key,
|
|
array;
|
|
|
|
for (key in objectOne) {
|
|
result[key] = objectOne[key];
|
|
}
|
|
|
|
for (key in objectTwo) {
|
|
if (result[key] !== objectTwo[key]) {
|
|
if (util.isArray(result[key])) {
|
|
_addUnique(result[key], objectTwo[key]);
|
|
} else {
|
|
array = [];
|
|
array.push(result[key]);
|
|
array.push(objectTwo[key]);
|
|
result[key] = array;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function _addUnique(array, value) {
|
|
var found = false,
|
|
idx,
|
|
len;
|
|
|
|
for (idx = 0, len = array.length; idx < len; idx++) {
|
|
found = array[idx] === value;
|
|
if (found) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
array.push(value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A socket register to take care of closing all sockets that are opened, if not already closed
|
|
*
|
|
* @constructor
|
|
*/
|
|
function SocketCleanUp() {
|
|
this._searches = [];
|
|
this._registered = false;
|
|
}
|
|
|
|
SocketCleanUp.prototype.add = function(search) {
|
|
this._searches.push(search);
|
|
};
|
|
|
|
SocketCleanUp.prototype.remove = function(search) {
|
|
var self = this
|
|
, searches = self._searches
|
|
, idx = searches.indexOf(search)
|
|
;
|
|
|
|
if (idx > -1) {
|
|
searches.splice(idx, 1)
|
|
}
|
|
};
|
|
|
|
SocketCleanUp.prototype.finished = function() {
|
|
var self = this
|
|
, searches = self._searches
|
|
;
|
|
|
|
searches.forEach(function(search) {
|
|
_close(search);
|
|
});
|
|
};
|
|
|
|
SocketCleanUp.prototype.registerExitListener = function() {
|
|
var self = this;
|
|
|
|
if (! self._registered) {
|
|
process.on("exit", self.finished.bind(self));
|
|
self._registered = true;
|
|
}
|
|
};
|