UNPKG

node-red-contrib-m-bus

Version:
598 lines (474 loc) 17.1 kB
module.exports = function init(RED) { const clientEvents = { mbError: 'error', mbClose: 'log', mbConnect: 'log', mbReconnect: 'debug', mbScan: 'debug', mbPrimarySet: 'debug', mbScanComplete: 'debug', mbDeviceUpdated: 'debug', mbDevicesLoaded: 'debug', mbCommandExec: 'debug', mbCommandDone: 'debug', }; function MbusClientNode(config) { RED.nodes.createNode(this, config); const MbusMaster = require('node-mbus'); const jsonfile = require('jsonfile'); const DEVICES_FILE = 'mbus_devices'; const PRIMARY_ID = 'Primary_'; const DELAY_TIMEOUT = 3000; const MAX_QUEUE_DIM = 10; const node = this; const emptyDevice = { SlaveInformation: {}, DataRecord: [], error: 'Not Updated' }; // POLYFILL if (!Array.isArray) { Array.isArray = (arg) => Object.prototype.toString.call(arg) === '[object Array]'; } // ----- NODE CONFIGURATION VARS -------------------------------------------- const RECONNECT_TIMEOUT = parseInt(config.reconnectTimeout) || 5000; node.name = config.name; node.clienttype = config.clienttype; node.tcpHost = config.tcpHost; node.tcpPort = parseInt(config.tcpPort) || 10001; node.tcpTimeout = parseInt(config.tcpTimeout) || 4000; node.serialPort = config.serialPort; node.serialBaudrate = config.serialBaudrate; node.storeDevices = config.storeDevices; node.disableLogs = config.disableLogs; node.autoScan = config.autoScan !== false; // ----- PRIVATE VARS ------------------------------------------------------- let client = null; let lastStatus = null; let reconnectTimeout = null; let delayTimeout = null; const closeTimeout = null; let started = false; let closed = false; let reconnecting = false; let doingOperation = false; // data let devices = []; let errors = {}; let devicesData = {}; let updateIndex = 0; let controllerQueue = []; //-------------------------------------------------------------------------- // -------- PRIVATE FUNCTIONS ----------------------------------------------- //-------------------------------------------------------------------------- // ----- UTILS ----- // store new devices configuration function storeDevices() { jsonfile.writeFile(devicesFile(), devices, (err) => { if (err) emitEvent('mbError', { data: err.message, message: `Error while writing devices file ${err.message}` }); }); } // load devices from file function loadDevices(scanIfFail) { jsonfile.readFile(devicesFile(), (err, data) => { if (err) emitEvent('mbError', { data: err.message, message: `Error while reading devices file ${err.message}` }); if (isValidArray(data)) { emitEvent('mbDevicesLoaded', { data }); } else if (scanIfFail) { delayFunction(scanSecondary); } }); } // Add empty device if it has errors since first read function addEmptyDevice(id) { const tmp = JSON.parse(JSON.stringify(emptyDevice)); if (isSecondaryID(id)) { tmp.secondaryID = id; id = parseSecondaryID(id); } else { tmp.primaryID = id; id = PRIMARY_ID + id; } devicesData[id] = tmp; return tmp; } // returns the filepath for storing/retriving devices list file. Example: /home/pi/.node-red function devicesFile() { return `${RED.settings.userDir}/${DEVICES_FILE}${node.name ? `_${node.name}` : ''}.json`; } // return a device by using his secondary or primary id function getDevice(addr) { let device; if (isSecondaryID(addr)) { device = devicesData[parseSecondaryID(addr)]; } else if (devicesData[PRIMARY_ID + addr]) { device = devicesData[PRIMARY_ID + addr]; } else { for (const id in devicesData) { if (devicesData[id].primaryID === addr) { device = devicesData[id]; break; } } } return device; } function parseSecondaryID(id) { id = id.toString().substr(0, 8); id = parseInt(id); // remove leading 0 return id.toString(); } function isSecondaryID(id) { return id.toString().length === 16; } // Delay a function function delayFunction(fun) { delayTimeout = setTimeout(fun, DELAY_TIMEOUT); } // Check devices array is valid (just contains numbers or strings) function isValidArray(data) { if (!Array.isArray(data)) { return false; } for (let i = 0; i < data.length; i++) { if (typeof data[i] !== 'string' && typeof data[i] !== 'number') return false; } return true; } // read next device function readDevices() { if (closed) { emitClose(); return; } if (!node.autoScan) return; if (!devices || devices.length === 0) { emitEvent('mbError', { data: 'No device to update', message: 'No device to update' }); return; } // all devices read, restart from 0 if (updateIndex >= devices.length) updateIndex = 0; const addr = devices[updateIndex]; getData(addr, (err, data) => { // move index to next (even if there is an error) updateIndex++; if (err) { errors[addr] = true; // all devices have an error, restart connection if (Object.keys(errors).length === devices.length) { restartConnection(); } else node.doNextOperation(); } else { // successfull read // remove error device if present if (errors[addr]) delete errors[addr]; node.doNextOperation(); } }); } // wraps a function to queue function wrapFunction(fn, context, params) { return () => { fn.apply(context, params); }; } // emit an event with data and log it if it contains a message function emitEvent(event, data) { const logEvent = clientEvents[event]; if (logEvent) { lastStatus = { event, data }; if (data && data.data) { node.emit(event, data.data); } else { node.emit(event); } if (data && data.message && !node.disableLogs) node[logEvent](data.message); } } function emitClose() { emitEvent('mbClose', { data: 'Closed', message: 'Connection closed' }); } // ----- CONNECTION MANAGEMENT ------ function isConnected() { return client && client.connect(); } function initDevices() { if (devices) { for (let i = 0; i < devices.length; i++) { addEmptyDevice(devices[i]); } } } // connect the client function connect() { // do a connect just if client isn't already started or there is a reconnection if ((started && !reconnecting) || closed) return; started = true; const mbusOptions = { autoConnect: true }; if (node.clienttype === 'tcp') { mbusOptions.host = node.tcpHost; mbusOptions.port = node.tcpPort; mbusOptions.timeout = node.tcpTimeout; } else { mbusOptions.serialPort = node.serialPort; mbusOptions.serialBaudrate = node.serialBaudrate; } client = new MbusMaster(mbusOptions); try { client.connect((err) => { reconnecting = false; // re-enable reconnection if (err) { emitEvent('mbError', { data: err.message, message: `Error while connecting ${err.message}` }); restartConnection(); } else { emitEvent('mbConnect', { message: 'Connected' }); if (node.autoScan) { if (node.storeDevices) loadDevices(true); else delayFunction(scanSecondary); } } }); } catch (e) { emitEvent('mbError', { data: e.message, message: `Exception while connecting ${e.message}` }); reconnecting = false; restartConnection(); } } // close the client function close(cb) { if (client) { client.close((err) => { if (err) emitEvent('mbError', { data: err.message, message: `Error while closing client ${err.message}` }); emitClose(); cb(); }); } else { emitClose(); cb(); } } function restartConnection() { // stop reconnection if node is being closed or there is a reconnection in progress if (closed || reconnecting) return; reconnecting = true; emitEvent('mbReconnect', { message: 'Restarting client...' }); reconnectTimeout = setTimeout(() => { close(() => { connect(); }); }, RECONNECT_TIMEOUT); } // ----- M-BUS METHODS ------ function canDoOperation() { if (reconnecting || closed || !isConnected() || closeTimeout) { return false; } return true; } // get device addr data by primary or secondary ID function getData(addr, cb) { if (!canDoOperation()) { if (cb) cb(new Error('Connection not open'), null); emitEvent('mbError', { data: 'Connection not open', message: `Error while reading device ${addr}: Connection not open` }); return; } client.getData(addr, (err, data) => { let device = getDevice(addr); if (err) { emitEvent('mbError', { data: err.message, message: `Error while reading device ${addr}: ${err.message}` }); // add an empty device so I know if it has an error if (!device) device = addEmptyDevice(addr); device.lastUpdate = new Date(); device.error = err.message; } else { // successfull read const id = data.SlaveInformation.Id; if (isSecondaryID(addr)) { data.secondaryID = addr; } else { data.primaryID = addr; } if (!devicesData[id]) { // add the new device devicesData[id] = data; } else { // update the existing one // scanned using primary id if (data.primaryID && devicesData[PRIMARY_ID + data.primaryID]) { // there isn't any device with this id if (!devicesData[id]) { devicesData[id] = JSON.parse( JSON.stringify(devicesData[PRIMARY_ID + data.primaryID]), ); } delete devicesData[PRIMARY_ID + data.primaryID]; } // update id if (data.primaryID) { devicesData[id].primaryID = data.primaryID; } else { devicesData[id].secondaryID = data.secondaryID; } devicesData[id].SlaveInformation = data.SlaveInformation; devicesData[id].DataRecord = data.DataRecord; } // update last update and remove error devicesData[id].lastUpdate = new Date(); devicesData[id].error = null; // the device to return to the callback device = devicesData[id]; emitEvent('mbDeviceUpdated', { data: device }); } if (cb) cb(err, device); }); } // get device addr data function setPrimary(oldAddr, newAddr, cb) { if (!canDoOperation()) { if (cb) cb(new Error('Connection not open'), null); emitEvent('mbError', { data: 'Connection not open', message: `Error while setting primary address ${newAddr} to ${oldAddr}: Connection not open` }); return; } client.setPrimaryId(oldAddr, newAddr, (err) => { if (cb) cb(err); if (err) { emitEvent('mbError', { data: err.message, message: `Error while setting primary address ${newAddr} to ${oldAddr}: ${err.message}` }); } else { // successfull read emitEvent('mbPrimarySet', { data: { old: oldAddr, new: newAddr } }); } }); } // scan secondary IDs function scanSecondary(cb) { if (!canDoOperation()) { if (cb) cb(new Error('Connection not open'), null); emitEvent('mbError', { data: 'Connection not open', message: 'Error while scanning: Connection not open' }); return; } emitEvent('mbScan', { message: 'Scan started...' }); client.scanSecondary((err, data) => { if (cb) cb(err, data); if (err) { emitEvent('mbError', { data: err.message, message: `Error while scanning: ${err.message}` }); restartConnection(); } else { emitEvent('mbScanComplete', { data, message: 'Scan completed' }); } }); } //-------------------------------------------------------------------------- // -------- PUBLIC FUNCTIONS ------------------------------------------------ //-------------------------------------------------------------------------- // add a command to queue node.queueOperation = (functionName, data) => { let fn; try { // eslint-disable-next-line no-eval fn = eval(functionName); } catch (e) { emitEvent('mbError', { data: e.message, message: `Exception while queuing command ${e.message}` }); } if (fn) { controllerQueue.push({ fn: wrapFunction(fn, node, data), args: data, name: functionName, }); } if (controllerQueue.length > MAX_QUEUE_DIM) controllerQueue.shift(); if ((!node.autoScan || devices.length === 0) && !doingOperation) { node.doNextOperation(); } }; // dequeue a command or restart reading devices if no more commands in queue node.doNextOperation = () => { if (controllerQueue.length > 0) { // there are operation in queue const op = controllerQueue.shift(); doingOperation = true; const fnName = op.name; let message = 'Executing command: '; switch (fnName) { case 'setPrimary': message += `setPrimary Old=${op.args ? op.args[0] : 'null'} New=${op.args ? op.args[1] : 'null'}`; break; case 'getData': message += `getDevice ID=${op.args ? op.args[0] : 'null'}`; break; case 'scanSecondary': message += 'scan'; break; default: message += 'Undefined'; } emitEvent('mbCommandExec', { data: message, message }); op.fn(); } else { // no more queued operations, restart reads if (node.autoScan) readDevices(); doingOperation = false; } }; // get devices data and errors node.getStats = () => ({ devices: devicesData, errors }); // get currently status or 'closed' node.getStatus = () => lastStatus || { event: 'mbClose' }; node.restart = () => { restartConnection(); }; node.setDevices = (data) => { devices = data; storeDevices(); restartConnection(); }; // check for registered nodes using configuration node.registeredNodeList = {}; node.registerForMbus = (mbusNode) => { node.registeredNodeList[mbusNode.id] = mbusNode; if (Object.keys(node.registeredNodeList).length === 1) { node.isClosing = false; // start the client (don't use connect here, will stuck the process) restartConnection(); } }; //-------------------------------------------------------------------------- // -------- NODE EVENTS ----------------------------------------------------- //-------------------------------------------------------------------------- node.on('mbScanComplete', (data) => { devices = data; initDevices(); if (node.storeDevices) storeDevices(); readDevices(); }); node.on('mbDevicesLoaded', (data) => { devices = data; initDevices(); delayFunction(readDevices); }); // triggered when node is removed or project is redeployed node.on('close', (done) => { // init private vars devices = []; errors = {}; devicesData = {}; updateIndex = 0; started = false; lastStatus = null; controllerQueue = []; closed = true; // stop running timeouts if (reconnectTimeout) { clearTimeout(reconnectTimeout); reconnectTimeout = null; } if (delayTimeout) { clearTimeout(delayTimeout); delayTimeout = null; } // close client close(done); }); // end on 'close' } RED.nodes.registerType('mbus-client', MbusClientNode); RED.httpAdmin.get('/mbus/serial/ports', RED.auth.needsPermission('serial.read'), async (req, res) => { const SerialPort = require('serialport'); try { const ports = await SerialPort.list(); res.json(ports); } catch (error) { res.json([]); // eslint-disable-next-line no-console console.error(error); } }); };