/* jshint -W097 */ /* jshint strict: false */ /* jslint node: true */ 'use strict'; const utils = require(__dirname + '/lib/utils'); let yuanqu = null; let fs; let adapter = utils.Adapter({ name: 'yuanqu', unload: stop }); process.on('SIGINT', stop); adapter.on('ready', function () { adapter.setState('info.connection', adapter.config.params.slave ? 0 : false, true); main(); }); adapter.on('message', function (obj) { if (obj) { switch (obj.command) { case 'listUart': adapter.log.warn('listUart'); adapter.sendTo(obj.from, obj.command, [{comName: 'Not available'}], obj.callback); break; } } }); function stop(callback) { if (yuanqu) { yuanqu.close(); yuanqu = null; } if (adapter && adapter.setState && adapter.config && adapter.config.params) { adapter.setState('info.connection', adapter.config.params.slave ? 0 : false, true); } if (typeof callback === 'function') callback(); setTimeout(function() { process.exit(); }, 5000); } let objects = {}; let enums = {}; let infoRegExp = new RegExp(adapter.namespace.replace('.', '\\.') + '\\.info\\.'); adapter.on('stateChange', (id, state) => { if (state && !state.ack && id && !infoRegExp.test(id)) { if (!yuanqu) { adapter.log.warn('No connection') } else { if (objects[id]) { yuanqu.write(id, state); } else { adapter.getObject(id, (err, data) => { if (!err) { objects[id] = data; yuanqu.write(id, state); } }); } } } }); function addToEnum(enumName, id, callback) { adapter.getForeignObject(enumName, function (err, obj) { if (!err && obj) { let pos = obj.common.members.indexOf(id); if (pos === -1) { obj.common.members.push(id); adapter.setForeignObject(obj._id, obj, function (err) { if (callback) callback(err); }); } else { if (callback) callback(err); } } else { if (callback) callback(err); } }); } function removeFromEnum(enumName, id, callback) { adapter.getForeignObject(enumName, function (err, obj) { if (!err && obj) { let pos = obj.common.members.indexOf(id); if (pos !== -1) { obj.common.members.splice(pos, 1); adapter.setForeignObject(obj._id, obj, function (err) { if (callback) callback(err); }); } else { if (callback) callback(err); } } else { if (callback) callback(err); } }); } function syncEnums(enumGroup, id, newEnumName, callback) { if (!enums[enumGroup]) { adapter.getEnum(enumGroup, function (err, _enums) { enums[enumGroup] = _enums; syncEnums(enumGroup, id, newEnumName, callback); }); return; } // try to find this id in enums let found = false; let count = 0; for (let e in enums[enumGroup]) { if (enums[enumGroup].hasOwnProperty(e) && enums[enumGroup][e].common && enums[enumGroup][e].common.members && enums[enumGroup][e].common.members.indexOf(id) !== -1) { if (enums[enumGroup][e]._id !== newEnumName) { count++; removeFromEnum(enums[enumGroup][e]._id, id, function () { if (!--count && typeof callback === 'function') callback(); }); } else { found = true; } } } if (!found && newEnumName) { count++; addToEnum(newEnumName, id, function () { if (!--count&& typeof callback === 'function') callback(); }); } if (!count && typeof callback === 'function') callback(); } const type_items_len = { 'uint8be': 1, 'string': 0, 'ieee754': 2 }; const _rmap = { 0: 15, 1: 14 }; const _dmap = { 0: 0, 1: 1 }; function address2alias(id, address, isDirect, offset) { if (typeof address === 'string') address = parseInt(address, 10); if (id === 'disInputs' || id === 'coils') { address = Math.floor(address / 16) * 16 + (isDirect ? _dmap[address % 16] : _rmap[address % 16]); address += offset; return address; } else { return address + offset; } } function createExtendObject(id, objData, callback) { adapter.getObject(id, function (err, oldObj) { if (!err && oldObj) { adapter.extendObject(id, objData, callback); } else { adapter.setObjectNotExists(id, objData, callback); } }); } function processTasks(tasks, callback) { if (!tasks || !tasks.length) { if (typeof callback === 'function') callback(); return; } let task = tasks.shift(); if (task.name === 'add') { createExtendObject(task.id, task.obj, function () { setTimeout(processTasks, 0, tasks, callback); }); } else if (task.name === 'del') { adapter.delObject(task.id, function () { setTimeout(processTasks, 0, tasks, callback); }); } else if (task.name === 'syncEnums') { syncEnums('rooms', task.id, task.obj, function () { setTimeout(processTasks, 0, tasks, callback); }); } else { throw 'Unknown task'; } } function prepareConfig(config) { let params = config.params; params.slave = parseInt(params.slave, 10) || 0; // required in stop let options = { config: { type: params.type || 'tcp', slave: params.slave, round: parseInt(params.round, 10) || 0, timeout: parseInt(params.timeout, 10) || 5000, defaultDeviceId: (params.deviceId === undefined || params.deviceId === null) ? 1 : (parseInt(params.deviceId, 10) || 0), }, devices: {} }; options.config.round = Math.pow(10, options.config.round); if (!options.config.slave) { options.config.multiDeviceId = params.multiDeviceId === true || params.multiDeviceId === 'true'; } let deviceIds = []; //checkDeviceIds(options, config.disInputs, deviceIds); //checkDeviceIds(options, config.coils, deviceIds); //checkDeviceIds(options, config.inputRegs, deviceIds); //checkDeviceIds(options, config.holdingRegs, deviceIds); deviceIds.sort(); // settings for master if (!options.config.slave) { options.config.poll = parseInt(params.poll, 10) || 1000; // default is 1 second options.config.recon = parseInt(params.recon, 10) || 60000; options.config.maxBlock = parseInt(params.maxBlock, 10) || 100; options.config.maxBoolBlock = parseInt(params.maxBoolBlock, 10) || 128; options.config.pulsetime = parseInt(params.pulsetime || 1000); } if (params.type === 'tcp' || params.type === 'tcprtu') { options.config.tcp = { port: parseInt(params.port, 10) || 502, bind: params.bind }; } for (let d = 0; d < deviceIds.length; d++) { let deviceId = deviceIds[d]; options.devices[deviceId] = {}; let device = options.devices[deviceId]; if (options.config.slave) { device.disInputs = { fullIds: [], changed: true, addressHigh: 0, addressLow: 0, values: [], mapping: {}, offset: parseInt(params.disInputsOffset, 10) }; device.coils = { fullIds: [], changed: true, addressHigh: 0, addressLow: 0, values: [], mapping: {}, offset: parseInt(params.coilsOffset, 10) }; device.inputRegs = { fullIds: [], changed: true, addressHigh: 0, addressLow: 0, values: [], mapping: {}, offset: parseInt(params.inputRegsOffset, 10) }; device.holdingRegs = { fullIds: [], changed: true, addressHigh: 0, addressLow: 0, values: [], mapping: {}, offset: parseInt(params.holdingRegsOffset, 10) }; } else { device.disInputs = { deviceId: deviceId, addressLow: 0, length: 0, config: [], blocks: [], offset: parseInt(params.disInputsOffset, 10) }; device.coils = { deviceId: deviceId, addressLow: 0, length: 0, config: [], blocks: [], cyclicWrite: [], // only holdingRegs and coils offset: parseInt(params.coilsOffset, 10) }; device.inputRegs = { deviceId: deviceId, addressLow: 0, length: 0, config: [], blocks: [], offset: parseInt(params.inputRegsOffset, 10) }; device.holdingRegs = { deviceId: deviceId, addressLow: 0, length: 0, config: [], blocks: [], cyclicWrite: [], // only holdingRegs and coils offset: parseInt(params.holdingRegsOffset, 10) }; } } options.objects = objects; return options; } function checkDeviceIds(options, config, deviceIds) { for (let i = config.length - 1; i >= 0; i--) { config[i].deviceId = !options.config.multiDeviceId ? options.config.defaultDeviceId : (config[i].deviceId !== undefined ? parseInt(config[i].deviceId, 10) : options.config.defaultDeviceId); if (isNaN(config[i].deviceId)) config[i].deviceId = options.config.defaultDeviceId; if (deviceIds.indexOf(config[i].deviceId) === -1) { deviceIds.push(config[i].deviceId); } } } function checkObjects(options, regType, regName, regFullName, tasks, newObjects) { let regs = options[regType]; for (let i = 0; regs.length > i; i++) { const id = adapter.namespace + '.' + regs[i].id; regs[i].fullId = id; objects[id] = { _id: regs[i].id, type: 'state', common: { name: regs[i].description, role: regs[i].role, type: regType === 'coils' || regType === 'disInputs' ? 'boolean' : ((regs[i].type === 'string' || regs[i].type === 'string') ? 'string' : 'number'), read: true, write: regType === 'coils' || regType === 'holdingRegs', def: regType === 'coils' || regType === 'disInputs' ? false : 0 }, native: { regType: regType, address: regs[i].address, deviceId: regs[i].deviceId } }; if (regType === 'coils') { objects[id].native.poll = regs[i].poll; objects[id].native.wp = regs[i].wp; } else if (regType === 'inputRegs' || regType === 'holdingRegs') { objects[id].common.unit = regs[i].unit || ''; objects[id].native.type = regs[i].type; objects[id].native.len = regs[i].len; objects[id].native.offset = regs[i].offset; objects[id].native.factor = regs[i].factor; if (regType === 'holdingRegs') { objects[id].native.poll = regs[i].poll; } } tasks.push({ id: regs[i].id, name: 'add', obj: objects[id] }); tasks.push({ id: id, name: 'syncEnums', obj: regs[i].room }); newObjects.push(id); } if (regs.length) { tasks.push({ id: regName, name: 'add', obj: { type: 'channel', common: { name: regFullName }, native: {} } }); } } function iterateAddresses(isBools, deviceId, result, regName, regType, localOptions) { const config = result.config; if (config && config.length) { result.addressLow = 0xFFFFFFFF; result.addressHigh = 0; for (let i = config.length - 1; i >= 0; i--) { if (config[i].deviceId !== deviceId) continue; const address = config[i].address = parseInt(config[i].address, 10); if (address < 0) { adapter.log.error(`Invalid ${regName} address: ${address}`); config.splice(i, 1); continue; } if (!isBools) { config[i].type = config[i].type || 'uint16be'; config[i].offset = parseFloat(config[i].offset) || 0; config[i].factor = parseFloat(config[i].factor) || 1; if (config[i].type === 'string') { config[i].len = parseInt(config[i].len, 10) || 1; } else { config[i].len = type_items_len[config[i].type]; } config[i].len = config[i].len || 1; } else { config[i].len = 1; } if (localOptions.multiDeviceId) { config[i].id = regName + '.' + deviceId + '.'; } else { config[i].id = regName + '.'; } if (localOptions.showAliases) { config[i].id += address2alias(regType, address, localOptions.directAddresses, result.offset); } else { config[i].id += address; } config[i].id += (config[i].name ? '_' + (config[i].name.replace('.', '_').replace(' ', '_')) : ''); // collect cyclic write registers if (config[i].cw) { result.cyclicWrite.push(adapter.namespace + '.' + config[i].id); } if (address < result.addressLow) result.addressLow = address; if (address + config[i].len > result.addressHigh) { result.addressHigh = address + (config[i].len || 1); } } const maxBlock = isBools ? localOptions.maxBoolBlock : localOptions.maxBlock; let lastAddress = null; let startIndex = 0; let blockStart = 0; let i; for (i = 0; i < config.length; i++) { if (config[i].deviceId !== deviceId) continue; if (lastAddress === null) { startIndex = i; blockStart = config[i].address; lastAddress = blockStart + config[i].len; } // try to detect next block if (result.blocks) { if ((config[i].address - lastAddress > 10 && config[i].len < 10) || (lastAddress - blockStart >= maxBlock)) { if (result.blocks.map(obj => obj.start).indexOf(blockStart) === -1) { result.blocks.push({start: blockStart, count: lastAddress - blockStart, startIndex: startIndex, endIndex: i}); } blockStart = config[i].address; startIndex = i; } } lastAddress = config[i].address + config[i].len; } if (lastAddress && lastAddress - blockStart && result.blocks && result.blocks.map(obj => obj.start).indexOf(blockStart) === -1) { result.blocks.push({start: blockStart, count: lastAddress - blockStart, startIndex: startIndex, endIndex: i}); } if (config.length) { result.length = result.addressHigh - result.addressLow; if (isBools && !localOptions.doNotRoundAddressToWord) { result.addressLow = Math.floor(result.addressLow / 16) * 16; if (result.length % 16) { result.length = (Math.floor(result.length / 16) + 1) * 16; } if (result.blocks) { for (let b = 0; b < result.blocks.length; b++) { result.blocks[b].start = Math.floor(result.blocks[b].start / 16) * 16; if (result.blocks[b].count % 16) { result.blocks[b].count = (Math.floor(result.blocks[b].count / 16) + 1) * 16; } } } } } else { result.length = 0; } if (result.mapping) { for (let i = 0; i < config.length; i++) { result.mapping[config[i].address - result.addressLow] = adapter.namespace + '.' + config[i].id; } } } } function parseConfig(callback) { adapter.log.warn('adapter.config=' + adapter.config); let options = prepareConfig(adapter.config); const params = adapter.config.params; adapter.log.warn('options=' + options); const localOptions = { multiDeviceId: options.config.multiDeviceId, showAliases: (params.showAliases === true || params.showAliases === 'true'), doNotRoundAddressToWord: (params.doNotRoundAddressToWord === true || params.doNotRoundAddressToWord === 'true'), directAddresses: (params.directAddresses === true || params.directAddresses === 'true'), maxBlock: options.config.maxBlock, maxBoolBlock: options.config.maxBoolBlock }; adapter.getForeignObjects(adapter.namespace + '.*', (err, list) => { let oldObjects = list; let newObjects = []; adapter.config.yz.sort(sortByAddress); adapter.config.disInputs.sort(sortByAddress); adapter.config.coils.sort(sortByAddress); adapter.config.inputRegs.sort(sortByAddress); adapter.config.holdingRegs.sort(sortByAddress); let tasks = []; for (let _deviceId in options.devices) { adapter.log.warn('_deviceId=' + _deviceId); if (!options.devices.hasOwnProperty(_deviceId)) continue; let device = options.devices[_deviceId]; let deviceId = parseInt(_deviceId, 10); device.disInputs.config = adapter.config.yz.filter(e => e.deviceId === deviceId); //device.disInputs.config = adapter.config.disInputs. filter(e => e.deviceId === deviceId); //device.coils.config = adapter.config.coils. filter(e => e.poll && e.deviceId === deviceId); //device.inputRegs.config = adapter.config.inputRegs. filter(e => e.deviceId === deviceId); //device.holdingRegs.config = adapter.config.holdingRegs.filter(e => e.poll && e.deviceId === deviceId); // Discrete inputs iterateAddresses(true, deviceId, device.yz, 'discreteInputs', 'disInputs', localOptions); //iterateAddresses(true, deviceId, device.disInputs, 'discreteInputs', 'disInputs', localOptions); //iterateAddresses(true, deviceId, device.coils, 'coils', 'coils', localOptions); //iterateAddresses(false, deviceId, device.inputRegs, 'inputRegisters', 'inputRegs', localOptions); //iterateAddresses(false, deviceId, device.holdingRegs, 'holdingRegisters', 'holdingRegs', localOptions); // ------------- create states and objects ---------------------------- checkObjects(adapter.config, 'yz', 'yz', 'yz', tasks, newObjects); //checkObjects(adapter.config, 'disInputs', 'discreteInputs', 'Discrete inputs', tasks, newObjects); //checkObjects(adapter.config, 'coils', 'coils', 'Coils', tasks, newObjects); //checkObjects(adapter.config, 'inputRegs', 'inputRegisters', 'Input registers', tasks, newObjects); //checkObjects(adapter.config, 'holdingRegs', 'holdingRegisters', 'Holding registers', tasks, newObjects); if (!options.config.multiDeviceId) { break; } } tasks.push({ id: 'info', name: 'add', obj: { type: 'channel', common: { name: 'info' }, native: {} } }); // create/ update 'info.connection' object adapter.getObject('info.connection', function (err, obj) { if (!obj) { obj = { type: 'state', common: { name: 'Number of connected partners', role: 'indicator.connected', write: false, read: true, type: options.config.slave ? 'number' : 'boolean' }, native: {} }; adapter.setObjectNotExists('info.connection', obj); } else if (options.config.slave && obj.common.type !== 'number') { obj.common.type = 'number'; obj.common.name = 'Number of connected masters'; adapter.setObjectNotExists('info.connection', obj); } else if (!options.config.slave && obj.common.type !== 'boolean') { obj.common.type = 'boolean'; obj.common.name = 'If master connected'; adapter.setObjectNotExists('info.connection', obj); } }); newObjects.push(adapter.namespace + '.info.connection'); // clear unused states for (let id_ in oldObjects) { if (oldObjects.hasOwnProperty(id_) && newObjects.indexOf(id_) === -1) { tasks.push({ id: id_, name: 'del' }); } } processTasks(tasks, function () { oldObjects = []; newObjects = []; adapter.subscribeStates('*'); callback(options); }); }); } function main() { parseConfig(options => { }); } function sortByAddress(a, b) { let ad = parseFloat(a.address); let bd = parseFloat(b.address); return ((ad < bd) ? -1 : ((ad > bd) ? 1 : 0)); }