node-hue-api
Version:
Philips Hue API Library for Node.js
138 lines (108 loc) • 3.57 kB
text/typescript
import * as dgram from 'dgram';
import * as events from 'events';
import { DiscoveryBridgeDefinition } from './discoveryTypes';
export class SSDPSearch {
private responseEmitter: events.EventEmitter;
private socket: dgram.Socket | undefined;
constructor() {
const self = this;
this.responseEmitter = new events.EventEmitter();
this.socket = dgram.createSocket('udp4');
this.socket.on('error', function (err) {
self.responseEmitter.emit('error', err);
});
this.socket.on('message', function onMessage(msg, rinfo) {
const msgStrings = msg.toString().split('\r\n');
// HTTP/#.# ### Response
if (msgStrings[0].match(/HTTP\/(\d{1})\.(\d{1}) (\d+) (.*)/)) {
self.responseEmitter.emit('response', _parseSearchResponse(msgStrings.slice(1)));
}
});
// Ensure the socket is released on exit of the process (in case of errors)
process.on('exit', this.finished.bind(this));
}
search(timeout?: number): Promise<DiscoveryBridgeDefinition[]> {
const self = this;
return new Promise((resolve, reject) => {
const results: any[] = [];
self.responseEmitter.on('error', err => {
self.finished();
reject(err);
});
self.responseEmitter.on('response', value => {
results.push(value);
});
// Await our timeout before returning any results
setTimeout(() => {
self.finished();
resolve(_processResults(results));
}, timeout || 5000);
self._start();
});
}
private _start() {
if (this.socket) {
const ip = '239.255.255.250',
port = 1900;
const pkt = Buffer.from(_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);
}
}
private finished() {
if (this.socket) {
this.socket.close();
this.socket = undefined;
}
}
}
function _buildSearchPacket(vars: any): string {
let 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: string[]) {
const result: { [key: string]: string } = {};
lines.forEach(line => {
const separatorIndex = line.indexOf(':');
if (separatorIndex > 0 && separatorIndex < line.length) {
const key = line.substring(0, separatorIndex).toLowerCase();
const value = line.substring(separatorIndex + 1, line.length).trim();
result[key] = value;
}
});
return result;
}
function _processResults(responses: any[]): DiscoveryBridgeDefinition[] {
const results: {[key: string]: DiscoveryBridgeDefinition} = {};
responses.forEach(response => {
const location = response.location;
// Older versions used to use hue-bridgeId, now newer software versions use hue-bridgeid, remove the older fallback
// once they are outdated.
let hueBridgeId = response['hue-bridgeid'];
if (hueBridgeId == undefined) {
hueBridgeId = response['hue-bridgeId'];
}
if (location && hueBridgeId) {
if (!results[location]) {
const ipAddress = /\/\/(.*):/.exec(location);
if (ipAddress) {
results[location] = {
id: hueBridgeId,
internalipaddress: ipAddress[1]
};
}
}
}
});
return Object.values(results);
}