UNPKG

node-red-contrib-blynk-ws

Version:

Node Red integration with Blynk App and Server through websockets

396 lines (338 loc) 13 kB
const ws = require('ws'); const url = require('url'); const HttpsProxyAgent = require('https-proxy-agent'); const fs = require('fs'); const path = require('path'); const { compareVersions } = require('compare-versions'); // blynk util const blynkUtil = require('../libs/blynk-util'); // blynk lib const blynkLib = require('../libs/blynk-lib'); module.exports = (RED) => { const LIBRARY_VERSION = '1.0.10'; // node-red lib version const LIBRARY_DATE = '2025-01-19'; // node-red lib date const RECONNECT_TIMEOUT_SECONDS = 5; // number of seconds for reconnection when disconnected or socket error // A node red node that setup a websocket to server function BlynkClientNode(n) { const node = this; // compatibility: enable config node from old version < 0.8.0 library if (n.enabled === undefined) { n.enabled = true; // eslint-disable-line no-param-reassign } // Create a RED node RED.nodes.createNode(this, n); // Store local copies of the node configuration (as defined in the .html) this.path = n.path; this.key = n.key; this.dbg_all = n.dbg_all; this.dbg_read = n.dbg_read; this.dbg_write = n.dbg_write; this.dbg_notify = n.dbg_notify; this.dbg_mail = n.dbg_mail; this.dbg_prop = n.dbg_prop; this.dbg_low = n.dbg_low; this.dbg_sync = n.dbg_sync; this.dbg_bridge = n.dbg_bridge; this.dbg_pins = n.dbg_pins; this.multi_cmd = n.multi_cmd; this.proxy_type = n.proxy_type; this.proxy_url = n.proxy_url; this.enabled = n.enabled; this.log_pins = []; if (typeof this.dbg_pins === 'string') { const tmpPins = this.dbg_pins.split(',').map((m) => parseInt(m.trim(), 10)); for (let i = 0; i < tmpPins.length; i++) { tmpPins[i] = +tmpPins[i]; if (!Number.isNaN(tmpPins[i]) && tmpPins[i] >= 0 && tmpPins[i] <= 255) { this.log_pins.push(tmpPins[i].toString()); } } } this.inputNodes = []; // collection of nodes that want to receive events this.closing = false; this.logged = false; this.msgId = 1; this.LIBRARY_VERSION = LIBRARY_VERSION; this.LIBRARY_DATE = LIBRARY_DATE; this.RECONNECT_TIMEOUT_SECONDS = RECONNECT_TIMEOUT_SECONDS; this.RED = RED; // heartbit this.lastActivityIn = 0; this.lastActivityOut = 0; this.lastHeartBeat = 0; this.setMaxListeners(100); this.pinger = setInterval(() => { // only ping if connected and working if (node.logged) { const now = blynkUtil.getTimestamp(); const diff = now - node.lastHeartBeat; // node.log("PING " + now + " - " + diff + " - "+node.lastHeartBeat+" "+node.last_activity_in+" "+node.last_activity_out); if (diff >= blynkLib.BLYNK_HEARTBEAT) { node.ping(); } } }, 1000); this.msgList = {}; this.reconnect_timeout = null; node.log(`LOG PINS ${JSON.stringify(this.log_pins)}`); function reconnect() { node.logged = false; if (!node.closing) { node.websocket.terminate(); node.log(`Reconnect ### in ${node.RECONNECT_TIMEOUT_SECONDS} seconds...`); clearTimeout(node.reconnect_timeout); node.reconnect_timeout = setTimeout(() => { // startconn(); // eslint-disable-line no-use-before-define if (node.enabled) { startconn(); // eslint-disable-line no-use-before-define } else { node.emit('disabled'); setTimeout(() => { node.emit('disabled'); node.log('Connection disabled by configuration'); }, 2000); } }, node.RECONNECT_TIMEOUT_SECONDS * 1000); // try to reconnect } } function startconn() { // Connect to remote endpoint // should not start connection if no server or key node.logged = false; let wsArgs = {}; let wsLogSecure = ''; if (node.path.indexOf('blynk-cloud.com') !== -1) { // eslint-disable-next-line max-len node.warn('Warning: You are trying to connect to the Blynk Cloud Server (blynk-cloud.com) which has not been shut down permanently as of year end 2022'); } if (node.path.startsWith('wss://')) { wsArgs = { rejectUnauthorized: false, strictSSL: false, key: null, cert: null, ca: null, }; // valid cert for blynk cloud if (node.path.startsWith('wss://blynk-cloud.com')) { const certsPath = path.join(__dirname, '../cert'); // node.log("crt path " + certsPath); wsArgs.rejectUnauthorized = false; // Warning: interim fix for server cert expiration wsArgs.ca = fs.readFileSync(path.join(certsPath, 'server.crt')); } // check if node >= 8.6.4 and apply curve parameter for ssl if (compareVersions(process.version, '8.6.4') >= 0) { wsArgs.ecdhCurve = 'auto'; } wsLogSecure = 'secure '; } let proxy = ''; if (node.proxy_type === 'system') { proxy = process.env.http_proxy || ''; if (proxy !== '') proxy = `http://${proxy}`; } if (node.proxy_type === 'custom') proxy = node.proxy_url || ''; if (node.proxy_type !== undefined && node.proxy_type !== 'no' && node.proxy !== '') { node.log(`Using proxy server ${proxy}`); const options = url.parse(proxy); const agent = new HttpsProxyAgent(options); wsArgs.agent = agent; } node.log(`Start ### ${wsLogSecure}connection: ${node.path}`); const websocket = new ws(node.path, wsArgs); // eslint-disable-line new-cap node.websocket = websocket; // keep for closing websocket.setMaxListeners(100); const currTimestamp = blynkUtil.getTimestamp(); node.lastHeartBeat = currTimestamp; node.lastActivityIn = currTimestamp; node.lastActivityOut = currTimestamp; websocket.on('open', () => { node.login(node.key); }); websocket.on('message', (msgData) => { let data = msgData.toString('binary'); // convert to string if (data.length > blynkLib.BLYNK_PROTOCOL_MAX_LENGTH) { node.warn(`Message too long: ${data.length}bytes`); node.sendRspIllegalCmd(node.msgId); return; } let msgcount = 0; if (node.dbg_low) { node.log(`RECV <- ${blynkUtil.messageToDebugString(data)}`); } while (data.length > 0) { msgcount++; if (msgcount > blynkLib.BLYNK_MAX_CMD_IN_MESSAGE) { node.warn(`Too Blynk commands in a single message: ${msgcount}`); node.sendRspIllegalCmd(node.msgId); break; } const cmd = blynkUtil.decodeCommand(data); data = data.substr(cmd.msgLength); // remove current message from data node.processCommand(cmd); } // process message }); websocket.on('close', () => { node.log(`Websocket closed: ${node.path}`); resetReconnect(); // eslint-disable-line no-use-before-define }); websocket.on('error', (err) => { node.error(`Websocket ${err}`); resetReconnect(); // eslint-disable-line no-use-before-define }); } function resetReconnect() { node.logged = false; if (node.websocket) { node.websocket.terminate(); } if (node.reconnect_timeout) { clearTimeout(node.reconnect_timeout); } reconnect(); } node.on('close', (removed, done) => { node.log('Client Close'); node.closing = true; if (!removed) resetReconnect(); if (done) { done(); } }); node.on('error', () => { node.log('Client Error'); node.closing = false; resetReconnect(); }); // test enabled check if (this.enabled) { startconn(); // start outbound connection } else { node.emit('disabled'); setTimeout(() => { node.emit('disabled'); node.log('Connection disabled by configuration'); }, 2000); } } RED.nodes.registerType('blynk-ws-client', BlynkClientNode); BlynkClientNode.prototype.registerInputNode = function registerInputNode(handler) { let pinlog = ''; if (handler.pin !== undefined) { pinlog = ` pin: ${handler.pin}`; } this.log(`${'Register input node - type: '}${handler.nodeType}${pinlog}`); this.inputNodes.push(handler); }; BlynkClientNode.prototype.removeInputNode = function removeInputNode(handler) { let pinlog = ''; if (handler.pin !== undefined) { pinlog = ` pin: ${handler.pin}`; } this.log(`${'Remove input node - type: '}${handler.nodeType}${pinlog}`); this.inputNodes.forEach((node, i, inputNodes) => { if (node === handler) { inputNodes.splice(i, 1); } }); }; BlynkClientNode.prototype.registerGenericNode = function registerGenericNode(handler, name) { let pinlog = ''; let nodeType = ''; if (handler.pin !== undefined) { pinlog = ` pin: ${handler.pin}`; } if (handler.nodeType !== undefined) { nodeType = ` type: ${handler.nodeType}`; } this.log(`Register ${name} node${nodeType}${pinlog}`); }; /* check is pin need to be logged */ BlynkClientNode.prototype.isLogPin = function isLogPin(pin) { return this.log_pins.indexOf(pin) > -1; }; /* Load property from Blink lib and add as client prototype */ const skipProperty = ['BLYNK_VERSION', 'BLYNK_HEARTBEAT', 'BLYNK_PROTOCOL_MAX_LENGTH', 'BLYNK_MAX_CMD_IN_MESSAGE']; const validProperty = Object.keys(blynkLib).filter((item) => (skipProperty.indexOf(item) === -1)); for (let i = 0; i < validProperty.length; i++) { BlynkClientNode.prototype[validProperty[i]] = blynkLib[validProperty[i]]; } BlynkClientNode.prototype.handleWriteEvent = function handleWriteEvent(command) { if (this.dbg_all || this.dbg_write || this.isLogPin(command.pin)) { this.log(`writeEvent: -> cmd ${JSON.stringify(command)}`); } const writeNodeTypes = ['write', 'zergba', 'style-btn']; /* 'image-gallery' */ for (let i = 0; i < this.inputNodes.length; i++) { if ((writeNodeTypes.indexOf(this.inputNodes[i].nodeType) !== -1) && (this.inputNodes[i].pin === command.pin || this.inputNodes[i].pin_all)) { let msg; switch (this.inputNodes[i].nodeType) { case 'write': case 'style-btn': case 'image-gallery': msg = { payload: command.value, pin: command.pin, }; if (command.array) { msg.arrayOfValues = command.array; } break; case 'zergba': msg = { payload: command.value, pin: command.pin, }; if (Array.isArray(command.array) && command.array.length === 3) { msg.hex = blynkUtil.rgbToHex(parseInt(command.array[0], 10), parseInt(command.array[1], 10), parseInt(command.array[2], 10)); const color = blynkUtil.hexToRgb(msg.hex); msg.r = color.r; msg.g = color.g; msg.b = color.b; msg.rgb = `${color.r};${color.g};${color.b}`; msg.payload = [color.r, color.g, color.b]; this.inputNodes[i].status({ fill: 'green', shape: 'dot', text: `${this.inputNodes[i].connected_label} [${color.r}, ${color.g}, ${color.b}]`, }); } else { this.warn(RED._('blynk-ws-client.warn.zergba')); } break; default: return; } if (this.dbg_all || this.dbg_write || this.isLogPin(command.pin)) { this.log(`writeEvent: -> output ${JSON.stringify(msg)}`); } this.inputNodes[i].send(msg); } } }; BlynkClientNode.prototype.handleReadEvent = function handleReadEvent(command) { if (this.dbg_all || this.dbg_read || this.isLogPin(command.pin)) { this.log(`readEvent: -> cmd ${JSON.stringify(command)}`); } for (let i = 0; i < this.inputNodes.length; i++) { if (this.inputNodes[i].nodeType === 'read' && (this.inputNodes[i].pin == command.pin || this.inputNodes[i].pin_all)) { // eslint-disable-line eqeqeq, max-len const msg = { payload: command.pin, }; if (this.dbg_all || this.dbg_read || this.isLogPin(command.pin)) { this.log(`readEvent: -> output ${JSON.stringify(msg)}`); } this.inputNodes[i].send(msg); } } }; BlynkClientNode.prototype.handleAppEvent = function handleAppEvent(command) { for (let i = 0; i < this.inputNodes.length; i++) { if (this.inputNodes[i].nodeType === 'app') { const msg = { payload: command, }; this.inputNodes[i].send(msg); } } }; };