blynk-app-client
Version:
Node.js module for accessing Blynk server through the App interface
370 lines (349 loc) • 10.1 kB
JavaScript
// Blynk app client
// The module offers a Node.js interface to the Blynk server app API on the SSL port
// It allows to execute the same commands as the the Blynk app
// It works only on private servers (not with the Blynk cloud server because it requires
// an SSL client authentication).
//
'use strict';
const tls = require('tls');
const zlib = require('zlib');
const crypto = require('crypto');
const SEND_TIMEOUT = 10000;
function BlynkAppClient(host, port) {
this.host = host;
this.port = port;
this.msgId = 1;
this.respPromises = new Map();
}
BlynkAppClient.prototype.connect = function(username, password) {
this.options = {
rejectUnauthorized: false
};
var p = new Promise(function(resolve, reject) {
var msgId = this.msgId;
this.respPromises.set(msgId, {
"resolve": resolve,
"reject": reject
});
this.socket = tls.connect(this.port, this.host, this.options, () => {
this.send("login " + username + " " + password);
}.bind(this)
);
this.respPromises.get(msgId).timeout = setTimeout(function () {
reject("connect timeout");
}
, SEND_TIMEOUT);
}.bind(this));
this.socket.on('data', function(data) {
var msgId = data.readUInt16BE(1);
var r = this.respPromises.get(msgId);
switch (data[0]) {
case MsgType.RESPONSE:
var responseCode = data.readUInt16BE(3);
r.resolve(responseCode);
clearTimeout(r.timeout);
break;
case MsgType.HARDWARE:
if (r != undefined) {
var resp = data.toString('utf8', 5);
var fields = resp.split('\0');
r.resolve(fields);
clearTimeout(r.timeout);
this.respPromises.delete(msgId);
}
break;
case MsgType.GET_TOKEN:
if (r != undefined) {
var resp = data.toString('utf8', 5);
r.resolve(resp);
clearTimeout(r.timeout);
}
break;
case MsgType.LOAD_PROFILE_GZIPPED:
if (r != undefined) {
var buf = new Buffer(data.length - 5);
data.copy(buf, 0, 5);
zlib.unzip(buf, (err, buffer) => {
if (!err) {
var resp = buffer.toString('utf8');
r.resolve(resp);
} else {
r.reject(err);
}
});
clearTimeout(r.timeout);
}
break;
case MsgType.SYNC:
break;
default:
console.log("Response raw data: " + data);
break;
}
}.bind(this));
this.socket.on('end', function() {
;
}.bind(this));
return p;
}
BlynkAppClient.prototype.deleteDashboard = function(dashboardId) {
var command = "deleteDash " + dashboardId;
var p = new Promise(function(resolve, reject) {
var msgId = this.msgId;
this.respPromises.set(msgId, {
"resolve": resolve,
"reject": reject
});
this.send(command);
this.respPromises.get(msgId).timeout = setTimeout(function () {
reject("deleteDashboard timeout");
}
, SEND_TIMEOUT);
}.bind(this));
return p;
}
BlynkAppClient.prototype.createDashboard = function(dashboardId, name, type) {
var command = "createDash {\"id\": " + dashboardId + ", \"name\": \"" + name + "\"" + ", \"boardType\": \"" + type + "\"}";
var p = new Promise(function(resolve, reject) {
var msgId = this.msgId;
this.respPromises.set(msgId, {
"resolve": resolve,
"reject": reject
});
this.send(command);
this.get(msgId).timeout = setTimeout(function () {
reject("createDashboard timeout");
}
, SEND_TIMEOUT);
}.bind(this));
return p;
}
BlynkAppClient.prototype.createWidget = function(dashboardId, widgetId, x, y, label, widgetType, pinType, pin) {
var command = "createWidget " + dashboardId +
"\0{\"id\":" + widgetId +
", \"x\":" + x +
", \"y\":" + y +
", \"label\":\"" + label +
"\", \"type\":\"" + widgetType +
"\", \"pinType\":\"" + pinType +
"\", \"pin\":" + pin +
", \"frequency\":1" +
"}";
var p = new Promise(function(resolve, reject) {
var msgId = this.msgId;
this.respPromises.set(msgId, {
"resolve": resolve,
"reject": reject
});
this.send(command);
this.respPromises.get(msgId).timeout = setTimeout(function () {
reject("createWidget timeout");
}
, SEND_TIMEOUT);
}.bind(this));
return p;
}
BlynkAppClient.prototype.getToken = function(dashboardId) {
var command = "getToken " + dashboardId;
var p = new Promise(function(resolve, reject) {
var msgId = this.msgId;
this.respPromises.set(msgId, {
"resolve": resolve,
"reject": reject
});
this.send(command);
this.respPromises.get(msgId).timeout = setTimeout(function () {
reject("getToken timeout");
}
, SEND_TIMEOUT);
}.bind(this));
return p;
}
BlynkAppClient.prototype.activate = function(dashboardId) {
var command = "activate " + dashboardId;
var p = new Promise(function(resolve, reject) {
var msgId = this.msgId;
this.respPromises.set(msgId, {
"resolve": resolve,
"reject": reject
});
this.send(command);
this.respPromises.get(msgId).timeout = setTimeout(function () {
reject("activate timeout");
}
, SEND_TIMEOUT);
}.bind(this));
return p;
}
BlynkAppClient.prototype.hardware = function(dashboardId, pinType, pinCommand, pinId, pinValue) {
var command = "hardware " + dashboardId + " " + pinType + pinCommand + " " + pinId;
if (pinValue != undefined)
command = command + " " + pinValue;
var p = new Promise(function(resolve, reject) {
var msgId = this.msgId;
this.respPromises.set(msgId, {
"resolve": resolve,
"reject": reject,
"command": command
});
this.send(command);
if (pinCommand == "w")
resolve("done");
else {
this.respPromises.get(msgId).timeout = setTimeout(function () {
reject("Hardware timeout: " + pinType+pinCommand + pinId + (pinValue != undefined ? pinValue : ""));
}
, SEND_TIMEOUT);
}
}.bind(this));
return p;
}
BlynkAppClient.prototype.loadProfileGzipped = function(dashboardId) {
var command = "loadprofilegzipped";
if (dashboardId != undefined)
command = command + " " + dashboardId;
var p = new Promise(function(resolve, reject) {
var msgId = this.msgId;
this.respPromises.set(msgId, {
"resolve": resolve,
"reject": reject
});
this.send(command);
this.respPromises.get(msgId).timeout = setTimeout(function () {
reject("loadProfileGzipped timeout");
}
, SEND_TIMEOUT);
}.bind(this));
return p;
}
BlynkAppClient.prototype.close = function() {
}
BlynkAppClient.prototype.send = function(data) {
if (!this.socket) {
return;
}
var commandAndBody = data.split(" ");
var message = this.createMessage(commandAndBody);
this.socket.write(message);
}
BlynkAppClient.prototype.createMessage = function(commandAndBody) {
var cmdBody = null;
var cmdString = commandAndBody[0];
var cmd = getCommandByString(cmdString);
if (cmd == MsgType.LOGIN) {
var username = commandAndBody[1];
var pwd = commandAndBody[2];
var hUser = crypto.createHash('sha256');
var hPwd = crypto.createHash('sha256');
var salt = hUser.update(username.toLowerCase()).digest();
hPwd.update(pwd, "utf8");
hPwd.update(salt, "utf8");
var finalHash = hPwd.digest('base64');
cmdBody = username + "\0" + finalHash;
} else if (cmd == MsgType.CREATE_DASH || cmd == MsgType.CREATE_WIDGET) {
cmdBody = commandAndBody.length > 1 ? commandAndBody.slice(1).join(' ') : null;
} else{
cmdBody = commandAndBody.length > 1 ? commandAndBody.slice(1).join('\0') : null;
}
return this.buildBlynkMessage(cmd, this.msgId++, cmdBody);
}
BlynkAppClient.prototype.buildBlynkMessage = function(cmd, msgId, cmdBody) {
const BLYNK_HEADER_SIZE = 5;
var bodyLength = (cmdBody ? cmdBody.length : 0);
var bufArray = new ArrayBuffer(BLYNK_HEADER_SIZE + bodyLength);
var dataview = new DataView(bufArray);
dataview.setInt8(0, cmd);
dataview.setInt16(1, msgId);
dataview.setInt16(3, bodyLength);
if (bodyLength > 0) {
//todo optimize. should be better way
var buf = new ArrayBuffer(bodyLength); // 2 bytes for each char
var bufView = new Uint8Array(buf);
for (var i = 0, offset = 5; i < cmdBody.length; i++, offset += 1) {
dataview.setInt8(offset, cmdBody.charCodeAt(i));
}
}
return new Buffer(bufArray);
}
var MsgType = {
RESPONSE : 0,
LOGIN : 2,
GET_TOKEN : 5,
PING : 6,
ACTIVATE_DASHBOARD : 7,
TWEET : 12,
EMAIL : 13,
NOTIFY : 14,
BRIDGE : 15,
HW_SYNC : 16,
HW_INFO : 17,
HARDWARE : 20,
LOAD_PROFILE_GZIPPED: 24,
SYNC : 25,
CREATE_DASH : 21,
DELETE_DASH : 23,
CREATE_WIDGET : 33
};
var MsgStatus = {
OK : 200,
USER_NOT_REGISTERED : 3,
ILLEGAL_COMMAND : 2,
NOT_ALLOWED : 6,
NO_ACTIVE_DASHBOARD : 8,
INVALID_TOKEN : 9,
ILLEGAL_COMMAND_BODY : 11,
DEVICE_WENT_OFFLINE : 18
};
function getCommandByString(cmdString) {
switch (cmdString) {
case "ping" :
return MsgType.PING;
case "login" :
return MsgType.LOGIN;
case "getToken" :
return MsgType.GET_TOKEN;
case "createDash":
return MsgType.CREATE_DASH;
case "deleteDash":
return MsgType.DELETE_DASH;
case "activate":
return MsgType.ACTIVATE_DASHBOARD;
case "createWidget":
return MsgType.CREATE_WIDGET;
case "hardware" :
return MsgType.HARDWARE;
case "loadprofilegzipped" :
return MsgType.LOAD_PROFILE_GZIPPED;
}
}
function getStringByCommandCode(cmd) {
switch (cmd) {
case 0 :
return "RESPONSE";
case 20 :
return "HARDWARE";
}
}
function getStatusByCode(statusCode) {
switch (statusCode) {
case 200 :
return "OK";
case 2 :
return "ILLEGAL_COMMAND";
case 3 :
return "USER_NOT_REGISTERED";
case 6 :
return "NOT_ALLOWED";
case 8 :
return "NO_ACTIVE_DASHBOARD";
case 9 :
return "INVALID_TOKEN";
case 11 :
return "ILLEGAL_COMMAND_BODY";
case 18 :
return "DEVICE_WENT_OFFLINE";
}
}
module.exports.createClient = function(host, port) {
return new BlynkAppClient(host, port);
}