UNPKG

node-red-contrib-blynk-api

Version:

Node Red integration with Blynk App and Server through api

861 lines (789 loc) 25.5 kB
/** * Copyright 2013,2015 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ module.exports = function(RED) { 'use strict'; var debug = require('debug')('blynk-api') var WebSocket = require('ws'); var inspect = require('util').inspect; var DataView = require('buffer-dataview'); var request = require('request'); //BLYNK STUFF function messageToDebugString(bufArray) { var dataview = new DataView(bufArray); var cmdString = getStringByCommandCode(dataview.getInt8(0)); var msgId = dataview.getUint16(1); var responseCode = getStatusByCode(dataview.getUint16(3)); return ( 'Command : ' + cmdString + ', msgId : ' + msgId + ', responseCode : ' + responseCode ); } function decodeCommand(bufArray) { var dataview = new DataView(bufArray); var command = {}; command.type = dataview.getInt8(0); command.typeString = getStringByCommandCode(dataview.getInt8(0)); command.msgId = dataview.getUint16(1); if (command.type == MsgType.HARDWARE) { command.len = dataview.getUint16(3); command.body = ''; for (var i = 0, offset = 5; i < command.len; i++, offset++) { //dataview.setInt8(offset, cmdBody.charCodeAt(i)); command.body += String.fromCharCode(dataview.getInt8(offset)); } if (command.body != '') { var values = command.body.split('\0'); if (values.length > 1) { command.operation = values[0]; command.pin = values[1]; if (values.length > 2) { command.value = values[2]; //we have an array of commands, return array as well command.array = values.slice(2, values.length); } } } } else { command.status = dataview.getUint16(3); } return command; } //send //create message //- get command by string //- build blynk message //send /* function send(socket, data) { if (socket.readyState == WebSocket.OPEN) { var commandAndBody = data.split(" "); var message = createMessage(commandAndBody); socket.send(message); } else { debug("The socket is not open."); } } function createMessage(commandAndBody) { var cmdString = commandAndBody[0]; var cmdBody = commandAndBody.length > 1 ? commandAndBody.slice(1).join('\0') : null; var cmd = getCommandByString(cmdString); return buildBlynkMessage(cmd, 1, cmdBody); } */ function encodeCommand(command, msgId, body) { var BLYNK_HEADER_SIZE = 5; var bodyLength = body ? body.length : 0; var bufArray = Buffer.alloc(BLYNK_HEADER_SIZE + bodyLength); var dataview = new DataView(bufArray); dataview.setInt8(0, command); 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 < body.length; i++, offset += 1) { dataview.setInt8(offset, body.charCodeAt(i)); } } return new Uint8Array(bufArray); } var MsgType = { RESPONSE: 0, LOGIN: 2, PING: 6, TWEET: 12, EMAIL: 13, NOTIFY: 14, BRIDGE: 15, HW_SYNC: 16, HW_INFO: 17, HARDWARE: 20 }; var MsgStatus = { OK: 200, ILLEGAL_COMMAND: 2, NO_ACTIVE_DASHBOARD: 8, INVALID_TOKEN: 9, ILLEGAL_COMMAND_BODY: 11 }; function getCommandByString(cmdString) { switch (cmdString) { case 'ping': return MsgType.PING; case 'login': return MsgType.LOGIN; case 'hardware': return MsgType.HARDWARE; } } 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 8: return 'NO_ACTIVE_DASHBOARD'; case 9: return 'INVALID_TOKEN'; case 11: return 'ILLEGAL_COMMAND_BODY'; } } //END BLYNK STUFF // A node red node that sets up a local websocket server function BlynkClientNode(n) { // Create a RED node RED.nodes.createNode(this, n); var node = this; // Store local copies of the node configuration (as defined in the .html) node.path = n.path; node.api = n.api; node.key = n.key; node.wholemsg = n.wholemsg === 'true'; node._inputNodes = []; // collection of nodes that want to receive events node._clients = {}; // match absolute url node.isServer = false; node.closing = false; node.working = false; node.setMaxListeners(100); node.pinger = setInterval(function() { //only ping if connected and working if (node.working) { node.ping(); } }, 5000); function startconn() { // Connect to remote endpoint //should not start connection if no server or key node.working = false; var socket = new WebSocket(node.path); //socket.binaryType = 'arraybuffer'; //probably does not work node.server = socket; // keep for closing handleConnection(socket); } function handleConnection(/*socket*/ socket) { var id = (1 + Math.random() * 4294967295).toString(16); socket.setMaxListeners(100); socket.on('open', function() { node.working = false; node.login(node.key); node.emit('opened', ''); }); socket.on('close', function() { node.emit('closed'); node.working = false; if (!node.closing) { node.tout = setTimeout(function() { startconn(); }, 5000); // try to reconnect every 5 secs... bit fast ? } }); socket.on('message', function(data, flags) { //should check if login message OK, then set to working state var cmd = decodeCommand(data); //first ok is a valid login if ( !node.working && cmd.type == MsgType.RESPONSE && cmd.status == MsgStatus.OK ) { // debug('valid login, start everything'); node.working = true; node.emit('connected', ''); //start ping } else { //should really just send command //node.handleEvent(id, socket, 'message', data, flags); switch (cmd.type) { //MsgType.LOGIN case MsgType.RESPONSE: //response, ignoring break; case MsgType.HARDWARE: switch (cmd.operation) { //input nodes case 'dw': case 'aw': case 'vw': node.handleWriteEvent(cmd); break; case 'dr': case 'ar': case 'vr': node.handleReadEvent(cmd); break; case 'pm': /** { type: 20, typeString: 'HARDWARE', msgId: 1, len: 39, body: 'pm\u00000\u0000out\u00001\u0000out\u00002\u0000out\u00003\u0000out\u00004\u0000out\u000023\u0000out', operation: 'pm', pin: '0', value: 'out', array: ['out', '1', 'out', '2', 'out', '3', 'out', '4', 'out', '23', 'out'] } */ break; default: console.log('Unhandled operation', cmd); } break; default: console.log('Unhandled response', cmd); } } }); socket.on('error', function(err) { node.emit('erro'); node.working = false; if (!node.closing) { node.tout = setTimeout(function() { startconn(); }, 5000); // try to reconnect every 5 secs... bit fast ? } }); } node.closing = false; startconn(); // start outbound connection node.on('close', function() { // Workaround https://github.com/einaros/ws/pull/253 // Remove listeners from RED.server node.closing = true; node.working = false; node.server.close(); if (node.tout) { clearTimeout(node.tout); } }); } RED.nodes.registerType('blynk-api-client', BlynkClientNode); BlynkClientNode.prototype.sendMessage = function (msg) { // Wait until the state of the socket is not ready and send the message when it is... var that = this; this.waitForSocketConnection(function () { debug("message sent!!!"); that.server.send(msg); }); } // Make the function wait until the connection is made... BlynkClientNode.prototype.waitForSocketConnection = function (callback) { var that = this; var socket = that.server; if (socket.readyState === 1) { debug("Connection is made") if (callback != null) { callback(); } if (that.socketTimeout) { clearTimeout(that.socketTimeout); } } else { if (that.socketTimeout) { clearTimeout(that.socketTimeout); that.socketTimeout = null; } that.socketTimeout = setTimeout( function () { debug("wait for connection...") that.waitForSocketConnection(callback); }, 5000); // wait 5 milisecond for the connection... } }; BlynkClientNode.prototype.registerInputNode = function(/*Node*/ handler) { this._inputNodes.push(handler); }; BlynkClientNode.prototype.removeInputNode = function(/*Node*/ handler) { this._inputNodes.forEach(function(node, i, inputNodes) { if (node === handler) { inputNodes.splice(i, 1); } }); }; BlynkClientNode.prototype.login = function(token) { this.sendMessage(encodeCommand(MsgType.LOGIN, 1, token)) }; BlynkClientNode.prototype.ping = function() { this.sendMessage(encodeCommand(MsgType.PING, 1, '')); }; BlynkClientNode.prototype.virtualWrite = function(node, val) { var that = this; var pinType = node.pin_type; var pin = node.pin; var typeConnect = node.type_connect; var values; switch (pinType) { case 'D': // Digital Pin values = ['dw', pin, val]; break; case 'A': // Analog Pin values = ['aw', pin, val]; break; default: // Virtual Pin values = ['vw', pin, val]; break; } var data = values.join('\0'); if (typeConnect == 'api') { var uri = this.api; uri = uri + this.key + '/update/' + pinType + pin; request( { method: 'PUT', uri: uri, headers: { 'Content-Type': 'application/json' }, body: '["' + val + '"]' }, function(error, response, body) { if (error) { that.sendMessage(encodeCommand(MsgType.HARDWARE, 1, data)); return; } } ); } else { that.sendMessage(encodeCommand(MsgType.HARDWARE, 1, data)); } }; BlynkClientNode.prototype.pinRead = function(node, val) { var server = this.server; var pinType = node.pin_type; var pin = node.pin; var typeConnect = node.type_connect; var values; var msg = {pin: pinType + pin}; switch (pinType) { case 'D': values = ['dr', pin]; break; case 'A': values = ['ar', pin]; break; default: // Virtual Pin values = ['vr', pin]; break; } var data = values.join('\0'); if (typeConnect == 'api') { var uri = this.api; uri = uri + this.key + '/get/' + pinType + pin; request(uri, function(error, response, body) { if (error) { // switch to socket return; } var data = JSON.parse(body); if(data && data.length) { msg.payload = data[0]; msg.timestamp = +new Date; node.send(msg); } else { node.error(body); } }); } else { // TODO: read pin value via socket // this.sendMessage(encodeCommand(MsgType.HARDWARE, 1, data)); node.error("TODO: read pin value via socket"); } }; BlynkClientNode.prototype.sendEmail = function(to, subject, message) { var values = [to, subject, message]; var data = values.join('\0'); this.sendMessage(encodeCommand(MsgType.EMAIL, null, data)); }; BlynkClientNode.prototype.handleWriteEvent = function(command) { for (var i = 0; i < this._inputNodes.length; i++) { if ( this._inputNodes[i].nodeType == 'write' && (this._inputNodes[i].pin_all || this._inputNodes[i].pin == command.pin) ) { var msg; msg = { payload: command.value, pin: command.pin }; switch (command.operation) { case 'dw': msg.pin_type = 'D'; break; case 'vw': msg.pin_type = 'V'; break; case 'aw': msg.pin_type = 'A'; break; default: console.error( 'handleWriteEvent command.operation not handle:', command.operation ); break; } if (command.array) { msg.arrayOfValues = command.array; } this._inputNodes[i].send(msg); } } }; BlynkClientNode.prototype.handleReadEvent = function(command) { //msg._session = {type:"websocket", id:id}; for (var i = 0; i < this._inputNodes.length; i++) { if ( this._inputNodes[i].nodeType == 'read' && (this._inputNodes[i].pin_all || this._inputNodes[i].pin == command.pin) ) { var msg; msg = { payload: /* this._inputNodes[i].pin ? this._inputNodes[i].pin : */ command.pin }; switch (command.operation) { case 'dr': msg.pin_type = 'D'; break; case 'vr': msg.pin_type = 'V'; break; case 'ar': msg.pin_type = 'A'; break; default: console.error( 'handleReadEvent command.operation not handle:', command.operation ); break; } this._inputNodes[i].send(msg); } } }; BlynkClientNode.prototype.handleEvent = function(command) { var msg; msg = { payload: 0 }; //msg._session = {type:"websocket", id:id}; for (var i = 0; i < this._inputNodes.length; i++) { if (this._inputNodes[i].nodeType == cmd.action) { this._inputNodes[i].send(msg); } } /*if (data instanceof Buffer) { console.log(">>>", data); console.log("Receive : " + messageToDebugString(data) + "\r\n"); } else { console.log("unexpected type : " + data + "\r\n"); } var msg; if (this.wholemsg) { try { msg = JSON.parse(data); } catch(err) { msg = { payload:data }; } } else { msg = { payload:data }; } msg._session = {type:"websocket",id:id}; for (var i = 0; i < this._inputNodes.length; i++) { this._inputNodes[i].send(msg); }*/ }; /* WebSocketListenerNode.prototype.broadcast = function(data) { try { this.sendMessage(data); } catch(e) { // swallow any errors this.warn("ws:"+i+" : "+e); } } */ /* WebSocketListenerNode.prototype.reply = function(id,data) { var session = this._clients[id]; if (session) { try { session.send(data); } catch(e) { // swallow any errors } } } */ function BlynkInReadNode(n) { RED.nodes.createNode(this, n); this.server = n.client ? n.client : n.server; var node = this; this.serverConfig = RED.nodes.getNode(this.server); this.nodeType = 'read'; this.pin = n.pin; this.pin_type = n.pin_type; this.pin_all = n.pin_all; if (this.serverConfig) { this.serverConfig.registerInputNode(this); // TODO: nls this.serverConfig.on('opened', function(n) { node.status({ fill: 'yellow', shape: 'dot', text: 'connecting ' + n }); }); this.serverConfig.on('connected', function(n) { node.status({ fill: 'green', shape: 'dot', text: 'connected ' + n }); }); this.serverConfig.on('erro', function() { node.status({ fill: 'red', shape: 'ring', text: 'error' }); }); this.serverConfig.on('closed', function() { node.status({ fill: 'red', shape: 'ring', text: 'disconnected' }); }); } else { this.error(RED._('websocket.errors.missing-conf')); } this.on('close', function() { node.serverConfig.removeInputNode(node); }); } RED.nodes.registerType('blynk-api-in-read', BlynkInReadNode); function BlynkInWriteNode(n) { RED.nodes.createNode(this, n); this.server = n.client ? n.client : n.server; var node = this; this.serverConfig = RED.nodes.getNode(this.server); this.nodeType = 'write'; this.pin = n.pin; this.pin_type = n.pin_type; this.pin_all = n.pin_all; if (this.serverConfig) { this.serverConfig.registerInputNode(this); // TODO: nls this.serverConfig.on('opened', function(n) { node.status({ fill: 'yellow', shape: 'dot', text: 'connecting ' + n }); }); this.serverConfig.on('connected', function(n) { node.status({ fill: 'green', shape: 'dot', text: 'connected ' + n }); }); this.serverConfig.on('erro', function() { node.status({ fill: 'red', shape: 'ring', text: 'error' }); }); this.serverConfig.on('closed', function() { node.status({ fill: 'red', shape: 'ring', text: 'disconnected' }); }); } else { this.error(RED._('websocket.errors.missing-conf')); } this.on('close', function() { node.serverConfig.removeInputNode(node); }); } RED.nodes.registerType('blynk-api-in-write', BlynkInWriteNode); function BlynkOutWriteNode(n) { RED.nodes.createNode(this, n); var node = this; this.server = n.client; this.pin = n.pin; this.pin_type = n.pin_type; this.type_connect = n.type_connect; this.serverConfig = RED.nodes.getNode(this.server); if (!this.serverConfig) { this.error(RED._('websocket.errors.missing-conf')); } else { // TODO: nls this.serverConfig.on('opened', function(n) { node.status({ fill: 'yellow', shape: 'dot', text: 'connecting ' + n }); }); this.serverConfig.on('connected', function(n) { node.status({ fill: 'green', shape: 'dot', text: 'connected ' + n }); }); this.serverConfig.on('erro', function() { node.status({ fill: 'red', shape: 'ring', text: 'error' }); }); this.serverConfig.on('closed', function() { node.status({ fill: 'red', shape: 'ring', text: 'disconnected' }); }); } this.on('input', function(msg) { var payload; // debug('writing'); /*if (this.serverConfig.wholemsg) { delete msg._session; payload = JSON.stringify(msg); } else if (msg.hasOwnProperty("payload")) { if (!Buffer.isBuffer(msg.payload)) { // if it's not a buffer make sure it's a string. payload = RED.util.ensureString(msg.payload); } else { payload = msg.payload; } } if (payload) { if (msg._session && msg._session.type == "websocket") { node.serverConfig.reply(msg._session.id, payload); } else { node.serverConfig.broadcast(payload,function(error){ if (!!error) { node.warn(RED._("websocket.errors.send-error")+inspect(error)); } }); } }*/ if (msg.hasOwnProperty('payload')) { if (!Buffer.isBuffer(msg.payload)) { // if it's not a buffer make sure it's a string. payload = RED.util.ensureString(msg.payload); } else { payload = msg.payload; } } if (payload) { // todo: check payload and validate // debug('write'); node.serverConfig.virtualWrite(node, payload); } }); } RED.nodes.registerType('blynk-api-out-write', BlynkOutWriteNode); RED.nodes.registerType('blynk-api-out-read', function(n) { RED.nodes.createNode(this, n); var node = this; this.server = n.client; this.pin_type = n.pin_type; this.pin = n.pin; this.type_connect = n.type_connect; this.serverConfig = RED.nodes.getNode(this.server); if (!this.serverConfig) { this.error(RED._('websocket.errors.missing-conf')); } else { // TODO: nls this.serverConfig.on('opened', function(n) { node.status({ fill: 'yellow', shape: 'dot', text: 'connecting ' + n }); }); this.serverConfig.on('connected', function(n) { node.status({ fill: 'green', shape: 'dot', text: 'connected ' + n }); }); this.serverConfig.on('erro', function() { node.status({ fill: 'red', shape: 'ring', text: 'error' }); }); this.serverConfig.on('closed', function() { node.status({ fill: 'red', shape: 'ring', text: 'disconnected' }); }); } this.on('input', function(msg) { var payload; if (msg.hasOwnProperty('payload')) { if (!Buffer.isBuffer(msg.payload)) { // if it's not a buffer make sure it's a string. payload = RED.util.ensureString(msg.payload); } else { payload = msg.payload; } } node.serverConfig.pinRead(node, payload); }); }); function BlynkOutEmailNode(n) { RED.nodes.createNode(this, n); var node = this; this.server = n.client; this.email = n.email; this.serverConfig = RED.nodes.getNode(this.server); if (!this.serverConfig) { this.error(RED._('websocket.errors.missing-conf')); } else { // TODO: nls this.serverConfig.on('opened', function(n) { node.status({ fill: 'yellow', shape: 'dot', text: 'connecting ' + n }); }); this.serverConfig.on('connected', function(n) { node.status({ fill: 'green', shape: 'dot', text: 'connected ' + n }); }); this.serverConfig.on('erro', function() { node.status({ fill: 'red', shape: 'ring', text: 'error' }); }); this.serverConfig.on('closed', function() { node.status({ fill: 'red', shape: 'ring', text: 'disconnected' }); }); } this.on('input', function(msg) { var payload; /*if (this.serverConfig.wholemsg) { delete msg._session; payload = JSON.stringify(msg); } else if (msg.hasOwnProperty("payload")) { if (!Buffer.isBuffer(msg.payload)) { // if it's not a buffer make sure it's a string. payload = RED.util.ensureString(msg.payload); } else { payload = msg.payload; } } if (payload) { if (msg._session && msg._session.type == "websocket") { node.serverConfig.reply(msg._session.id, payload); } else { node.serverConfig.broadcast(payload,function(error){ if (!!error) { node.warn(RED._("websocket.errors.send-error")+inspect(error)); } }); } }*/ if (msg.hasOwnProperty('payload')) { if (!Buffer.isBuffer(msg.payload)) { // if it's not a buffer make sure it's a string. payload = RED.util.ensureString(msg.payload); } else { payload = msg.payload; } } if (payload) { // todo: check payload and validate // debug('write'); var subject = payload; if (msg.topic) { subject = msg.topic; } node.serverConfig.sendEmail(node.email, subject, payload); } }); } RED.nodes.registerType('blynk-api-out-email', BlynkOutEmailNode); };