183 lines
5.7 KiB
JavaScript
183 lines
5.7 KiB
JavaScript
'use strict';
|
|
|
|
const dgram = require('dgram');
|
|
const util = require('util');
|
|
const EventEmitter = require('events').EventEmitter;
|
|
const crypto = require('crypto');
|
|
const Devices = require('./devices');
|
|
|
|
function Hub(options) {
|
|
if (!(this instanceof Hub)) return new Hub(options);
|
|
|
|
options = options || {};
|
|
options.port = parseInt(options.port, 10) || 9898;
|
|
|
|
this.sensors = {};
|
|
|
|
this.key = options.key;
|
|
this.keys = {};
|
|
this.token = {};
|
|
this.iv = Buffer.from([0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, 0x56, 0x2e]);
|
|
|
|
if (options.keys) {
|
|
for (let i = 0; i < options.keys.length; i++) {
|
|
this.keys[options.keys[i].ip] = options.keys[i].key;
|
|
}
|
|
}
|
|
|
|
/* this.clickTypes = {
|
|
click: 'click',
|
|
double_click: 'double_click',
|
|
long_click_press: 'long_click_press',
|
|
long_click_release: 'long_click_release'
|
|
}; */
|
|
|
|
this.listen = function () {
|
|
this.socket = dgram.createSocket('udp4');
|
|
this.socket.on('message', this.onMessage.bind(this));
|
|
this.socket.on('error', this.onError.bind(this));
|
|
this.socket.on('listening', this.onListening.bind(this));
|
|
this.socket.bind(options.port);
|
|
};
|
|
|
|
this.stop = function (cb) {
|
|
if (this._state === 'CLOSED') return false;
|
|
this._state = 'CLOSED';
|
|
|
|
if (this.socket) {
|
|
try {
|
|
this.socket.removeAllListeners();
|
|
this.socket.close(cb);
|
|
this.socket = null;
|
|
} catch (e) {
|
|
cb && cb();
|
|
}
|
|
} else {
|
|
cb && cb();
|
|
}
|
|
};
|
|
|
|
this.onListening = function () {
|
|
this._state = 'CONNECTED';
|
|
this.socket.setBroadcast(true);
|
|
this.socket.setMulticastTTL(128);
|
|
if (options.bind && options.bind !== '0.0.0.0') {
|
|
this.socket.addMembership('224.0.0.50', options.bind);
|
|
} else {
|
|
this.socket.addMembership('224.0.0.50');
|
|
}
|
|
const whoIsCommand = '{"cmd": "whois"}';
|
|
this.socket.send(whoIsCommand, 0, whoIsCommand.length, 4321, '224.0.0.50');
|
|
};
|
|
|
|
this.onError = function (err) {
|
|
if (this._state === 'CLOSED') return false;
|
|
this.emit('error', err);
|
|
};
|
|
|
|
this.onMessage = function (msgBuffer, rinfo) {
|
|
if (this._state === 'CLOSED') return false;
|
|
let msg;
|
|
try {
|
|
msg = JSON.parse(msgBuffer.toString());
|
|
}
|
|
catch (e) {
|
|
return;
|
|
}
|
|
|
|
let sensor = this.getSensor(msg.sid);
|
|
if (!sensor) {
|
|
// {"model":"lumi.lock.v1","did":"lumi.1xxxxxxxxxxxxx8","name":"Front door lock"}
|
|
if (!msg.model) {
|
|
return;
|
|
}
|
|
try {
|
|
if (options.browse) {
|
|
if (msg.model === 'gateway') {
|
|
this.emit('browse', {ip: rinfo.address});
|
|
}
|
|
return;
|
|
} else {
|
|
sensor = this.sensorFactory(msg.sid, msg.model, rinfo.address, msg.name);
|
|
}
|
|
}
|
|
catch (e) {
|
|
this.emit('warning', 'Could not add new sensor: ' + e.message);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (sensor) {
|
|
if (msg.data && typeof msg.data === 'string') {
|
|
try {
|
|
msg.data = JSON.parse(msg.data);
|
|
} catch (e) {
|
|
this.emit('warning', 'Could not parse: ' + msg.data);
|
|
msg.data = null;
|
|
}
|
|
}
|
|
if (msg.token) {
|
|
this.token[rinfo.address] = msg.token;
|
|
}
|
|
|
|
if (msg.cmd === 'heartbeat') {
|
|
sensor.heartBeat(msg.token, msg.data);
|
|
} else {
|
|
sensor.heartBeat();
|
|
}
|
|
|
|
if (msg.data && (msg.cmd === 'report' || msg.cmd.indexOf('_ack') !== -1)) {
|
|
sensor.onMessage(msg);
|
|
}
|
|
}
|
|
|
|
this.emit('message', msg);
|
|
};
|
|
|
|
this.getKey = function (ip) {
|
|
if (!this.token[ip]) return null;
|
|
const key = this.keys[ip] || this.key;
|
|
const cipher = crypto.createCipheriv('aes-128-cbc', key, this.iv);
|
|
const crypted = cipher.update(this.token[ip], 'ascii', 'hex');
|
|
cipher.final('hex'); // Useless data, don't know why yet.
|
|
return crypted;
|
|
};
|
|
|
|
this.sendMessage = function (message, ip) {
|
|
if (this._state === 'CLOSED') return false;
|
|
const json = JSON.stringify(message);
|
|
this.socket.send(json, 0, json.length, options.port, ip || '224.0.0.50');
|
|
};
|
|
|
|
this.sensorFactory = function (sid, model, ip, name) {
|
|
if (this._state === 'CLOSED') return false;
|
|
let sensor = null;
|
|
const dev = Object.keys(Devices).find(id => Devices[id].type === model);
|
|
|
|
if (Devices[dev] && Devices[dev].ClassName) {
|
|
sensor = new Devices[dev].ClassName(sid, ip, this, model, options);
|
|
} else {
|
|
throw new Error('Type "' + model + '" is not valid, use one of Hub::sensorTypes');
|
|
}
|
|
this.registerSensor(sensor, name);
|
|
return sensor;
|
|
};
|
|
this.getSensor = function (sid) {
|
|
return this.sensors[sid] || null;
|
|
};
|
|
this.registerSensor = function (sensor, name) {
|
|
if (this._state === 'CLOSED') return false;
|
|
this.emit('device', sensor, name);
|
|
this.sensors[sensor.sid] = sensor;
|
|
};
|
|
|
|
return this;
|
|
}
|
|
|
|
// extend the EventEmitter class using our Radio class
|
|
util.inherits(Hub, EventEmitter);
|
|
|
|
module.exports = {
|
|
Hub,
|
|
Devices
|
|
}; |