UNPKG

yunkong2.js-controller

Version:

Updated by reinstall.js on 2018-06-11T15:19:56.688Z

1,155 lines (1,071 loc) 242 kB
/* jshint -W097 */ /* jshint strict: false */ /* jslint node: true */ 'use strict'; // This is file, that makes all communication with controller. All options are optional except name. // following options are available: // name: name of the adapter. Must be exactly the same as directory name. // dirname: adapter directory name // instance: instance number of adapter // objects: true or false, if desired to have oObjects. This is a list with all states, channels and devices of this adapter and it will be updated automatically. // states: true or false, if desired to have oStates. This is a list with all states values and it will be updated automatically. // systemConfig: if required system configuration. Store it in systemConfig attribute // objectChange: callback function (id, obj) that will be called if object changed // stateChange: callback function (id, obj) that will be called if state changed // message: callback to inform about new message the adapter // unload: callback to stop the adapter // config: configuration of the connection to controller // noNamespace: return short names of objects and states in objectChange and in stateChange const net = require('net'); const fs = require('fs'); const extend = require('node.extend'); const util = require('util'); const EventEmitter = require('events').EventEmitter; const tools = require('./tools'); const pidusage = require('pidusage'); const getConfigFileName = tools.getConfigFileName; let schedule; const password = require('./password'); let config = null; let that; let defaultObjs; const FORBIDDEN_CHARS = /[\]\[*,;'"`<>\\?]/g; //const ACCESS_EVERY_EXEC = 0x1; const ACCESS_EVERY_WRITE = 0x2; const ACCESS_EVERY_READ = 0x4; //const ACCESS_EVERY_RW = ACCESS_EVERY_WRITE | ACCESS_EVERY_READ; //const ACCESS_EVERY_ALL = ACCESS_EVERY_WRITE | ACCESS_EVERY_READ | ACCESS_EVERY_EXEC; //const ACCESS_GROUP_EXEC = 0x10; const ACCESS_GROUP_WRITE = 0x20; const ACCESS_GROUP_READ = 0x40; //const ACCESS_GROUP_RW = ACCESS_GROUP_WRITE | ACCESS_GROUP_READ; //const ACCESS_GROUP_ALL = ACCESS_GROUP_WRITE | ACCESS_GROUP_READ | ACCESS_GROUP_EXEC; //const ACCESS_USER_EXEC = 0x100; const ACCESS_USER_WRITE = 0x200; const ACCESS_USER_READ = 0x400; //const ACCESS_USER_RW = ACCESS_USER_WRITE | ACCESS_USER_READ; //const ACCESS_USER_ALL = ACCESS_USER_WRITE | ACCESS_USER_READ | ACCESS_USER_EXEC; // const ACCESS_EXEC = 0x1; // const ACCESS_WRITE = 0x2; // const ACCESS_READ = 0x4; // const ACCESS_LIST = 'list'; // const ACCESS_DELETE = 'delete'; // const ACCESS_CREATE = 'create'; const ERROR_OBJ_NOT_FOUND = 'Not exists'; if (fs.existsSync(getConfigFileName())) { config = JSON.parse(fs.readFileSync(getConfigFileName(), 'utf8')); if (!config.states) config.states = {type: 'file'}; if (!config.objects) config.objects = {type: 'file'}; } else { throw 'Cannot find ' + getConfigFileName(); } /** * Adapter class * * @class * @param {string|object} options object like {name: "adapterName", systemConfig: true} or just "adapterName" * @return {object} object instance */ function Adapter(options) { if (!(this instanceof Adapter)) return new Adapter(options); if (!options || (!config && !options.config)) throw 'Configuration not set!'; if (options.config && !options.config.log) options.config.log = config.log; config = options.config || config; const regUser = /^system\.user\./; const regGroup = /^system\.group\./; let firstConnection = true; that = this; that.logList = []; // possible arguments // 0,1,.. - instance // info, debug, warn, error - log level // --force // --logs // --silent // --install if (process.argv) { for (let a = 1; a < process.argv.length; a++) { if (process.argv[a] === 'info' || process.argv[a] === 'debug' || process.argv[a] === 'error' || process.argv[a] === 'warn' || process.argv[a] === 'silly') { config.log.level = process.argv[a]; } else if (process.argv[a] === '--silent') { config.isInstall = true; process.argv[a] = '--install'; } else if (process.argv[a] === '--install') { config.isInstall = true; } else if (process.argv[a] === '--logs') { config.consoleOutput = true; } else if (process.argv[a] === '--force') { config.forceIfDisabled = true; } else if (parseInt(process.argv[a], 10).toString() === process.argv[a]) { config.instance = parseInt(process.argv[a], 10); } } } config.log.level = config.log.level || 'info'; if (config.log.noStdout && process.argv && process.argv.indexOf('--console') !== -1) { config.log.noStdout = false; } const logger = require(__dirname + '/logger.js')(config.log); // compatibility if (!logger.silly) { logger.silly = logger.debug; } // enable "var adapter = require(__dirname + '/../../lib/adapter.js')('adapterName');" call if (typeof options === 'string') options = {name: options}; if (!options) throw 'Empty options!'; if (!options.name) throw 'No name of adapter!'; // If installed as npm module if (options.dirname) { this.adapterDir = options.dirname.replace(/\\/g, '/'); } else { this.adapterDir = __dirname.replace(/\\/g, '/').split('/'); // it can be .../node_modules/appName.js-controller/node_modules/appName.adapter // .../appName.js-controller/node_modules/appName.adapter // .../appName.js-controller/adapter/adapter // remove "lib" this.adapterDir.pop(); const jsc = this.adapterDir.pop(); if ((jsc === tools.appName + '.js-controller' || jsc === tools.appName.toLowerCase() + '.js-controller') && this.adapterDir.pop() === 'node_modules') { // js-controller is installed as npm const appName = tools.appName.toLowerCase(); this.adapterDir = this.adapterDir.join('/'); if (fs.existsSync(this.adapterDir + '/node_modules/' + appName + '.' + options.name)) { this.adapterDir += '/node_modules/' + appName + '.' + options.name; } else if (fs.existsSync(this.adapterDir + '/node_modules/' + appName + '.js-controller/node_modules/' + appName + '.' + options.name)) { this.adapterDir += '/node_modules/' + appName + '.js-controller/node_modules/' + appName + '.' + options.name; } else if (fs.existsSync(this.adapterDir + '/node_modules/' + appName + '.js-controller/adapter/' + options.name)) { this.adapterDir += '/node_modules/' + appName + '.js-controller/adapter/' + options.name; } else if (fs.existsSync(this.adapterDir + '/node_modules/' + tools.appName + '.js-controller/node_modules/' + appName + '.' + options.name)) { this.adapterDir += '/node_modules/' + tools.appName + '.js-controller/node_modules/' + appName + '.' + options.name; } else { logger.error('Cannot find directory of adapter ' + options.name); process.exit(10); } } else { this.adapterDir = __dirname.replace(/\\/g, '/'); // remove "/lib" this.adapterDir = this.adapterDir.substring(0, this.adapterDir.length - 4); if (fs.existsSync(this.adapterDir + '/node_modules/' + tools.appName + '.' + options.name)) { this.adapterDir += '/node_modules/' + tools.appName + '.' + options.name; } else if (fs.existsSync(this.adapterDir + '/../node_modules/' + tools.appName + '.' + options.name)) { const parts = this.adapterDir.split('/'); parts.pop(); this.adapterDir = parts.join('/') + '/node_modules/' + tools.appName + '.' + options.name; } else { logger.error('Cannot find directory of adapter ' + options.name); process.exit(10); } } } if (fs.existsSync(this.adapterDir + '/package.json')) { this.pack = JSON.parse(fs.readFileSync(this.adapterDir + '/package.json', 'utf8')); } else { logger.info('Non npm module. No package.json'); } if (!this.pack || !this.pack.io) { if (fs.existsSync(this.adapterDir + '/io-package.json')) { this.ioPack = JSON.parse(fs.readFileSync(this.adapterDir + '/io-package.json', 'utf8')); } else { logger.error('Cannot find: ' + this.adapterDir + '/io-package.json'); process.exit(10); } } else { this.ioPack = this.pack.io; } // If required system configuration. Store it in systemConfig attribute if (options.systemConfig) that.systemConfig = config; let States; if (config.states && config.states.type) { if (config.states.type === 'file') { States = require(__dirname + '/states/statesInMemClient'); } else if (config.states.type === 'redis') { States = require(__dirname + '/states/statesInRedis'); } else { throw 'Unknown objects type: ' + config.states.type; } } else { States = require(__dirname + '/states'); } let Objects; if (config.objects && config.objects.type) { if (config.objects.type === 'file') { Objects = require(__dirname + '/objects/objectsInMemClient'); } else if (config.objects.type === 'redis') { Objects = require(__dirname + '/objects/objectsInRedis'); } else if (config.objects.type === 'couch') { Objects= require(__dirname + '/objects/objectsInCouch'); } else { throw 'Unknown objects type: ' + config.objects.type; } } else { Objects = require(__dirname + '/objects'); } const os = require('os'); const ifaces = os.networkInterfaces(); const ipArr = []; for (const dev in ifaces) { if (!ifaces.hasOwnProperty(dev)) continue; /*jshint loopfunc:true */ ifaces[dev].forEach(details => !details.internal && ipArr.push(details.address)); } const instance = (options.instance !== undefined) ? options.instance : (config.instance || 0); that.name = options.name; that.namespace = options.name + '.' + instance; that.users = []; // cache of user groups that.defaultHistory = null; that.autoSubscribe = null; // array of instances, that support auto subscribe that.inputCount = 0; that.outputCount = 0; that.FORBIDDEN_CHARS = FORBIDDEN_CHARS; // show to adapter unexaptable chars let reportInterval; let callbackId = 1; that.getPortRunning = null; /** * Helper function to find next free port * * Looks for first free TCP port starting with given one: * <pre><code> * adapter.getPort(8081, function (port) { * adapter.log.debug('Followinf port is free: ' + port); * }); * </code></pre> * * @alias getPort * @memberof Adapter * @param {number} port port number to start the search for free port * @param {function} callback return result * <pre><code>function (port) {}</code></pre> */ that.getPort = function adapterGetPort(port, callback) { if (!port) throw 'adapterGetPort: no port'; if (typeof port === "string") port = parseInt(port, 10); that.getPortRunning = {port: port, callback: callback}; const server = net.createServer(); try { server.listen(port, (/* err */) => { server.once('close', () => (typeof callback === 'function') && callback(port)); server.close(); }); server.on('error', (/* err */) => { setTimeout(() => that.getPort(port + 1, callback), 100); }); } catch (e) { setImmediate(() => that.getPort(port + 1, callback)); } }; /** * Promise-version of Adapter.getPort */ that.getPortAsync = tools.promisifyNoError(that.getPort, that); /** * validates user and password * * * @alias checkPassword * @memberof Adapter * @param {string} user user name as text * @param {string} pw password as text * @param {object} options optional user context * @param {function} callback return result * <pre><code> * function (result) { * adapter.log.debug('User is valid'); * } * </code></pre> */ that.checkPassword = function checkPassword(user, pw, options, callback) { if (typeof options === 'function') { callback = options; options = null; } if (!callback) throw 'checkPassword: no callback'; if (user && !regUser.test(user)) { user = 'system.user.' + user; } that.getForeignObject(user, options, (err, obj) => { if (err || !obj || !obj.common || (!obj.common.enabled && user !== 'system.user.admin')) { callback(false); return; } password(pw).check(obj.common.password, (err, res) => { callback(res); }); }); }; /** * Promise-version of Adapter.checkPassword */ that.checkPasswordAsync = tools.promisifyNoError(that.checkPassword, that); /** * sets the user's password * * @alias setPassword * @memberof Adapter * @param {string} user user name as text * @param {string} pw password as text * @param {object} options optional user context * @param {function} callback return result * <pre><code> * function (err) { * if (err) adapter.log.error('Cannot set password: ' + err); * } * </code></pre> */ that.setPassword = function setPassword(user, pw, options, callback) { if (typeof options === 'function') { callback = options; options = null; } if (user && !regUser.test(user)) { user = 'system.user.' + user; } that.getForeignObject(user, options, (err, obj) => { if (err || !obj) { if (typeof callback === 'function') callback('User does not exist'); return; } password(pw).hash(null, null, (err, res) => { if (err) { if (typeof callback === 'function') callback(err); return; } that.extendForeignObject(user, { common: { password: res } }, options, () => (typeof callback === 'function') && callback(null)); }); }); }; /** * Promise-version of Adapter.setPassword */ that.setPasswordAsync = tools.promisify(that.setPassword, that); /** * returns if user exists and is in the group * * This function used mostly internally and the adapter developer do not require it. * * @alias checkGroup * @memberof Adapter * @param {string} user user name as text * @param {string} group group name * @param {object} options optional user context * @param {function} callback return result * <pre><code> * function (result) { * if (result) adapter.log.debug('User exists and in the group'); * } * </code></pre> */ that.checkGroup = function checkGroup(user, group, options, callback) { if (typeof options === 'function') { callback = options; options = null; } if (user && !regUser.test(user)) { user = 'system.user.' + user; } if (group && !regGroup.test(group)) { group = 'system.group.' + group; } this.getForeignObject(user, options, (err, obj) => { if (err || !obj) { callback(false); return; } this.getForeignObject(group, options, (err, obj) => { if (err || !obj) { callback(false); return; } if (obj.common.members.indexOf(user) !== -1) { callback(true); } else { callback(false); } }); }); }; /** * Promise-version of Adapter.checkGroup */ that.checkGroupAsync = tools.promisifyNoError(that.checkGroup, that); /** @typedef {{[permission: string]: {type: 'object' | 'state' | '' | 'other' | 'file', operation: string}}} CommandsPermissions */ /** * get the user permissions * * This function used mostly internally and the adapter developer do not require it. * The function reads permissions of user's groups (it can be more than one) and merge permissions together * * @alias calculatePermissions * @memberof Adapter * @param {string} user user name as text * @param {CommandsPermissions} commandsPermissions object that describes the access rights like * <pre><code> * // static information * var commandsPermissions = { * getObject: {type: 'object', operation: 'read'}, * getObjects: {type: 'object', operation: 'list'}, * getObjectView: {type: 'object', operation: 'list'}, * setObject: {type: 'object', operation: 'write'}, * subscribeObjects: {type: 'object', operation: 'read'}, * unsubscribeObjects: {type: 'object', operation: 'read'}, * * getStates: {type: 'state', operation: 'list'}, * getState: {type: 'state', operation: 'read'}, * setState: {type: 'state', operation: 'write'}, * getStateHistory: {type: 'state', operation: 'read'}, * subscribe: {type: 'state', operation: 'read'}, * unsubscribe: {type: 'state', operation: 'read'}, * getVersion: {type: '', operation: ''}, * * httpGet: {type: 'other', operation: 'http'}, * sendTo: {type: 'other', operation: 'sendto'}, * sendToHost: {type: 'other', operation: 'sendto'}, * * readFile: {type: 'file', operation: 'read'}, * readFile64: {type: 'file', operation: 'read'}, * writeFile: {type: 'file', operation: 'write'}, * writeFile64: {type: 'file', operation: 'write'}, * unlink: {type: 'file', operation: 'delete'}, * rename: {type: 'file', operation: 'write'}, * mkdir: {type: 'file', operation: 'write'}, * readDir: {type: 'file', operation: 'list'}, * chmodFile: {type: 'file', operation: 'write'}, * * authEnabled: {type: '', operation: ''}, * disconnect: {type: '', operation: ''}, * listPermissions: {type: '', operation: ''}, * getUserPermissions: {type: 'object', operation: 'read'} * }; * </code></pre> * @param {object} options optional user context * @param {function} callback return result * <pre><code> * function (acl) { * // Access control object for admin looks like: * // { * // file: { * // read: true, * // write: true, * // 'delete': true, * // create: true, * // list: true * // }, * // object: { * // read: true, * // write: true, * // 'delete': true, * // list: true * // }, * // state: { * // read: true, * // write: true, * // 'delete': true, * // create: true, * // list: true * // }, * // user: 'admin', * // users: { * // read: true, * // write: true, * // create: true, * // 'delete': true, * // list: true * // }, * // other: { * // execute: true, * // http: true, * // sendto: true * // }, * // groups: ['administrator'] // can be more than one * // } * } * </code></pre> */ that.calculatePermissions = function (user, commandsPermissions, options, callback) { if (typeof options === 'function') { callback = options; options = null; } if (!regUser.test(user)) { user = 'system.user.' + user; } // read all groups let acl = {user: user}; if (user === 'system.user.admin') { acl.groups = ['system.group.administrator']; for (const c in commandsPermissions) { if (!commandsPermissions.hasOwnProperty(c) || !commandsPermissions[c].type) continue; acl[commandsPermissions[c].type] = acl[commandsPermissions[c].type] || {}; acl[commandsPermissions[c].type][commandsPermissions[c].operation] = true; } if (callback) callback(acl); return; } acl.groups = []; that.getForeignObjects('*', 'group', options, (err, groups) => { // aggregate all groups permissions, where this user is if (groups) { for (const g in groups) { if (!groups.hasOwnProperty(g)) continue; if (groups[g] && groups[g].common && groups[g].common.members && groups[g].common.members.indexOf(user) !== -1) { acl.groups.push(groups[g]._id); if (groups[g]._id === 'system.group.administrator') { acl = { file: { read: true, write: true, 'delete': true, create: true, list: true }, object: { read: true, write: true, 'delete': true, list: true }, state: { read: true, write: true, 'delete': true, create: true, list: true }, user: user, users: { read: true, write: true, create: true, 'delete': true, list: true }, other: { execute: true, http: true, sendto: true }, groups: acl.groups }; break; } const gAcl = groups[g].common.acl; try { for (const type in gAcl) { if (!gAcl.hasOwnProperty(type)) continue; // fix bug. Some version have user instead of users. if (type === 'user') { acl.users = acl.users || {}; } else { acl[type] = acl[type] || {}; } for (const op in gAcl[type]) { if (gAcl[type].hasOwnProperty(op)) { // fix error if (type === 'user') { acl.users[op] = acl.users[op] || gAcl.user[op]; } else { acl[type][op] = acl[type][op] || gAcl[type][op]; } } } } } catch (e) { that.log.error('Cannot set acl: ' + e); that.log.error('Cannot set acl: ' + JSON.stringify(gAcl)); that.log.error('Cannot set acl: ' + JSON.stringify(acl)); } } } } if (callback) callback(acl); }); }; /** * Promise-version of Adapter.calculatePermissions */ that.calculatePermissionsAsync = tools.promisifyNoError(that.calculatePermissions, that); function readFileCertificate(cert) { if (typeof cert === 'string') { try { if (cert.length < 1024 && fs.existsSync(cert)) { cert = fs.readFileSync(cert).toString(); // start watcher of this file fs.watch(cert, (eventType, filename) => { that.log.warn('New certificate "' + filename + '" detected. Restart adapter'); setTimeout(stop, 2000, false, true); }); } } catch (e) { // ignore } } return cert; } /** * returns SSL certificates by name * * This function returns SSL certificates (private key, public cert and chained certificate). * Names are defined in the system's configuration in admin, e.g. "defaultPrivate", "defaultPublic". * The result can be directly used for creation of https server. * * @alias getCertificates * @memberof Adapter * @param {string} publicName public certificate name * @param {string} privateName private certificate name * @param {string} chainedName optional chained certificate name * @param {function} callback return result * <pre><code> * function (err, certs) { * adapter.log.debug('private key: ' + certs.key); * adapter.log.debug('public cert: ' + certs.cert); * adapter.log.debug('chained cert: ' + certs.ca); * } * </code></pre> */ that.getCertificates = function (publicName, privateName, chainedName, callback) { if (typeof publicName === 'function') { callback = publicName; publicName = null; } if (typeof privateName === 'function') { callback = privateName; privateName = null; } if (typeof chainedName === 'function') { callback = chainedName; chainedName = null; } publicName = publicName || that.config.certPublic; privateName = privateName || that.config.certPrivate; chainedName = chainedName || that.config.certChained; // Load certificates that.getForeignObject('system.certificates', (err, obj) => { if (err || !obj || !obj.native.certificates || !publicName || !privateName || !obj.native.certificates[publicName] || !obj.native.certificates[privateName] || (chainedName && !obj.native.certificates[chainedName]) ) { that.log.error('Cannot enable secure web server, because no certificates found: ' + publicName + ', ' + privateName + ', ' + chainedName); if (callback) callback(ERROR_OBJ_NOT_FOUND); } else { let ca; if (chainedName) { const chained = readFileCertificate(obj.native.certificates[chainedName]).split('-----END CERTIFICATE-----\r\n'); ca = []; for (let c = 0; c < chained.length; c++) { if (chained[c].replace(/[\r\n|\r|\n]+/, '').trim()) { ca.push(chained[c] + '-----END CERTIFICATE-----\r\n'); } } } if (callback) { callback(null, { key: readFileCertificate(obj.native.certificates[privateName]), cert: readFileCertificate(obj.native.certificates[publicName]), ca: ca }, obj.native.letsEncrypt); } } }); }; /** * Promise-version of Adapter.getCertificates */ that.getCertificatesAsync = tools.promisify(that.getCertificates, that); /** * stops the execution of adapter, but not disables it. * * Sometimes, the adapter must be stopped if some libraries are missing. * * @alias terminate * @memberof Adapter * @param {string} reason optional termination description */ that.terminate = function (reason) { if (reason) logger.warn('Terminated: ' + reason); process.exit(11); }; /** * Restarts an instance of the adapter. * * @memberof Adapter */ that.restart = function restart() { // Restarting an adapter can easily be done by writing the adapter object without changing it process.exit(-100); }; /** * Updates the adapter config with new values. Only a subset of the configuration has to be provided, * since merging with the existing config is done automatically, e.g. like this: * * `adapter.updateConfig({prop1: "newValue1"})` * * After updating the configuration, the adapter is automatically restarted. * * @param {Record<string, any>} newConfig The new config values to be stored */ that.updateConfig = function updateConfig(newConfig) { // merge the old and new configuration const config = Object.assign({}, that.config, newConfig); // update the adapter config object const configObjId = `system.adapter.${that.namespace}`; that.getForeignObjectAsync(configObjId) .then(obj => { obj.native = config; return that.setForeignObjectAsync(configObjId, obj); }) .catch(err => logger.error(`Updating the adapter config failed: ${err}`)) ; }; /** * Disables and stops the adapter instance. */ that.disable = function disable() { // update the adapter config object const configObjId = `system.adapter.${that.namespace}`; that.getForeignObjectAsync(configObjId) .then(obj => { obj.common.enabled = false; return that.setForeignObjectAsync(configObjId, obj); }) .catch(err => logger.error(`Disabling the adapter instance failed: ${err}`)) ; }; // Can be later deleted if no more appears that.inited = false; initObjects(() => { if (that.inited) { if (that.log) that.log.warn('Reconnection to DB.'); return; } that.inited = true; // auto oObjects if (options.objects) { that.getAdapterObjects(objs => { that.oObjects = objs; that.subscribeObjects('*'); initStates(prepareInitAdapter); }); } else { initStates(prepareInitAdapter); } }); function createInstancesObjects(callback, objs) { if (!objs) { objs = that.ioPack.instanceObjects; } if (!objs || !objs.length) { callback(); } else { const obj = objs.shift(); that.getObject(obj._id, (err, _obj) => { if (!_obj) { if (obj.common) { if (obj.common.name) { obj.common.name = obj.common.name.replace('%INSTANCE%', instance); } if (obj.common.desc) { obj.common.desc = obj.common.desc.replace('%INSTANCE%', instance); } } that.setObject(obj._id, obj, err => { if (err && that.log) that.log.error('Cannot setObject: ' + err); setImmediate(createInstancesObjects, callback, objs); }); } else { setImmediate(createInstancesObjects, callback, objs); } }); } } function prepareInitAdapter() { that.getForeignState('system.adapter.' + that.namespace + '.alive', (err, res) => { if (options.instance !== undefined) { initAdapter(options); } else if (!config.isInstall && res && res.val === true && !config.forceIfDisabled) { logger.error(options.name + '.' + instance + ' already running'); process.exit(7); } else { that.getForeignObject('system.adapter.' + that.namespace, (err, res) => { if ((err || !res) && !config.isInstall) { logger.error(options.name + '.' + instance + ' invalid config'); process.exit(2); } else { createInstancesObjects(() => initAdapter(res)); } }); } }); } function autoSubscribeOn(cb) { if (!that.autoSubscribe) { // collect all that.objects.getObjectView('system', 'instance', {startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, options, (err, res) => { if (res && res.rows) { that.autoSubscribe = []; for (let c = res.rows.length - 1; c >= 0; c--) { if (res.rows[c].value.common.subscribable) { const _id = res.rows[c].id.substring(15); if (that.autoSubscribe.indexOf(_id) === -1) { that.autoSubscribe.push(_id); } } } } if (typeof cb === 'function') cb(); }); // because of autoSubscribe that.objects.subscribe('system.adapter.*'); } else if (typeof cb === 'function') { cb(); } } function initObjects(cb) { that.objects = new Objects({ namespace: that.namespace, connection: config.objects, logger: logger, connected: () => { that.connected = true; // Read dateformat if using of formatDate is announced if (options.useFormatDate) { that.getForeignObject('system.config', (err, data) => { if (data && data.common) { that.dateFormat = data.common.dateFormat; that.isFloatComma = data.common.isFloatComma; } if (typeof cb === 'function') cb(); }); } else if (typeof cb === 'function') { cb(); } }, disconnected: () => that.connected = false, change: (id, obj) => { if (obj === 'null') obj = null; if (!id) { logger.error(that.namespace + ' change ID is empty: ' + JSON.stringify(obj)); return; } // If desired, that adapter must be terminated if (id === 'system.adapter.' + that.namespace && obj && obj.common && obj.common.enabled === false) { that.log.info('Adapter is disabled => stop'); if (!obj.common.enabled) { stop(); setTimeout(() => process.exit(), 4000); } } // update oObjects structure if desired if (that.oObjects) { if (obj) { that.oObjects[id] = obj; } else { delete that.oObjects[id]; } } // process autosubscribe adapters if (id.match(/^system\.adapter\./)) { if (obj && obj.common.subscribable) { const _id = id.substring(15); // 'system.adapter.'.length if (obj.common.enabled) { if (that.autoSubscribe.indexOf(_id) === -1) { that.autoSubscribe.push(_id); } } else { const pos = that.autoSubscribe.indexOf(_id); if (pos !== -1) { that.autoSubscribe.splice(pos, 1); } } } } // It was an error in the calculation if ((options.noNamespace || config.noNamespace) && that._namespaceRegExp.test(id)) { // emit 'objectChange' event instantly setImmediate(() => { if (typeof options.objectChange === 'function') options.objectChange(id.substring(that.namespace.length + 1), obj); that.emit('objectChange', id.substring(that.namespace.length + 1), obj); }); } else { setImmediate(() => { if (typeof options.objectChange === 'function') options.objectChange(id, obj); // emit 'objectChange' event instantly that.emit('objectChange', id, obj); }); } }, connectTimeout: (/* err */) => { if (config.isInstall) { if (logger) logger.warn(that.namespace + ' no connection to objects DB'); process.exit(0); } else { if (logger) logger.error(that.namespace + ' no connection to objects DB'); } } }); that._namespaceRegExp = new RegExp('^' + that.namespace); // chache the regex object 'adapter.0' that._fixId = function _fixId(id, isPattern/* , type */) { let result = ''; // If id is an object if (typeof id === 'object') { // Add namespace + device + channel result = that.namespace + '.' + (id.device ? id.device + '.' : '') + (id.channel ? id.channel + '.' : '') + (id.state ? id.state : ''); } else { result = id; if (!that._namespaceRegExp.test(id)) { if (!isPattern) { result = that.namespace + (id ? '.' + id : ''); } else { result = that.namespace + '.' + (id ? id : ''); } } } return result; }; /** * Creates or overwrites object in objectDB. * * This function can create or overwrite objects in objectDB for this adapter. * Only Ids that belong to this adapter can be modified. So the function automatically adds "adapter.X." to ID. * <b>common</b>, <b>native</b> and <b>type</b> attributes are mandatory and it will be checked. * Additionally type "state" requires <b>role</b>, <b>type</b> and <b>name</b>, e.g.: * <pre><code>{ * common: { * name: 'object name', * type: 'number', // string, boolean, object, mixed, array * role: 'value' // see https://github.com/yunkong2/yunkong2/blob/master/doc/SCHEMA.md#state-commonrole * }, * native: {}, * type: 'state' // channel, device * }</code></pre> * * @alias setObject * @memberof Adapter * @param {string} id object ID, that must be overwritten or created. * @param {object} obj new object * @param {object} options optional user context * @param {function} callback return result * <pre><code> * function (err, obj) { * // obj is {id: id} * if (err) adapter.log.error('Cannot write object: ' + err); * } * </code></pre> */ that.setObject = function setObject(id, obj, options, callback) { if (typeof options === 'function') { callback = options; options = null; } if (!defaultObjs) { defaultObjs = require(__dirname + '/defaultObjs.js')('de', '°C', 'EUR'); } if (!id && obj.type !== 'meta') { logger.error(tools.appendStackTrace(that.namespace + ' setObject id missing!!')); if (typeof callback === 'function') callback('id missing!'); return; } if (!obj) { logger.error(that.namespace + ' setObject ' + id + ' object missing!'); if (typeof callback === 'function') callback('object missing!'); return; } if (obj.hasOwnProperty('type')) { if (!obj.hasOwnProperty('native')) { logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property native missing!'); obj.native = {}; } // Check property 'common' if (!obj.hasOwnProperty('common')) { logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common missing!'); obj.common = {}; } else if (obj.type === 'state') { // Try to extend the model for type='state' // Check property 'role' by 'state' if (obj.common.hasOwnProperty('role') && defaultObjs[obj.common.role]) { obj.common = extend(true, {}, defaultObjs[obj.common.role], obj.common); } else if (!obj.common.hasOwnProperty('role')) { logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common.role missing!'); } if (!obj.common.hasOwnProperty('type')) { logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common.type missing!'); } } if (!obj.common.hasOwnProperty('name')) { obj.common.name = id; logger.debug(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common.name missing, using id as name'); } id = that._fixId(id, false, obj.type); if (obj.children || obj.parent) { logger.warn(that.namespace + ' Do not use parent or children for ' + id); } if (!obj.from) obj.from = 'system.adapter.' + that.namespace; if (!obj.user) obj.user = (options ? options.user : '') || 'system.user.admin'; if (!obj.ts) obj.ts = Date.now(); that.objects.setObject(id, obj, options, callback); } else { logger.error(that.namespace + ' setObject ' + id + ' mandatory property type missing!'); if (typeof callback === 'function') callback('mandatory property type missing!'); } }; /** * Promise-version of Adapter.setObject */ that.setObjectAsync = tools.promisify(that.setObject, that); /** * Get all states, channels and devices of this adapter. * * @alias getAdapterObjects * @memberof Adapter * @param {function} callback return result * <pre><code> * function (objects) { * for (var id in objects) { * adapter.log.debug(id); * } * } * </code></pre> */ that.getAdapterObjects = function (callback) { let objects = {}; that.objects.getObjectView('system', 'state', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999', include_docs: true}, (err, _states) => { that.objects.getObjectView('system', 'channel', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999', include_docs: true}, (err, _channels) => { that.objects.getObjectView('system', 'device', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999', include_docs: true}, (err, _devices) => { if (_channels) { for (let c = _channels.rows.length - 1; c >= 0; c--) { objects[_channels.rows[c].id] = _channels.rows[c].value; } } if (_devices) { for (let d = _devices.rows.length - 1; d >= 0; d--) { objects[_devices.rows[d].id] = _devices.rows[d].value; } } if (_states) { if (options.states) that.oStates = {}; for (let s = _states.rows.length - 1; s >= 0; s--) { objects[_states.rows[s].id] = _states.rows[s].value; if (that.oStates) { that.oStates[_states.rows[s].id] = null; } } } if (typeof callback === 'function') callback(objects); }); }); }); }; /** * Promise-version of Adapter.getAdapterObjects */ that.getAdapterObjectsAsy