UNPKG

node-red-contrib-melsec

Version:

Node-RED Node to communicate with Mitsubishi FX over Programming Port

446 lines (365 loc) 13.3 kB
//@ts-check /* Copyright: (c) 2021, ST-One GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) */ try { var {melsecAdapter} = require('@protocols/node-melsec'); } catch (error) { var melsecAdapter = null; } const MIN_CYCLE_TIME = 50; module.exports = function (RED) { // ----------- Melsec Endpoint ----------- function generateStatus(status, val) { var obj; if (typeof val != 'string' && typeof val != 'number' && typeof val != 'boolean') { val = RED._('melsec.endpoint.status.online'); } switch (status) { case 'online': obj = { fill: 'green', shape: 'dot', text: val.toString() }; break; case 'offline': obj = { fill: 'red', shape: 'dot', text: RED._('melsec.endpoint.status.offline') }; break; case 'connecting': obj = { fill: 'yellow', shape: 'dot', text: RED._('melsec.endpoint.status.connecting') }; break; default: obj = { fill: 'grey', shape: 'dot', text: RED._('melsec.endpoint.status.unknown') }; } return obj; } function createTranslationTable(vars) { var res = {}; vars.forEach(function (elm) { if (!elm.name || !elm.addr) { //skip incomplete entries return; } res[elm.name] = elm.addr; }); return res; } function equals(a, b) { if (a === b) return true; if (a == null || b == null) return false; if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime(); if (Array.isArray(a) && Array.isArray(b)) { if (a.length != b.length) return false; for (var i = 0; i < a.length; ++i) { if (a[i] !== b[i]) return false; } return true; } return false; } function nrInputShim(node, fn) { node.on('input', function (msg, send, done) { send = send || node.send; done = done || (err => err && node.error(err, msg)); fn(msg, send, done); }); } // <Begin> --- Endpoint --- function MelsecEndpoint(config) { let oldValues = {}; let readInProgress = false; let readDeferred = 0; let currentCycleTime = config.cycletime; let _cycleInterval; let _reconnectTimeout = null; let connected = false; let status; let that = this; let melsec = null; RED.nodes.createNode(this, config); //avoids warnings when we have a lot of Melsec In nodes this.setMaxListeners(0); function manageStatus(newStatus) { if (status == newStatus) return; status = newStatus; that.emit('__STATUS__', status); } function doCycle() { if (!readInProgress && connected) { melsec.readAllAddresses().then(cycleCallback).catch(e => { that.error(e); readInProgress = false; }); readInProgress = true; } else { readDeferred++; } } function cycleCallback(values) { readInProgress = false; if (readDeferred && connected) { doCycle(); readDeferred = 0; } manageStatus('online'); var changed = false; that.emit('__ALL__', values); Object.keys(values).forEach(function (key) { if (!equals(oldValues[key], values[key])) { changed = true; that.emit(key, values[key]); that.emit('__CHANGED__', { key: key, value: values[key] }); oldValues[key] = values[key]; } }); if (changed) that.emit('__ALL_CHANGED__', values); } function updateCycleTime(interval) { let time = parseInt(interval); if (isNaN(time) || time < 0) { that.error(RED._("melsec.endpoint.error.invalidtimeinterval", { interval: interval })); return false } clearInterval(_cycleInterval); // don't set a new timer if value is zero if (!time) return false; if (time < MIN_CYCLE_TIME) { that.warn(RED._("melsec.endpoint.info.cycletimetooshort", { min: MIN_CYCLE_TIME })); time = MIN_CYCLE_TIME; } currentCycleTime = time; _cycleInterval = setInterval(doCycle, time); return true; } function removeListeners() { melsec.removeListener('connect', onConnect); melsec.removeListener('disconnect', onDisconnect); melsec.removeListener('error', onError); } async function connect() { if (!melsecAdapter) return that.error('Missing "@protocols/node-melsec" dependency, avaliable only on the ST-One hardware. Please contact us at "st-one.io" for pricing and more information.') if (_reconnectTimeout !== null) { clearTimeout(_reconnectTimeout); _reconnectTimeout = null; } if (melsec !== null) { await melsec.close().catch(onError); removeListeners(); melsec = null; } melsec = new melsecAdapter(); melsec.on('connect', onConnect); melsec.on('disconnect', onDisconnect); melsec.on('error', onError); manageStatus('connecting'); melsec.open().catch((e) => { onError(e); onDisconnect(); }); } function onConnect() { readInProgress = false; readDeferred = 0; connected = true; manageStatus('online'); let _vars = createTranslationTable(config.vartable); melsec.setTranslationCB(k => _vars[k]); let varKeys = Object.keys(_vars); if (!varKeys || !varKeys.length) { that.warn(RED._("melsec.endpoint.info.novars")); } else { melsec.addAddress(varKeys); } updateCycleTime(currentCycleTime); } function onDisconnect() { manageStatus('offline'); connected = false; if (!_reconnectTimeout) { _reconnectTimeout = setTimeout(connect, 5000); } } function onError(e) { manageStatus('offline'); that.error(e && e.toString()); } function getStatus() { that.emit('__STATUS__', status); } function updateCycleEvent(obj) { if (connected) { obj.err = updateCycleTime(obj.msg.payload); that.emit('__UPDATE_CYCLE_RES__', obj); } } manageStatus('offline'); this.on('__DO_CYCLE__', doCycle); this.on('__UPDATE_CYCLE__', updateCycleEvent); this.on('__GET_STATUS__', getStatus); connect(); this.on('close', done => { manageStatus('offline'); if (_cycleInterval) clearInterval(_cycleInterval); if (_reconnectTimeout) clearTimeout(_reconnectTimeout); this.removeListener('__DO_CYCLE__', doCycle); this.removeListener('__UPDATE_CYCLE__', updateCycleEvent); this.removeListener('__GET_STATUS__', getStatus); removeListeners(); melsec.close() .then(done) .catch(e => { that.error(e); done(e); }); }); } RED.nodes.registerType('melsec fx endpoint', MelsecEndpoint); // <End> --- Endpoint // <Begin> --- Melsec In function MelsecIn(config) { RED.nodes.createNode(this, config); let statusVal; let that = this let endpoint = RED.nodes.getNode(config.endpoint); if (!endpoint) { that.error(RED._("melsec.error.missingconfig")); return; } function sendMsg(data, key, status) { if (key === undefined) key = ''; if (data instanceof Date) data = data.getTime(); var msg = { payload: data, topic: key }; statusVal = status !== undefined ? status : data; that.send(msg); endpoint.emit('__GET_STATUS__'); } function onChanged(variable) { sendMsg(variable.value, variable.key, null); } function onDataSplit(data) { Object.keys(data).forEach(function (key) { sendMsg(data[key], key, null); }); } function onData(data) { sendMsg(data, config.mode == 'single' ? config.variable : ''); } function onDataSelect(data) { onData(data[config.variable]); } function onEndpointStatus(status) { that.status(generateStatus(status, statusVal)); } endpoint.on('__STATUS__', onEndpointStatus); endpoint.emit('__GET_STATUS__'); if (config.diff) { switch (config.mode) { case 'all-split': endpoint.on('__CHANGED__', onChanged); break; case 'single': endpoint.on(config.variable, onData); break; case 'all': default: endpoint.on('__ALL_CHANGED__', onData); } } else { switch (config.mode) { case 'all-split': endpoint.on('__ALL__', onDataSplit); break; case 'single': endpoint.on('__ALL__', onDataSelect); break; case 'all': default: endpoint.on('__ALL__', onData); } } this.on('close', function (done) { endpoint.removeListener('__ALL__', onDataSelect); endpoint.removeListener('__ALL__', onDataSplit); endpoint.removeListener('__ALL__', onData); endpoint.removeListener('__ALL_CHANGED__', onData); endpoint.removeListener('__CHANGED__', onChanged); endpoint.removeListener('__STATUS__', onEndpointStatus); endpoint.removeListener(config.variable, onData); done(); }); } RED.nodes.registerType('melsec fx in', MelsecIn); // <End> --- Melsec In // <Begin> --- Melsec Control function MelsecControl(config) { let that = this; RED.nodes.createNode(this, config); let endpoint = RED.nodes.getNode(config.endpoint); if (!endpoint) { this.error(RED._("melsec.error.missingconfig")); return; } function onEndpointStatus(status) { that.status(generateStatus(status)); } function onMessage(msg, send, done) { let func = config.function || msg.function; switch (func) { case 'cycletime': endpoint.emit('__UPDATE_CYCLE__', { msg: msg, send: send, done: done }); break; case 'trigger': endpoint.emit('__DO_CYCLE__'); send(msg); done(); break; default: this.error(RED._("melsec.error.invalidcontrolfunction", { function: config.function }), msg); } } function onUpdateCycle(res) { let err = res.err; if (!err) { res.done(err); } else { res.send(res.msg); res.done(); } } endpoint.on('__STATUS__', onEndpointStatus); endpoint.on('__UPDATE_CYCLE_RES__', onUpdateCycle); endpoint.emit('__GET_STATUS__'); nrInputShim(this, onMessage); this.on('close', function (done) { endpoint.removeListener('__STATUS__', onEndpointStatus); endpoint.removeListener('__UPDATE_CYCLE_RES__', onUpdateCycle); done(); }); } RED.nodes.registerType("melsec fx control", MelsecControl); // <End> --- Melsec Control };