UNPKG

node-red-contrib-blynk-ws

Version:

Node Red integration with Blynk App and Server through websockets

452 lines (409 loc) 13.3 kB
// blynk enum const srs = require('secure-random-string'); const utf8 = require('utf8'); const blynkEnum = require('./blynk-enum'); const MsgStatus = blynkEnum.MsgStatus; const MsgType = blynkEnum.MsgType; // blynk util const blynkUtil = require('./blynk-util'); const messageToDebugString = blynkUtil.messageToDebugString; const commandToDebugString = blynkUtil.commandToDebugString; // var decodeCommand = blynkUtil.decodeCommand; const blynkHeader = blynkUtil.blynkHeader; const getStatusByCode = blynkUtil.getStatusByCode; const getTimestamp = blynkUtil.getTimestamp; // iniziate SRS srs({ length: 16, alphanumeric: true }); /* ##### BLYNK STUFF ###### */ // C++ Library Version // 0.4.7 - 2017-04-09 // 0.5.1 - 2018-02-20 // 0.5.2 - 2018-03-04 // 0.5.3 - 2018-06-11 // 0.5.4 - 2018-09-05 // 0.6.0 - 2018-02-01 // 0.6.1 - 2018-02-19 // Server Version // 0.23.5 - 2017-04-07 // 0.32.2 - 2018-02-28 // 0.33.3 - 2018-03-20 // 0.34.0 - 2018-04-06 // 0.36.2 - 2018-05-11 // 0.39.4 - 2018-07-29 // Blynk library constant const BLYNK_VERSION = '0.6.1'; // blynk library version const BLYNK_HEARTBEAT = 15; // seconds const BLYNK_PROTOCOL_MAX_LENGTH = 32767; // java Short.MAX_VALUE const BLYNK_MAX_CMD_IN_MESSAGE = 1024; // max command in a single message // ### PROTOCOL ### /* build a blynk command */ const blynkCmd = function blynkCmd(msgType, vals, forceMsgId) { const self = this; let msgId = forceMsgId; let values = vals || ['']; if (msgType === MsgType.RSP) { return blynkHeader(msgType, (forceMsgId - 1), values); } // set id and check max 2 byte limit if (self.msgId > 0xFFFF) self.msgId = 1; if (msgId > 0xFFFF) msgId = 1; msgId = msgId || (self.msgId++); if (typeof values === 'string' || typeof values === 'number') { values = [values]; } // utf8 convert if (Array.isArray(values) && values.length > 0) { for (let i = 0; i < values.length; i++) { if (typeof values[i] === 'string') { values[i] = utf8.encode(values[i]); } } } const data = values.join('\0'); return blynkHeader(msgType, msgId, data.length) + data; }; const sendRsp = function sendRsp(msgType, msgId, msgLen, msgData) { const data = msgData || ''; let msg = null; if (msgType === MsgType.RSP) { msg = this.blynkCmd(msgType, msgLen, msgId); } else { msg = this.blynkCmd(msgType, data, msgId); } this.sendMsg(msg); }; /* send binary message througth websocket */ const sendMsg = function sendMsg(data) { if (data.length < 5 || data === undefined) { this.log('ERROR sendMsg - No data to send'); return; } if (data.length > BLYNK_PROTOCOL_MAX_LENGTH) { this.log(`ERROR sendMsg - Message too long: ${data.length}bytes`); return; } if (this.dbg_low) { this.log(`SEND -> ${messageToDebugString(data)}`); } // convert to Uint8Array const rawdata = new Uint8Array(data.length); for (let i = 0, j = data.length; i < j; ++i) { rawdata[i] = data.charCodeAt(i); } const currTimestamp = getTimestamp(); this.lastHeartBeat = currTimestamp; this.lastActivityOut = currTimestamp; // this.log(rawdata); this.websocket.send(rawdata); }; /* Send multiple command message - delete key */ const sendMsgMulti = function sendMsgMulti(key) { this.sendMsg(this.msgList[key]); delete this.msgList[key]; // unset key }; /* Start multiple command message - generate key */ const startMsgMulti = function startMsgMulti() { const key = srs({ length: 16, alphanumeric: true }); this.msgList[key] = ''; return key; }; const processCommand = function processCommand(cmd) { const RED = this.RED; if (!this.logged) { if (cmd.type === MsgType.RSP && cmd.msgId === 1) { if ( cmd.len === MsgStatus.OK || cmd.len === MsgStatus.ALREADY_REGISTERED ) { this.log('Client logged'); this.logged = true; this.sendInfo(); this.emit('connected', ''); } else if (cmd.len === MsgStatus.INVALID_TOKEN) { this.log('Invalid auth token'); } else if (cmd.len === MsgStatus.NOT_AUTHENTICATED) { this.log('Not autenticated'); } else { this.log(`Connect failed. code: ${cmd.len}`); } } if (cmd.type === MsgType.RSP && cmd.len === MsgStatus.NOT_AUTHENTICATED) { this.log('Not autenticated'); } if (cmd.type === MsgType.REDIRECT) { // handle server redirect let schema = 'ws://'; let port = 80; if (this.path.startsWith('wss://')) { schema = 'wss://'; port = 443; } const values = cmd.body.split('\0'); const serverip = values[0]; if (values[1] > 0) port = values[1]; const newpath = `${schema + serverip}:${port}/websockets`; this.path = newpath; this.warn(RED._(`Connection redirecting to: ${newpath}`)); } } else { // logged // update received activity not update heartbeat if (cmd.type !== MsgType.RSP) { this.lastActivityIn = getTimestamp(); } switch (cmd.type) { case MsgType.RSP: if (cmd.status !== MsgStatus.OK) { // handle not ok response message let error = false; for (const k in MsgStatus) {// eslint-disable-line if (MsgStatus.hasOwnProperty(k)) { // eslint-disable-line if (cmd.status == MsgStatus[k]) error = true; // eslint-disable-line } } if (error) { // handle know error switch (cmd.status) { case MsgStatus.NOT_AUTHENTICATED: this.log('Not autenticated'); break; case MsgStatus.INVALID_TOKEN: this.log('Invalid auth token'); break; case MsgStatus.ILLEGAL_COMMAND_BODY: // this.log("Illegal command body"); break; default: this.warn( RED._(`Response Error: ${getStatusByCode(cmd.status)}`), ); break; } } else { this.warn( RED._(`Unhandled RSP type: ${commandToDebugString(cmd)}`), ); } } break; case MsgType.LOGIN: case MsgType.PING: this.sendRsp(MsgType.RSP, this.msgId, MsgStatus.OK); break; case MsgType.HW: case MsgType.BRIDGE: // this.warn(cmd.typeString+" cmd: " + JSON.stringify(cmd)); switch (cmd.operation) { // input nodes case 'vw': this.handleWriteEvent(cmd); break; case 'vr': this.handleReadEvent(cmd); break; case 'pm': // skip message "pin mode" break; default: this.warn( RED._( `Invalid ${ cmd.typeString } cmd: ${ commandToDebugString(cmd)}`, ), ); // this.sendRsp(MsgType.RSP, this.msgId, MsgStatus.ILLEGAL_COMMAND); this.sendRspIllegalCmd(this.msgId); } break; case MsgType.INTERNAL: switch (cmd.body) { // app event node case 'acon': case 'adis': this.handleAppEvent(cmd.body); break; default: this.warn( RED._(`Invalid INTERNAL cmd: ${commandToDebugString(cmd)}`), ); // this.sendRsp(MsgType.RSP, this.msgId, MsgStatus.ILLEGAL_COMMAND); } break; case MsgType.DEBUG_PRINT: this.log(`Server: ${cmd.body}`); break; default: this.warn(RED._(`Invalid header type: ${commandToDebugString(cmd)}`)); // this.sendRsp(MsgType.RSP, this.msgId, MsgStatus.ILLEGAL_COMMAND); this.sendRspIllegalCmd(this.msgId); } } }; // ### PROTOCOL COMMAND ### /* send login message */ const login = function login(token) { if (this.dbg_all) { this.log( `login -> ${ String(`********************************${token.slice(-5)}`).slice(-32)}`, ); } this.sendMsg(this.blynkCmd(MsgType.LOGIN, token, 1)); }; /* send Illegal command response message */ const sendRspIllegalCmd = function sendRspIllegalCmd(msgId) { this.sendRsp(MsgType.RSP, msgId, MsgStatus.ILLEGAL_COMMAND); }; /* send info message */ const sendInfo = function sendInfo() { const info = [ 'ver', BLYNK_VERSION, 'h-beat', BLYNK_HEARTBEAT, 'buff-in', BLYNK_PROTOCOL_MAX_LENGTH, 'dev', 'node-red', 'con', 'Blynk-ws', 'fw', this.LIBRARY_VERSION, 'build', this.LIBRARY_DATE, ]; this.msgId++; this.sendMsg(this.blynkCmd(MsgType.INTERNAL, info)); }; /* send ping message */ const ping = function ping() { if (this.dbg_all || this.dbg_read) { this.log('ping'); } this.lastHeartbeat = getTimestamp(); this.sendMsg(this.blynkCmd(MsgType.PING)); }; /* send syncAll message */ const syncAll = function syncAll(msgkey) { if (this.dbg_all || this.dbg_sync) { this.log('syncAll: -> all'); } const msg = this.blynkCmd(MsgType.HW_SYNC); if (this.multi_cmd && msgkey !== undefined) { this.msgList[msgkey] = this.msgList[msgkey] + msg; // eslint-disable-line operator-assignment } else this.sendMsg(msg); }; /* send syncVirtual message */ const syncVirtual = function syncVirtual(vpin, msgkey) { if (this.dbg_all || this.dbg_sync || this.isLogPin(vpin)) { this.log(`syncVirtual: -> ${JSON.stringify(['vr', vpin])}`); } const msg = this.blynkCmd(MsgType.HW_SYNC, ['vr', vpin]); if (this.multi_cmd && msgkey !== undefined) { this.msgList[msgkey] = this.msgList[msgkey] + msg; // eslint-disable-line operator-assignment } else this.sendMsg(msg); }; /* send virtualWrite message */ const virtualWrite = function virtualWrite(vpin, val, msgkey) { if (this.dbg_all || this.dbg_write || this.isLogPin(vpin)) { this.log(`virtualWrite: -> ${JSON.stringify(['vw', vpin].concat(val))}`); } const msg = this.blynkCmd(MsgType.HW, ['vw', vpin].concat(val)); if (this.multi_cmd && msgkey !== undefined) { this.msgList[msgkey] = this.msgList[msgkey] + msg; // eslint-disable-line operator-assignment } else this.sendMsg(msg); }; /* send setProperty message - set msgkey form multimple command message */ const setProperty = function setProperty(vpin, prop, val, msgkey) { if (this.dbg_all || this.dbg_prop || this.isLogPin(vpin)) { this.log(`setProperty -> ${JSON.stringify([vpin, prop].concat(val))}`); } const msg = this.blynkCmd(MsgType.PROPERTY, [vpin, prop].concat(val)); if (this.multi_cmd && msgkey !== undefined) { this.msgList[msgkey] = this.msgList[msgkey] + msg; // eslint-disable-line operator-assignment } else this.sendMsg(msg); }; const sendEmail = function sendEmail(to, subject, message) { if (this.dbg_all || this.dbg_mail) { this.log(`sendEmail -> ${JSON.stringify([to, subject, message])}`); } this.sendMsg(this.blynkCmd(MsgType.EMAIL, [to, subject, message])); }; const sendNotify = function sendNotify(message) { if (this.dbg_all || this.dbg_notify) { this.log(`sendNotify -> ${JSON.stringify([message])}`); } this.sendMsg(this.blynkCmd(MsgType.NOTIFY, [message])); }; /* send bridgeSetAuthToken message */ const bridgeSetAuthToken = function bridgeSetAuthToken(bpin, token) { if (this.dbg_all || this.dbg_bridge || this.isLogPin(bpin)) { const logtoken = String( `********************************${token.slice(-5)}`, ).slice(-32); this.log(`bridgeSetAuthToken: -> ${JSON.stringify([bpin, 'i', logtoken])}`); } const msg = this.blynkCmd(MsgType.BRIDGE, [bpin, 'i', token]); this.sendMsg(msg); }; /* send bridgeVirtualWrite message */ const bridgeVirtualWrite = function bridgeVirtualWrite(bpin, pin, val) { if (this.dbg_all || this.dbg_bridge || this.isLogPin(bpin)) { this.log( `bridgeVirtualWrite: -> ${JSON.stringify([bpin, 'vw', pin].concat(val))}`, ); } const msg = this.blynkCmd(MsgType.BRIDGE, [bpin, 'vw', pin].concat(val)); this.sendMsg(msg); }; /* send bridgeAnalogWrite message */ const bridgeAnalogWrite = function bridgeAnalogWrite(bpin, pin, val) { if (this.dbg_all || this.dbg_bridge || this.isLogPin(bpin)) { this.log( `bridgeAnalogWrite: -> ${JSON.stringify([bpin, 'aw', pin].concat(val))}`, ); } const msg = this.blynkCmd(MsgType.BRIDGE, [bpin, 'aw', pin].concat(val)); this.sendMsg(msg); }; /* send bridgeDigitalWrite message */ const bridgeDigitalWrite = function bridgeDigitalWrite(bpin, pin, val) { if (this.dbg_all || this.dbg_bridge || this.isLogPin(bpin)) { this.log( `bridgeDigitalWrite: -> ${JSON.stringify([bpin, 'dw', pin].concat(val))}`, ); } const msg = this.blynkCmd(MsgType.BRIDGE, [bpin, 'dw', pin].concat(val)); this.sendMsg(msg); }; module.exports = { // constants BLYNK_VERSION, BLYNK_HEARTBEAT, BLYNK_PROTOCOL_MAX_LENGTH, BLYNK_MAX_CMD_IN_MESSAGE, // protocol functions blynkCmd, sendRsp, sendMsg, sendMsgMulti, startMsgMulti, processCommand, // protocol commands login, sendInfo, sendRspIllegalCmd, ping, syncAll, syncVirtual, virtualWrite, setProperty, sendEmail, sendNotify, bridgeSetAuthToken, bridgeVirtualWrite, bridgeAnalogWrite, bridgeDigitalWrite, };