xiaomi-mqtt
Version:
Bridge between Xiaomi Mi Smart Home Gateway and Mqtt broker.
236 lines (213 loc) • 7.29 kB
JavaScript
;
var dgram = require('dgram');
var Mqtt = require('./lib/mqtt.js').Mqtt;
var Utils = require('./lib/utils.js').Utils;
var log = require('loglevel');
const crypto = require('crypto');
var sidAddress = {};
var sidPort = {};
var token = {};
var payload = {};
var gateway_sid; // todo: multliple gateway
const IV = Buffer.from([0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, 0x56, 0x2e]);
const package_name = Utils.read_packageName();
const package_version = Utils.read_packageVersion();
const config = Utils.loadConfig("config.json");
var serverPort = config.xiaomi.serverPort || 9898;
var multicastAddress = config.xiaomi.multicastAddress || '224.0.0.50';
var multicastPort = config.xiaomi.multicastPort || 4321;
var password = config.xiaomi.password || "";
var level = config.loglevel || "info";
var heartbeatfreq = config.heartbeatfreq || 1;
var dataFormat = config.dataFormat || "parsed"
global.hb_count = heartbeatfreq;
Utils.setlogPrefix(log);
log.setLevel(level);
log.info("Start "+package_name+", version "+package_version);
log.trace("config " + JSON.stringify(config, null, 2));
var params = {
"config": config,
"package_name": package_name,
"get_id_list": get_id_list,
"read": read,
"write": write,
"log": log
}
var mqtt = new Mqtt(params);
mqtt.connect();
const server = dgram.createSocket('udp4');
server.bind(serverPort);
sendWhois();
server.on('listening', function() {
var address = server.address();
log.info("Start a UDP server, listening on port "+address.port);
server.addMembership(multicastAddress);
})
server.on('message', function(buffer, rinfo) {
var msg;
try {
msg = JSON.parse(buffer);
log.trace("msg "+JSON.stringify(msg));
} catch (err) {
log.error("invalid message: "+buffer);
return;
}
switch (msg.cmd) {
case "iam":
log.trace("msg "+JSON.stringify(msg));
var sid = msg.sid;
sidAddress[sid] = msg.ip;
sidPort[sid] = msg.port;
gateway_sid = msg.sid;
log.info("Gateway sid "+msg.sid+" Address "+sidAddress[sid]+", Port "+sidPort[sid]);
get_id_list(sid);
break;
case "get_id_list_ack":
var data = JSON.parse(msg.data);
var sid;
for(var index in data) {
sid = data[index];
sidAddress[sid] = rinfo.address;
sidPort[sid] = rinfo.port;
}
log.trace(JSON.stringify(sidAddress)+ " "+JSON.stringify(sidPort))
payload = {"cmd":msg.cmd, "sid":msg.sid, "data":JSON.parse(msg.data)};
log.debug(JSON.stringify(payload));
mqtt.publish(payload);
break;
case "read_ack":
case "report":
var data = JSON.parse(msg.data);
switch (msg.model) {
case "sensor_ht":
if (dataFormat === "parsed") {
data.temperature = data.temperature ? data.temperature / 100 : null;
data.humidity = data.humidity ? data.humidity / 100: null;
}
payload = {"cmd":msg.cmd ,"model":msg.model, "sid":msg.sid, "short_id":msg.short_id, "data": data};
log.debug(JSON.stringify(payload));
break;
case "gateway":
case "motion":
case "sensor_motion.aq2":
case "sensor_wleak.aq1":
case "magnet":
case "switch":
case "86sw1":
case "86sw2":
case "cube":
case "ctrl_neutral1":
case "ctrl_neutral2":
case "ctrl_ln1.aq1":
payload = {"cmd":msg.cmd ,"model":msg.model, "sid":msg.sid, "short_id":msg.short_id, "data": data};
log.debug(JSON.stringify(payload));
break;
default:
payload = {"cmd":msg.cmd ,"model":msg.model, "sid":msg.sid, "short_id":msg.short_id, "data": data};
log.debug("untested "+JSON.stringify(payload));
}
mqtt.publish(payload);
break;
case "write_ack":
var data = JSON.parse(msg.data);
payload = {"cmd":msg.cmd ,"model":msg.model, "sid":msg.sid, "short_id":msg.short_id, "data": data};
log.debug(JSON.stringify(payload));
mqtt.publish(payload);
break;
case "heartbeat":
var data = JSON.parse(msg.data);
if (msg.model === "gateway") {
token[msg.sid] = msg.token;
if (hb_count > 0) {
hb_count = hb_count - 1;
//log.info("heartbeat not published, "+hb_count+" before next publish");
} else {
// reset counter, if this is done, it's time to publish this one
hb_count = heartbeatfreq;
}
}
payload = {"cmd":msg.cmd ,"model":msg.model, "sid":msg.sid, "short_id":msg.short_id, "token":msg.token, "data": data};
if (msg.model !== "gateway" || hb_count===heartbeatfreq ) {
mqtt.publish(payload);
}
break;
default:
log.warn("unknown msg "+JSON.stringify(msg)+" from client "+rinfo.address+":"+rinfo.port);
}
});
// https://nodejs.org/api/errors.html
server.on('error', function(err) {
log.error("server.on('error') "+err.message);
if (err.message.includes("EADDRINUSE")) {
log.info("use 'lsof -i -P' to check for ports used.");
}
log.trace(err.stack);
try {
server.close();
} catch(err) {
log.error("server.close() "+err.message);
log.trace(err.stack);
process.exit(2);
}
});
function sendWhois() {
var msg = '{"cmd": "whois"}';
log.trace("Send "+msg+" to a multicast address "+multicastAddress+":"+multicastPort);
server.send(msg, 0, msg.length, multicastPort, multicastAddress);
}
function get_id_list(sid) {
var msg = '{"cmd":"get_id_list"}';
log.trace("Send "+msg+" to "+sidAddress[sid]+":"+sidPort[sid]);
server.send(msg, 0, msg.length, sidPort[sid], sidAddress[sid]);
}
function read(sid) {
if (sid in sidPort) {
var msg = '{"cmd":"read", "sid":"' + sid + '"}';
log.trace("Send "+msg+" to "+sidAddress[sid]+":"+sidPort[sid]);
server.send(msg, 0, msg.length, sidPort[sid], sidAddress[sid]);
} else {
payload = {"cmd":"xm","msg":"sid >"+sid+"< unknown."};
log.warn(JSON.stringify(payload));
mqtt.publish(payload);
}
}
function write(mqtt_payload) {
var msg;
payload = mqtt_payload;
var sid = payload.sid;
if (sid in sidPort) {
try {
var cipher = crypto.createCipheriv('aes-128-cbc', password, IV);
} catch (e) {
payload = {"cmd":"xm","msg":"Cipher "+JSON.stringify(cipher)+", check the password in config.json."};
log.error(JSON.stringify(payload));
mqtt.publish(payload);
return;
}
if (token[gateway_sid]) {
var key = cipher.update(token[gateway_sid], 'ascii', 'hex');
payload.data.key = key;
switch (payload.model) {
case "gateway":
if ("rgb" in payload.data) {
payload.data.rgb = Utils.rgb_buf(payload.data.rgb);
}
break;
default:
// nothing
}
msg = JSON.stringify(payload);
log.debug(msg);
server.send(msg, 0, msg.length, sidPort[sid], sidAddress[sid]);
} else {
payload = {"cmd":"xm","msg":"gateway token unknown."};
log.warn(JSON.stringify(payload));
mqtt.publish(payload);
}
} else {
payload = {"cmd":"xm","msg":"sid >"+sid+"< unknown."};
log.warn(JSON.stringify(payload));
mqtt.publish(payload);
}
}