iobroker.mihome
Version:
Control Xiaomi Mi Home smarthome devices/sensors
289 lines (257 loc) • 9.69 kB
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.sids = {};
this.token = {};
this.iv = Buffer.from([0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, 0x56, 0x2e]);
this.protoVer = {};
if (options.keys) {
for (let i = 0; i < options.keys.length; i++) {
this.keys[options.keys[i].ip] = options.keys[i].key;
}
}
if (options.sids) {
for (let i = 0; i < options.sids.length; i++) {
this.sids[options.sids[i].sid] = options.sids[i].model;
console.log('SIDS: '+ JSON.stringify(this.sids));
}
}
/* this.clickTypes = {
click: 'click',
double_click: 'double_click',
long_click_press: 'long_click_press',
long_click_release: 'long_click_release'
}; */
this.protoMajor = function (ip) {
if (typeof this.protoVer[ip] !== 'object') {
return '1'; // default protocol version.
}
return this.protoVer[ip][0];
};
this.params2data = function (params) {
const data = {};
for (let i = 0; i < params.length; i++) {
for (let key in params[i]) {
if (key === 'battery_voltage') {
data['voltage'] = params[i][key];
} else if (key === 'energy_consumed') {
data['power_consumed'] = params[i][key]/1000.0;
} else if (key === 'button_0') {
data['channel_0'] = params[i][key];
} else if (key === 'button_1') {
data['channel_1'] = params[i][key];
} else if (key === 'dual_channel') {
if (params[i][key] === 'click') {
data['dual_channel'] = 'both_click';
}
} else {
data[key] = params[i][key];
}
}
}
return data;
};
this.data2params = function (data) {
const params = [];
for (const k in data) {
let p = {};
p[k] = data[k];
params.push(p);
}
return params;
};
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) {
this.socket = null;
cb && cb();
}
} else {
cb && cb();
}
};
this.onListening = function () {
this._state = 'CONNECTED';
this.socket.setBroadcast(true);
this.socket.setMulticastTTL(128);
try {
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');
}
} catch (err) {
console.error('ERROR addMembership: ' + err)
}
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());
if ((msg.model === 'gateway' || msg.model === 'acpartner.v3') && msg.sid.length < 12) { // Issue with missing leading zero in gateway's sid
msg.sid = String('000000000000' + msg.sid).slice(-12);
}
if (msg.proto_version) {
this.protoVer[rinfo.address] = msg.proto_version.split('.');
}
if (msg.params && (this.protoMajor(rinfo.address) === '2')) {
// Transfer params in protocol 2.0.x to data on protocol 1.0.x
msg.data = this.params2data(msg.params);
}
}
catch (e) {
return;
}
let sensor = this.getSensor(msg.sid);
if (!msg.model && msg.sid) {
msg.model=this.sids[msg.sid];
console.log('Updated model: '+ JSON.stringify(msg)+' [' +this.sids[msg.sid] +']');
}
//console.log("MSG:" + JSON.stringify(msg));
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') || (msg.model === 'acpartner.v3')) {
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 (this.protoMajor(rinfo.address) === '2') {
if (msg.cmd === 'read_rsp') msg.cmd = 'read_ack';
if (msg.cmd === 'write_rsp') msg.cmd = 'write_ack';
if (msg.cmd === 'discovery_rsp') {
const sids = [];
for (let i = 0; i < msg.dev_list.length; i++) {
sids.push(msg.dev_list[i].sid);
}
msg.data = sids;
msg.cmd = 'get_id_list_ack';
}
}
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;
try {
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;
} catch (err) {
this.emit('error', `Cannot get Key for ${ip}: ${err.message}`);
return null;
}
};
this.sendMessage = function (message, ip) {
if (this._state === 'CLOSED') {
return false;
} else
if (this.protoMajor(ip) === '2') {
if (message.hasOwnProperty('short_id')) {
delete message.short_id;
}
if (message.hasOwnProperty('data')) {
message['key'] = message.data.key;
delete message.data.key;
message['params'] = this.data2params(message.data);
delete message.data;
}
}
const json = JSON.stringify(message);
this.emit('debug', 'Send json: ' + json); // Added for debugging
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
};